diff --git a/.github/ISSUE_TEMPLATE/BugReport.yml b/.github/ISSUE_TEMPLATE/BugReport.yml index a343f4a7fc..c2f9bc0145 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.yml +++ b/.github/ISSUE_TEMPLATE/BugReport.yml @@ -21,35 +21,27 @@ body: - type: input id: ios_version attributes: - label: iOS system version + label: iOS system version (if applicable) description: What iOS version are you using? Open Settings, scroll down until "General", select "Info" and report "Software version". placeholder: "14.7" validations: required: false - - type: input - id: ios_monal__version - attributes: - label: iOS Monal version - description: What Monal version are you using? Open Monal, select top left icon, scoll down until "Version". - placeholder: "5.0.1" - validations: - required: false - type: input id: mac_version attributes: - label: macOS system version + label: macOS system version (if applicable) description: What macOS version are you using? placeholder: "14.7" validations: required: false - type: input - id: mac_monal__version + id: monal_version attributes: - label: macOS Monal version - description: What Monal version are you using? - placeholder: "5.0.1" + label: Monal version + description: What Monal version are you using? Open Monal, select top left icon, scoll down until "Version". + placeholder: "1.0" validations: - required: false + required: true - type: input id: xmpp_server attributes: @@ -131,13 +123,13 @@ body: options: - label: I have checked if my issue can be solved with [Considerations for XMPP users](https://github.com/monal-im/Monal/wiki/Considerations-for-XMPP-users) and [Considerations for XMPP server admins](https://github.com/monal-im/Monal/wiki/Considerations-for-XMPP-server-admins) required: true - - type: checkboxes - id: cross-check-checkbox - attributes: - label: Related Issues - options: - - label: I have cross-checked this overview https://github.com/monal-im/Monal/issues/322 as well as filtered for related labels https://github.com/monal-im/Monal/labels - required: true + # - type: checkboxes + # id: cross-check-checkbox + # attributes: + # label: Related Issues + # options: + # - label: I have cross-checked this overview https://github.com/monal-im/Monal/issues/322 as well as filtered for related labels https://github.com/monal-im/Monal/labels + # required: true - type: checkboxes id: xep-checkbox attributes: diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index f1bff59cc6..fc380aa429 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -73,6 +73,27 @@ jobs: run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id maccatalyst.G7YU7X7KRJ.SworIM # - name: Update xmpp.org client list with new timestamp # run: ./scripts/push_xmpp.org.sh + - name: Extract version number and changelog from newest merge commit + id: releasenotes + run: | + buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') + echo "tag=Build_iOS_$buildNumber" >> "$GITHUB_OUTPUT" + echo "name=$(git log -n 1 --merges --pretty=format:%s)" >> "$GITHUB_OUTPUT" + echo "notes=$(git log -n 1 --merges --pretty=format:%b)" >> "$GITHUB_OUTPUT" + - name: Release + uses: softprops/action-gh-release@v2 + with: + name: Release ${{ steps.releasenotes.outputs.name }} + tag_name: ${{ steps.releasenotes.outputs.tag }} + target_commitish: stable + generate_release_notes: false + body: ${{ steps.releasenotes.outputs.notes }} + files: | + ./Monal/build/ipa/Monal.ipa + ./Monal/build/app/Monal.zip + fail_on_unmatched_files: true + token: ${{ secrets.GITHUB_TOKEN }} + draft: false - uses: actions/upload-artifact@v3 with: name: monal-catalyst-pkg diff --git a/Monal/Classes/AVCallUI.swift b/Monal/Classes/AVCallUI.swift index bd1ad177f6..76ba379ed5 100644 --- a/Monal/Classes/AVCallUI.swift +++ b/Monal/Classes/AVCallUI.swift @@ -525,33 +525,23 @@ struct AVCallUI: View { } .buttonStyle(BorderlessButtonStyle()) - if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { - Spacer().frame(width: 32) - Button(action: { - call.speaker = !call.speaker - }) { - if #available(iOS 15, *) { + //the button somehow does not work in iOS 15 and we don't know how to fix that + //--> don't show the button on iOS 14 and 15 + if #available(iOS 16, *) { + if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { + Spacer().frame(width: 32) + Button(action: { + call.speaker = !call.speaker + }) { Image(systemName: "speaker.wave.2.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) .symbolRenderingMode(.palette) .foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black) .shadow(radius: 7) - } else { - ZStack { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(call.speaker ? .black : .white) - Image(systemName: "speaker.wave.2.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(call.speaker ? .white : .black) - .shadow(radius: 7) - } } + .buttonStyle(BorderlessButtonStyle()) } - .buttonStyle(BorderlessButtonStyle()) } Spacer() diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index db05d4e368..039a0ccb6f 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -54,11 +54,13 @@ @implementation ActiveChatsViewController static NSMutableSet* _mamWarningDisplayed; static NSMutableSet* _smacksWarningDisplayed; +static NSMutableSet* _pushWarningDisplayed; +(void) initialize { _mamWarningDisplayed = [NSMutableSet new]; _smacksWarningDisplayed = [NSMutableSet new]; + _pushWarningDisplayed = [NSMutableSet new]; } #pragma mark view lifecycle @@ -113,6 +115,7 @@ -(void) viewDidLoad [nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalDeletedMessageNotice object:nil]; [nc addObserver:self selector:@selector(messageSent:) name:kMLMessageSentToContact object:nil]; [nc addObserver:self selector:@selector(handleDeviceRotation) name:UIDeviceOrientationDidChangeNotification object:nil]; + [nc addObserver:self selector:@selector(showWarningsIfNeeded) name:kMonalFinishedCatchup object:nil]; [_chatListTable registerNib:[UINib nibWithNibName:@"MLContactCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"ContactCell"]; @@ -392,40 +395,6 @@ -(void) viewDidAppear:(BOOL) animated { DDLogDebug(@"active chats view did appear"); [super viewDidAppear:animated]; - - for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList]) - { - NSNumber* accountNo = accountDict[kAccountID]; - xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:accountNo]; - if(!account) - @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Connected xmpp* object for accountNo is nil!" userInfo:accountDict]; - if(![_mamWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) - { - if(!account.connectionProperties.supportsMam2) - { - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support MAM (XEP-0313). That means you could frequently miss incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - [_mamWarningDisplayed addObject:accountNo]; - }]]; - [self presentViewController:messageAlert animated:YES completion:nil]; - } - else - [_mamWarningDisplayed addObject:accountNo]; - } - if(![_smacksWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound) - { - if(!account.connectionProperties.supportsSM3) - { - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support Stream Management (XEP-0198). That means your outgoing messages can get lost frequently!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - [_smacksWarningDisplayed addObject:accountNo]; - }]]; - [self presentViewController:messageAlert animated:YES completion:nil]; - } - else - [_smacksWarningDisplayed addObject:accountNo]; - } - } } -(void) didReceiveMemoryWarning @@ -466,9 +435,66 @@ -(void) segueToIntroScreensIfNeeded } if(![[HelperTools defaultsDB] boolForKey:@"HasSeenPrivacySettings"]) { - [self performSegueWithIdentifier:@"showPrivacySettings" sender:self]; + [self showPrivacySettings]; return; } + + [self showWarningsIfNeeded]; +} + +-(void) showWarningsIfNeeded +{ + dispatch_async(dispatch_get_main_queue(), ^{ + for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList]) + { + NSNumber* accountNo = accountDict[kAccountID]; + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:accountNo]; + if(!account) + @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Connected xmpp* object for accountNo is nil!" userInfo:accountDict]; + + if(![_mamWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) + { + if(!account.connectionProperties.supportsMam2) + { + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support MAM (XEP-0313). That means you could frequently miss incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + [_mamWarningDisplayed addObject:accountNo]; + }]]; + [self presentViewController:messageAlert animated:YES completion:nil]; + } + else + [_mamWarningDisplayed addObject:accountNo]; + } + + if(![_smacksWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound) + { + if(!account.connectionProperties.supportsSM3) + { + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support Stream Management (XEP-0198). That means your outgoing messages can get lost frequently!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + [_smacksWarningDisplayed addObject:accountNo]; + }]]; + [self presentViewController:messageAlert animated:YES completion:nil]; + } + else + [_smacksWarningDisplayed addObject:accountNo]; + } + + if(![_pushWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) + { + if(!account.connectionProperties.supportsMam2) + { + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support PUSH (XEP-0357). That means you have to manually open the app to retrieve new incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + [_pushWarningDisplayed addObject:accountNo]; + }]]; + [self presentViewController:messageAlert animated:YES completion:nil]; + } + else + [_pushWarningDisplayed addObject:accountNo]; + } + } + }); } -(void) openConversationPlaceholder:(MLContact*) contact @@ -484,7 +510,8 @@ -(void) openConversationPlaceholder:(MLContact*) contact -(void) showPrivacySettings { - [self performSegueWithIdentifier:@"showPrivacySettings" sender:self]; + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsPrivacySettings"]; + [self presentViewController:view animated:YES completion:^{}]; } -(void) showSettings @@ -713,16 +740,30 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N MLContact* chatContact = nil; // Select correct contact array - if(indexPath.section == pinnedChats) { + if(indexPath.section == pinnedChats) chatContact = [self.pinnedContacts objectAtIndex:indexPath.row]; - } else { + else chatContact = [self.unpinnedContacts objectAtIndex:indexPath.row]; - } + // Display msg draft or last msg MLMessage* messageRow = [[DataLayer sharedInstance] lastMessageForContact:chatContact.contactJid forAccount:chatContact.accountId]; [cell initCell:chatContact withLastMessage:messageRow]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + // Highlight the selected chat + if([MLNotificationManager sharedInstance].currentContact != nil && [chatContact isEqual:[MLNotificationManager sharedInstance].currentContact]) + { + cell.backgroundColor = [UIColor lightGrayColor]; + cell.statusText.textColor = [UIColor whiteColor]; + } + else + { + cell.backgroundColor = [UIColor clearColor]; + cell.statusText.textColor = [UIColor lightGrayColor]; + } + return cell; } diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index e223913230..b030a79a5f 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -23,6 +23,7 @@ extern NSString* const kDomain; extern NSString* const kEnabled; extern NSString* const kNeedsPasswordMigration; extern NSString* const kSupportsSasl2; +extern NSString* const kPlainActivated; extern NSString* const kServer; extern NSString* const kPort; @@ -163,6 +164,8 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) pinSasl2ForAccount:(NSNumber*) accountNo; -(BOOL) isSasl2PinnedForAccount:(NSNumber*) accountNo; +-(BOOL) isPlainActivatedForAccount:(NSNumber*) accountNo; +-(BOOL) deactivatePlainForAccount:(NSNumber*) accountNo; -(NSMutableDictionary* _Nullable) readStateForAccount:(NSNumber*) accountNo; -(void) persistState:(NSDictionary*) state forAccount:(NSNumber*) accountNo; @@ -211,7 +214,9 @@ extern NSString* const kMessageTypeFiletransfer; -(void) deleteMessageHistory:(NSNumber *) messageNo; -(void) deleteMessageHistoryLocally:(NSNumber*) messageNo; -(void) updateMessageHistory:(NSNumber*) messageNo withText:(NSString*) newText; --(NSNumber* _Nullable) getHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getLMCHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getRetractionHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getRetractionHistoryIDForModeratedStanzaId:(NSString*) stanzaId from:(NSString*) from andAccount:(NSNumber*) accountNo; -(NSDate* _Nullable) returnTimestampForQuote:(NSNumber*) historyID; -(BOOL) checkLMCEligible:(NSNumber*) historyID encrypted:(BOOL) encrypted historyBaseID:(NSNumber* _Nullable) historyBaseID; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index d8fea8450a..4de33cc7a3 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -34,6 +34,7 @@ @implementation DataLayer NSString *const kEnabled = @"enabled"; NSString *const kNeedsPasswordMigration = @"needs_password_migration"; NSString *const kSupportsSasl2 = @"supports_sasl2"; +NSString *const kPlainActivated = @"plain_activated"; NSString *const kServer = @"server"; NSString *const kPort = @"other_port"; @@ -127,7 +128,7 @@ -(MLSQLite*) db -(NSString* _Nullable) exportDB { NSFileManager* fileManager = [NSFileManager defaultManager]; - NSString* temporaryFilename = [NSString stringWithFormat:@"%@.db", [[NSProcessInfo processInfo] globallyUniqueString]]; + NSString* temporaryFilename = [NSString stringWithFormat:@"sworim_%@.db", [[NSProcessInfo processInfo] globallyUniqueString]]; NSString* temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:temporaryFilename]; //checkpoint db before copying db file @@ -140,7 +141,7 @@ -(NSString* _Nullable) exportDB [fileManager copyItemAtPath:dbPath toPath:temporaryFilePath error:&error]; if(error) { - DDLogError(@"Could not copy logfile to export location!"); + DDLogError(@"Could not copy database to export location!"); return NO; } return YES; @@ -265,7 +266,7 @@ -(BOOL) updateAccounWithDictionary:(NSDictionary*) dictionary -(NSNumber*) addAccountWithDictionary:(NSDictionary*) dictionary { return [self.db idWriteTransaction:^{ - NSString* query = @"INSERT INTO account (server, other_port, resource, domain, enabled, directTLS, username, rosterName, statusMessage) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?);"; + NSString* query = @"INSERT INTO account (server, other_port, resource, domain, enabled, directTLS, username, rosterName, statusMessage, plain_activated) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; NSString* server = (NSString*) [dictionary objectForKey:kServer]; NSString* port = (NSString*)[dictionary objectForKey:kPort]; NSArray* params = @[ @@ -277,7 +278,8 @@ -(NSNumber*) addAccountWithDictionary:(NSDictionary*) dictionary [dictionary objectForKey:kDirectTLS], ((NSString *)[dictionary objectForKey:kUsername]), [dictionary objectForKey:kRosterName] ? ((NSString*)[dictionary objectForKey:kRosterName]) : @"", - [dictionary objectForKey:@"statusMessage"] ? ((NSString*)[dictionary objectForKey:@"statusMessage"]) : @"" + [dictionary objectForKey:@"statusMessage"] ? ((NSString*)[dictionary objectForKey:@"statusMessage"]) : @"", + [dictionary objectForKey:kPlainActivated] != nil ? [dictionary objectForKey:kPlainActivated] : [NSNumber numberWithBool:NO], ]; BOOL result = [self.db executeNonQuery:query andArguments:params]; // return the accountID @@ -346,6 +348,24 @@ -(BOOL) isSasl2PinnedForAccount:(NSNumber*) accountNo }]; } +-(BOOL) isPlainActivatedForAccount:(NSNumber*) accountNo +{ + return [self.db boolReadTransaction:^{ + NSNumber* plainActivated = (NSNumber*)[self.db executeScalar:@"SELECT plain_activated FROM account WHERE account_id=?;" andArguments:@[accountNo]]; + if(plainActivated == nil) + return NO; + else + return [plainActivated boolValue]; + }]; +} + +-(BOOL) deactivatePlainForAccount:(NSNumber*) accountNo +{ + return [self.db boolReadTransaction:^{ + return [self.db executeNonQuery:@"UPDATE account SET plain_activated=0 WHERE account_id=?;" andArguments:@[accountNo]]; + }]; +} + -(NSMutableDictionary*) readStateForAccount:(NSNumber*) accountNo { if(accountNo == nil) @@ -1487,7 +1507,7 @@ -(void) updateMessageHistory:(NSNumber*) messageNo withText:(NSString*) newText }]; } --(NSNumber*) getHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) getLMCHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo { return [self.db idReadTransaction:^{ return [self.db executeScalar:@"SELECT M.message_history_id FROM message_history AS M INNER JOIN account AS A ON M.account_id=A.account_id INNER JOIN buddylist AS B on M.buddy_name = B.buddy_name AND M.account_id = B.account_id WHERE messageid=? AND M.account_id=? AND (\ @@ -1504,12 +1524,24 @@ -(NSNumber*) getHistoryIDForMessageId:(NSString*) messageid from:(NSString*) fro }]; } -/* -CF6DE818-6036-4B2E-A228-717303D1E9FF -bififufuva@conference.xmpp.eightysoft.de -bififufuva@conference.xmpp.eightysoft.de -43 -*/ +-(NSNumber* _Nullable) getRetractionHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo +{ + return [self.db idReadTransaction:^{ + return [self.db executeScalar:@"SELECT M.message_history_id FROM message_history AS M INNER JOIN account AS A ON M.account_id=A.account_id INNER JOIN buddylist AS B on M.buddy_name = B.buddy_name AND M.account_id = B.account_id WHERE M.account_id=? AND ( \ + (B.Muc=0 AND M.messageid=? AND ((M.buddy_name=? AND M.inbound=1) OR ((A.username || '@' || A.domain)=? AND M.inbound=0))) OR \ + (B.Muc=1 AND M.stanzaid=? AND M.buddy_name=? AND (M.participant_jid=? OR (M.participant_jid IS NULL AND M.actual_from=?))) \ + );" andArguments:@[accountNo, messageid, from, from, messageid, from, nilWrapper(participantJid), nilWrapper(actualFrom)]]; + }]; +} + +-(NSNumber* _Nullable) getRetractionHistoryIDForModeratedStanzaId:(NSString*) stanzaId from:(NSString*) from andAccount:(NSNumber*) accountNo +{ + return [self.db idReadTransaction:^{ + return [self.db executeScalar:@"SELECT M.message_history_id FROM message_history AS M INNER JOIN account AS A ON M.account_id=A.account_id INNER JOIN buddylist AS B on M.buddy_name = B.buddy_name AND M.account_id = B.account_id \ + WHERE M.account_id=? AND B.Muc=1 AND M.stanzaid=? AND M.buddy_name=?;" + andArguments:@[accountNo, stanzaId, from]]; + }]; +} -(NSDate* _Nullable) returnTimestampForQuote:(NSNumber*) historyID { diff --git a/Monal/Classes/DataLayerMigrations.m b/Monal/Classes/DataLayerMigrations.m index 80b379c3e9..8d9d5b8328 100644 --- a/Monal/Classes/DataLayerMigrations.m +++ b/Monal/Classes/DataLayerMigrations.m @@ -1034,6 +1034,25 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \ [db executeNonQuery:@"DROP TABLE IF EXISTS ver_timestamp;"]; }]; + [self updateDB:db withDataLayer:dataLayer toVersion:6.202 withBlock:^{ + //intentionally left blank + }]; + + //fix empty domain in db for weird setups + [self updateDB:db withDataLayer:dataLayer toVersion:6.203 withBlock:^{ + [db executeNonQuery:@"UPDATE account SET domain=TRIM(server) WHERE domain='';"]; + }]; + + //add new setting to force deactivate sasl2 and fallback to sals1 and plain + [self updateDB:db withDataLayer:dataLayer toVersion:6.204 withBlock:^{ + [db executeNonQuery:@"ALTER TABLE account ADD COLUMN plain_activated BOOL DEFAULT false;"]; + + //make sure that all users are still able to connect if the server supports SASL2 and the account is disabled + //--> possibly disabled because it only supports PLAIN + //==> the next connect will (re)set the plain_activated and supports_sasl2 flags to the correct values + [db executeNonQuery:@"UPDATE account SET plain_activated=true, supports_sasl2=false WHERE NOT enabled AND supports_sasl2;"]; + }]; + //check if device id changed and invalidate state, if so //but do so only for non-sandbox (e.g. non-development) installs diff --git a/Monal/Classes/DebugView.swift b/Monal/Classes/DebugView.swift new file mode 100644 index 0000000000..ea12caff8a --- /dev/null +++ b/Monal/Classes/DebugView.swift @@ -0,0 +1,203 @@ +// +// LogView.swift +// Monal +// +// Created by Zain Ashraf on 3/23/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +class DebugDefaultDB: ObservableObject { + @defaultsDB("udpLoggerEnabled") + var udpLoggerEnabled:Bool + + @defaultsDB("udpLoggerPort") + var udpLoggerPort: String + + @defaultsDB("udpLoggerHostname") + var udpLoggerHostname: String + + @defaultsDB("udpLoggerKey") + var udpLoggerKey: String +} + +struct LogFilesView: View { + @State private var sortedLogFileInfos: [DDLogFileInfo] = [] + @State private var showShareSheet:Bool = false + @State private var fileURL: URL? + @State private var showingDBExportFailedAlert = false + + func refreshSortedLogfiles() { + if let sortedLogFileInfos = HelperTools.fileLogger?.logFileManager.sortedLogFileInfos { + self.sortedLogFileInfos = sortedLogFileInfos + } + DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { + refreshSortedLogfiles() + } + } + + var body: some View { + VStack(alignment: .leading) { + if #available(iOS 15, *) { + Text("This can be used to export logfiles.\n[Learn how to read them](https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#view-the-log).") + } + List { + Section(header: Text("Logfiles")) { + ForEach(sortedLogFileInfos, id: \.self) { logFileInfo in + Button(logFileInfo.fileName) { + fileURL = URL(fileURLWithPath: logFileInfo.filePath) + }.foregroundColor(monalDarkGreen) + } + } + Section(header: Text("Database Files")) { + Button("Main Database") { + if let dbFile = DataLayer.sharedInstance().exportDB() { + self.fileURL = URL(fileURLWithPath: dbFile) + } else { + showingDBExportFailedAlert = true + } + }.foregroundColor(monalDarkGreen) + Button("IPC Database") { + if let dbFile = HelperTools.exportIPCDatabase() { + self.fileURL = URL(fileURLWithPath: dbFile) + } else { + showingDBExportFailedAlert = true + } + }.foregroundColor(monalDarkGreen) + } + } + .applyClosure { view in + if #available(iOS 15, *) { + view.listStyle(.grouped) + } + } + } + .alert(isPresented: $showingDBExportFailedAlert) { + Alert(title: Text("Database Export Failed"), message: Text("Failed to export the database, please check the logfile for errors and try again."), dismissButton: .default(Text("Close"))) + } + .sheet(isPresented:$fileURL.optionalMappedToBool()) { + if let fileURL = fileURL { + ActivityViewController(activityItems: [fileURL]) + } + } + .onAppear { + refreshSortedLogfiles() + } + } +} + +struct UDPConfigView: View { + @ObservedObject var defaultDB = DebugDefaultDB() + + var body: some View { + VStack(alignment: .leading) { + if #available(iOS 16, *) { + Text("The UDP logger allows you to livestream the log to the configured IP. Please use a secure key when streaming over the internet!\n[Learn how to receive the log stream](https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#stream-the-log).") + Form { + Section(header: Text("UDP Logger Configuration")) { + Toggle("Enable", isOn: $defaultDB.udpLoggerEnabled) + LabeledContent("Logserver IP:") { + TextField("Logserver IP", text: $defaultDB.udpLoggerHostname, prompt: Text("Required")) + } + LabeledContent("Logserver Port:") { + TextField("Logserver Port", text: $defaultDB.udpLoggerPort, prompt: Text("Required")) + }.keyboardType(.numberPad) + LabeledContent("AES Encryption Key:") { + TextField("AES Encryption Key", text: $defaultDB.udpLoggerKey, prompt: Text("Required")) + } + } + } + .padding(0) + .textFieldStyle(.roundedBorder) + } else { + Text("The UDP logger allows you to livestream the log to the configured IP.") + Link("Learn more", destination: URL(string: "https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#stream-the-log")!) + Spacer().frame(height: 32) + Text("UDP Logging UI not supported on iOS < 16").foregroundColor(.red) + } + } + } +} + +struct CrashTestingView: View { + var body: some View { + VStack(alignment:.leading, spacing: 25) { + Text("This allows you to forcefully crash the app using several different methods to test the crash handling.") + + Group { + Button("Try to call unknown handler method") { + DispatchQueue.global(qos: .default).async(execute: { + HelperTools.flushLogs(withTimeout: 0.100) + let handler = MLHandler(delegate: self, handlerName: "IDontKnowThis", andBoundArguments: [:]) + handler.call(withArguments: nil) + }) + } + Button("Bad Access Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + let delegate: AnyClass? = NSClassFromString("MonalAppDelegate") + print(delegate.unsafelyUnwrapped.audiovisualTypes()) + + } + Button("Assertion Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + assert(false) + } + Button("Fatal Error Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + fatalError("fatalError_example") + } + Button("Nil Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + let crasher:Int? = nil + print(crasher!) + } + }.foregroundColor(.red) + Spacer() + } + } +} + +struct DebugView: View { + @State private var isReconnecting: Bool = false + @StateObject private var overlay = LoadingOverlayState() + var body: some View { + TabView { + LogFilesView() + .tabItem { + Image(systemName: "list.bullet") + Text("Logs") + } + UDPConfigView() + .tabItem { + Image(systemName: "gear") + Text("UDP Logger") + } + CrashTestingView() + .tabItem { + Image(systemName: "bolt.fill") + Text("Crash Testing") + } + } + .padding() + .addLoadingOverlay(overlay) + .onChange(of: isReconnecting) { _ in + if isReconnecting{ + showLoadingOverlay(overlay, headline: "Reconnecting", description: "Will log out and reconnect all (connected) accounts.") + } else { + hideLoadingOverlay(overlay) + } + } + .navigationBarItems(trailing:Button("Reconnect All") { + isReconnecting = true + MLXMPPManager.sharedInstance().reconnectAll() + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + isReconnecting = false + } + }) + } +} + +#Preview { + NavigationView { + DebugView() + } +} diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index d263b0f84f..fb02f2877d 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -37,6 +37,18 @@ typedef NS_ENUM(NSUInteger, MLVersionType) { MLVersionTypeLog, }; +typedef NS_ENUM(NSUInteger, MLDefinedIdentifier) { + MLDefinedIdentifier_kAppGroup, + MLDefinedIdentifier_kMonalOpenURL, + MLDefinedIdentifier_kBackgroundProcessingTask, + MLDefinedIdentifier_kBackgroundRefreshingTask, + MLDefinedIdentifier_kMonalKeychainName, + MLDefinedIdentifier_SHORT_PING, + MLDefinedIdentifier_LONG_PING, + MLDefinedIdentifier_MUC_PING, + MLDefinedIdentifier_BGFETCH_DEFAULT_INTERVAL, +}; + typedef NS_ENUM(NSUInteger, MLRunLoopIdentifier) { MLRunLoopIdentifierNetwork, }; @@ -46,7 +58,7 @@ void swizzle(Class c, SEL orig, SEL new); @interface HelperTools : NSObject -@property (class, nonatomic, strong) DDFileLogger* fileLogger; +@property (class, nonatomic, strong, nullable) DDFileLogger* fileLogger; +(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage counter:(uint64_t*) counter andError:(NSError** _Nullable) error; +(void) initSystem; @@ -55,6 +67,7 @@ void swizzle(Class c, SEL orig, SEL new); +(void) flushLogsWithTimeout:(double) timeout; +(void) __attribute__((noreturn)) MLAssertWithText:(NSString*) text andUserData:(id _Nullable) additionalData andFile:(const char* const) file andLine:(int) line andFunc:(const char* const) func; +(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBacktrace:(NSString*) backtrace; ++(void) __attribute__((noreturn)) throwExceptionWithName:(NSString*) name reason:(NSString*) reason userInfo:(NSDictionary* _Nullable) userInfo; +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere andDisableAccount:(BOOL) disableAccount; +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere; +(NSString*) extractXMPPError:(XMPPStanza*) stanza withDescription:(NSString* _Nullable) description; @@ -73,6 +86,8 @@ void swizzle(Class c, SEL orig, SEL new); +(MLXMLNode* _Nullable) candidate2xml:(NSString*) candidate withMid:(NSString*) mid pwd:(NSString* _Nullable) pwd ufrag:(NSString* _Nullable) ufrag andInitiator:(BOOL) initiator; +(NSString* _Nullable) xml2candidate:(MLXMLNode*) xml withInitiator:(BOOL) initiator; ++(void) busyWaitForOperationQueue:(NSOperationQueue*) queue; ++(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier; +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier; +(NSError* _Nullable) hardLinkOrCopyFile:(NSString*) from to:(NSString*) to; +(NSString*) getQueueThreadLabelFor:(DDLogMessage*) logMessage; @@ -93,6 +108,7 @@ void swizzle(Class c, SEL orig, SEL new); +(UIColor*) generateColorFromJid:(NSString*) jid; +(NSString*) bytesToHuman:(int64_t) bytes; +(NSString*) stringFromToken:(NSData*) tokenIn; ++(NSString* _Nullable) exportIPCDatabase; +(void) configureFileProtection:(NSString*) protectionLevel forFile:(NSString*) file; +(void) configureFileProtectionFor:(NSString*) file; +(BOOL) isContactBlacklistedForEncryption:(MLContact*) contact; @@ -157,6 +173,10 @@ void swizzle(Class c, SEL orig, SEL new); +(NSNumber*) currentTimestampInSeconds; +(NSNumber*) dateToNSNumberSeconds:(NSDate*) date; ++(BOOL) constantTimeCompareAttackerString:(NSString* _Nonnull) str1 withKnownString:(NSString* _Nonnull) str2; + ++(BOOL) isIP:(NSString*) host; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 5314446f9e..0054d9e159 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -7,6 +7,11 @@ // #include +#include +#include +#include +#include +#include #include #include #include @@ -51,6 +56,7 @@ #import "MLStreamRedirect.h" #import "commithash.h" #import "MLContactSoftwareVersionInfo.h" +#import "IPC.h" @import UserNotifications; @import CoreImage; @@ -60,8 +66,6 @@ @import UniformTypeIdentifiers; @import QuickLookThumbnailing; -#import "SCRAM.h" - @interface KSCrash() @property(nonatomic,readwrite,retain) NSString* basePath; @end @@ -81,6 +85,13 @@ @interface KSCrash() static objc_exception_preprocessor _oldExceptionPreprocessor = NULL; #endif +//shamelessly stolen from utils.ip in conversations source +static NSRegularExpression* IPV4; +static NSRegularExpression* IPV6_HEX4DECCOMPRESSED; +static NSRegularExpression* IPV6_6HEX4DEC; +static NSRegularExpression* IPV6_HEXCOMPRESSED; +static NSRegularExpression* IPV6; + //add own crash info (used by rust panic handler) //see https://alastairs-place.net/blog/2013/01/10/interesting-os-x-crash-report-tidbits/ //and kscrash sources (KSDynamicLinker.c) @@ -98,6 +109,40 @@ @interface KSCrash() #pragma pack() +// see: https://developer.apple.com/library/archive/qa/qa1361/_index.html +// Returns true if the current process is being debugged (either +// running under the debugger or has a debugger attached post facto). +bool isDebugerActive(void) +{ +#ifdef IS_ALPHA + int junk; + int mib[4]; + struct kinfo_proc info; + size_t size; + + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl + size = sizeof(info); + junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + assert(junk == 0); + + // We're being debugged if the P_TRACED flag is set. + return ( (info.kp_proc.p_flag & P_TRACED) != 0 ); +#else + return 0; +#endif +} + //see https://stackoverflow.com/a/2180788 int asyncSafeCopyFile(const char* from, const char* to) { @@ -201,15 +246,14 @@ void logException(NSException* exception) void uncaughtExceptionHandler(NSException* exception) { logException(exception); -//don't let kscrash handle the exception if we are in the simulator -//(this makes sure xcode will catch the exception and show proper backtraces etc.) -#if TARGET_OS_SIMULATOR - return; -#else + + //don't report that crash through KSCrash if the debugger is active + if(isDebugerActive()) + return; + //make sure this crash will be recorded by kscrash using the NSException rather than the c++ exception thrown by the objc runtime //this will make sure that the stacktrace matches the objc exception rather than being a top level c++ stacktrace KSCrash.sharedInstance.uncaughtExceptionHandler(exception); -#endif } //this function will only be in use under macos alpha builds to log every exception (even when catched with @try-@catch constructs) @@ -234,7 +278,6 @@ void swizzle(Class c, SEL orig, SEL new) method_exchangeImplementations(origMethod, newMethod); } - @implementation HelperTools +(void) initialize @@ -244,6 +287,13 @@ +(void) initialize u_int32_t i = arc4random(); _processID = [self hexadecimalString:[NSData dataWithBytes:&i length:sizeof(i)]]; + + //shamelessly stolen from utils.ip in conversations source + IPV4 = [NSRegularExpression regularExpressionWithPattern:@"\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; + IPV6_HEX4DECCOMPRESSED = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; + IPV6_6HEX4DEC = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; + IPV6_HEXCOMPRESSED = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z" options:0 error:nil]; + IPV6 = [NSRegularExpression regularExpressionWithPattern:@"\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z" options:0 error:nil]; } +(void) installExceptionHandler @@ -279,7 +329,7 @@ +(void) __attribute__((noreturn)) MLAssertWithText:(NSString*) text andUserData: NSArray* filePathComponents = [fileStr pathComponents]; if([filePathComponents count]>1) fileStr = [NSString stringWithFormat:@"%@/%@", filePathComponents[[filePathComponents count]-2], filePathComponents[[filePathComponents count]-1]]; - //DDLogError(@"Assertion triggered at %@:%d in %s", fileStr, line, func); + DDLogError(@"Assertion triggered at %@:%d in %s", fileStr, line, func); @throw [NSException exceptionWithName:[NSString stringWithFormat:@"MLAssert triggered at %@:%d in %s with reason '%@' and userInfo: %@", fileStr, line, func, text, userInfo] reason:text userInfo:userInfo]; } @@ -303,17 +353,23 @@ +(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBa abort(); } ++(void) __attribute__((noreturn)) throwExceptionWithName:(NSString*) name reason:(NSString*) reason userInfo:(NSDictionary* _Nullable) userInfo +{ + @throw [NSException exceptionWithName:name reason:reason userInfo:userInfo]; +} + +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere andDisableAccount:(BOOL) disableAccount { [self postError:description withNode:node andAccount:account andIsSevere:isSevere]; + //disconnect and reset state (including pipelined auth etc.) + //this has to be done before disabling the account to not trigger an assertion + [[MLXMPPManager sharedInstance] disconnectAccount:account.accountNo withExplicitLogout:YES]; + //make sure we don't try this again even when the mainapp/appex gets restarted NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountNo] copyItems:YES]; accountDic[kEnabled] = @NO; - [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; - - //disconnect and reset state (including pipelined auth etc.) - [[MLXMPPManager sharedInstance] disconnectAccount:account.accountNo withExplicitLogout:YES]; + [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; } +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere @@ -378,7 +434,9 @@ +(void) initSystem if(enableDefaultLogAndCrashFramework) { [self configureLogging]; - [self installCrashHandler]; + //don't install KSCrash if the debugger is active + if(!isDebugerActive()) + [self installCrashHandler]; [self installExceptionHandler]; } else @@ -461,6 +519,42 @@ +(NSString*) getSelectedPushServerBasedOnLocale ]; } ++(void) busyWaitForOperationQueue:(NSOperationQueue*) queue +{ + //apparently setting someQueue.suspended = YES does return before the queue is actually suspended + //--> busy wait for someQueue.suspended == YES + int busyWaitCounter = 0; + NSTimeInterval waitTime = 0.0; + NSDate* startTime = [NSDate date]; + while(queue.suspended != YES) + { + busyWaitCounter++; + waitTime = [[NSDate date] timeIntervalSinceDate:startTime]; + MLAssert(waitTime <= 4.0, @"Busy wait for queue freeze took longer than 4.0 seconds!", (@{@"queue": queue, @"name": queue.name})); + + } + if(busyWaitCounter > 0) + DDLogWarn(@"busyWaitFor:%@ --> busyWaitCounter=%d, waitTime=%f", queue.name, busyWaitCounter, waitTime); +} + ++(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier +{ + switch(identifier) + { + case MLDefinedIdentifier_kAppGroup: return kAppGroup; break; + case MLDefinedIdentifier_kMonalOpenURL: return kMonalOpenURL; break; + case MLDefinedIdentifier_kBackgroundProcessingTask: return kBackgroundProcessingTask; break; + case MLDefinedIdentifier_kBackgroundRefreshingTask: return kBackgroundRefreshingTask; break; + case MLDefinedIdentifier_kMonalKeychainName: return kMonalKeychainName; break; + case MLDefinedIdentifier_SHORT_PING: return @(SHORT_PING); break; + case MLDefinedIdentifier_LONG_PING: return @(LONG_PING); break; + case MLDefinedIdentifier_MUC_PING: return @(MUC_PING); break; + case MLDefinedIdentifier_BGFETCH_DEFAULT_INTERVAL: return @(BGFETCH_DEFAULT_INTERVAL); break; + default: + unreachable(@"unknown MLDefinedIdentifier!"); + } +} + +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier { static NSMutableDictionary* runloops = nil; @@ -558,8 +652,7 @@ +(BOOL) shouldProvideVoip #if TARGET_OS_MACCATALYST shouldProvideVoip = NO; #else - NSLocale* userLocale = [NSLocale currentLocale]; - shouldProvideVoip = !([userLocale.countryCode containsString: @"CN"] || [userLocale.countryCode containsString: @"CHN"]); + shouldProvideVoip = YES; #endif return shouldProvideVoip; } @@ -1245,6 +1338,12 @@ +(NSString*) stringFromToken:(NSData*) tokenIn return token; } +//proxy to not have full IPC class accessible from UI ++(NSString* _Nullable) exportIPCDatabase +{ + return [[IPC sharedInstance] exportDB]; +} + +(void) configureFileProtection:(NSString*) protectionLevel forFile:(NSString*) file { #if TARGET_OS_IPHONE @@ -1301,9 +1400,11 @@ +(void) configureFileProtectionFor:(NSString*) file else retval[@"host"] = [[parts objectAtIndex:0] lowercaseString]; //intended to not break code that expects lowercase - //log sanity check errors (this checks 'host' and 'user'at once because without node host==user) + //don't assert to not have a dos vector here, but still log the error if([retval[@"host"] isEqualToString:@""]) - DDLogError(@"jid '%@' has no host part!", jid); + DDLogError(@"jid has no host part: %@", jid); + //assert on sanity check errors (this checks 'host' and 'user' at once because without node host==user) + //MLAssert(![retval[@"host"] isEqualToString:@""], @"jid has no host part!", @{@"jid": jid}); //sanitize retval if([retval[@"node"] isEqualToString:@""]) @@ -1448,7 +1549,7 @@ +(void) updateSyncErrorsWithDeleteOnly:(BOOL) removeOnly andWaitForCompletion:(B //we always want to post sync errors if we are in the appex (because an incoming push means the server has //*possibly* queued some messages for us) //if we are in the main app we only want to post sync errors if we are in one of these states: - //1. we are NOT doing a full reconnect and the smacks queue contains some unacked message stanzas having a body + //1. we are NOT doing a full reconnect and the smacks queue does not contain some unacked message stanzas having a body //--> (briefly) opening the app while not having an internet connection does not generate sync errors (if no //outgoing message is pending) //2. we are doing a full reconnect --> we always want to post sync erros because we have to rejoin mucs, @@ -1666,19 +1767,12 @@ +(void) flushLogsWithTimeout:(double) timeout +(void) configureXcodeLogging { - [DDLog addLogger:[DDTTYLogger sharedInstance]]; + //only start console logger + [DDLog addLogger:[DDOSLogger sharedInstance]]; } +(void) configureLogging { - //don't log to the console (aka stderr) to not create loops with our redirected stderr -// //start console logger first (this one will *not* log own additional (and duplicated) informations like DDOSLogger would) -// #if TARGET_OS_SIMULATOR -// [DDLog addLogger:[DDTTYLogger sharedInstance]]; -// #else -// [DDLog addLogger:[DDOSLogger sharedInstance]]; -// #endif - //network logger (start as early as possible) MLUDPLogger* udpLogger = [MLUDPLogger new]; [DDLog addLogger:udpLogger]; @@ -1715,7 +1809,6 @@ +(void) configureLogging //log version info as early as possible DDLogInfo(@"Starting: %@", [self appBuildVersionInfoFor:MLVersionTypeLog]); - //[SCRAM SSDPXepOutput]; [DDLog flushLog]; DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_USER_INTERACTIVE", QOS_CLASS_USER_INTERACTIVE); @@ -1892,7 +1985,7 @@ +(NSSet*) getOwnFeatureSet @"http://jabber.org/protocol/chatstates", @"urn:xmpp:chat-markers:0", @"urn:xmpp:eme:0", - @"urn:xmpp:message-retract:0", + @"urn:xmpp:message-retract:1", @"urn:xmpp:message-correct:0", @@ -2413,6 +2506,11 @@ +(NSData*) dataWithHexString:(NSString*) hex return [NSData new]; } unsigned char* bytes = malloc([hex length] / 2); + if(bytes == NULL) + { + [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil]; + return nil; + } unsigned char* bp = bytes; for (unsigned int i = 0; i < [hex length]; i += 2) { buf[0] = (unsigned char) [hex characterAtIndex:i]; @@ -2562,4 +2660,52 @@ +(NSArray*) splitString:(NSString*) string withSeparator:(NSString*) separator a return result; } +//see https://nachtimwald.com/2017/04/02/constant-time-string-comparison-in-c/ ++(BOOL) constantTimeCompareAttackerString:(NSString* _Nonnull) str1 withKnownString:(NSString* _Nonnull) str2 +{ + if(str1 == nil || str2 == nil) + return NO; + + const char* s1 = str1.UTF8String; + const char* s2 = str2.UTF8String; + volatile int m = 0; + volatile size_t i = 0; + volatile size_t j = 0; + volatile size_t k = 0; + + while(1) + { + //this will only turn on bits in m, but never turn them off + m |= s1[i] ^ s2[j]; + + // + if(s1[i] == '\0') + break; + i++; + + //always balance increments even if s2 is shorter than s1 + if(s2[j] != '\0') + j++; + if(s2[j] == '\0') + k++; + } + + return m == 0; //check if we never turned on any bit in m +} + ++(BOOL) isIP:(NSString*) host +{ + if([[IPV4 matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) + return YES; + if([[IPV6_HEX4DECCOMPRESSED matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) + return YES; + if([[IPV6_6HEX4DEC matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) + return YES; + if([[IPV6_HEXCOMPRESSED matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) + return YES; + if([[IPV6 matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) + return YES; + return NO; +} + @end diff --git a/Monal/Classes/IPC.h b/Monal/Classes/IPC.h index 3a90644f85..a40880643d 100644 --- a/Monal/Classes/IPC.h +++ b/Monal/Classes/IPC.h @@ -24,6 +24,7 @@ typedef void (^IPC_response_handler_t)(NSDictionary*); -(void) sendMessage:(NSString*) name withData:(NSData* _Nullable) data to:(NSString*) destination withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler; -(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data; -(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler; +-(NSString* _Nullable) exportDB; -(void) respondToMessage:(NSDictionary*) message withData:(NSData* _Nullable) data; diff --git a/Monal/Classes/IPC.m b/Monal/Classes/IPC.m index 71a362aba4..99843391f7 100755 --- a/Monal/Classes/IPC.m +++ b/Monal/Classes/IPC.m @@ -99,6 +99,33 @@ -(void) respondToMessage:(NSDictionary*) message withData:(NSData* _Nullable) da [self writeIpcMessage:message[@"name"] withData:data andResponseId:message[@"id"] to:message[@"source"]]; } +-(NSString* _Nullable) exportDB +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSString* temporaryFilename = [NSString stringWithFormat:@"ipc_%@.db", [[NSProcessInfo processInfo] globallyUniqueString]]; + NSString* temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:temporaryFilename]; + + //checkpoint db before copying db file + [self.db checkpointWal]; + + //this transaction creates a new wal log and makes sure the file copy is atomic/consistent + BOOL success = [self.db boolWriteTransaction:^{ + //copy db file to temp file + NSError* error; + [fileManager copyItemAtPath:self->_dbFile toPath:temporaryFilePath error:&error]; + if(error) + { + DDLogError(@"Could not copy database to export location!"); + return NO; + } + return YES; + }]; + + if(success) + return temporaryFilePath; + return nil; +} + -(id) initWithProcessName:(NSString*) processName { self = [super init]; diff --git a/Monal/Classes/ImageViewer.swift b/Monal/Classes/ImageViewer.swift index f23d4135cf..06d7012c31 100644 --- a/Monal/Classes/ImageViewer.swift +++ b/Monal/Classes/ImageViewer.swift @@ -56,18 +56,36 @@ struct ImageViewer: View { Color.background .edgesIgnoringSafeArea(.all) - let image = UIImage(contentsOfFile:info["cacheFile"] as! String)! - - VStack { - ZoomableContainer(maxScale:8.0, doubleTapScale:4.0) { - if (info["mimeType"] as! String).hasPrefix("image/gif") { - GIFViewer(data:Binding(get: { try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data }, set: { _ in })) - .scaledToFit() + if let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { + VStack { + ZoomableContainer(maxScale:8.0, doubleTapScale:4.0) { + if (info["mimeType"] as! String).hasPrefix("image/gif") { + GIFViewer(data:Binding(get: { try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data }, set: { _ in })) + .scaledToFit() + } else { + Image(uiImage: image) + .resizable() + .scaledToFit() + } + } + } + } else { + VStack { + Spacer() + Text("Invalid image file!") + Spacer().frame(height: 24) + if #available(iOS 15, *) { + Image(systemName: "xmark.square.fill") + .resizable() + .frame(width: 128.0, height: 128.0) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) } else { - Image(uiImage: image) + Image(systemName: "xmark.square.fill") .resizable() - .scaledToFit() + .frame(width: 128.0, height: 128.0) } + Spacer() } } @@ -80,7 +98,7 @@ struct ImageViewer: View { Spacer().frame(width:20) Text(info["filename"] as! String).foregroundColor(.primary) Spacer() - if #available(iOS 16, *) { + if #available(iOS 16, *), let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { if (info["mimeType"] as! String).hasPrefix("image/gif") { ShareLink( item: GifRepresentation(getData: { diff --git a/Monal/Classes/LogViewController.h b/Monal/Classes/LogViewController.h deleted file mode 100644 index 2fd06db971..0000000000 --- a/Monal/Classes/LogViewController.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// LogViewController.h -// Monal -// -// Created by Anurodh Pokharel on 12/20/13. -// -// - -#import - -@interface LogViewController : UIViewController - -@property (nonatomic,weak) IBOutlet UITextView* logView; - --(IBAction) reconnect:(id) sender; - -@end diff --git a/Monal/Classes/LogViewController.m b/Monal/Classes/LogViewController.m deleted file mode 100644 index 338c15b699..0000000000 --- a/Monal/Classes/LogViewController.m +++ /dev/null @@ -1,164 +0,0 @@ -// -// LogViewController.m -// Monal -// -// Created by Anurodh Pokharel on 12/20/13. -// -// - -#import "LogViewController.h" -#import "MonalAppDelegate.h" -#import "HelperTools.h" -#import "DataLayer.h" -#import "MBProgressHUD.h" -#import "MLXMPPManager.h" -#import "MLUDPLogger.h" - -@interface LogViewController () -@property (weak, nonatomic) IBOutlet UITextField* logUDPHostname; -@property (weak, nonatomic) IBOutlet UITextField* logUDPPort; -@property (weak, nonatomic) IBOutlet UISwitch* logUDPSwitch; -@property (weak, nonatomic) IBOutlet UITextField* logUDPAESKey; -@property (nonatomic, strong) MBProgressHUD* hud; -@property (weak, nonatomic) IBOutlet UINavigationItem* navbar; - -@end - -@implementation LogViewController - -DDLogFileInfo* _logInfo; - -- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - if (self) { - // Custom initialization - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - // Do any additional setup after loading the view from its nib. - - self.logUDPHostname.delegate = self; - self.logUDPPort.delegate = self; - self.logUDPAESKey.delegate = self; - - UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)]; - [self.view addGestureRecognizer:tapGestureRecognizer]; -} - --(void) viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - NSArray* sortedLogFileInfos = [HelperTools.fileLogger.logFileManager sortedLogFileInfos]; - _logInfo = [sortedLogFileInfos objectAtIndex: 0]; - - self.logUDPSwitch.on = [[HelperTools defaultsDB] boolForKey:@"udpLoggerEnabled"]; - self.logUDPPort.text = [[HelperTools defaultsDB] stringForKey: @"udpLoggerPort"]; - self.logUDPHostname.text = [[HelperTools defaultsDB] stringForKey: @"udpLoggerHostname"]; - self.logUDPAESKey.text = [[HelperTools defaultsDB] stringForKey:@"udpLoggerKey"]; - - [self reloadLog]; - - [self scrollToBottom]; -} - --(void) viewWillDisappear:(BOOL)animated { - [super viewWillDisappear:animated]; - - [[HelperTools defaultsDB] setBool:self.logUDPSwitch.on forKey:@"udpLoggerEnabled"]; - [[HelperTools defaultsDB] setObject:self.logUDPHostname.text forKey:@"udpLoggerHostname"]; - [[HelperTools defaultsDB] setObject:self.logUDPPort.text forKey:@"udpLoggerPort"]; - [[HelperTools defaultsDB] setObject:self.logUDPAESKey.text forKey:@"udpLoggerKey"]; - [[HelperTools defaultsDB] synchronize]; -} - --(IBAction) sqliteExportAction:(id)sender { - NSString* dbFile = [[DataLayer sharedInstance] exportDB]; - if(dbFile == nil) - return; - UIActivityViewController* shareController = [[UIActivityViewController alloc] initWithActivityItems:@[[NSURL fileURLWithPath:dbFile]] applicationActivities:nil]; - shareController.popoverPresentationController.sourceView = self.view; - [self presentViewController:shareController animated:YES completion:^{}]; -} - --(IBAction) shareAction:(id)sender -{ - UIActivityViewController* shareController = [[UIActivityViewController alloc] initWithActivityItems:@[[NSURL fileURLWithPath:_logInfo.filePath]] applicationActivities:nil]; - shareController.popoverPresentationController.sourceView = self.view; - [self presentViewController:shareController animated:YES completion:^{}]; -} - --(void) reloadLog { - self.logView.text = NSLocalizedString(@"Only shareable for now", @""); //[NSString stringWithContentsOfFile:_logInfo.filePath encoding:NSUTF8StringEncoding error:&error]; -} - --(void) scrollToBottom { - NSRange range = NSMakeRange(self.logView.text.length - 1, 1); - [self.logView scrollRangeToVisible:range]; -} - --(void) scrollToTop { - NSRange range = NSMakeRange(0, 0); - [self.logView scrollRangeToVisible:range]; -} - -/* - * Toolbar button - */ - --(IBAction) rewindButton:(id)sender { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - [HelperTools flushLogsWithTimeout:0.100]; - $call($newHandler(self, IDontKwnowThis)); - }); -} - --(IBAction) fastForwardButton:(id)sender { - [HelperTools flushLogsWithTimeout:0.100]; - //[[NSArray arrayWithObject:@"object"] objectAtIndex:1]; - id delegate = NSClassFromString(@"MonalAppDelegate"); - SEL sel = NSSelectorFromString(@"UnknownSelectorExample"); - NSInvocation* inv = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:sel]]; - [inv setTarget:delegate]; - [inv setSelector:sel]; - [inv invoke]; -} - --(IBAction) refreshButton:(id)sender { - [HelperTools flushLogsWithTimeout:0.100]; - NSAssert(NO, @"assertion_example"); -} - --(void) didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - --(BOOL) textFieldShouldReturn:(UITextField *)textField -{ - [textField resignFirstResponder]; - return YES; -} - --(void) hideKeyboard -{ - [self.view endEditing:YES]; -} - --(IBAction) reconnect:(id) sender -{ - self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - self.hud.removeFromSuperViewOnHide = YES; - self.hud.label.text = NSLocalizedString(@"Reconnecting", @""); - self.hud.detailsLabel.text = NSLocalizedString(@"Will logout and reconnect any connected accounts.", @""); - [[MLXMPPManager sharedInstance] reconnectAll]; - [self.hud hideAnimated:YES afterDelay:3.0f]; - self.hud = nil; -} - -@end diff --git a/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.h b/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.h deleted file mode 100644 index 83170fc642..0000000000 --- a/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// MLAutoDownloadFiletransferSettingViewController.h -// Monal -// -// Created by jim on 2021/3/5. -// Copyright © 2021 Monal.im. All rights reserved. -// - -#import -#import "MLSettingCell.h" -#import "HelperTools.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface MLAutoDownloadFiletransferSettingViewController : UITableViewController - -@end - -NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.m b/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.m deleted file mode 100644 index 45729bc7d7..0000000000 --- a/Monal/Classes/MLAutoDownloadFiletransferSettingViewController.m +++ /dev/null @@ -1,180 +0,0 @@ -// -// MLAutoDownloadFiletransferSettingViewController.m -// Monal -// -// Created by jim on 2021/3/5. -// Copyright © 2021 Monal.im. All rights reserved. -// - -#import "MLAutoDownloadFiletransferSettingViewController.h" -#import "MLSwitchCell.h" - -enum MLAutoDownloadFiletransferSettingViewController { - FiletransferSettingsGeneralSettings, - FiletransferSettingsAdvancedDownloadSettings, - FiletransferSettingsAdvancedUploadSettings, - FiletransferSettingSectionCnt -}; - -@interface MLAutoDownloadFiletransferSettingViewController () -{ - UITableView* filetransferSettingTableView; - UILabel* sliderResultLabel; - UISlider* slider; -} -@end - -@implementation MLAutoDownloadFiletransferSettingViewController - -- (void)viewDidLoad { - [super viewDidLoad]; - - [self.tableView registerNib:[UINib nibWithNibName:@"MLSwitchCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"AccountCell"]; - - self.navigationItem.title = NSLocalizedString(@"Auto-Download Media", @""); -} - --(nullable NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteger) section -{ - NSString* sectionTitle = nil; - switch(section) - { - case FiletransferSettingsGeneralSettings: - sectionTitle = NSLocalizedString(@"General File Transfer Settings", @""); - break; - case FiletransferSettingsAdvancedDownloadSettings: - sectionTitle = NSLocalizedString(@"Download Settings", @""); - break; - case FiletransferSettingsAdvancedUploadSettings: - sectionTitle = NSLocalizedString(@"Upload Settings", @""); - break; - default: - break; - } - - return sectionTitle; -} - --(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return FiletransferSettingSectionCnt; -} - --(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - switch(section) - { - case FiletransferSettingsGeneralSettings: - return 1; - case FiletransferSettingsAdvancedDownloadSettings: - return 2; - case FiletransferSettingsAdvancedUploadSettings: - return 1; - default: - unreachable(); - break; - } - unreachable(); - return 0; -} - --(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - MLSwitchCell* cell = (MLSwitchCell *)[tableView dequeueReusableCellWithIdentifier:@"AccountCell"]; - [cell clear]; - - switch(indexPath.section) - { - case FiletransferSettingsGeneralSettings: - switch(indexPath.row) - { - case 0: - { - [cell initCell:NSLocalizedString(@"Auto-Download Media", @"") withToggleDefaultsKey:@"AutodownloadFiletransfers"]; - break; - } - default: - unreachable(); - } - break; - case FiletransferSettingsAdvancedDownloadSettings: - switch(indexPath.row) - { - case 0: - { - [cell initCell:NSLocalizedString(@"Load over WiFi upto", @"") withSliderDefaultsKey:@"AutodownloadFiletransfersWifiMaxSize" minValue:1.0 maxValue:100.0 withLoadFunc:^(UILabel* labelToUpdate, float sliderValue) { - // byte -> mb - float mb = sliderValue / 1024 / 1024; - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Load over WiFi upto: %.fMB", @""), mb]; - - return mb; - } withUpdateFunc:^(UILabel* labelToUpdate, float sliderValue) { - float newValue = roundf(sliderValue); - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Load over WiFi upto: %.fMB", @""), newValue]; - return newValue * 1024 * 1024; - }]; - break; - } - case 1: - { - [cell initCell:NSLocalizedString(@"Load over cellular upto", @"") withSliderDefaultsKey:@"AutodownloadFiletransfersMobileMaxSize" minValue:1.0 maxValue:100.0 withLoadFunc:^(UILabel* labelToUpdate, float sliderValue) { - // byte -> mb - float mb = sliderValue / 1024 / 1024; - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Load over cellular upto: %.fMB", @""), mb]; - - return mb; - } withUpdateFunc:^(UILabel* labelToUpdate, float sliderValue) { - float newValue = roundf(sliderValue); - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Load over cellular upto: %.fMB", @""), newValue]; - // save in MB - return newValue * 1024 * 1024; - }]; - break; - } - default: - unreachable(); - break; - } - break; - case FiletransferSettingsAdvancedUploadSettings: - switch(indexPath.row) - { - case 0: - { - [cell initCell:NSLocalizedString(@"Image Upload Quality", @"") withSliderDefaultsKey:@"ImageUploadQuality" minValue:0.33f maxValue:1.0 withLoadFunc:^(UILabel* labelToUpdate, float sliderValue) { - float rate = roundf(sliderValue * 100) / 100; - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Image Upload Quality: %.2f", @""), rate]; - return rate; - } withUpdateFunc:^(UILabel* labelToUpdate, float sliderValue) { - float rate = roundf(sliderValue * 100) / 100; - labelToUpdate.text = [NSString stringWithFormat:NSLocalizedString(@"Image Upload Quality: %.2f", @""), rate]; - return rate; - }]; - break; - } - default: - unreachable(); - break; - } - break; - default: - unreachable(); - } - return cell; -} - --(void) sliderValueChanged:(UISlider*) slider -{ - int maxFileSize = (int)slider.value; - [sliderResultLabel setText:[NSString stringWithFormat:@"%d MB", maxFileSize]]; - [sliderResultLabel adjustsFontSizeToFitWidth]; - [[HelperTools defaultsDB] setInteger:(maxFileSize * 1024 * 1024) forKey:@"AutodownloadFiletransfersMaxSize"]; -} - --(void) updateUI -{ - BOOL isAutodownloadFiletransfers = [[HelperTools defaultsDB] boolForKey:@"AutodownloadFiletransfers"]; - [slider setUserInteractionEnabled:isAutodownloadFiletransfers]; -} - -@end diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 4cbbf0b101..9abd0df732 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -64,12 +64,6 @@ typedef void (^monal_void_block_t)(void); typedef void (^monal_id_block_t)(id _Nonnull); typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error); -typedef enum NotificationPrivacySettingOption { - DisplayNameAndMessage, - DisplayOnlyName, - DisplayOnlyPlaceholder -} NotificationPrivacySettingOption; - typedef NS_ENUM(NSUInteger, MLAudioState) { MLAudioStateNormal, MLAudioStateCall, @@ -132,13 +126,13 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMonalIncomingICECandidate @"kMonalIncomingICECandidate" #define kMonalWillBeFreezed @"kMonalWillBeFreezed" #define kMonalIsFreezed @"kMonalIsFreezed" -#define kMonalNewMessageNotice @"kMLNewMessageNotice" +#define kMonalNewMessageNotice @"kMonalNewMessageNotice" #define kMonalMucSubjectChanged @"kMonalMucSubjectChanged" #define kMonalDeletedMessageNotice @"kMonalDeletedMessageNotice" #define kMonalDisplayedMessagesNotice @"kMonalDisplayedMessagesNotice" #define kMonalHistoryMessagesNotice @"kMonalHistoryMessagesNotice" #define kMLMessageSentToContact @"kMLMessageSentToContact" -#define kMonalSentMessageNotice @"kMLSentMessageNotice" +#define kMonalSentMessageNotice @"kMonalSentMessageNotice" #define kMonalMessageFiletransferUpdateNotice @"kMonalMessageFiletransferUpdateNotice" #define kMonalAccountDiscoDone @"kMonalAccountDiscoDone" diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index ed8ccaedf7..4999d65ae9 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -173,8 +173,8 @@ +(BOOL) supportsSecureCoding +(NSString*) ownDisplayNameForAccount:(xmpp*) account { NSDictionary* accountDic = [[DataLayer sharedInstance] detailsForAccount:account.accountNo]; - DDLogVerbose(@"Own nickname in accounts table %@: %@", account.accountNo, accountDic[kRosterName]); NSString* displayName = accountDic[kRosterName]; + DDLogVerbose(@"Own nickname in accounts table %@: '%@'", account.accountNo, displayName); if(!displayName || !displayName.length) { // default is local part, see https://docs.modernxmpp.org/client/design/#contexts diff --git a/Monal/Classes/MLContactCell.h b/Monal/Classes/MLContactCell.h index 52485eef77..aac4bf0cde 100644 --- a/Monal/Classes/MLContactCell.h +++ b/Monal/Classes/MLContactCell.h @@ -20,6 +20,7 @@ @property (nonatomic, weak) IBOutlet UIButton* _Nullable badge; @property (nonatomic, weak) IBOutlet UIImageView* _Nullable muteBadge; @property (nonatomic, weak) IBOutlet UIImageView* _Nullable mentionBadge; +@property (weak, nonatomic) IBOutlet UIImageView* _Nullable pinBadge; @property (nonatomic, assign) BOOL isPinned; diff --git a/Monal/Classes/MLContactCell.m b/Monal/Classes/MLContactCell.m index 40d8a164ac..3ed53907e0 100644 --- a/Monal/Classes/MLContactCell.m +++ b/Monal/Classes/MLContactCell.m @@ -116,7 +116,7 @@ -(void) showStatusText:(NSString *) text inboundDir:(BOOL) inboundDir fromUser:( { NSString* statusMessage = @""; if(inboundDir == NO) - statusMessage = [NSString stringWithFormat:@"%@ ", NSLocalizedString(@"Me", @"")]; + statusMessage = [NSString stringWithFormat:@"%@ ", NSLocalizedString(@"Me:", @"Prefix for own messages in chat overview")]; else if(inboundDir == YES && fromUser != nil && fromUser.length > 0) statusMessage = [NSString stringWithFormat:@"%@: ", fromUser]; @@ -200,11 +200,11 @@ -(void) setCount:(long)count -(void) setPinned:(BOOL) pinned { self.isPinned = pinned; - + if(pinned) { - self.backgroundColor = [UIColor colorNamed:@"activeChatsPinnedColor"]; + self.pinBadge.hidden = NO; } else { - self.backgroundColor = nil; + self.pinBadge.hidden = YES; } } diff --git a/Monal/Classes/MLContactCell.xib b/Monal/Classes/MLContactCell.xib index 62ea43074e..102a64adf7 100644 --- a/Monal/Classes/MLContactCell.xib +++ b/Monal/Classes/MLContactCell.xib @@ -1,8 +1,9 @@ - + - + + @@ -12,16 +13,16 @@ - + + + @@ -94,12 +104,12 @@ + + - - @@ -107,18 +117,20 @@ - + + - + - - + + + diff --git a/Monal/Classes/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index e1a91e63ec..fcb64a0337 100644 --- a/Monal/Classes/MLIQProcessor.m +++ b/Monal/Classes/MLIQProcessor.m @@ -26,9 +26,17 @@ @implementation MLIQProcessor +(void) processUnboundIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account { - //only handle these iqs if the remote user is on our roster + //only handle these iqs if the remote user is on our roster, + //if the are coming from our own domain, + //or if they are from a muc group, not a channel MLContact* contact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:account.accountNo]; - if(![account.connectionProperties.identity.jid isEqualToString:iqNode.fromUser] && !(contact.isSubscribedFrom && !contact.isGroup)) + if(!( + //we have to check for .isGroup because mucs always set .isSubscribedFrom to YES + (!contact.isGroup && contact.isSubscribedFrom) || + contact.isSelfChat || + [account.connectionProperties.identity.domain isEqualToString:iqNode.fromUser] || + (contact.isGroup && [@"group" isEqualToString:contact.mucType]) + )) DDLogWarn(@"Invalid sender for iq (!subscribedFrom || isGroup), ignoring: %@", iqNode); if([iqNode check:@"/"]) @@ -399,6 +407,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } NSSet* features = [NSSet setWithArray:[iqNode find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; + account.connectionProperties.accountFeatures = features; if( [iqNode check:@"{http://jabber.org/protocol/disco#info}query/identity"] && //xep-0163 support @@ -726,6 +735,33 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode userInfo:@{@"versionInfo": newSoftwareVersionInfo}]; $$ +$$class_handler(handleModerationResponse, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLMessage*, msg)) + [msg updateWithMessage:[[DataLayer sharedInstance] messageForHistoryID:msg.messageDBId]]; //make sure our msg is up to date + if([iqNode check:@"/"]) + { + DDLogError(@"Moderating message %@ returned an error: %@", msg, [iqNode findFirst:@"error"]); + [HelperTools postError:[NSString stringWithFormat:NSLocalizedString(@"Failed to moderate message in group/channel '%@'", @""), iqNode.fromUser] withNode:iqNode andAccount:account andIsSevere:YES]; + return; + } + + DDLogInfo(@"Successfully moderated message in muc: %@", msg); + [[DataLayer sharedInstance] deleteMessageHistory:msg.messageDBId]; + + //update ui + DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", msg.messageDBId); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ + @"message": msg, + @"historyId": msg.messageDBId, + @"contact": msg.contact, + }]; + + //update unread count in active chats list + [msg.contact updateUnreadCount]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ + @"contact": msg.contact, + }]; +$$ + +(void) respondWithErrorTo:(XMPPIQ*) iqNode onAccount:(xmpp*) account { XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; diff --git a/Monal/Classes/MLLogFileManager.m b/Monal/Classes/MLLogFileManager.m index 0c0439cf32..18ce9484bc 100755 --- a/Monal/Classes/MLLogFileManager.m +++ b/Monal/Classes/MLLogFileManager.m @@ -40,7 +40,7 @@ -(NSData*) dataForString:(NSString*) string originatingFromMessage:(DDLogMessage } //add 32bit length prefix - NSAssert(rawData.length < (NSUInteger)1<<30, @"LogMessage is longer than 1<<30 bytes!"); + NSAssert(rawData.length < (NSUInteger)1<<26, @"LogMessage is longer than 1<<26 bytes!"); uint32_t length = CFSwapInt32HostToBig((uint32_t)rawData.length); NSMutableData* data = [[NSMutableData alloc] initWithBytes:&length length:sizeof(length)]; [data appendData:rawData]; diff --git a/Monal/Classes/MLMessage.h b/Monal/Classes/MLMessage.h index 46d30eb6d2..2f5a26b4a0 100644 --- a/Monal/Classes/MLMessage.h +++ b/Monal/Classes/MLMessage.h @@ -15,8 +15,9 @@ NS_ASSUME_NONNULL_BEGIN /** message object intended to be passed around and eventually used to render */ -@interface MLMessage : NSObject +@interface MLMessage : NSObject ++(BOOL) supportsSecureCoding; /** account number in the database should be an integer @@ -118,6 +119,7 @@ The of the message in the DB , should be int +(MLMessage*) messageFromDictionary:(NSDictionary*) dic; -(void) updateWithMessage:(MLMessage*) msg; +@property (nonatomic, readonly) MLContact* contact; -(BOOL) isEqualToContact:(MLContact*) contact; -(BOOL) isEqualToMessage:(MLMessage*) message; diff --git a/Monal/Classes/MLMessage.m b/Monal/Classes/MLMessage.m index ccbe7169d9..444f99eefd 100644 --- a/Monal/Classes/MLMessage.m +++ b/Monal/Classes/MLMessage.m @@ -10,6 +10,9 @@ #import "MLContact.h" @implementation MLMessage +{ + MLContact* _contact; +} +(MLMessage*) messageFromDictionary:(NSDictionary*) dic { @@ -52,6 +55,73 @@ +(MLMessage*) messageFromDictionary:(NSDictionary*) dic return message; } ++(BOOL) supportsSecureCoding +{ + return YES; +} + +-(void) encodeWithCoder:(NSCoder*) coder +{ + [coder encodeObject:self.accountId forKey:@"accountId"]; + [coder encodeObject:self.buddyName forKey:@"buddyName"]; + [coder encodeBool:self.inbound forKey:@"inbound"]; + [coder encodeObject:self.actualFrom forKey:@"actualFrom"]; + [coder encodeObject:self.messageText forKey:@"messageText"]; + [coder encodeBool:self.isMuc forKey:@"isMuc"]; + [coder encodeObject:self.messageId forKey:@"messageId"]; + [coder encodeObject:self.stanzaId forKey:@"stanzaId"]; + [coder encodeObject:self.messageDBId forKey:@"messageDBId"]; + [coder encodeObject:self.timestamp forKey:@"timestamp"]; + [coder encodeObject:self.messageType forKey:@"messageType"]; + [coder encodeObject:self.mucType forKey:@"mucType"]; + [coder encodeObject:self.participantJid forKey:@"participantJid"]; + [coder encodeBool:self.hasBeenDisplayed forKey:@"hasBeenDisplayed"]; + [coder encodeBool:self.hasBeenReceived forKey:@"hasBeenReceived"]; + [coder encodeBool:self.hasBeenSent forKey:@"hasBeenSent"]; + [coder encodeBool:self.encrypted forKey:@"encrypted"]; + [coder encodeBool:self.unread forKey:@"unread"]; + [coder encodeBool:self.displayMarkerWanted forKey:@"displayMarkerWanted"]; + [coder encodeObject:self.previewText forKey:@"previewText"]; + [coder encodeObject:self.previewImage forKey:@"previewImage"]; + [coder encodeObject:self.errorType forKey:@"errorType"]; + [coder encodeObject:self.errorReason forKey:@"errorReason"]; + [coder encodeObject:self.filetransferMimeType forKey:@"filetransferMimeType"]; + [coder encodeObject:self.filetransferSize forKey:@"filetransferSize"]; + [coder encodeBool:self.retracted forKey:@"retracted"]; +} + +-(instancetype) initWithCoder:(NSCoder*) coder +{ + self = [self init]; + self.accountId = [coder decodeObjectForKey:@"accountId"]; + self.buddyName = [coder decodeObjectForKey:@"buddyName"]; + self.inbound = [coder decodeBoolForKey:@"inbound"]; + self.actualFrom = [coder decodeObjectForKey:@"actualFrom"]; + self.messageText = [coder decodeObjectForKey:@"messageText"]; + self.isMuc = [coder decodeBoolForKey:@"isMuc"]; + self.messageId = [coder decodeObjectForKey:@"messageId"]; + self.stanzaId = [coder decodeObjectForKey:@"stanzaId"]; + self.messageDBId = [coder decodeObjectForKey:@"messageDBId"]; + self.timestamp = [coder decodeObjectForKey:@"timestamp"]; + self.messageType = [coder decodeObjectForKey:@"messageType"]; + self.mucType = [coder decodeObjectForKey:@"mucType"]; + self.participantJid = [coder decodeObjectForKey:@"participantJid"]; + self.hasBeenDisplayed = [coder decodeBoolForKey:@"hasBeenDisplayed"]; + self.hasBeenReceived = [coder decodeBoolForKey:@"hasBeenReceived"]; + self.hasBeenSent = [coder decodeBoolForKey:@"hasBeenSent"]; + self.encrypted = [coder decodeBoolForKey:@"encrypted"]; + self.unread = [coder decodeBoolForKey:@"unread"]; + self.displayMarkerWanted = [coder decodeBoolForKey:@"displayMarkerWanted"]; + self.previewText = [coder decodeObjectForKey:@"previewText"]; + self.previewImage = [coder decodeObjectForKey:@"previewImage"]; + self.errorType = [coder decodeObjectForKey:@"errorType"]; + self.errorReason = [coder decodeObjectForKey:@"errorReason"]; + self.filetransferMimeType = [coder decodeObjectForKey:@"filetransferMimeType"]; + self.filetransferSize = [coder decodeObjectForKey:@"filetransferSize"]; + self.retracted = [coder decodeBoolForKey:@"retracted"]; + return self; +} + -(void) updateWithMessage:(MLMessage*) msg { self.accountId = msg.accountId; @@ -95,6 +165,13 @@ -(NSString*) contactDisplayName return [MLContact createContactFromJid:self.buddyName andAccountNo:self.accountId].contactDisplayName; } +-(MLContact*) contact +{ + if(self->_contact != nil) + return self->_contact; + return self->_contact = [MLContact createContactFromJid:self.buddyName andAccountNo:self.accountId]; +} + -(BOOL) isEqualToContact:(MLContact*) contact { return contact != nil && diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index a783006f83..9f11a9e598 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -132,7 +132,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if([messageNode check:@"{urn:xmpp:jingle-message:0}*"] && ![HelperTools shouldProvideVoip]) { - DDLogWarn(@"China locale detected, ignoring incoming JMI message!"); + DDLogWarn(@"VoIP not supported, ignoring incoming JMI message!"); return nil; } else if([messageNode check:@"{urn:xmpp:jingle-message:0}*"]) @@ -154,7 +154,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //only handle jmi stanzas exchanged with contacts allowed to see us and ignore all others //--> no presence leak and no unwanted spam calls //but: outgoing calls are still allowed even without presence subscriptions in either direction - if(!jmiContact.isSubscribedFrom) + if(![[HelperTools defaultsDB] boolForKey:@"allowCallsFromNonRosterContacts"] && !jmiContact.isSubscribedFrom) { DDLogWarn(@"Ignoring incoming JMI propose coming from a contact we are not subscribed from"); return nil; @@ -437,11 +437,19 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag #endif //handle message retraction (XEP-0424) - if([messageNode check:@"{urn:xmpp:fasten:0}apply-to/{urn:xmpp:message-retract:0}retract"]) + if([messageNode check:@"{urn:xmpp:message-retract:1}retract"]) { - NSString* originIdToRetract = [messageNode findFirst:@"{urn:xmpp:fasten:0}apply-to@id"]; - //this checks if this message is from the same jid as the message it tries to retract for (e.g. inbound can only retract inbound and outbound only outbound) - NSNumber* historyIdToRetract = [[DataLayer sharedInstance] getHistoryIDForMessageId:originIdToRetract from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + NSString* idToRetract = [messageNode findFirst:@"{urn:xmpp:message-retract:1}retract@id"]; + NSNumber* historyIdToRetract = nil; + if(possiblyUnknownContact.isGroup && [[account.mucProcessor getRoomFeaturesForMuc:possiblyUnknownContact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"] && [messageNode findFirst:@"{urn:xmpp:message-retract:1}retract/{urn:xmpp:message-moderate:1}moderated"]) + { + historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForModeratedStanzaId:idToRetract from:messageNode.fromUser andAccount:account.accountNo]; + } + else + { + //this checks for all spelled out in the business rules of XEP-0424 + historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForMessageId:idToRetract from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + } if(historyIdToRetract != nil) { @@ -452,46 +460,60 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ @"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject], @"historyId": historyIdToRetract, - @"contact": [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo], + @"contact": possiblyUnknownContact, + }]; + + //update unread count in active chats list + [possiblyUnknownContact updateUnreadCount]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ + @"contact": possiblyUnknownContact, }]; } else - DDLogWarn(@"Could not find history ID for originIdToRetract '%@' from '%@' on account %@", originIdToRetract, messageNode.fromUser, account.accountNo); + DDLogWarn(@"Could not find history ID for idToRetract '%@' from '%@' on account %@", idToRetract, messageNode.fromUser, account.accountNo); } //handle retraction tombstone in MAM (XEP-0424) - else if([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && [messageNode check:@"{urn:xmpp:message-retract:0}retracted/{urn:xmpp:sid:0}origin-id@id"]) + else if([outerMessageNode check:@"{urn:xmpp:mam:2}result"] && [messageNode check:@"{urn:xmpp:message-retract:1}retracted@id"]) { - //first add an empty message into our history db... - NSString* retractedOriginId = [messageNode findFirst:@"{urn:xmpp:message-retract:0}retracted/{urn:xmpp:sid:0}origin-id@id"]; - NSNumber* historyIdToRetract = [[DataLayer sharedInstance] - addMessageToChatBuddy:buddyName - withInboundDir:inbound - forAccount:account.accountNo - withBody:@"" - actuallyfrom:actualFrom - participantJid:participantJid - sent:YES - unread:NO - messageId:retractedOriginId - serverMessageId:stanzaid - messageType:kMessageTypeText - andOverrideDate:[messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"] - encrypted:NO - displayMarkerWanted:NO - usingHistoryId:historyIdToUse - checkForDuplicates:[messageNode check:@"{urn:xmpp:sid:0}origin-id"] || (stanzaid != nil) - ]; - - //...then retract this message (e.g. mark as retracted) - [[DataLayer sharedInstance] deleteMessageHistory:historyIdToRetract]; - - //update ui - DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); - [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ - @"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject], - @"historyId": historyIdToRetract, - @"contact": [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo], - }]; + //ignore tombstones if not supported by server (someone probably faked them) + if( + (!possiblyUnknownContact.isGroup && [account.connectionProperties.accountFeatures containsObject:@"urn:xmpp:message-retract:1#tombstone"]) || + (possiblyUnknownContact.isGroup && [[account.mucProcessor getRoomFeaturesForMuc:possiblyUnknownContact.contactJid] containsObject:@"urn:xmpp:message-retract:1#tombstone"]) + ) + { + //first add an empty message into our history db... + NSNumber* historyIdToRetract = [[DataLayer sharedInstance] + addMessageToChatBuddy:buddyName + withInboundDir:inbound + forAccount:account.accountNo + withBody:@"" + actuallyfrom:actualFrom + participantJid:participantJid + sent:YES + unread:NO + messageId:messageId + serverMessageId:stanzaid + messageType:kMessageTypeText + andOverrideDate:[messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"] + encrypted:NO + displayMarkerWanted:NO + usingHistoryId:historyIdToUse + checkForDuplicates:[messageNode check:@"{urn:xmpp:sid:0}origin-id"] || (stanzaid != nil) + ]; + + //...then retract this message (e.g. mark as retracted) + [[DataLayer sharedInstance] deleteMessageHistory:historyIdToRetract]; + + //update ui + DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ + @"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject], + @"historyId": historyIdToRetract, + @"contact": possiblyUnknownContact, + }]; + } + else + DDLogWarn(@"Got faked tombstone without server supporting them, ignoring it!"); } //ignore encrypted body messages coming from our own device id (most probably a muc reflection) else if(([messageNode check:@"body#"] || decrypted) && !sentByOwnOmemoDevice) @@ -548,7 +570,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag NSString* messageIdToReplace = [messageNode findFirst:@"{urn:xmpp:message-correct:0}replace@id"]; DDLogVerbose(@"Message id to LMC-replace: %@", messageIdToReplace); //this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound) - historyId = [[DataLayer sharedInstance] getHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; DDLogVerbose(@"History id to LMC-replace: %@", historyId); //now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion) //historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation) @@ -587,18 +609,17 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //send receive markers if requested, but DON'T do so for MLhistory messages (and don't do so for channel type mucs) if( [[HelperTools defaultsDB] boolForKey:@"SendReceivedMarkers"] && - ([messageNode check:@"{urn:xmpp:receipts}request"] || [messageNode check:@"{urn:xmpp:chat-markers:0}markable"]) && + [messageNode check:@"{urn:xmpp:receipts}request"] && ![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid] && !isMLhistory ) { - MLContact* contact = [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo]; //ignore unknown groupchats or channel-type mucs or stanzas from the groupchat itself (e.g. not from a participant having a full jid) if( //1:1 with user in our contact list that subscribed us (e.g. is allowed to see us) - (!contact.isGroup && contact.isSubscribedFrom) || + (!possiblyUnknownContact.isGroup && possiblyUnknownContact.isSubscribedFrom) || //muc group message from a user of this group - ([contact.mucType isEqualToString:@"group"] && messageNode.fromResource) + ([possiblyUnknownContact.mucType isEqualToString:@"group"] && messageNode.fromResource) ) { XMPPMessage* receiptNode = [XMPPMessage new]; @@ -607,8 +628,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag receiptNode.attributes[@"to"] = messageNode.fromUser; if([messageNode check:@"{urn:xmpp:receipts}request"]) [receiptNode setReceipt:messageId]; - if([messageNode check:@"{urn:xmpp:chat-markers:0}markable"]) - [receiptNode setChatmarkerReceipt:messageId]; [receiptNode setStoreHint]; [account send:receiptNode]; } @@ -630,9 +649,12 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //update unread count in active chats list if([unread count]) + { + [possiblyUnknownContact updateUnreadCount]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ - @"contact": [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo], + @"contact": possiblyUnknownContact, }]; + } } [[DataLayer sharedInstance] addActiveBuddies:buddyName forAccount:account.accountNo]; @@ -642,7 +664,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag @"message": message, @"historyId": historyId, @"showAlert": @(showAlert), - @"contact": [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo], + @"contact": possiblyUnknownContact, }]; //try to automatically determine content type of filetransfers @@ -654,31 +676,24 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //handle message receipts if( - ([messageNode check:@"{urn:xmpp:receipts}received@id"] || [messageNode check:@"{urn:xmpp:chat-markers:0}received@id"]) && + [messageNode check:@"{urn:xmpp:receipts}received@id"] && [messageNode.toUser isEqualToString:account.connectionProperties.identity.jid] ) { - NSString* msgId; - if([messageNode check:@"{urn:xmpp:receipts}received@id"]) - msgId = [messageNode findFirst:@"{urn:xmpp:receipts}received@id"]; - else - msgId = [messageNode findFirst:@"{urn:xmpp:chat-markers:0}received@id"]; //fallback only - if(msgId) - { - //save in DB - [[DataLayer sharedInstance] setMessageId:msgId received:YES]; - - //Post notice - [[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageReceivedNotice object:self userInfo:@{kMessageId:msgId}]; - } + NSString* msgId = [messageNode findFirst:@"{urn:xmpp:receipts}received@id"]; + + //save in DB + [[DataLayer sharedInstance] setMessageId:msgId received:YES]; + + //Post notice + [[MLNotificationQueue currentQueue] postNotificationName:kMonalMessageReceivedNotice object:self userInfo:@{kMessageId:msgId}]; } //handle chat-markers in groupchats slightly different if([messageNode check:@"{urn:xmpp:chat-markers:0}displayed@id"] && ownNick != nil) { - MLContact* groupchatContact = [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo]; //ignore unknown groupchats or channel-type mucs or stanzas from the groupchat itself (e.g. not from a participant having a full jid) - if(groupchatContact.isGroup && [groupchatContact.mucType isEqualToString:@"group"] && messageNode.fromResource) + if(possiblyUnknownContact.isGroup && [possiblyUnknownContact.mucType isEqualToString:@"group"] && messageNode.fromResource) { //incoming chat markers from own account (muc echo, muc "carbon") //WARNING: kMonalMessageDisplayedNotice goes to chatViewController, kMonalDisplayedMessagesNotice goes to MLNotificationManager and activeChatsViewController/chatViewController @@ -693,9 +708,13 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}]; //update unread count in active chats list - [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ - @"contact": groupchatContact - }]; + if([unread count]) + { + [possiblyUnknownContact updateUnreadCount]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ + @"contact": possiblyUnknownContact, + }]; + } } //incoming chat markers from participant //this will mark groupchat messages as read as soon as one of the participants sends a displayed chat-marker @@ -737,9 +756,13 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}]; //update unread count in active chats list - [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ - @"contact": [MLContact createContactFromJid:messageNode.toUser andAccountNo:account.accountNo] - }]; + if([unread count]) + { + [possiblyUnknownContact updateUnreadCount]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ + @"contact": possiblyUnknownContact, + }]; + } } } diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 404b8ddaf1..7a698f4b6c 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -563,7 +563,6 @@ -(void) handleStatusCodes:(XMPPStanza*) node //update nick in database DDLogInfo(@"Updating muc %@ nick in database to nick provided by server: '%@'...", node.fromUser, node.fromResource); [[DataLayer sharedInstance] updateOwnNickName:node.fromResource forMuc:node.fromUser forAccount:_account.accountNo]; - selfPrecenceHandled = YES; } break; } diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index 2e3dbab28f..b424b87b70 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -16,6 +16,7 @@ #import "MLFiletransfer.h" #import "MLNotificationQueue.h" #import "MLXMPPManager.h" +#import @import UserNotifications; @import CoreServices; @@ -24,7 +25,7 @@ @import UniformTypeIdentifiers; @interface MLNotificationManager () -@property (nonatomic, assign) NotificationPrivacySettingOption notificationPrivacySetting; +@property (nonatomic, readonly) NotificationPrivacySettingOption notificationPrivacySetting; @end @implementation MLNotificationManager @@ -49,11 +50,16 @@ -(id) init [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleXMPPError:) name:kXMPPError object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil]; - - self.notificationPrivacySetting = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"]; return self; } +-(NotificationPrivacySettingOption) notificationPrivacySetting +{ + NotificationPrivacySettingOption value = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"]; + DDLogVerbose(@"Current NotificationPrivacySettingOption: %d", (int)value); + return value; +} + -(void) handleContactRefresh:(NSNotification*) notification { //these will not survive process switches, but that's enough for now @@ -88,11 +94,17 @@ -(void) handleContactRefresh:(NSNotification*) notification [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[idval]]; } }]; - [removed addObject:idval]; + @synchronized(removed) { + [removed addObject:idval]; + } }; //only try to remove once - if(![removed containsObject:idval]) + BOOL isContained = NO; + @synchronized(removed) { + isContained = [removed containsObject:idval]; + } + if(!isContained) { //do this in its own thread because we don't want to block the main thread or other threads here (the removal can take ~50ms) //but DON'T do this in the appex because this can try to mess with notifications after the parse queue was frozen (see appex code for explanation what this means) @@ -101,12 +113,16 @@ -(void) handleContactRefresh:(NSNotification*) notification else dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), block); } + + //return because we don't want to display any contact request notification return; } //don't alert twice - if([displayed containsObject:idval]) - return; + @synchronized(displayed) { + if([displayed containsObject:idval]) + return; + } UNMutableNotificationContent* content = [UNMutableNotificationContent new]; content.title = xmppAccount.connectionProperties.identity.jid; @@ -121,7 +137,9 @@ -(void) handleContactRefresh:(NSNotification*) notification DDLogDebug(@"Publishing notification with id %@", idval); [self publishNotificationContent:content withID:idval]; - [displayed addObject:idval]; + @synchronized(displayed) { + [displayed addObject:idval]; + } } -(void) handleXMPPError:(NSNotification*) notification @@ -252,8 +270,6 @@ -(void) handleDisplayedMessages:(NSNotification*) notification [center removePendingNotificationRequestsWithIdentifiers:@[idval]]; [center removeDeliveredNotificationsWithIdentifiers:@[idval]]; } - //update app badge - [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateUnread object:nil]; }; //do this in its own thread because we don't want to block the main thread or other threads here (the removal can take ~50ms) @@ -263,6 +279,9 @@ -(void) handleDisplayedMessages:(NSNotification*) notification else dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), block); + //update app badge + [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateUnread object:nil]; + } -(void) handleDeletedMessage:(NSNotification*) notification @@ -346,7 +365,7 @@ -(void) playNotificationSoundForMessage:(MLMessage*) message withSound:(BOOL) so -(void) showNotificationForMessage:(MLMessage*) message withSound:(BOOL) sound andAccount:(xmpp*) account { // always use legacy notifications if we should only show a generic "New Message" notifiation without name or content - if(self.notificationPrivacySetting > DisplayOnlyName) + if(self.notificationPrivacySetting > NotificationPrivacySettingOptionDisplayOnlyName) return [self showLegacyNotificationForMessage:message withSound:sound]; // use modern communication notifications on ios >= 15.0 and legacy ones otherwise @@ -367,7 +386,7 @@ -(void) showModernNotificationForMessage:(MLMessage*) message withSound:(BOOL) s NSString* msgText = NSLocalizedString(@"Open app to see more", @""); //only show msgText if allowed - if(self.notificationPrivacySetting == DisplayNameAndMessage) + if(self.notificationPrivacySetting == NotificationPrivacySettingOptionDisplayNameAndMessage) { //XEP-0245: The slash me Command if([message.messageText hasPrefix:@"/me "]) @@ -689,7 +708,7 @@ -(void) showLegacyNotificationForMessage:(MLMessage*) message withSound:(BOOL) s NSString* idval = [self identifierWithMessage:message]; //Only show contact name if allowed - if(self.notificationPrivacySetting <= DisplayOnlyName) + if(self.notificationPrivacySetting <= NotificationPrivacySettingOptionDisplayOnlyName) { content.title = [contact contactDisplayName]; if(message.isMuc) @@ -699,7 +718,7 @@ -(void) showLegacyNotificationForMessage:(MLMessage*) message withSound:(BOOL) s content.title = NSLocalizedString(@"New Message", @""); //only show msgText if allowed - if(self.notificationPrivacySetting == DisplayNameAndMessage) + if(self.notificationPrivacySetting == NotificationPrivacySettingOptionDisplayNameAndMessage) { NSString* msgText = message.messageText; diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index d64005b3cf..d3e327dbe5 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -349,7 +349,10 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe //mark devicelist fetch as done [self.state.openDevicelistFetches removeObject:jid]; - [self handleOwnDevicelistFetchError]; + //our own devicelist fetch can't be invalidated because of a iq timeout introduced by a slow s2s connection + //--> the only reason for such an invalidation can be a disconnect/bind and in this case we don't need to do something + // because the fetch will be retriggered after the next catchup + //[self handleOwnDevicelistFetchError]; //retrigger queued key transport elements for this jid (if any) [self retriggerKeyTransportElementsForJid:jid]; diff --git a/Monal/Classes/MLPrivacySettingsViewController.h b/Monal/Classes/MLPrivacySettingsViewController.h deleted file mode 100644 index 61cf66f8b4..0000000000 --- a/Monal/Classes/MLPrivacySettingsViewController.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// MLPrivacySettingsViewController.h -// Monal -// -// Created by Friedrich Altheide on 06.08.20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import "MLSettingCell.h" -#import -#import "MLAutoDownloadFiletransferSettingViewController.h" -NS_ASSUME_NONNULL_BEGIN - -@interface MLPrivacySettingsViewController : UITableViewController -{ - UITextField* _currentField; -} - -@property (nonatomic, strong) UITableView* settingsTable; - --(IBAction)close:(id)sender ; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MLPrivacySettingsViewController.m b/Monal/Classes/MLPrivacySettingsViewController.m deleted file mode 100644 index 2be7104b14..0000000000 --- a/Monal/Classes/MLPrivacySettingsViewController.m +++ /dev/null @@ -1,360 +0,0 @@ -// -// MLPrivacySettingsViewController.m -// Monal -// -// Created by Friedrich Altheide on 06.08.20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import "MLPrivacySettingsViewController.h" -#import "HelperTools.h" -#import "MLConstants.h" -#import "MLSwitchCell.h" - -typedef NS_ENUM(NSInteger, NSNotificationPrivacyOptionRow) { - DisplayNameAndMessageRow = 1, - DisplayOnlyNameRow = 2, - DisplayOnlyPlaceholderRow = 3 -}; -const long NotificationPrivacyOptionCnt = 3; - -@interface MLPrivacySettingsViewController() - -@property (nonatomic, strong) NSArray* sectionArray; -@property (nonatomic) BOOL isNotificationPrivacyOpened; - -@end - -@implementation MLPrivacySettingsViewController - -- (void)viewDidLoad -{ - [super viewDidLoad]; - [self.tableView registerNib:[UINib nibWithNibName:@"MLSwitchCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"AccountCell"]; - - // Do any additional setup after loading the view. - self.navigationItem.title = NSLocalizedString(@"Privacy Settings", @""); - - _settingsTable = self.tableView; - _settingsTable.delegate = self; - _settingsTable.dataSource = self; - _settingsTable.backgroundView = nil; - [_settingsTable setAllowsSelection:YES]; - self.isNotificationPrivacyOpened = NO; - - self.sectionArray = @[NSLocalizedString(@"General", @"")]; -} - --(void) viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - [[HelperTools defaultsDB] setObject:@YES forKey:@"HasSeenPrivacySettings"]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; -} - -#pragma mark tableview datasource delegate - - --(NSInteger) numberOfSectionsInTableView:(UITableView*) tableView -{ - return [self.sectionArray count]; -} - --(NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteger) section -{ - return [self.sectionArray objectAtIndex:section]; -} - --(UIView*) tableView:(UITableView*) tableView viewForHeaderInSection:(NSInteger) section -{ - NSString* sectionTitle = [self tableView:tableView titleForHeaderInSection:section]; - return [HelperTools MLCustomViewHeaderWithTitle:sectionTitle]; -} - --(NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section -{ - switch (section) - { - case 0: - { -#ifdef DISABLE_OMEMO - return 13 + (self.isNotificationPrivacyOpened ? NotificationPrivacyOptionCnt : 0); -#else// DISABLE_OMEMO - return 14 + (self.isNotificationPrivacyOpened ? NotificationPrivacyOptionCnt : 0); -#endif// DISABLE_OMEMO - } - default: - { - return 0; - } - break; - } -} - --(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath -{ - - MLSwitchCell* cell = (MLSwitchCell*)[tableView dequeueReusableCellWithIdentifier:@"AccountCell"]; - [cell clear]; - - switch(indexPath.section) - { - case 0: - { - long row = indexPath.row; - // + non expanded notification options - row += (self.isNotificationPrivacyOpened || row == 0) ? 0 : NotificationPrivacyOptionCnt; - - switch(row) - { - case 0: - { - NotificationPrivacySettingOption nOptions = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"]; - [cell initCell:NSLocalizedString(@"Notification", @"") withLabel:[self getNsNotificationPrivacyOption:nOptions]]; - break; - } - //Notification options - case 1: - { - [cell initCell:[@" - " stringByAppendingString:NSLocalizedString(@"Display Name And Message", @"")] withLabel:nil]; - [self checkStatusForCell:cell atIndexPath:indexPath]; - break; - } - case 2: - { - [cell initCell:[@" - " stringByAppendingString:NSLocalizedString(@"Display Only Name", @"")] withLabel:nil]; - [self checkStatusForCell:cell atIndexPath:indexPath]; - break; - } - case 3: - { - [cell initCell:[@" - " stringByAppendingString:NSLocalizedString(@"Display Only Placeholder", @"")] withLabel:nil]; - [self checkStatusForCell:cell atIndexPath:indexPath]; - break; - } - case 4: - { - [cell initCell:NSLocalizedString(@"Show Inline Geo Location", @"") withToggleDefaultsKey:@"ShowGeoLocation"]; - break; - } - case 5: - { - [cell initCell:NSLocalizedString(@"Send Last Interaction Time", @"") withToggleDefaultsKey:@"SendLastUserInteraction"]; - break; - } - case 6: - { - [cell initCell:NSLocalizedString(@"Send Typing Notifications", @"") withToggleDefaultsKey:@"SendLastChatState"]; - break; - } - case 7: - { - [cell initCell:NSLocalizedString(@"Send message received state", @"") withToggleDefaultsKey:@"SendReceivedMarkers"]; - break; - } - case 8: - { - [cell initCell:NSLocalizedString(@"Sync Read-Markers", @"") withToggleDefaultsKey:@"SendDisplayedMarkers"]; - break; - } - case 9: - { - [cell initCell:NSLocalizedString(@"Show URL previews", @"") withToggleDefaultsKey:@"ShowURLPreview"]; - break; - } - case 10: - { - [cell initTapCell:NSLocalizedString(@"Media Upload & Download Settings", @"")]; - break; - } - case 11: - { - [cell initCell:NSLocalizedString(@"Autodelete all messages after 3 days", @"") withToggleDefaultsKey:@"AutodeleteAllMessagesAfter3Days"]; - break; - } - case 12: - { - [cell initCell:NSLocalizedString(@"Calls: Allow P2P sessions", @"") withToggleDefaultsKey:@"webrtcAllowP2P"]; - break; - } - case 13: - { - [cell initCell:NSLocalizedString(@"Calls: Allow TURN fallback to Monal-Servers", @"") withToggleDefaultsKey:@"webrtcUseFallbackTurn"]; - break; - } - case 14: - { - [cell initCell:NSLocalizedString(@"Allow approved contacts to query my Monal and iOS version", @"") withToggleDefaultsKey:@"allowVersionIQ"]; - break; - } - case 15: - { -//flow into default case for non-omemo builds -#ifndef DISABLE_OMEMO - [cell initCell:NSLocalizedString(@"Enable encryption by default for new chats", @"") withToggleDefaultsKey:@"OMEMODefaultOn"]; - break; -#endif// DISABLE_OMEMO - } - case 16: - { - [cell initCell:NSLocalizedString(@"Allow contacts not in my Contact list to contact me", @"") withToggleDefaultsKey:@"allowNonRosterContacts"]; - break; - } - default: - unreachable(); - break; - } - break; - } - default: - unreachable(); - break; - } - return cell; -} - --(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath -{ - switch (indexPath.section) - { - case 0: - { - long row = indexPath.row; - // + non expanded notification options - row += (self.isNotificationPrivacyOpened || row == 0) ? 0 : NotificationPrivacyOptionCnt; - switch(row) - { - case 0: - { - [self openNotificationPrivacyFolder]; - break; - } - case 1: - case 2: - case 3: - { - MLSwitchCell* cell = [tableView cellForRowAtIndexPath:indexPath]; - cell.accessoryType = UITableViewCellAccessoryCheckmark; - [self setNotificationPrivacyOption:indexPath]; - [self openNotificationPrivacyFolder]; - break; - } - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - break; - case 10: - { - [self performSegueWithIdentifier:@"fileTransferSettings" sender:nil]; - break; - } - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - break; - } - break; - } - default: - { - break; - } - } -} - --(IBAction)close:(id)sender -{ - [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; -} - --(void) openNotificationPrivacyFolder -{ - self.isNotificationPrivacyOpened = !self.isNotificationPrivacyOpened; - [self refershTable]; -} - --(void) refershTable -{ - [_settingsTable reloadData]; -} - --(void) checkStatusForCell:(MLSwitchCell*) cell atIndexPath:(NSIndexPath*) idxPath -{ - NotificationPrivacySettingOption privacySettionOption = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"]; - // default: remove checkmark - cell.accessoryType = UITableViewCellAccessoryNone; - - switch (idxPath.row) { - case DisplayNameAndMessageRow: - if(privacySettionOption == DisplayNameAndMessage) - { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } - break; - case DisplayOnlyNameRow: - if(privacySettionOption == DisplayOnlyName) - { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } - break; - case DisplayOnlyPlaceholderRow: - if(privacySettionOption == DisplayOnlyPlaceholder) - { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } - break; - default: - break; - } -} - --(NSString*)getNsNotificationPrivacyOption:(NotificationPrivacySettingOption) option -{ - NSString* optionStr = @""; - switch (option) - { - case DisplayNameAndMessage: - optionStr = NSLocalizedString(@"Display Name And Message", @""); - break; - case DisplayOnlyName: - optionStr = NSLocalizedString(@"Display Only Name", @""); - break; - case DisplayOnlyPlaceholder: - optionStr = NSLocalizedString(@"Display Only Placeholder", @""); - break; - default: - break; - } - - return optionStr; -} - --(void) setNotificationPrivacyOption:(NSIndexPath*) idxPath -{ - switch (idxPath.row) { - case DisplayNameAndMessageRow: - [[HelperTools defaultsDB] setInteger:DisplayNameAndMessage forKey:@"NotificationPrivacySetting"]; - break; - case DisplayOnlyNameRow: - [[HelperTools defaultsDB] setInteger:DisplayOnlyName forKey:@"NotificationPrivacySetting"]; - break; - case DisplayOnlyPlaceholderRow: - [[HelperTools defaultsDB] setInteger:DisplayOnlyPlaceholder forKey:@"NotificationPrivacySetting"]; - break; - - default: - break; - } - [[HelperTools defaultsDB] synchronize]; -} -@end diff --git a/Monal/Classes/MLPubSub.m b/Monal/Classes/MLPubSub.m index a280833ea8..75b6120384 100644 --- a/Monal/Classes/MLPubSub.m +++ b/Monal/Classes/MLPubSub.m @@ -95,7 +95,7 @@ -(void) fetchNode:(NSString*) node from:(NSString*) jid withItemsList:(NSArray* DDLogInfo(@"Fetching node '%@' at jid '%@' using callback %@...", node, jid, handler); xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedFetchNodeHandler, handleFetchInvalidation, $ID(node), $ID(jid), $ID(itemsList), $HANDLER(handler))]; @@ -143,7 +143,7 @@ -(void) subscribeToNode:(NSString*) node onJid:(NSString*) jid withHandler:(MLHa DDLogInfo(@"Subscribing to node '%@' at jid '%@' using callback %@...", node, jid, handler); xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedSubscribeToNodeHandler, handleSubscribeInvalidation, $ID(node), $ID(jid), $HANDLER(handler))]; @@ -226,7 +226,7 @@ -(void) configureNode:(NSString*) node withConfigOptions:(NSDictionary*) configO { xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedConfigureNodeHandler, handleConfigFormResultInvalidation, $ID(node), $ID(configOptions), $HANDLER(handler))]; @@ -303,7 +303,7 @@ -(void) retractItemWithId:(NSString*) itemId onNode:(NSString*) node andHandler: { xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedRetractItemWithIdHandler, handleRetractResultInvalidation, $ID(itemId), $ID(node), $HANDLER(handler))]; @@ -340,7 +340,7 @@ -(void) purgeNode:(NSString*) node andHandler:(MLHandler* _Nullable) handler { xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedPurgeNodeNodeHandler, handlePurgeOrDeleteResultInvalidation, $ID(node), $HANDLER(handler))]; @@ -374,7 +374,7 @@ -(void) deleteNode:(NSString*) node andHandler:(MLHandler* _Nullable) handler { xmpp* account = _account; - if(!account.connectionProperties.accountDiscoDone) + if(account.accountState < kStateBound || !account.connectionProperties.accountDiscoDone) { DDLogWarn(@"Queueing pubsub call until account disco is resolved..."); [_queue addObject:$newHandlerWithInvalidation(self, queuedDeleteNodeHandler, handlePurgeOrDeleteResultInvalidation, $ID(node), $HANDLER(handler))]; @@ -600,13 +600,14 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig return retval; } -$$instance_handler(handleSubscribeInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $$ID(NSString*, jid), $$HANDLER(handler)) +$$instance_handler(handleSubscribeInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $$ID(NSString*, jid), $$HANDLER(handler)) //invalidate our user handler $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(jid), + $ID(reason), ); $$ @@ -652,13 +653,14 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig } $$ -$$instance_handler(handleUnsubscribeInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $$ID(NSString*, jid), $_HANDLER(handler)) +$$instance_handler(handleUnsubscribeInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $$ID(NSString*, jid), $_HANDLER(handler)) //invalidate our user handler $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), - $ID(jid) + $ID(jid), + $ID(reason), ); $$ @@ -704,13 +706,14 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig } $$ -$$instance_handler(handleFetchInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $$ID(NSString*, jid), $$HANDLER(handler)) +$$instance_handler(handleFetchInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $$ID(NSString*, jid), $$HANDLER(handler)) //invalidate user handler $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), - $ID(jid) + $ID(jid), + $ID(reason), ); $$ @@ -777,9 +780,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig [self callHandlersForNode:node andJid:jid withType:@"publish" andData:data]; $$ -$$instance_handler(handleConfigFormResultInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $_HANDLER(handler)) +$$instance_handler(handleConfigFormResultInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ $$instance_handler(handleConfigFormResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) @@ -844,9 +847,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig )]; $$ -$$instance_handler(handleConfigureResultInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $_HANDLER(handler)) +$$instance_handler(handleConfigureResultInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ $$instance_handler(handleConfigureResult, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $$ID(XMPPIQ*, iqNode), $_HANDLER(handler)) @@ -862,9 +865,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $$ //this is a user handler for configureNode: called from handlePublishResult -$$instance_handler(handlePublishAgainInvalidation, account.pubsub, $$ID(xmpp*, account), $$BOOL(success), $$ID(NSString*, node), $_HANDLER(handler)) +$$instance_handler(handlePublishAgainInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$BOOL(success), $$ID(NSString*, node), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success), $ID(node), $ID(reason)); $$ //this is a user handler for configureNode: called from handlePublishResult @@ -881,9 +884,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $$ //this is a user handler for internalPublishItem: called from handlePublishResult -$$instance_handler(handleConfigureAfterPublishInvalidation, account.pubsub, $$ID(xmpp*, account), $$BOOL(success), $$ID(NSString*, node), $_HANDLER(handler)) +$$instance_handler(handleConfigureAfterPublishInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$BOOL(success), $$ID(NSString*, node), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success), $ID(node), $ID(reason)); $$ //this is a user handler for internalPublishItem: called from handlePublishResult @@ -899,9 +902,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig [self configureNode:node withConfigOptions:configOptions andHandler:handler]; $$ -$$instance_handler(handlePublishResultInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) +$$instance_handler(handlePublishResultInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ $$instance_handler(handlePublishResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) @@ -916,7 +919,7 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig NSMutableDictionary* publishPreconditions = [NSMutableDictionary new]; if(configOptions[@"pubsub#persist_items"]) publishPreconditions[@"pubsub#persist_items"] = configOptions[@"pubsub#persist_items"]; - if(configOptions[@"pubsub#persist_items"]) + if(configOptions[@"pubsub#access_model"]) publishPreconditions[@"pubsub#access_model"] = configOptions[@"pubsub#access_model"]; [self internalPublishItem:item onNode:node withConfigOptions:publishPreconditions andHandler:$newHandlerWithInvalidation(self, handleConfigureAfterPublish, handleConfigureAfterPublishInvalidation, @@ -949,9 +952,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $call(handler, $ID(account), $BOOL(success, YES), $ID(node)); $$ -$$instance_handler(handleRetractResultInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $$ID(NSString*, itemId), $_HANDLER(handler)) +$$instance_handler(handleRetractResultInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $$ID(NSString*, itemId), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(itemId)); + $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(itemId), $ID(reason)); $$ $$instance_handler(handleRetractResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, node), $$ID(NSString*, itemId), $_HANDLER(handler)) @@ -964,9 +967,9 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $call(handler, $ID(account), $BOOL(success, YES), $ID(node), $ID(itemId)); $$ -$$instance_handler(handlePurgeOrDeleteResultInvalidation, account.pubsub, $$ID(xmpp*, account), $$ID(NSString*, node), $_HANDLER(handler)) +$$instance_handler(handlePurgeOrDeleteResultInvalidation, account.pubsub, $$ID(xmpp*, account), $_ID(NSString*, reason), $$ID(NSString*, node), $_HANDLER(handler)) //invalidate user handler - $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node)); + $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ $$instance_handler(handlePurgeOrDeleteResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, node), $_HANDLER(handler)) diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index 3cbd5e7091..d06615ecde 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -32,6 +32,35 @@ -(NSString*) calculateNickForMuc:(NSString*) room; @implementation MLPubSubProcessor +$$class_handler(mdsHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) + DDLogDebug(@"Got new mds displayed status from '%@'", jid); + if(![jid isEqualToString:account.connectionProperties.identity.jid]) + { + DDLogWarn(@"Ignoring mds update not coming from our own jid"); + return; + } + + if([type isEqualToString:@"publish"]) + [account updateMdsData:data]; +$$ + +$$class_handler(handleMdsFetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) + if(!success) + { + //item-not-found means: no mds items in storage --> use an empty data dict + if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) + data = @{}; + else + { + DDLogWarn(@"Could not fetch mds from pep, doing nothing!"); + return; + } + } + + //call +notify handler to process our data dictionary containing all mds items + [account updateMdsData:data]; +$$ + $$class_handler(avatarHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) DDLogDebug(@"Got new avatar metadata from '%@'", jid); if([type isEqualToString:@"publish"]) diff --git a/Monal/Classes/MLResizingTextView.m b/Monal/Classes/MLResizingTextView.m index a5869fd3dd..8b0a2288bf 100644 --- a/Monal/Classes/MLResizingTextView.m +++ b/Monal/Classes/MLResizingTextView.m @@ -17,6 +17,7 @@ - (void) layoutSubviews if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) { [self invalidateIntrinsicContentSize]; } + [self becomeFirstResponder]; } - (CGSize)intrinsicContentSize diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index 7ad6b9800d..e022cb5047 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -321,9 +321,12 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) } case kSettingSectionApp: { switch(indexPath.row) { - case PrivacySettingsRow: - [self performSegueWithIdentifier:@"showPrivacySettings" sender:self]; + + case PrivacySettingsRow: { + UIViewController* privacyViewController = [[SwiftuiInterface new] makeViewWithName:@"PrivacySettings"]; + [self showDetailViewController:privacyViewController sender:self]; break; + } case NotificationsRow: { UIViewController* notificationSettingsController = [[SwiftuiInterface new] makeViewWithName:@"NotificationSettings"]; [self showDetailViewController:notificationSettingsController sender:self]; @@ -372,9 +375,11 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) #ifdef DEBUG case LogRow: #endif - case SettingsAboutRowsCntORLogRow: - [self performSegueWithIdentifier:@"showLogs" sender:self]; + case SettingsAboutRowsCntORLogRow:{ + UIViewController* logView = [[SwiftuiInterface new] makeViewWithName:@"logView"]; + [self showDetailViewController:logView sender:self]; break; + } case VersionRow: { #ifndef DEBUG if(_tappedVersionInfo >= 16) diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index e65af8e0d6..549fceab4e 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -427,6 +427,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host nw_parameters_configure_protocol_block_t tcp_options = ^(nw_protocol_options_t tcp_options) { nw_tcp_options_set_enable_fast_open(tcp_options, YES); //enable tcp fast open //nw_tcp_options_set_no_delay(tcp_options, YES); //disable nagle's algorithm + //nw_tcp_options_set_connection_timeout(tcp_options, 4); }; nw_parameters_configure_protocol_block_t configure_tls_block = ^(nw_protocol_options_t tls_options) { sec_protocol_options_t options = nw_tls_copy_sec_protocol_options(tls_options); @@ -549,27 +550,34 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host return; } } + //always handle errors regardless of current state (cert errors etc.) + if(error != nil) + { + DDLogVerbose(@"%@: %@ got error in state %du and reporting: %@", logtag, self, state, error); + NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error)); + @synchronized(shared_state) { + shared_state.error = st_error; + } + [input generateEvent:NSStreamEventErrorOccurred]; + [output generateEvent:NSStreamEventErrorOccurred]; + } + if(state == nw_connection_state_waiting) { //do nothing here, documentation says the connection will be automatically retried "when conditions are favourable" //which seems to mean: if the network path changed (for example connectivity regained) //if this happens inside the connection timeout all is ok //if not, the connection will be cancelled already and everything will be ok, too - DDLogVerbose(@"%@: got nw_connection_state_waiting and ignoring it, see comments in code...", logtag); + DDLogVerbose(@"%@: got nw_connection_state_waiting and ignoring it, see comments in code: %@ (%@)", logtag, self, error); } else if(state == nw_connection_state_failed) { - DDLogError(@"%@: Connection failed", logtag); - NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error)); - @synchronized(shared_state) { - shared_state.error = st_error; - } - [input generateEvent:NSStreamEventErrorOccurred]; - [output generateEvent:NSStreamEventErrorOccurred]; + //errors already reported by generic handling above + DDLogError(@"%@: Connection failed (error already reported): %@", logtag, error); } else if(state == nw_connection_state_ready) { - DDLogInfo(@"%@: Connection established, wasOpenOnce: %@", bool2str(wasOpenOnce), logtag); + DDLogInfo(@"%@: Connection established, wasOpenOnce: %@", logtag, bool2str(wasOpenOnce)); if(!wasOpenOnce) { wasOpenOnce = YES; diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index a3bcb37fb8..8d66b90af8 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -363,7 +363,29 @@ -(void) initWebRTCForPendingCall:(MLCall*) call NSMutableDictionary* serviceWithCredentials = [NSMutableDictionary dictionaryWithDictionary:service]; [serviceWithCredentials addEntriesFromDictionary:(NSDictionary*)data]; DDLogDebug(@"Got new external service credentials: %@", serviceWithCredentials); - [iceServers addObject:[[RTCIceServer alloc] initWithURLStrings:@[[NSString stringWithFormat:@"%@:%@:%@", serviceWithCredentials[@"type"], serviceWithCredentials[@"host"], serviceWithCredentials[@"port"]]] username:serviceWithCredentials[@"username"] credential:serviceWithCredentials[@"password"]]]; + //transport is only defined for turn/turns, but not for stun/stuns from M110 onwards + NSString* transport = @""; + if([@[@"turn", @"turns"] containsObject:serviceWithCredentials[@"type"]]) + { + if(serviceWithCredentials[@"username"] == nil || serviceWithCredentials[@"password"] == nil) + { + DDLogWarn(@"Invalid turn/turns credentials: missing username or password, ignoring!"); + return; + } + if(serviceWithCredentials[@"transport"] != nil) + transport = [NSString stringWithFormat:@"?transport=%@", serviceWithCredentials[@"transport"]]; + } + [iceServers addObject:[[RTCIceServer alloc] + initWithURLStrings:@[[NSString stringWithFormat:@"%@:%@:%@%@", + serviceWithCredentials[@"type"], + [HelperTools isIP:serviceWithCredentials[@"host"]] ? [NSString stringWithFormat:@"[%@]", serviceWithCredentials[@"host"]] : serviceWithCredentials[@"host"], + serviceWithCredentials[@"port"], + transport + ]] + username:serviceWithCredentials[@"username"] + credential:serviceWithCredentials[@"password"] + tlsCertPolicy:RTCTlsCertPolicyInsecureNoCheck + ]]; //proceed only if all ice servers have been processed if([iceServers count] == [call.account.connectionProperties.discoveredStunTurnServers count]) diff --git a/Monal/Classes/MLXMLNode.m b/Monal/Classes/MLXMLNode.m index ed98c55f6b..3df5af0288 100644 --- a/Monal/Classes/MLXMLNode.m +++ b/Monal/Classes/MLXMLNode.m @@ -732,7 +732,8 @@ -(NSMutableDictionary*) parseQueryEntry:(NSString* _Nonnull) entry arguments:(va //instead of starting with a fresh copy (which would always extract only the first variadic argument //regardless of the position in the format string we are at). char* dest = NULL; - rpl_vasprintf(&dest, [attributeFilterValue UTF8String], args); + if(rpl_vasprintf(&dest, [attributeFilterValue UTF8String], args) == -1) + [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc in MLXMLNode's usage of rpl_vasprintf" arguments:nil]; MLAssert(dest != NULL, @"dest should *never* be NULL!"); NSString* unescapedAttributeFilterValue = [NSString stringWithUTF8String:dest]; free(dest); diff --git a/Monal/Classes/MLXMPPConnection.h b/Monal/Classes/MLXMPPConnection.h index 464f0dfd76..b9b1878fdc 100644 --- a/Monal/Classes/MLXMPPConnection.h +++ b/Monal/Classes/MLXMPPConnection.h @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN //server details @property (nonatomic, strong) NSSet* serverFeatures; +@property (nonatomic, strong) NSSet* accountFeatures; @property (nonatomic, strong) NSMutableArray* discoveredServices; @property (nonatomic, strong) NSMutableArray* discoveredStunTurnServers; @@ -48,8 +49,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL supportsMam2; @property (nonatomic, assign) BOOL supportsSM3; @property (nonatomic, assign) BOOL supportsPush; -@property (nonatomic, assign) BOOL supportsBookmarksCompat; @property (nonatomic, assign) BOOL pushEnabled; +@property (nonatomic, assign) BOOL supportsBookmarksCompat; @property (nonatomic, assign) BOOL usingCarbons2; @property (nonatomic, assign) BOOL supportsRosterVersion; @property (nonatomic, assign) BOOL supportsRosterPreApproval; diff --git a/Monal/Classes/MLXMPPIdentity.m b/Monal/Classes/MLXMPPIdentity.m index 3cb42cac5f..471515902d 100644 --- a/Monal/Classes/MLXMPPIdentity.m +++ b/Monal/Classes/MLXMPPIdentity.m @@ -22,16 +22,13 @@ @implementation MLXMPPIdentity -(id) initWithJid:(NSString*) jid password:(NSString*) password andResource:(NSString*) resource { self = [super init]; - self.jid = jid; + NSDictionary* parts = [HelperTools splitJid:jid]; + self.jid = parts[@"user"]; self.resource = resource; - _fullJid = resource ? [NSString stringWithFormat:@"%@/%@", jid, resource] : jid; + _fullJid = resource ? [NSString stringWithFormat:@"%@/%@", self.jid, self.resource] : jid; self.password = password; - - NSArray* elements = [self.jid componentsSeparatedByString:@"@"]; - self.user = elements[0]; - if(elements.count > 1) - self.domain = elements[1]; - + self.user = parts[@"node"]; + self.domain = parts[@"host"]; return self; } @@ -42,11 +39,26 @@ -(void) updatPassword:(NSString*) newPassword -(void) bindJid:(NSString*) jid { - _fullJid = jid; NSDictionary* parts = [HelperTools splitJid:jid]; - self.jid = parts[@"user"]; - self.resource = parts[@"resource"]; + + //we don't allow this because several parts in monal rely on stable bare jids not changing after login/bind + MLAssert([self.jid isEqualToString:parts[@"user"]], @"trying to bind to different bare jid!", (@{ + @"bind_to_jid": jid, + @"current_bare_jid": self.jid + })); + + //don't set new full jid if we don't have a resource + if(parts[@"resource"] != nil) + { + //these won't change because of the MLAssert above, but we keep this + //to make sure user and domain match the jid once the assertion gets removed + self.jid = parts[@"user"]; + self.user = parts[@"node"]; + self.domain = parts[@"host"]; + + self.resource = parts[@"resource"]; + _fullJid = [NSString stringWithFormat:@"%@/%@", self.jid, self.resource]; + } } - @end diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 440c02d9db..5b4a347d0c 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -16,6 +16,7 @@ #import "MLNotificationQueue.h" #import "MLNotificationManager.h" #import "MLOMEMO.h" +#import @import Network; @import MobileCoreServices; @@ -52,7 +53,7 @@ -(void) defaultSettings [[HelperTools defaultsDB] setBool:YES forKey:@"ShowURLPreview"]; // Message Settings / Privacy - [[HelperTools defaultsDB] setInteger:DisplayNameAndMessage forKey:@"NotificationPrivacySetting"]; + [[HelperTools defaultsDB] setInteger:NotificationPrivacySettingOptionDisplayNameAndMessage forKey:@"NotificationPrivacySetting"]; // udp logger [[HelperTools defaultsDB] setBool:NO forKey:@"udpLoggerEnabled"]; @@ -95,21 +96,18 @@ -(void) defaultSettings [self upgradeObjectUserSettingsIfUnset:@"udpLoggerKey" toDefault:@""]; // upgrade Message Settings / Privacy - [self upgradeIntegerUserSettingsIfUnset:@"NotificationPrivacySetting" toDefault:DisplayNameAndMessage]; - + [self upgradeIntegerUserSettingsIfUnset:@"NotificationPrivacySetting" toDefault:NotificationPrivacySettingOptionDisplayNameAndMessage]; + // upgrade filetransfer settings [self upgradeBoolUserSettingsIfUnset:@"AutodownloadFiletransfers" toDefault:YES]; -#ifdef IS_ALPHA - [self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersMaxSize" toDefault:16*1024*1024]; // 16 MiB -#else - [self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersMaxSize" toDefault:5*1024*1024]; // 5 MiB -#endif //upgrade syncErrorsDisplayed list [self upgradeObjectUserSettingsIfUnset:@"syncErrorsDisplayed" toDefault:@{}]; - [self upgradeFloatUserSettingsIfUnset:@"AutodownloadFiletransfersMobileMaxSize" toDefault:5*1024*1024]; // 5 MiB - [self upgradeFloatUserSettingsIfUnset:@"AutodownloadFiletransfersWifiMaxSize" toDefault:32*1024*1024]; // 32 MiB + [self upgradeFloatUserSettingsToInteger:@"AutodownloadFiletransfersMobileMaxSize"]; + [self upgradeFloatUserSettingsToInteger:@"AutodownloadFiletransfersWifiMaxSize"]; + [self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersMobileMaxSize" toDefault:5*1024*1024]; // 5 MiB + [self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersWifiMaxSize" toDefault:32*1024*1024]; // 32 MiB // upgrade default image quality [self upgradeFloatUserSettingsIfUnset:@"ImageUploadQuality" toDefault:0.75]; @@ -142,6 +140,16 @@ -(void) defaultSettings //anti spam/privacy setting, but default to yes (current behavior, conversations behavior etc.) [self upgradeBoolUserSettingsIfUnset:@"allowNonRosterContacts" toDefault:YES]; + [self upgradeBoolUserSettingsIfUnset:@"allowCallsFromNonRosterContacts" toDefault:YES]; +} + +-(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName +{ + if([[HelperTools defaultsDB] objectForKey:settingsName] == nil) + return; + NSInteger value = (NSInteger)[[HelperTools defaultsDB] floatForKey:settingsName]; + [[HelperTools defaultsDB] setInteger:value forKey:settingsName]; + [[HelperTools defaultsDB] synchronize]; } -(void) upgradeBoolUserSettingsIfUnset:(NSString*) settingsName toDefault:(BOOL) defaultVal @@ -533,7 +541,7 @@ -(void) disconnectAccount:(NSNumber*) accountNo withExplicitLogout:(BOOL) explic if(account) { DDLogVerbose(@"got account and cleaning up.. "); - [account disconnect:YES]; + [account disconnect:explicitLogout]; account = nil; DDLogVerbose(@"done cleaning up account "); } @@ -662,6 +670,7 @@ -(void) sendChatState:(BOOL) isTyping toContact:(MLContact*) contact #pragma mark - login/register +//this will NOT set plain_activated to YES, only using the advanced account creation ui can do this -(NSNumber*) login:(NSString*) jid password:(NSString*) password { //if it is a JID diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 052efb1d53..472e10a80a 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -221,7 +221,7 @@ struct MemberList: View { if self.group.mucType != "group" { return AnyView( affiliationButton(selectedMember, affiliation: "outcast", label: { - Text("Block grom group") + Text("Block from group") }) ) } else { diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 51d1a6bf2a..e07a8c237f 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -348,15 +348,16 @@ -(void) application:(UIApplication*) application didFailToRegisterForRemoteNotif -(void) updateUnread { + DDLogInfo(@"Updating unread called"); //make sure unread badge matches application badge NSNumber* unreadMsgCnt = [[DataLayer sharedInstance] countUnreadMessages]; - dispatch_async(dispatch_get_main_queue(), ^{ + [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSInteger unread = 0; if(unreadMsgCnt != nil) unread = [unreadMsgCnt integerValue]; DDLogInfo(@"Updating unread badge to: %ld", (long)unread); [UIApplication sharedApplication].applicationIconBadgeNumber = unread; - }); + }]; } #pragma mark - app life cycle @@ -814,7 +815,7 @@ -(void) handleXMPPURL:(NSURL*) url //add given jid to our roster if in roster mode (e.g. the jid is not the jid we just registered as like in register mode) if(account != nil && isRoster) //silence memory warning despite assertion above - [self handleXMPPURL:url]; + return [self handleXMPPURL:url]; }]; } //I know this if is moot, but I wanted to preserve the different cases: @@ -950,10 +951,12 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center didReceiveNoti NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:fromContact.contactJid andAccount:fromContact.accountId tillStanzaId:messageId wasOutgoing:NO]; DDLogDebug(@"Marked as read: %@", unread); - //send displayed marker for last unread message (XEP-0333) - //but only for 1:1 or group-type mucs,not for channe-type mucs (privacy etc.) + //send displayed marker for last unread message *marked as wanting chat markers* (XEP-0333) +// for(MLMessage* msg in unread) +// ; //TODO: implement this!! + MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage && (!fromContact.isGroup || [@"group" isEqualToString:fromContact.mucType])) + if(lastUnreadMessage) { DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); [account sendDisplayMarkerForMessage:lastUnreadMessage]; @@ -1132,6 +1135,7 @@ -(void) prepareForFreeze:(NSNotification*) notification { for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) [account freeze]; + [MLProcessLock unlock]; _wasFreezed = YES; @synchronized(self) { DDLogVerbose(@"Setting _shutdownPending to NO..."); diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift new file mode 100644 index 0000000000..a7fb6754b2 --- /dev/null +++ b/Monal/Classes/PrivacySettings.swift @@ -0,0 +1,258 @@ +// +// PrivacySettings.swift +// Monal +// +// Created by Vaidik Dubey on 22/03/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + + +func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { + switch option{ + case .DisplayNameAndMessage: + return NSLocalizedString("Display Name And Message", comment: "") + case .DisplayOnlyName: + return NSLocalizedString("Display Only Name", comment: "") + case .DisplayOnlyPlaceholder: + return NSLocalizedString("Display Only Placeholder", comment: "") + } +} + +class PrivacyDefaultDB: ObservableObject { + @defaultsDB("NotificationPrivacySetting") + var notificationPrivacySetting: Int + + @defaultsDB("OMEMODefaultOn") + var omemoDefaultOn:Bool + + @defaultsDB("AutodeleteAllMessagesAfter3Days") + var autodeleteAllMessagesAfter3Days: Bool + + @defaultsDB("SendLastUserInteraction") + var sendLastUserInteraction: Bool + + @defaultsDB("SendLastChatState") + var sendLastChatState: Bool + + @defaultsDB("SendReceivedMarkers") + var sendReceivedMarkers: Bool + + @defaultsDB("SendDisplayedMarkers") + var sendDisplayedMarkers: Bool + + @defaultsDB("ShowGeoLocation") + var showGeoLocation: Bool + + @defaultsDB("ShowURLPreview") + var showURLPreview: Bool + + @defaultsDB("webrtcAllowP2P") + var webrtcAllowP2P: Bool + + @defaultsDB("webrtcUseFallbackTurn") + var webrtcUseFallbackTurn: Bool + + @defaultsDB("allowVersionIQ") + var allowVersionIQ: Bool + + @defaultsDB("allowNonRosterContacts") + var allowNonRosterContacts: Bool + + @defaultsDB("allowCallsFromNonRosterContacts") + var allowCallsFromNonRosterContacts: Bool + + @defaultsDB("HasSeenPrivacySettings") + var hasSeenPrivacySettings: Bool + + @defaultsDB("AutodownloadFiletransfers") + var autodownloadFiletransfers : Bool + + @defaultsDB("AutodownloadFiletransfersWifiMaxSize") + var autodownloadFiletransfersWifiMaxSize : UInt + + @defaultsDB("AutodownloadFiletransfersMobileMaxSize") + var autodownloadFiletransfersMobileMaxSize : UInt + + @defaultsDB("ImageUploadQuality") + var imageUploadQuality : Float +} + + +struct PrivacySettings: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Section(header:Text("Privacy and security settings")) { + NavigationLink(destination: PrivacyScreen()) { + HStack{ + Image(systemName: "lock.shield") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Privacy & Security") + } + } + NavigationLink(destination: PublishingScreen()) { + HStack{ + Image(systemName: "eye") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Publishing") + } + } + NavigationLink(destination: PreviewsScreen()) { + HStack{ + Image(systemName: "doc.text.magnifyingglass") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Previews") + } + } + NavigationLink(destination: CommunicationScreen()) { + HStack{ + Image(systemName: "bubble.left.and.bubble.right") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Communication") + } + } + + NavigationLink(destination: MLAutoDownloadFiletransferSettingView()) { + HStack{ + Image(systemName: "square.and.arrow.down") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Media Upload & Download") + } + } + } + } + .navigationBarTitle("Privacy Settings") + .onAppear { + privacyDefaultDB.hasSeenPrivacySettings = true + } + } +} + +struct PrivacyScreen: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Picker("Notification privacy", selection: $privacyDefaultDB.notificationPrivacySetting) { + ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in + Text(getNotificationPrivacyOption(option)).tag(option.rawValue) + } + } + Toggle("Enable encryption by default for new chats", isOn: $privacyDefaultDB.omemoDefaultOn) + Toggle("Autodelete all messages after 3 days", isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) + } + .navigationBarTitle("Privacy & security", displayMode: .inline) + } +} + +struct PublishingScreen: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Toggle("Send last interaction time", isOn: $privacyDefaultDB.sendLastUserInteraction) + Toggle("Send typing notifications", isOn: $privacyDefaultDB.sendLastChatState) + Toggle("Send message received state", isOn: $privacyDefaultDB.sendReceivedMarkers) + Toggle("Send message displayed state", isOn: $privacyDefaultDB.sendDisplayedMarkers) + } + .navigationBarTitle("Publishing", displayMode: .inline) + } +} + +struct PreviewsScreen: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Toggle("Show inline geo location", isOn: $privacyDefaultDB.showGeoLocation) + Toggle("Show URL previews", isOn: $privacyDefaultDB.showURLPreview) + } + .navigationBarTitle("Previews", displayMode: .inline) + } +} + +struct CommunicationScreen: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Toggle("Allow contacts not in my contact list to contact me", isOn: $privacyDefaultDB.allowNonRosterContacts) + Toggle("Allow approved contacts to query my Monal and iOS version", isOn: $privacyDefaultDB.allowVersionIQ) + Toggle("Calls: Allow contacts not in my contact list to call me", isOn: $privacyDefaultDB.allowCallsFromNonRosterContacts) + Toggle("Calls: Allow P2P sessions", isOn: $privacyDefaultDB.webrtcAllowP2P) + Toggle("Calls: Allow TURN fallback to Monal-Servers", isOn: $privacyDefaultDB.webrtcUseFallbackTurn) + } + .navigationBarTitle("Communication", displayMode: .inline) + } +} + +struct MLAutoDownloadFiletransferSettingView: View { + @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() + + var body: some View { + Form { + Section(header: Text("General File Transfer Settings")) { + Toggle("Auto-Download Media", isOn: $privacyDefaultDB.autodownloadFiletransfers) + } + + Section(header: Text("Download Settings")) { + + Text("Adjust the maximum file size for auto-downloads over WiFi") + .foregroundColor(.secondary) + .font(.footnote) + Slider(value: $privacyDefaultDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024), + in: 1.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: {Text("Load over wifi")} + ) + Text("Load over WiFi upto : \(UInt(privacyDefaultDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") + } + + Text("Adjust the maximum file size for auto-downloads over cellular network") + .foregroundColor(.secondary) + .font(.footnote) + Slider(value: $privacyDefaultDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), + in: 0.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: {Text("Load over Cellular")} + ) + Text("Load over cellular upto : \(Int(privacyDefaultDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") + + Section(header: Text("Upload Settings")) { + Text("Adjust the quality of images uploaded") + .foregroundColor(.secondary) + .font(.footnote) + Slider(value: $privacyDefaultDB.imageUploadQuality, + in: 0.33...1.0, + step: 0.01, + minimumValueLabel: Text("33%"), + maximumValueLabel: Text("100%"), + label: {Text("Upload Settings") + }) + Text("Image Upload Quality : \(String(format: "%.0f%%", privacyDefaultDB.imageUploadQuality*100))") + } + } + } +} + + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + PrivacySettings() + } +} diff --git a/Monal/Classes/SCRAM.m b/Monal/Classes/SCRAM.m index 0ce10f4bf2..e83eeb80a4 100644 --- a/Monal/Classes/SCRAM.m +++ b/Monal/Classes/SCRAM.m @@ -112,7 +112,7 @@ -(MLScramStatus) parseServerFirstMessage:(NSString*) str _ssdpSupported = YES; //calculate base64 encoded SSDP hash and compare it to server sent value NSString* ssdpHash =[HelperTools encodeBase64WithData:[self hash:[_ssdpString dataUsingEncoding:NSUTF8StringEncoding]]]; - if(![ssdpHash isEqualToString:msg[@"d"]]) + if(![HelperTools constantTimeCompareAttackerString:msg[@"d"] withKnownString:ssdpHash]) return MLScramStatusSSDPTriggered; } if(_iterationCount < 4096) @@ -164,7 +164,7 @@ -(MLScramStatus) parseServerFinalMessage:(NSString*) str MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); NSDictionary* msg = [self parseScramString:str]; //wrong v-value - if(![_expectedServerSignature isEqualToString:msg[@"v"]]) + if(![HelperTools constantTimeCompareAttackerString:msg[@"v"] withKnownString:_expectedServerSignature]) return MLScramStatusWrongServerProof; //server sent a SCRAM error if(msg[@"e"] != nil) @@ -241,8 +241,8 @@ -(NSDictionary* _Nullable) parseScramString:(NSString*) str -(NSString*) quote:(NSString*) str { //TODO: use proper saslprep to allow for non-ascii chars - str = [str stringByReplacingOccurrencesOfString:@"," withString:@"=2C"]; str = [str stringByReplacingOccurrencesOfString:@"=" withString:@"=3D"]; + str = [str stringByReplacingOccurrencesOfString:@"," withString:@"=2C"]; return str; } diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index e002a1919a..ffd0887d55 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -12,6 +12,18 @@ @_exported import Logging import CocoaLumberjackSwiftLogBackend import LibMonalRustSwiftBridge +import Combine + +//import some defines in MLConstants.h into swift +let kAppGroup = HelperTools.getObjcDefinedValue(.kAppGroup) +let kMonalOpenURL = HelperTools.getObjcDefinedValue(.kMonalOpenURL) +let kBackgroundProcessingTask = HelperTools.getObjcDefinedValue(.kBackgroundProcessingTask) +let kBackgroundRefreshingTask = HelperTools.getObjcDefinedValue(.kBackgroundRefreshingTask) +let kMonalKeychainName = HelperTools.getObjcDefinedValue(.kMonalKeychainName) +let SHORT_PING = HelperTools.getObjcDefinedValue(.SHORT_PING) +let LONG_PING = HelperTools.getObjcDefinedValue(.LONG_PING) +let MUC_PING = HelperTools.getObjcDefinedValue(.MUC_PING) +let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_INTERVAL) public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; @@ -45,6 +57,12 @@ public func nilExtractor(_ value: Any?) -> Any? { } } +@objc public enum NotificationPrivacySettingOption: Int, CaseIterable, RawRepresentable { + case DisplayNameAndMessage + case DisplayOnlyName + case DisplayOnlyPlaceholder +} + class KVOObserver: NSObject { var obj: NSObject var keyPath: String @@ -107,7 +125,14 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, public subscript(member: String) -> T { get { - return self.getWrapper(for:member) as! T + if let value = self.getWrapper(for:member) as? T { + return value + } else { + HelperTools.throwException(withName:"ObservableKVOWrapperCastingError", reason:"Could not cast member '\(String(describing:member))' to expected type \(String(describing:T.self))", userInfo:[ + "key": "\(String(describing:member))", + "type": "\(String(describing:T.self))", + ]) + } } set { self.setWrapper(for:member, value:newValue as AnyObject?) @@ -116,7 +141,14 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, public subscript(dynamicMember member: String) -> T { get { - return self.getWrapper(for:member) as! T + if let value = self.getWrapper(for:member) as? T { + return value + } else { + HelperTools.throwException(withName:"ObservableKVOWrapperCastingError", reason:"Could not cast dynamicMember '\(String(describing:member))' to expected type \(String(describing:T.self))", userInfo:[ + "key": "\(String(describing:member))", + "type": "\(String(describing:T.self))", + ]) + } } set { self.setWrapper(for:member, value:newValue as AnyObject?) @@ -140,6 +172,60 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, } } +struct RuntimeError: LocalizedError { + let description: String + + init(_ description: String) { + self.description = description + } + + var errorDescription: String? { + description + } +} + +//see https://www.avanderlee.com/swift/property-wrappers/ +//and https://fatbobman.com/en/posts/adding-published-ability-to-custom-property-wrapper-types/ +@propertyWrapper +public struct defaultsDB { + private let key: String + private var container: UserDefaults = HelperTools.defaultsDB() + + public init(_ key: String) { + self.key = key + } + + public var wrappedValue: Value { + get { + if let value = container.object(forKey: key) as? Value { + return value + } else { + HelperTools.throwException(withName:"DefaultsDBCastingError", reason:"Could not cast deaultsDB entry '\(String(describing:key))' to expected type \(String(describing: Value.self))", userInfo:[ + "key": "\(String(describing:key))", + "type": "\(String(describing: Value.self))", + ]) + } + } + set { container.set(newValue, forKey: key) } + } + + public static subscript( + _enclosingInstance observed: OuterSelf, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { observed[keyPath: storageKeyPath].wrappedValue } + set { + if let subject = observed.objectWillChange as? ObservableObjectPublisher { + subject.send() // Before modifying wrappedValue + observed[keyPath: storageKeyPath].wrappedValue = newValue + } else { + observed[keyPath: storageKeyPath].wrappedValue = newValue + } + } + } +} + @objcMembers public class SwiftHelpers: NSObject { public static func initSwiftHelpers() { diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 4f25c63345..3e60317783 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -36,6 +36,27 @@ public extension Color { #endif } +extension Binding { + func optionalMappedToBool() -> Binding where Value == Wrapped? { + Binding( + get: { self.wrappedValue != nil }, + set: { newValue in + MLAssert(!newValue, "New value should never be true when writing to a binding created by optionalMappedToBool()") + self.wrappedValue = nil + } + ) + } +} + +extension Binding { + func bytecount(mappedTo: Double) -> Binding where Value == UInt { + Binding( + get: { Double(self.wrappedValue) / mappedTo }, + set: { newValue in self.wrappedValue = UInt(newValue * mappedTo) } + ) + } +} + class SheetDismisserProtocol: ObservableObject { weak var host: UIHostingController? = nil func dismiss() { @@ -157,6 +178,20 @@ extension DocumentPickerViewController: UIDocumentPickerDelegate { } } +struct ActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? = nil + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { + + } +} + // clear button for text fields, see https://stackoverflow.com/a/58896723/3528174 struct ClearButton: ViewModifier { let isEditing: Bool @@ -448,6 +483,8 @@ class SwiftuiInterface : NSObject { switch(name) { // TODO names are currently taken from the segue identifier, an enum would be nice once everything is ported to SwiftUI case "NotificationSettings": host.rootView = AnyView(UIKitWorkaround(NotificationSettings(delegate:delegate))) + case "logView": + host.rootView = AnyView(UIKitWorkaround(DebugView())) case "WelcomeLogIn": host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate))) case "LogIn": @@ -458,6 +495,10 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) case "ChatPlaceholder": host.rootView = AnyView(ChatPlaceholder()) + case "PrivacySettings" : + host.rootView = AnyView(UIKitWorkaround(PrivacySettings())) + case "ActiveChatsPrivacySettings": + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) default: unreachable() } diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index f0847cd9eb..308a4d86aa 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -60,6 +60,7 @@ SettingsServerRow, SettingsPortRow, SettingsDirectTLSRow, + SettingsPlainActivatedRow, SettingsResourceRow, SettingsAdvancedRowsCnt }; @@ -111,7 +112,12 @@ @interface XMPPEdit() @property (nonatomic) BOOL avatarChanged; @property (nonatomic) BOOL rosterNameChanged; @property (nonatomic) BOOL statusMessageChanged; +@property (nonatomic) BOOL detailsChanged; + @property (nonatomic) BOOL sasl2Supported; +@property (nonatomic) BOOL plainActivated; + +@property (nonatomic) BOOL deactivateSave; @end @implementation XMPPEdit @@ -125,6 +131,7 @@ -(void) hideKeyboard -(void) viewDidLoad { + self.deactivateSave = NO; [super viewDidLoad]; [self.tableView registerNib:[UINib nibWithNibName:@"MLSwitchCell" @@ -179,14 +186,9 @@ -(void) viewDidLoad //edit DDLogVerbose(@"reading account number %@", self.accountNo); NSDictionary* settings = [_db detailsForAccount:self.accountNo]; - if(!settings) - { - //present another UI here. - return; - } + MLAssert(settings != nil, @"Settings dict should never be nil here!"); self.jid = [NSString stringWithFormat:@"%@@%@", [settings objectForKey:@"username"], [settings objectForKey:@"domain"]]; - self.sectionDictionary[@(kSettingSectionAccount)] = [NSString stringWithFormat:NSLocalizedString(@"Account (%@)", @""), self.jid]; NSString* pass = [SAMKeychain passwordForService:kMonalKeychainName account:self.accountNo.stringValue]; if(pass) @@ -206,6 +208,8 @@ -(void) viewDidLoad self.sasl2Supported = [[settings objectForKey:kSupportsSasl2] boolValue]; + self.plainActivated = [[settings objectForKey:kPlainActivated] boolValue]; + //overwrite account section heading in edit mode self.sectionDictionary[@(kSettingSectionAccount)] = [NSString stringWithFormat:NSLocalizedString(@"Account (%@)", @""), self.jid]; } @@ -219,6 +223,8 @@ -(void) viewDidLoad self.statusMessage = @""; self.enabled = YES; self.sasl2Supported = NO; + self.plainActivated = NO; + //overwrite account section heading in new mode self.sectionDictionary[@(kSettingSectionAccount)] = NSLocalizedString(@"Account (new)", @""); } @@ -229,13 +235,13 @@ -(void) viewDidLoad #endif } -- (void) viewWillAppear:(BOOL) animated +-(void) viewWillAppear:(BOOL) animated { [super viewWillAppear:animated]; DDLogVerbose(@"xmpp edit view will appear"); } -- (void) viewWillDisappear:(BOOL) animated +-(void) viewWillDisappear:(BOOL) animated { [super viewWillDisappear:animated]; DDLogVerbose(@"xmpp edit view will hide"); @@ -261,6 +267,12 @@ -(void) alertWithTitle:(NSString*) title andMsg:(NSString*) msg -(IBAction) save:(id) sender { + if(self.deactivateSave) + { + DDLogWarn(@"Save pressed but already deactivated!"); + return; + } + NSError* error; [self.currentTextField resignFirstResponder]; @@ -297,7 +309,7 @@ -(IBAction) save:(id) sender } } - NSArray* elements = [self.jid componentsSeparatedByString:@"@"]; + NSArray* elements = [lowerJid componentsSeparatedByString:@"@"]; //if it is a JID if([elements count] > 1) @@ -307,10 +319,10 @@ -(IBAction) save:(id) sender } else { - user = self.jid; + user = lowerJid; domain = @""; } - if([domain isEqualToString:@""] && !self.server) + if([domain isEqualToString:@""]) { [self alertWithTitle:NSLocalizedString(@"Domain missing", @"") andMsg:NSLocalizedString(@"Your entered XMPP ID is missing the domain", @"")]; return; @@ -332,8 +344,11 @@ -(IBAction) save:(id) sender [dic setObject:[self.rosterName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:kRosterName]; if(self.statusMessage) [dic setObject:[self.statusMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:@"statusMessage"]; + [dic setObject:[NSNumber numberWithBool:self.sasl2Supported] forKey:kSupportsSasl2]; - + + [dic setObject:[NSNumber numberWithBool:self.plainActivated] forKey:kPlainActivated]; + if(!self.editMode) { @@ -397,45 +412,32 @@ -(IBAction) save:(id) sender DDLogError(@"Could not delete all SiriKit interactions: %@", error); }]; } + //this case makes sure we recreate a completely new account instance below (using our new settings) if the account details changed + else if(self.detailsChanged) + [[MLXMPPManager sharedInstance] disconnectAccount:self.accountNo withExplicitLogout:NO]; + DDLogVerbose(@"Now updating DB with account dict..."); - BOOL updatedAccount = [[DataLayer sharedInstance] updateAccounWithDictionary:dic]; - if(updatedAccount) + [[DataLayer sharedInstance] updateAccounWithDictionary:dic]; + if(self.password.length) { - DDLogVerbose(@"DB update succeeded: %@", self.accountNo); - if(self.password.length) - { - DDLogVerbose(@"Now setting password for account %@ in SAMKeychain...", self.accountNo); - [[MLXMPPManager sharedInstance] updatePassword:self.password forAccount:self.accountNo]; - } - if(self.enabled) - { - DDLogVerbose(@"Account is still enabled, updating in-memory structures to reflect new settings: %@", self.accountNo); - [[MLXMPPManager sharedInstance] connectAccount:self.accountNo]; - xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountNo]; - //it is okay to only update the server settings here: - //1) if the account was not yet connected, the settings from our db (which got updated with our dict prior - // to connecting) will be used upon connecting - //2) if the account is already connected, the settings will be updated (account.connectionProperties.identity and account.connectionProperties.server) - // and used when connecting next time (still using the old smacks session of course) - account.connectionProperties.identity = [[MLXMPPIdentity alloc] initWithJid:[NSString stringWithFormat:@"%@@%@", [dic objectForKey:kUsername], [dic objectForKey:kDomain]] password:self.password andResource:[dic objectForKey:kResource]]; - MLXMPPServer* oldServer = account.connectionProperties.server; - account.connectionProperties.server = [[MLXMPPServer alloc] initWithHost:(dic[kServer] == nil ? @"" : dic[kServer]) andPort:(dic[kPort] == nil ? @"5222" : dic[kPort]) andDirectTLS:[[dic objectForKey:kDirectTLS] boolValue]]; - [account.connectionProperties.server updateConnectServer:[oldServer connectServer]]; - [account.connectionProperties.server updateConnectPort:[oldServer connectPort]]; - [account.connectionProperties.server updateConnectTLS:[oldServer isDirectTLS]]; - if(self.statusMessageChanged) - [account publishStatusMessage:self.statusMessage]; - if(self.rosterNameChanged) - [account publishRosterName:self.rosterName]; - if(self.avatarChanged) - [account publishAvatar:self.selectedAvatarImage]; - } - //trigger view updates to make sure enabled/disabled account state propagates to all ui elements - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - [self showSuccessHUD]; + DDLogVerbose(@"Now setting password for account %@ in SAMKeychain...", self.accountNo); + [[MLXMPPManager sharedInstance] updatePassword:self.password forAccount:self.accountNo]; } - else - DDLogError(@"DB update failed!"); + if(self.enabled) + { + DDLogVerbose(@"Account is (still) enabled, connecting it: %@", self.accountNo); + [[MLXMPPManager sharedInstance] connectAccount:self.accountNo]; + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountNo]; + if(self.statusMessageChanged) + [account publishStatusMessage:self.statusMessage]; + if(self.rosterNameChanged) + [account publishRosterName:self.rosterName]; + if(self.avatarChanged) + [account publishAvatar:self.selectedAvatarImage]; + } + //trigger view updates to make sure enabled/disabled account state propagates to all ui elements + [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; + [self showSuccessHUD]; } } @@ -465,6 +467,7 @@ - (IBAction) removeAccountClicked: (id) sender }]; UIAlertAction* yesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { DDLogVerbose(@"Removing accountNo %@", self.accountNo); + self.deactivateSave = YES; [[MLXMPPManager sharedInstance] removeAccountForAccountNo:self.accountNo]; MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; @@ -512,6 +515,7 @@ -(IBAction) deleteAccountClicked:(id) sender }]; UIAlertAction* yesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { DDLogVerbose(@"Deleting account on server: %@", xmppAccount); + self.deactivateSave = YES; [xmppAccount removeFromServerWithCompletion:^(NSString* error) { dispatch_async(dispatch_get_main_queue(), ^{ if(error != nil) @@ -697,6 +701,12 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS [thecell initCell:NSLocalizedString(@"Always use direct TLS, not STARTTLS", @"") withToggle:self.directTLS andTag:2]; break; } + case SettingsPlainActivatedRow: { + [thecell initCell:NSLocalizedString(@"Allow MITM-prone PLAIN authentication", @"") withToggle:self.plainActivated andTag:3]; + if(self.editMode) + [thecell.toggleSwitch setEnabled:NO]; + break; + } case SettingsResourceRow: { [thecell initCell:NSLocalizedString(@"Resource", @"") withLabel:self.resource]; break; @@ -940,18 +950,22 @@ -(void) textFieldDidEndEditing:(UITextField*) textField } case 2: { self.jid = textField.text; + self.detailsChanged = YES; break; } case 3: { self.password = textField.text; + self.detailsChanged = YES; break; } case 4: { self.server = textField.text; + self.detailsChanged = YES; break; } case 5: { self.port = textField.text; + self.detailsChanged = YES; break; } case 6: { @@ -977,22 +991,25 @@ -(void) toggleSwitch:(id) sender switch (toggle.tag) { case 1: { - if(toggle.on) - { - self.enabled = YES; - } - else { - self.enabled = NO; - } + self.enabled = toggle.on; break; } case 2: { - if(toggle.on) + self.directTLS = toggle.on; + self.detailsChanged = YES; + break; + } + case 3: { + self.plainActivated = toggle.on; + self.detailsChanged = YES; + if(self.plainActivated) { - self.directTLS = YES; - } - else { - self.directTLS = NO; + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Warning", @"") + message:NSLocalizedString(@"If you turn this on, you will no longer be safe from man-in-the-middle attacks. Such attacks enable the adversary to manipulate your incoming and outgoing messages, add their own OMEMO keys, change your account details and even know or change your password!\n\nYou should rather switch to another server than turning this on.", @"") preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Understood", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { + [alert dismissViewControllerAnimated:YES completion:nil]; + }]]; + [self presentViewController:alert animated:YES completion:nil]; } break; } diff --git a/Monal/Classes/XMPPMessage.h b/Monal/Classes/XMPPMessage.h index 007555629d..7691e168f1 100644 --- a/Monal/Classes/XMPPMessage.h +++ b/Monal/Classes/XMPPMessage.h @@ -41,6 +41,7 @@ FOUNDATION_EXPORT NSString* const kMessageHeadlineType; -(void) setReceipt:(NSString*) messageId; -(void) setChatmarkerReceipt:(NSString*) messageId; -(void) setDisplayed:(NSString*) messageId; +-(void) setMDSDisplayed:(NSString*) stanzaId withStanzaIdBy:(NSString*) by; /** Hint saying the message should be stored diff --git a/Monal/Classes/XMPPMessage.m b/Monal/Classes/XMPPMessage.m index d93fb7ed8a..eee696fc36 100644 --- a/Monal/Classes/XMPPMessage.m +++ b/Monal/Classes/XMPPMessage.m @@ -118,6 +118,18 @@ -(void) setDisplayed:(NSString*) messageId [self addChildNode:[[MLXMLNode alloc] initWithElement:@"displayed" andNamespace:@"urn:xmpp:chat-markers:0" withAttributes:@{@"id":messageId} andChildren:@[] andData:nil]]; } +-(void) setMDSDisplayed:(NSString*) stanzaId withStanzaIdBy:(NSString*) by +{ + [self addChildNode: + [[MLXMLNode alloc] initWithElement:@"displayed" andNamespace:@"urn:xmpp:mds:displayed:0" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"stanza-id" andNamespace:@"urn:xmpp:sid:0" withAttributes:@{ + @"by": by, + @"id": stanzaId, + } andChildren:@[] andData:nil] + ] andData:nil] + ]; +} + -(void) setStoreHint { [self addChildNode:[[MLXMLNode alloc] initWithElement:@"store" andNamespace:@"urn:xmpp:hints"]]; diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 8fb9211f6f..1ed71c276e 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -813,19 +813,29 @@ -(void) viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; #ifndef DISABLE_OMEMO - if(self.xmppAccount) { + if(self.xmppAccount) + { BOOL omemoDeviceForContactFound = [self.xmppAccount.omemo knownDevicesForAddressName:self.contact.contactJid].count > 0; - if(!omemoDeviceForContactFound) { - if(self.contact.isEncrypted && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo] && self.contact.isGroup && ![self.contact.mucType isEqualToString:@"group"]) + if(!omemoDeviceForContactFound && self.contact.isEncrypted && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo]) + { + if(!self.contact.isGroup && [[HelperTools splitJid:self.contact.contactJid][@"host"] isEqualToString:@"cheogram.com"]) + { + // cheogram.com does not support OMEMO encryption as it is a PSTN gateway + // --> disable it + self.contact.isEncrypted = NO; + [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; + } + else if(self.contact.isGroup && ![self.contact.mucType isEqualToString:@"group"]) { - // a group that does not support OMEMO has encryption enabled - // disable it + // a channel type muc has OMEMO encryption enabled, but channels don't support encryption + // --> disable it self.contact.isEncrypted = NO; [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; } - else if(self.contact.isEncrypted && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo] && (!self.contact.isGroup || (self.contact.isGroup && ![self.contact.mucType isEqualToString:@"group"]))) + else if(!self.contact.isGroup || (self.contact.isGroup && [self.contact.mucType isEqualToString:@"group"])) { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No OMEMO keys found", @"") message:NSLocalizedString(@"This contact may not support OMEMO encrypted messages. Please try again in a few seconds.", @"") preferredStyle:UIAlertControllerStyleAlert]; + // a 1:1 contact or a group type muc has OMEMO encryption enabled + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No OMEMO keys found", @"") message:NSLocalizedString(@"This contact may not support OMEMO encrypted messages. Please try to enable encryption again in a few seconds, if you think this is wrong.", @"") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Disable Encryption", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { // Disable encryption self.contact.isEncrypted = NO; @@ -833,9 +843,6 @@ -(void) viewDidAppear:(BOOL)animated [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; [alert dismissViewControllerAnimated:YES completion:nil]; }]]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Ignore", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; [self presentViewController:alert animated:YES completion:nil]; } @@ -968,10 +975,9 @@ -(void) refreshCounter //get list of unread messages NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:self.contact.contactJid andAccount:self.contact.accountId tillStanzaId:nil wasOutgoing:NO]; - //send displayed marker for last unread message (XEP-0333) - //but only for 1:1 or group-type mucs,not for channe-type mucs (privacy etc.) + //publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333) MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage && (!self.contact.isGroup || [@"group" isEqualToString:self.contact.mucType])) + if(lastUnreadMessage) { DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); [self.xmppAccount sendDisplayMarkerForMessage:lastUnreadMessage]; @@ -2483,17 +2489,26 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe quoteAction.image = [[[UIImage systemImageNamed:@"quote.bubble.fill"] imageWithHorizontallyFlippedOrientation] imageWithTintColor:UIColor.whiteColor renderingMode:UIImageRenderingModeAutomatic]; UIContextualAction* retractAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:NSLocalizedString(@"Retract", @"Chat msg action") handler:^(UIContextualAction* action, UIView* sourceView, void (^completionHandler)(BOOL actionPerformed)) { - [self.xmppAccount retractMessage:message.messageId toContact:self.contact]; - [[DataLayer sharedInstance] deleteMessageHistory:message.messageDBId]; - [message updateWithMessage:[[[DataLayer sharedInstance] messagesForHistoryIDs:@[message.messageDBId]] firstObject]]; + //only delete directly if we sent that message, try to moderate otherwise + if(!message.inbound) + { + [self.xmppAccount retractMessage:message]; + [[DataLayer sharedInstance] deleteMessageHistory:message.messageDBId]; + [message updateWithMessage:[[[DataLayer sharedInstance] messagesForHistoryIDs:@[message.messageDBId]] firstObject]]; - //update table entry - [self->_messageTable beginUpdates]; - [self->_messageTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; - [self->_messageTable endUpdates]; - - //update active chats if necessary - [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self.xmppAccount userInfo:@{@"contact": self.contact}]; + //update table entry + [self->_messageTable beginUpdates]; + [self->_messageTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [self->_messageTable endUpdates]; + + //update active chats if necessary + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self.xmppAccount userInfo:@{@"contact": self.contact}]; + } + else + { + //hardcode reason for now (change this when rewriting chatui using swiftui) + [self.xmppAccount moderateMessage:message withReason:@"This message contains inappropriate content for this forum."]; + } return completionHandler(YES); }]; @@ -2510,7 +2525,7 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe //update active chats if necessary [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self.xmppAccount userInfo:@{@"contact": self.contact}]; - + return completionHandler(YES); }]; localDeleteAction.backgroundColor = UIColor.systemYellowColor; @@ -2538,8 +2553,8 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe LMCEditAction, retractAction, ]]; - //only allow retraction for outgoing messages - else if(!message.inbound) + //only allow retraction for outgoing messages or if we are the moderator of that muc + else if(!message.inbound || (self.contact.isGroup && [[[DataLayer sharedInstance] getOwnRoleInGroupOrChannel:self.contact] isEqualToString:@"moderator"] && [[self.xmppAccount.mucProcessor getRoomFeaturesForMuc:self.contact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"])) return [UISwipeActionsConfiguration configurationWithActions:@[ quoteAction, copyAction, diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index 158380c27e..a34be781da 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -122,7 +122,8 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); /** send a message to a contact with xmpp id */ --(void) retractMessage:(NSString*) messageId toContact:(MLContact*) contact; +-(void) retractMessage:(MLMessage*) msg; +-(void) moderateMessage:(MLMessage*) msg withReason:(NSString*) reason; -(void) sendMessage:(NSString*) message toContact:(MLContact*) contact isEncrypted:(BOOL) encrypt isUpload:(BOOL) isUpload andMessageId:(NSString*) messageId; -(void) sendMessage:(NSString*) message toContact:(MLContact*) contact isEncrypted:(BOOL) encrypt isUpload:(BOOL) isUpload andMessageId:(NSString*) messageId withLMCId:(NSString* _Nullable) LMCId; -(void) sendChatState:(BOOL) isTyping toContact:(nonnull MLContact*) contact; @@ -246,6 +247,8 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); -(void) markCapsQueryCompleteFor:(NSString*) ver; +-(void) updateMdsData:(NSDictionary*) mdsData; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 59c7ae6f46..55d4e3ca86 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -49,7 +49,7 @@ @import AVFoundation; @import WebRTC; -#define STATE_VERSION 12 +#define STATE_VERSION 15 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -116,6 +116,7 @@ @interface xmpp() NSString* _internalID; NSString* _logtag; NSMutableDictionary* _inCatchup; + NSMutableDictionary* _mdsData; //registration related stuff BOOL _registration; @@ -218,6 +219,9 @@ -(id) initWithServer:(nonnull MLXMPPServer*) server andIdentity:(nonnull MLXMPPI //we now support the modern bookmarks protocol (XEP-0402) [self.pubsub registerForNode:@"urn:xmpp:bookmarks:1" withHandler:$newHandler(MLPubSubProcessor, bookmarks2Handler)]; + //we support mds + [self.pubsub registerForNode:@"urn:xmpp:mds:displayed:0" withHandler:$newHandler(MLPubSubProcessor, mdsHandler)]; + //autodelete messages old enough (first invocation) if([[HelperTools defaultsDB] boolForKey:@"AutodeleteAllMessagesAfter3Days"]) [[DataLayer sharedInstance] autodeleteAllMessagesAfter3Days]; @@ -262,6 +266,7 @@ -(void) setupObjects _runningCapsQueries = [NSMutableSet new]; _runningMamQueries = [NSMutableDictionary new]; _inCatchup = [NSMutableDictionary new]; + _mdsData = [NSMutableDictionary new]; _pipeliningState = kPipelinedNothing; _cachedStreamFeaturesBeforeAuth = nil; _cachedStreamFeaturesAfterAuth = nil; @@ -717,12 +722,21 @@ -(BOOL) parseQueueFrozen -(void) freezeParseQueue { + //don't do this in a block on the parse queue because the parse queue could potentially have a significant amount of blocks waiting + //to be synchronously dispatched to the receive queue and processed and we don't want to wait for all these stanzas to be processed + //and rather freeze the parse queue as soon as possible + _parseQueue.suspended = YES; + + //apparently setting _parseQueue.suspended = YES does return before the queue is actually suspended + //--> busy wait for _parseQueue.suspended == YES + [HelperTools busyWaitForOperationQueue:_parseQueue]; + //this has to be synchronous because we want to be sure no further stanzas are leaking from the parse queue //into the receive queue once we leave this method - _parseQueue.suspended = YES; + //--> wait for all blocks put into the receive queue by the parse queue right before it was frozen [self dispatchOnReceiveQueue: ^{ MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES (in receive queue)!"); - DDLogWarn(@"Parse queue is frozen now!"); + DDLogInfo(@"Parse queue is frozen now!"); }]; MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); } @@ -732,7 +746,7 @@ -(void) unfreezeParseQueue //this has to be synchronous because we want to be sure the parse queue is operating again once we leave this method [self dispatchOnReceiveQueue: ^{ self->_parseQueue.suspended = NO; - DDLogWarn(@"Parse queue is UNfrozen now!"); + DDLogInfo(@"Parse queue is UNfrozen now!"); }]; } @@ -748,6 +762,7 @@ -(void) freezeSendQueue [self->_sendQueue addOperations: @[[NSBlockOperation blockOperationWithBlock:^{ self->_sendQueue.suspended = YES; }]] waitUntilFinished:YES]; //block until finished because we are closing the socket directly afterwards + [HelperTools busyWaitForOperationQueue:_sendQueue]; } -(void) unfreezeSendQueue @@ -771,27 +786,39 @@ -(void) freeze //this does not have to be synchronized with the freezing of the parse queue and receive queue [self freezeSendQueue]; - //freezing the parse queue will sync dispatch to the receive queue, let's do a sync dispatch here - //to synchronize the parse queue freezing with the receive queue freezing + //don't merge the sync dispatch to freeze the receive queue with the sync dispatch done by freezeParseQueue + //merging those might leave some tasks in the receive queue that got added to it after the parse queue freeze + //was signalled but before it actually completed the freeze + //statement 1: + //this is not okay because leaked stanzas while frozen could be processed twice if the complete app gets frozen afterwards, + //then these stanzas get processed by the appex and afterwards the complete app and subsequently the receive queue gets unfrozen again + //statement 2: + //stanzas still in the parse queue when unfreezing the account will be dropped because self.accountState < kStateConnected + //will instruct the block inside prepareXMPPParser to drop any stanzas still queued in the parse queue + //and having even self.accountState < kStateReconnecting will make a call to [self connect] mandatory, + //which will cancel all operations still queued on the parse queue + //statement 3: + //normally a complete app freeze will only occur after calling [MLXMPPManager disconnectAll] and subsequently [xmpp freeze], + //so self.accountState < kStateReconnecting should always be true on unfreeze (which will make statement 2 above always hold true) + //statement 4: + //if an app freeze takes too long, for example because disconnecting does not finish in time, or if the app still holds the MLProcessLock, + //the app will be killed by iOS, which will immediately invalidate every block in every queue + [self freezeParseQueue]; [self dispatchOnReceiveQueue:^{ - [self freezeParseQueue]; - //this is the last block running in the receive queue (it will be frozen once this block finishes execution) self->_receiveQueue.suspended = YES; }]; + [HelperTools busyWaitForOperationQueue:_receiveQueue]; } -(void) unfreeze { DDLogInfo(@"Unfreezing account: %@", self); - [self unfreezeSendQueue]; - //make sure we don't have any race conditions by dispatching this to our receive queue //this operation has highest priority to make sure it will be executed first once unfrozen NSBlockOperation* unfreezeOperation = [NSBlockOperation blockOperationWithBlock:^{ - //this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system - [self unfreezeParseQueue]; + //this has to be the very first thing even before unfreezing the parse or send queues if(self.accountState < kStateReconnecting) { DDLogInfo(@"Reloading UNfrozen account %@", self.accountNo); @@ -800,8 +827,13 @@ -(void) unfreeze } else DDLogInfo(@"Not reloading UNfrozen account %@, already connected", self.accountNo); + + //this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system + [self unfreezeParseQueue]; + + [self unfreezeSendQueue]; }]; - unfreezeOperation.queuePriority = NSOperationQueuePriorityVeryHigh; //make sure this will become the first operation executed once unfreezed + unfreezeOperation.queuePriority = NSOperationQueuePriorityVeryHigh; //make sure this will become the first operation executed once unfrozen [self->_receiveQueue addOperations: @[unfreezeOperation] waitUntilFinished:NO]; //unfreeze receive queue and execute block added above @@ -971,7 +1003,7 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit { DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); if(self->_iqHandlers[iqid][@"handler"] != nil) - $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self)); + $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect")); else if(self->_iqHandlers[iqid][@"errorHandler"]) ((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil); } @@ -1032,9 +1064,7 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit [self->_sendQueue addOperations: @[[NSBlockOperation blockOperationWithBlock:^{ //disable push for this node if(self.connectionProperties.supportsPush) - { [self disablePush]; - } [self sendLastAck]; }]] waitUntilFinished:YES]; //block until finished because we are closing the xmpp stream directly afterwards [self->_sendQueue addOperations: @[[NSBlockOperation blockOperationWithBlock:^{ @@ -1061,7 +1091,7 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit { DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); if(self->_iqHandlers[iqid][@"handler"] != nil) - $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self)); + $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect")); else if(self->_iqHandlers[iqid][@"errorHandler"]) ((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil); } @@ -2460,7 +2490,7 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR BOOL deactivate_account = NO; NSString* innerSASLData = [[NSString alloc] initWithData:[parsedStanza findFirst:@"/{urn:xmpp:sasl:2}challenge#|base64"] encoding:NSUTF8StringEncoding]; switch([self->_scramHandler parseServerFirstMessage:innerSASLData]) { - case MLScramStatusSSDPTriggered: deactivate_account = YES; message = NSLocalizedString(@"Detected ongoing MITM attack via SSDP, aborting authentication and disabling account to limit damage. You should reenable your account once you are in a clean networking environment again.", @""); break; + case MLScramStatusSSDPTriggered: deactivate_account = YES; message = NSLocalizedString(@"Detected ongoing MITM attack via SSDP, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @""); break; case MLScramStatusNonceError: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (nonce error), disconnecting!", @"parenthesis should be verbatim"); break; case MLScramStatusUnsupportedMAttribute: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (m-attr error), disconnecting!", @"parenthesis should be verbatim"); break; case MLScramStatusIterationCountInsecure: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (iteration count too low), disconnecting!", @"parenthesis should be verbatim"); break; @@ -2605,11 +2635,7 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR self.connectionProperties.channelBindingTypes = channelBindings; //update user identity using authorization-identifier, including support for fullJids (as specified by BIND2) - NSString* authid = [parsedStanza findFirst:@"authorization-identifier#"]; - NSDictionary* authidParts = [HelperTools splitJid:authid]; - self.connectionProperties.identity.jid = authidParts[@"user"]; - if(authidParts[@"user"] != nil) - self.connectionProperties.identity.resource = authidParts[@"resource"]; + [self.connectionProperties.identity bindJid:[parsedStanza findFirst:@"authorization-identifier#"]]; //record SDDP support self.connectionProperties.supportsSSDP = self->_scramHandler.ssdpSupported; @@ -2804,8 +2830,11 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR [oStream startTLS]; if(!oStream.hasTLS) { + //only show this error if the connection was not closed but timed out (this is the case we want to debug here) + //other cases (cert errors etc.) should not trigger this notification + if([oStream streamStatus] != NSStreamStatusClosed) + showErrorOnAlpha(self, @"Failed to complete TLS handshake while using STARTTLS, retrying!"); DDLogError(@"Failed to complete TLS handshake, reconnecting!"); - showErrorOnAlpha(self, @"Failed to complete TLS handshake while using STARTTLS, retrying!"); [self reconnect]; return; } @@ -2875,9 +2904,28 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza //called below, if neither SASL1 nor SASL2 could be used to negotiate a valid SASL mechanism monal_void_block_t noAuthSupported = ^{ DDLogWarn(@"No supported auth mechanism!"); - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"No supported auth mechanism found, disabling account!", @"")); + + //sasl2 will be pinned if we saw sasl2 support and PLAIN was NOT allowed by creating this account using the advanced account creation menu + //display scary warning message if sasl2 is pinned and login was successful at least once + //or display a message pointing to the advanced account creation menu if sasl2 is pinned and login was NOT successful at least once + //(e.g. we are trying to create this account just now) + if([[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo]) + { + if(self->_loggedInOnce) + { + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server suddenly lacks support for SASL2-SCRAM, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @"")); + } + else if([self->_supportedSaslMechanisms containsObject:@"PLAIN"]) + { + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server only supports authentication methods not safe against man-in-the-middle attacks! Use the advanced account creation menu if you absolutely must use this server.", @"")); + } + } + else + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"No supported auth mechanism found, disabling account!", @"")); }; + MLAssert(!([[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo] && [[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]), @"SASL2 pinned AND plain auth enabled, that should never happen!", @{@"account": self}); + if([parsedStanza check:@"{urn:xmpp:ibr-token:0}register"]) { DDLogInfo(@"Server supports Pre-Authenticated IBR"); @@ -2903,7 +2951,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza [self submitRegForm]; } //prefer SASL2 over SASL1 - else if([parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"]) + else if([parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && ![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) { weakify(self); _blockToCallOnTCPOpen = ^{ @@ -2916,7 +2964,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza //but only do so, if we are using channel-binding for additional security //(a MITM could passively intercept the new SCRAM hash which is roughly equivalent to intercepting the plaintext password) self->_upgradeTask = nil; - if([self channelBindingToUse] != nil) + if([self channelBindingToUse] != nil && ![kServerDoesNotFollowXep0440Error isEqualToString:[self channelBindingToUse]]) { NSSet* upgradesOffered = [NSSet setWithArray:[parsedStanza find:@"{urn:xmpp:sasl:2}authentication/{urn:xmpp:sasl:upgrade:0}upgrade#"]]; for(NSString* method in [SCRAM supportedMechanismsIncludingChannelBinding:NO]) @@ -2983,7 +3031,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza //directly call our continuation block if SCRAM is not supported, because _blockToCallOnTCPOpen() will throw an error then //(we currently only support SCRAM for SASL2) - //pipelining can also be done immediately if we are sure the tls handshake is complete (e.g. we're not in direct tls mode) + //pipelining can also be done immediately if we are sure the tls handshake is complete (e.g. we're NOT in direct tls mode) //and if we are not pipelining the auth, we can call the block immediately, too //(because the TLS connection was obviously already established and that made us receive the non-cached stream features used here) //if we don't call it here, the continuation block will be called automatically once the TLS connection got established @@ -2995,9 +3043,20 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza else DDLogWarn(@"Waiting until TLS stream is connected before pipelining the auth element due to channel binding..."); } - //SASL1 is fallback only if SASL2 isn't supported + //SASL1 is fallback only if SASL2 isn't supported with something better than PLAIN else if([parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"] && ![[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo]) { + //check if we SASL2 is supported with something better than PLAIN and, if so, switch off plain_activated + NSSet* supportedSasl2Mechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:xmpp:sasl:2}authentication/mechanism#"]]; + for(NSString* mechanism in [SCRAM supportedMechanismsIncludingChannelBinding:YES]) + if([supportedSasl2Mechanisms containsObject:mechanism]) + { + DDLogInfo(@"We detected SASL2 SCRAM support, deactivating forced SASL1 PLAIN fallback and retrying using SASL2..."); + [[DataLayer sharedInstance] deactivatePlainForAccount:self.accountNo]; + //try again, this time using sasl2 + return [self handleFeaturesBeforeAuth:parsedStanza]; + } + //extract menchanisms presented NSSet* supportedSaslMechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism#"]]; @@ -3046,7 +3105,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza } //if the above case didn't trigger, this is a downgrade attack downgrading from SASL2 to SASL1, report is as such - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"SASL2 to SASL1 downgrade attack detected, aborting authentication and disabling account to limit damage. You should reenable your account once you are in a clean networking environment again.", @"")); + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"SASL2 to SASL1 downgrade attack detected, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @"")); } } @@ -3109,7 +3168,7 @@ -(void) handleScramInSuccessOrContinue:(MLXMLNode*) parsedStanza BOOL deactivate_account = NO; NSString* innerSASLData = [[NSString alloc] initWithData:[parsedStanza findFirst:@"additional-data#|base64"] encoding:NSUTF8StringEncoding]; switch([self->_scramHandler parseServerFinalMessage:innerSASLData]) { - case MLScramStatusWrongServerProof: deactivate_account = YES; message = NSLocalizedString(@"SCRAM server proof wrong, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should reenable your account once you are in a clean networking environment again.", @""); break; + case MLScramStatusWrongServerProof: deactivate_account = YES; message = NSLocalizedString(@"SCRAM server proof wrong, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @""); break; case MLScramStatusServerError: deactivate_account = NO; message = NSLocalizedString(@"Unexpected error authenticating server using SASL2 (does your server have a bug?), disconnecting!", @""); break; case MLScramStatusOK: deactivate_account = NO; message = nil; break; //everything is okay default: unreachable(@"wrong status for scram message!"); break; @@ -3263,19 +3322,20 @@ -(void) logStanza:(MLXMLNode*) stanza withPrefix:(NSString*) prefix #pragma mark messaging --(void) retractMessage:(NSString*) messageId toContact:(MLContact*) contact +-(void) retractMessage:(MLMessage*) msg { - XMPPMessage* messageNode = [[XMPPMessage alloc] initToContact:contact]; + MLAssert([msg.accountId isEqual:self.accountNo], @"Can not retract message from one account on another account!", (@{@"self.accountNo": self.accountNo, @"msg": msg})); + XMPPMessage* messageNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:msg.buddyName]; - //fasten retraction - [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"apply-to" andNamespace:@"urn:xmpp:fasten:0" withAttributes:@{ - @"id": messageId - } andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:message-retract:0"] - ] andData:nil]]; + //retraction + [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:message-retract:1" withAttributes:@{ + @"id": msg.isMuc ? msg.stanzaId : msg.messageId, + } andChildren:@[] andData:nil]]; //add fallback indication and fallback body - [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"fallback" andNamespace:@"urn:xmpp:fallback:0"]]; + [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"fallback" andNamespace:@"urn:xmpp:fallback:0" withAttributes:@{ + @"for": @"urn:xmpp:message-retract:1", + } andChildren:@[] andData:nil]]; [messageNode setBody:@"This person attempted to retract a previous message, but it's unsupported by your client."]; //for MAM @@ -3284,6 +3344,20 @@ -(void) retractMessage:(NSString*) messageId toContact:(MLContact*) contact [self send:messageNode]; } +-(void) moderateMessage:(MLMessage*) msg withReason:(NSString*) reason +{ + MLAssert(msg.isMuc, @"Moderated message must be in a muc!"); + + XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqSetType to:msg.buddyName]; + [iqNode addChildNode:[[MLXMLNode alloc] initWithElement:@"moderate" andNamespace:@"urn:xmpp:message-moderate:1" withAttributes:@{ + @"id": msg.stanzaId, + } andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:message-retract:1"], + [[MLXMLNode alloc] initWithElement:@"reason" andData:reason], + ] andData:nil]]; + [self sendIq:iqNode withHandler:$newHandler(MLIQProcessor, handleModerationResponse, $ID(msg))]; +} + -(void) addEME:(NSString*) encryptionNamesapce withName:(NSString* _Nullable) name toMessageNode:(XMPPMessage*) messageNode { if(name) @@ -3418,6 +3492,8 @@ -(void) realPersistState } [values setValue:[self.connectionProperties.serverFeatures copy] forKey:@"serverFeatures"]; + [values setValue:[self.connectionProperties.accountFeatures copy] forKey:@"accountFeatures"]; + if(self.connectionProperties.uploadServer) [values setObject:self.connectionProperties.uploadServer forKey:@"uploadServer"]; if(self.connectionProperties.conferenceServer) @@ -3444,6 +3520,8 @@ -(void) realPersistState [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsBlocking] forKey:@"supportsBlocking"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.accountDiscoDone] forKey:@"accountDiscoDone"]; [values setObject:[self->_inCatchup copy] forKey:@"inCatchup"]; + [values setObject:[self->_mdsData copy] forKey:@"mdsData"]; + if(self->_cachedStreamFeaturesBeforeAuth != nil) [values setObject:self->_cachedStreamFeaturesBeforeAuth forKey:@"cachedStreamFeaturesBeforeAuth"]; if(self->_cachedStreamFeaturesAfterAuth != nil) @@ -3472,7 +3550,7 @@ -(void) realPersistState [[DataLayer sharedInstance] persistState:values forAccount:self.accountNo]; //debug output - DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientState=%d\n\tsupportsBookmarksCompat=%d\n\t_inCatchup=%@\n\tomemo.state=%@", + DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientState=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@", self.accountNo, values[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3492,6 +3570,7 @@ -(void) realPersistState self.connectionProperties.supportsBlocking, self.connectionProperties.supportsClientState, self.connectionProperties.supportsBookmarksCompat, + self.connectionProperties.accountDiscoDone, self->_inCatchup, self.omemo.state ); @@ -3568,6 +3647,8 @@ -(void) realReadState } self.connectionProperties.serverFeatures = [dic objectForKey:@"serverFeatures"]; + self.connectionProperties.accountFeatures = [dic objectForKey:@"accountFeatures"]; + self.connectionProperties.discoveredServices = [[dic objectForKey:@"discoveredServices"] mutableCopy]; self.connectionProperties.discoveredStunTurnServers = [[dic objectForKey:@"discoveredStunTurnServers"] mutableCopy]; self.connectionProperties.discoveredAdhocCommands = [[dic objectForKey:@"discoveredAdhocCommands"] mutableCopy]; @@ -3690,6 +3771,9 @@ -(void) realReadState if([dic objectForKey:@"inCatchup"]) _inCatchup = [[dic objectForKey:@"inCatchup"] mutableCopy]; + if([dic objectForKey:@"mdsData"]) + _mdsData = [[dic objectForKey:@"mdsData"] mutableCopy]; + if([dic objectForKey:@"cachedStreamFeaturesBeforeAuth"]) _cachedStreamFeaturesBeforeAuth = [dic objectForKey:@"cachedStreamFeaturesBeforeAuth"]; if([dic objectForKey:@"cachedStreamFeaturesAfterAuth"]) @@ -3699,7 +3783,7 @@ -(void) realReadState self.omemo.state = [dic objectForKey:@"omemoState"]; //debug output - DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientSate=%d\n\tsupportsBookmarksCompat=%d\n\t_inCatchup=%@\n\tomemo.state=%@", + DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientSate=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@", self.accountNo, dic[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3719,6 +3803,7 @@ -(void) realReadState self.connectionProperties.supportsBlocking, self.connectionProperties.supportsClientState, self.connectionProperties.supportsBookmarksCompat, + self.connectionProperties.accountDiscoDone, self->_inCatchup, self.omemo.state ); @@ -3806,6 +3891,79 @@ -(void) bindResource:(NSString*) resource self.isDoingFullReconnect = YES; _accountState = kStateBinding; + + //delete old resources because we get new presences once we're done initializing the session + [[DataLayer sharedInstance] resetContactsForAccount:self.accountNo]; + + //inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards + @synchronized(_iqHandlers) { + //make sure this works even if the invalidation handlers add a new iq to the list + NSMutableDictionary* handlersCopy = [_iqHandlers mutableCopy]; + [_iqHandlers removeAllObjects]; + + for(NSString* iqid in handlersCopy) + { + DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); + if(handlersCopy[iqid][@"handler"] != nil) + $invalidate(handlersCopy[iqid][@"handler"], $ID(account, self), $ID(reason, @"bind")); + else if(handlersCopy[iqid][@"errorHandler"]) + ((monal_iq_handler_t)handlersCopy[iqid][@"errorHandler"])(nil); + } + + } + + //invalidate pubsub queue (a pubsub operation will be either invalidated by an iq handler above OR by the invalidation here, but never twice!) + [self.pubsub invalidateQueue]; + + //clean up all idle timers + [[DataLayer sharedInstance] cleanupIdleTimerOnAccountNo:self.accountNo]; + + //force new disco queries because we landed here because of a failed smacks resume + //(or the account got forcibly disconnected/reconnected or this is the very first login of this account) + //--> all of this reasons imply that we had to start a new xmpp stream and our old cached disco data + // and other state values are stale now + //(smacks state will be reset/cleared later on if appropriate, no need to handle smacks here) + self.connectionProperties.serverFeatures = [NSSet new]; + self.connectionProperties.accountFeatures = [NSSet new]; + self.connectionProperties.discoveredServices = [NSMutableArray new]; + self.connectionProperties.discoveredStunTurnServers = [NSMutableArray new]; + self.connectionProperties.discoveredAdhocCommands = [NSMutableDictionary new]; + self.connectionProperties.serverVersion = nil; + self.connectionProperties.conferenceServer = nil; + self.connectionProperties.supportsHTTPUpload = NO; + self.connectionProperties.uploadServer = nil; + //self.connectionProperties.supportsClientState = NO; //already set by stream feature parsing + self.connectionProperties.supportsMam2 = NO; + //self.connectionProperties.supportsSM3 = NO; //already set by stream feature parsing + self.connectionProperties.supportsPush = NO; + self.connectionProperties.pushEnabled = NO; + self.connectionProperties.supportsBookmarksCompat = NO; + self.connectionProperties.usingCarbons2 = NO; + //self.connectionProperties.supportsRosterVersion = NO; //already set by stream feature parsing + //self.connectionProperties.supportsRosterPreApproval = NO; //already set by stream feature parsing + //self.connectionProperties.serverIdentity = @""; //already set by stream feature parsing + self.connectionProperties.supportsBlocking = NO; + self.connectionProperties.supportsPing = NO; + self.connectionProperties.supportsExternalServiceDiscovery = NO; + self.connectionProperties.supportsPubSub = NO; + self.connectionProperties.supportsPubSubMax = NO; + self.connectionProperties.supportsModernPubSub = NO; + //self.connectionProperties.supportsPreauthIbr = NO; //already set by stream feature parsing + self.connectionProperties.accountDiscoDone = NO; + + //clear list of running mam queries + _runningMamQueries = [NSMutableDictionary new]; + + //clear list of running caps queries + _runningCapsQueries = [NSMutableSet new]; + + //clear old catchup state (technically all stanzas still in delayedMessageStanzas could have also been + //in the parseQueue in the last run and deleted there) + //--> no harm in deleting them when starting a new session (but DON'T DELETE them when resuming the old smacks session) + _inCatchup = [NSMutableDictionary new]; + [[DataLayer sharedInstance] deleteDelayedMessageStanzasForAccount:self.accountNo]; + + //send bind iq XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqSetType]; [iqNode setBindWithResource:resource]; [self sendIq:iqNode withHandler:$newHandler(MLIQProcessor, handleBind)]; @@ -3908,72 +4066,10 @@ -(void) initSession DDLogInfo(@"Now bound, initializing new xmpp session"); self.isDoingFullReconnect = YES; - //delete old resources because we get new presences once we're done initializing the session - [[DataLayer sharedInstance] resetContactsForAccount:self.accountNo]; - //we are now bound _connectedTime = [NSDate date]; _reconnectBackoffTime = 0; - //inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards - @synchronized(_iqHandlers) { - //make sure this works even if the invalidation handlers add a new iq to the list - NSMutableDictionary* handlersCopy = [_iqHandlers mutableCopy]; - [_iqHandlers removeAllObjects]; - - for(NSString* iqid in handlersCopy) - { - DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); - if(handlersCopy[iqid][@"handler"] != nil) - $invalidate(handlersCopy[iqid][@"handler"], $ID(account, self)); - else if(handlersCopy[iqid][@"errorHandler"]) - ((monal_iq_handler_t)handlersCopy[iqid][@"errorHandler"])(nil); - } - - } - - //invalidate pubsub queue (a pubsub operation will be either invalidated by an iq handler above OR by the invalidation here, but never twice!) - [self.pubsub invalidateQueue]; - - //clean up all idle timers - [[DataLayer sharedInstance] cleanupIdleTimerOnAccountNo:self.accountNo]; - - //force new disco queries because we landed here because of a failed smacks resume - //(or the account got forcibly disconnected/reconnected or this is the very first login of this account) - //--> all of this reasons imply that we had to start a new xmpp stream and our old cached disco data - // and other state values are stale now - //(smacks state will be reset/cleared later on if appropriate, no need to handle smacks here) - self.connectionProperties.serverFeatures = [NSSet new]; - self.connectionProperties.discoveredServices = [NSMutableArray new]; - self.connectionProperties.discoveredStunTurnServers = [NSMutableArray new]; - self.connectionProperties.discoveredAdhocCommands = [NSMutableDictionary new]; - self.connectionProperties.uploadServer = nil; - self.connectionProperties.conferenceServer = nil; - self.connectionProperties.usingCarbons2 = NO; - self.connectionProperties.supportsPush = NO; - self.connectionProperties.supportsBookmarksCompat = NO; - self.connectionProperties.pushEnabled = NO; - self.connectionProperties.supportsMam2 = NO; - self.connectionProperties.supportsPubSub = NO; - self.connectionProperties.supportsPubSubMax = NO; - self.connectionProperties.supportsModernPubSub = NO; - self.connectionProperties.supportsHTTPUpload = NO; - self.connectionProperties.supportsPing = NO; - self.connectionProperties.supportsExternalServiceDiscovery = NO; - self.connectionProperties.supportsRosterPreApproval = NO; - - //clear list of running mam queries - _runningMamQueries = [NSMutableDictionary new]; - - //clear list of running caps queries - _runningCapsQueries = [NSMutableSet new]; - - //clear old catchup state (technically all stanzas still in delayedMessageStanzas could have also been - //in the parseQueue in the last run and deleted there) - //--> no harm in deleting them when starting a new session (but DON'T DELETE them when resuming the old smacks session) - _inCatchup = [NSMutableDictionary new]; - [[DataLayer sharedInstance] deleteDelayedMessageStanzasForAccount:self.accountNo]; - //indicate we are bound now, *after* initializing/resetting all the other data structures to avoid race conditions _accountState = kStateBound; @@ -4014,6 +4110,9 @@ -(void) initSession [self resendUnackedMessageStanzasOnly:self.unAckedStanzas]; } + //fetch current mds state + [self.pubsub fetchNode:@"urn:xmpp:mds:displayed:0" from:self.connectionProperties.identity.jid withItemsList:nil andHandler:$newHandler(MLPubSubProcessor, handleMdsFetchResult)]; + //NOTE: mam query will be done in MLIQProcessor once the disco result for our own jid/account returns //initialize stanza counter for statistics @@ -4730,6 +4829,11 @@ -(void)stream:(NSStream*) stream handleEvent:(NSStreamEvent) eventCode break; } + case errSSLBadCert: { + message = NSLocalizedString(@"TLS Error: Bad certificate", @""); + break; + } + } [self postError:message withIsSevere:NO]; @@ -4913,6 +5017,11 @@ -(BOOL) writeToStream:(NSString*) messageOut DDLogVerbose(@"could not send all bytes of outgoing stanza: %lu of %lu sent, %lu remaining", (unsigned long)sentLen, (unsigned long)rawstringLen, (unsigned long)(rawstringLen-sentLen)); //allocate new _outputBuffer _outputBuffer=malloc(sizeof(uint8_t) * (rawstringLen-sentLen)); + if(_outputBuffer == NULL) + { + [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil]; + return NO; //since the stanza was partially written, neither YES nor NO as return value will result in a consistent state + } //copy the remaining data into the buffer and set the buffer pointer accordingly memcpy(_outputBuffer, rawstring+(size_t)sentLen, (size_t)(rawstringLen-sentLen)); _outputBufferByteCount=(size_t)(rawstringLen-sentLen); @@ -5149,6 +5258,10 @@ -(void) _handleInternalMamFinishedFor:(NSString*) archiveJid DDLogInfo(@"Catchup finished for jid %@", archiveJid); [self->_inCatchup removeObjectForKey:archiveJid]; //catchup done and replay finished + //handle cached mds data for this jid + if(self->_mdsData[archiveJid] != nil) + [self handleMdsData:self->_mdsData[archiveJid] forJid:archiveJid]; + //handle old mamFinished code as soon as all delayed messages have been processed //we need to wait for all delayed messages because at least omemo needs the pep headline messages coming in during mam catchup if([self.connectionProperties.identity.jid isEqualToString:archiveJid]) @@ -5232,6 +5345,59 @@ -(void) handleFinishedCatchup [[NSNotificationCenter defaultCenter] postNotificationName:kMonalFinishedCatchup object:self userInfo:nil]; } +-(void) updateMdsData:(NSDictionary*) mdsData +{ + for(NSString* jid in mdsData) + _mdsData[jid] = mdsData[jid]; +} + +-(void) handleMdsData:(MLXMLNode*) data forJid:(NSString*) jid +{ + NSString* stanzaId = [data findFirst:@"{urn:xmpp:mds:displayed:0}displayed/{urn:xmpp:sid:0}stanza-id@id"]; + NSString* by = [data findFirst:@"{urn:xmpp:mds:displayed:0}displayed/{urn:xmpp:sid:0}stanza-id@by"]; + DDLogInfo(@"Got mds displayed element for chat %@ by %@: %@", jid, by, stanzaId); + + if([[DataLayer sharedInstance] isBuddyMuc:jid forAccount:self.accountNo]) + { + if(![jid isEqualToString:by]) + { + DDLogWarn(@"Mds stanza-id by not equal to muc jid, ignoring!"); + return; + } + + //NSString* ownNick = [[DataLayer sharedInstance] ownNickNameforMuc:jid forAccount:self.accountNo] + NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:jid andAccount:self.accountNo tillStanzaId:stanzaId wasOutgoing:NO]; + DDLogDebug(@"Muc marked as read: %@", unread); + + //remove notifications of all remotely read messages (indicated by sending a display marker) + [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:self userInfo:@{@"messagesArray":unread}]; + + //update unread count in active chats list + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self userInfo:@{ + @"contact": [MLContact createContactFromJid:jid andAccountNo:self.accountNo] + }]; + } + else + { + if(![self.connectionProperties.identity.jid isEqualToString:by]) + { + DDLogWarn(@"Mds stanza-id by not equal to own bare jid, ignoring!"); + return; + } + + NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:jid andAccount:self.accountNo tillStanzaId:stanzaId wasOutgoing:NO]; + DDLogDebug(@"1:1 marked as read: %@", unread); + + //remove notifications of all remotely read messages (indicated by sending a display marker) + [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:self userInfo:@{@"messagesArray":unread}]; + + //update unread count in active chats list + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self userInfo:@{ + @"contact": [MLContact createContactFromJid:jid andAccountNo:self.accountNo] + }]; + } +} + -(void) addMessageToMamPageArray:(NSDictionary*) messageDictionary { @synchronized(_mamPageArrays) { @@ -5253,18 +5419,40 @@ -(NSMutableArray*) getOrderedMamPageFor:(NSString*) mamQueryId return array; } +-(void) publishMDSMarkerForMessage:(MLMessage*) msg +{ + NSString* max_items = @"255"; //fallback for servers not supporting "max" + if(self.connectionProperties.supportsPubSubMax) + max_items = @"max"; + [self.pubsub publishItem:[[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{kId: msg.buddyName} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"displayed" andNamespace:@"urn:xmpp:mds:displayed:0" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"stanza-id" andNamespace:@"urn:xmpp:sid:0" withAttributes:@{ + @"by": msg.isMuc ? msg.buddyName : self.connectionProperties.identity.jid, + @"id": msg.stanzaId, + } andChildren:@[] andData:nil] + ] andData:nil] + ] andData:nil] onNode:@"urn:xmpp:mds:displayed:0" withConfigOptions:@{ + @"pubsub#persist_items": @"true", + @"pubsub#access_model": @"whitelist", + @"pubsub#max_items": max_items, + @"pubsub#send_last_published_item": @"never", + }]; +} + -(void) sendDisplayMarkerForMessage:(MLMessage*) msg { if(![[HelperTools defaultsDB] boolForKey:@"SendDisplayedMarkers"]) { DDLogVerbose(@"Not sending chat marker, configured to not do so..."); + [self publishMDSMarkerForMessage:msg]; //always publish mds marker return; } //don't send chatmarkers in channels if(msg.isMuc && [@"channel" isEqualToString:msg.mucType]) { - DDLogVerbose(@"Not sending chat marker in channel..."); + DDLogVerbose(@"Not sending XEP-0333 chat marker in channel..."); + [self publishMDSMarkerForMessage:msg]; //always publish mds marker return; } @@ -5273,14 +5461,20 @@ -(void) sendDisplayMarkerForMessage:(MLMessage*) msg if(!contact.isGroup && !contact.isSubscribedFrom) { DDLogVerbose(@"Not sending chat marker, we are not subscribed from this contact..."); + [self publishMDSMarkerForMessage:msg]; //always publish mds marker return; } XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; [displayedNode setDisplayed:msg.isMuc && msg.stanzaId != nil ? msg.stanzaId : msg.messageId]; + if([self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) + [displayedNode setMDSDisplayed:msg.stanzaId withStanzaIdBy:(msg.isMuc ? msg.buddyName : self.connectionProperties.identity.jid)]; [displayedNode setStoreHint]; DDLogVerbose(@"Sending display marker: %@", displayedNode); [self send:displayedNode]; + + if(![self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) + [self publishMDSMarkerForMessage:msg]; //always publish mds marker } -(void) removeFromServerWithCompletion:(void (^)(NSString* _Nullable error)) completion diff --git a/Monal/Images.xcassets/colors/chatBackgroundColor.colorset/Contents.json b/Monal/Images.xcassets/colors/chatBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000000..a72cd2503c --- /dev/null +++ b/Monal/Images.xcassets/colors/chatBackgroundColor.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x13", + "green" : "0x13", + "red" : "0x13" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index e5d7f9b90f..e8969e929d 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 08CAF17FA202CF3CB760D93C /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B7A5555D807EE78C95217FD /* Pods_NotificationService.framework */; }; 1D3623260D0F684500981E51 /* MonalAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* MonalAppDelegate.m */; }; 1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; + 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ED55842BADDA5C0005783E /* PrivacySettings.swift */; }; 2601D9CB0FBF25EF004DB939 /* sworim.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 2601D9CA0FBF25EF004DB939 /* sworim.sqlite */; }; 260773C4232FC4E800BFD50F /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 260773C3232FC4E800BFD50F /* NotificationService.m */; }; 2609B5291FD5B26800F09FA1 /* MLSplitViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2609B5281FD5B26800F09FA1 /* MLSplitViewDelegate.m */; }; @@ -47,7 +48,6 @@ 26B0CA8921AE2E3C0080B133 /* MLSoundsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26B0CA8821AE2E3C0080B133 /* MLSoundsTableViewController.m */; }; 26B0CA8B21AE410E0080B133 /* AlertSounds in Resources */ = {isa = PBXBuildFile; fileRef = 26B0CA8A21AE410E0080B133 /* AlertSounds */; }; 26B2A4BB1B73061400272E63 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 26B2A4BA1B73061400272E63 /* Images.xcassets */; }; - 26B51C4918654D98002134C3 /* LogViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26B51C4718654D98002134C3 /* LogViewController.m */; }; 26CC579623A0867400ABB92A /* monalxmpp.h in Headers */ = {isa = PBXBuildFile; fileRef = 26CC579423A0867400ABB92A /* monalxmpp.h */; settings = {ATTRIBUTES = (Public, ); }; }; 26CC57A223A086AA00ABB92A /* xmpp.m in Sources */ = {isa = PBXBuildFile; fileRef = 260C51D0177F08F50039634B /* xmpp.m */; }; 26CC57A323A086AA00ABB92A /* MLDNSLookup.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C4B22321B6B91300610282 /* MLDNSLookup.m */; }; @@ -80,7 +80,6 @@ 26E8462824EABAED00ECE419 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 26E8462A24EABAED00ECE419 /* Main.storyboard */; }; 26F9794D1ACAC73A0008E005 /* MLContactCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26F9794C1ACAC73A0008E005 /* MLContactCell.xib */; }; 26FE3BCB1C61A6C3003CC230 /* MLResizingTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FE3BCA1C61A6C3003CC230 /* MLResizingTextView.m */; }; - 384E4EA725F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 384E4EA625F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.m */; }; 38720923251EDE07001837EB /* MLXEPSlashMeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 38720921251EDE07001837EB /* MLXEPSlashMeHandler.m */; }; 389E298C25E901CA009A5268 /* MLAudioRecoderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 389E298925E901CA009A5268 /* MLAudioRecoderManager.m */; }; 389E298D25E901CA009A5268 /* MLAudioRecoderManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 389E298B25E901CA009A5268 /* MLAudioRecoderManager.h */; }; @@ -149,20 +148,24 @@ 846DF27C2937FAA600AAB9C0 /* ChatPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846DF27B2937FAA600AAB9C0 /* ChatPlaceholder.swift */; }; 848717F3295ED64600B8D288 /* MLCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 848717F1295ED64500B8D288 /* MLCall.m */; }; 848904A9289C82C30097E19C /* SCRAM.m in Sources */ = {isa = PBXBuildFile; fileRef = 848904A8289C82C30097E19C /* SCRAM.m */; }; + 848C73E02BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */; }; + 848C73E12BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */; }; + 848C73E22BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */; }; 849248492AD4CEC400986C1A /* ZoomableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849248482AD4CEC400986C1A /* ZoomableContainer.swift */; }; 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 849A53E3287135B2007E941A /* MLVoIPProcessor.m */; }; + 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */; }; + 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */; }; + 849ADF432BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */; }; 84C1CD502A8C764D007076ED /* SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C1CD4F2A8C764D007076ED /* SwiftHelpers.swift */; }; 84C1CD522A8F617F007076ED /* MLStreamRedirect.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1CD512A8F617F007076ED /* MLStreamRedirect.m */; }; 84C1CD542A8F6196007076ED /* MLStreamRedirect.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */; }; 84D31CE628653B83006D7926 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D31CE528653B83006D7926 /* WebRTCClient.swift */; }; - 84DD7EF92B28512C005AC131 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 84DD7EF82B28512C005AC131 /* CocoaLumberjack */; }; - 84DD7EFB2B28512C005AC131 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 84DD7EFA2B28512C005AC131 /* CocoaLumberjackSwift */; }; - 84DD7EFD2B28512C005AC131 /* CocoaLumberjackSwiftLogBackend in Frameworks */ = {isa = PBXBuildFile; productRef = 84DD7EFC2B28512C005AC131 /* CocoaLumberjackSwiftLogBackend */; }; 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 261A6284176C156500059090 /* ActiveChatsViewController.m */; }; 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */; }; 84FC37552897521500634E3E /* snprintf.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FC37542897521400634E3E /* snprintf.m */; }; 84FC37572897523500634E3E /* metamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FC37562897523500634E3E /* metamacros.h */; }; 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC375828981A5600634E3E /* PasswordMigration.swift */; }; + 952EBC802BAF72F300183DBF /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952EBC7F2BAF72F300183DBF /* DebugView.swift */; }; BE8B63D2491B1E5582965A8F /* Pods_monalxmpp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B9C86E0A568734587FE9BA2 /* Pods_monalxmpp.framework */; }; C10490492612ED2F0054AC9E /* MLEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10490482612ED2F0054AC9E /* MLEmoji.swift */; }; C10490E32612F3D00054AC9E /* MLCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10490E22612F3D00054AC9E /* MLCrypto.swift */; }; @@ -175,7 +178,6 @@ C117F7E22B0863B3001F2BC6 /* ContactPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D631822294BAB1D00026BE7 /* ContactPicker.swift */; }; C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = C12436132434AB5D00B8F074 /* MLAttributedLabel.m */; }; C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */; }; - C13EBB8E24DC685C008AADDA /* MLPrivacySettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C13EBB8D24DC685C008AADDA /* MLPrivacySettingsViewController.m */; }; C1414E9D24312F0100948788 /* MLChatMapsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C1414E9C24312F0100948788 /* MLChatMapsCell.m */; }; C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */; }; C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15382612B89C38300EA83EC /* EditGroupName.swift */; }; @@ -304,6 +306,7 @@ 1D3623240D0F684500981E51 /* MonalAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MonalAppDelegate.h; path = Classes/MonalAppDelegate.h; sourceTree = ""; }; 1D3623250D0F684500981E51 /* MonalAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MonalAppDelegate.m; path = Classes/MonalAppDelegate.m; sourceTree = ""; }; 1D46F251C198E3D8FA55692F /* Pods-Monal.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore.xcconfig"; sourceTree = ""; }; + 20ED55842BADDA5C0005783E /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; 213F5BFD4599EC9317B99E97 /* Pods-Monal.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore-quicksy.xcconfig"; sourceTree = ""; }; 21E99538324C14220843F325 /* Pods-shareSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shareSheet.debug.xcconfig"; path = "Target Support Files/Pods-shareSheet/Pods-shareSheet.debug.xcconfig"; sourceTree = ""; }; 222F09C97CFF93A2CF1007F3 /* Pods-MonalUITests.alpha-ios.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.alpha-ios.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.alpha-ios.xcconfig"; sourceTree = ""; }; @@ -410,8 +413,6 @@ 26B0CA8821AE2E3C0080B133 /* MLSoundsTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLSoundsTableViewController.m; sourceTree = ""; }; 26B0CA8A21AE410E0080B133 /* AlertSounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = AlertSounds; sourceTree = ""; }; 26B2A4BA1B73061400272E63 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 26B51C4618654D98002134C3 /* LogViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LogViewController.h; sourceTree = ""; }; - 26B51C4718654D98002134C3 /* LogViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LogViewController.m; sourceTree = ""; }; 26C3456C1C68F9F0007F4BEC /* MLHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLHTTPRequest.h; sourceTree = ""; }; 26C3456D1C68F9F0007F4BEC /* MLHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLHTTPRequest.m; sourceTree = ""; }; 26C4B22221B6B91300610282 /* MLDNSLookup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLDNSLookup.h; sourceTree = ""; }; @@ -495,8 +496,6 @@ 2C59BAF969550DFAC27E5F2B /* Pods_MonalXMPPUnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MonalXMPPUnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2E5021A8D40FCC591D952104 /* Pods-NotificaionService.alpha-ios.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificaionService.alpha-ios.xcconfig"; path = "Target Support Files/Pods-NotificaionService/Pods-NotificaionService.alpha-ios.xcconfig"; sourceTree = ""; }; 32CA4F630368D1EE00C91783 /* MonalSourceCodePrefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MonalSourceCodePrefix.pch; sourceTree = ""; }; - 384E4EA525F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLAutoDownloadFiletransferSettingViewController.h; sourceTree = ""; }; - 384E4EA625F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLAutoDownloadFiletransferSettingViewController.m; sourceTree = ""; }; 3872091F251EDE07001837EB /* MLXEPSlashMeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLXEPSlashMeHandler.h; sourceTree = ""; }; 38720921251EDE07001837EB /* MLXEPSlashMeHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLXEPSlashMeHandler.m; sourceTree = ""; }; 389E298925E901CA009A5268 /* MLAudioRecoderManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLAudioRecoderManager.m; sourceTree = ""; }; @@ -584,6 +583,7 @@ 848717F2295ED64500B8D288 /* MLCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MLCall.h; path = Classes/MLCall.h; sourceTree = SOURCE_ROOT; }; 848904A8289C82C30097E19C /* SCRAM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCRAM.m; sourceTree = ""; }; 848904AA289C82DB0097E19C /* SCRAM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCRAM.h; sourceTree = ""; }; + 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 849248482AD4CEC400986C1A /* ZoomableContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomableContainer.swift; sourceTree = ""; }; 849A53E3287135B2007E941A /* MLVoIPProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MLVoIPProcessor.m; path = Classes/MLVoIPProcessor.m; sourceTree = SOURCE_ROOT; }; 849A53E5287135D7007E941A /* MLVoIPProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MLVoIPProcessor.h; path = Classes/MLVoIPProcessor.h; sourceTree = SOURCE_ROOT; }; @@ -600,6 +600,7 @@ 8D1107310486CEB800E47090 /* Monal-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Monal-Info.plist"; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; 8F1488206B764014DD7EC92A /* Pods_shareSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_shareSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9162E3E9FCE2280BF75F5102 /* Pods-Monal.alpha-ios.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.alpha-ios.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.alpha-ios.xcconfig"; sourceTree = ""; }; + 952EBC7F2BAF72F300183DBF /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = ""; }; 9705AFFB59AF72A9B79C1D7B /* Pods-MonalXMPPUnitTests.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalXMPPUnitTests.adhoc.xcconfig"; path = "Target Support Files/Pods-MonalXMPPUnitTests/Pods-MonalXMPPUnitTests.adhoc.xcconfig"; sourceTree = ""; }; 9760CF4718351300C4256921 /* Pods-shareSheet.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shareSheet.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-shareSheet/Pods-shareSheet.appstore-quicksy.xcconfig"; sourceTree = ""; }; 9899D670570190DCBE9EEDDB /* Pods-monalxmpp.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-monalxmpp.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-monalxmpp/Pods-monalxmpp.appstore-quicksy.xcconfig"; sourceTree = ""; }; @@ -633,8 +634,6 @@ C13E640625BD405700763D6F /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "external/zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; C13E640825BD406600763D6F /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "external/pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; C13E640925BD406700763D6F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "external/pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; - C13EBB8C24DC685C008AADDA /* MLPrivacySettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLPrivacySettingsViewController.h; sourceTree = ""; }; - C13EBB8D24DC685C008AADDA /* MLPrivacySettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLPrivacySettingsViewController.m; sourceTree = ""; }; C1414E9B24312F0100948788 /* MLChatMapsCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatMapsCell.h; sourceTree = ""; }; C1414E9C24312F0100948788 /* MLChatMapsCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatMapsCell.m; sourceTree = ""; }; C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDetailsEdit.swift; sourceTree = ""; }; @@ -803,10 +802,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 84DD7EF92B28512C005AC131 /* CocoaLumberjack in Frameworks */, + 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */, BE8B63D2491B1E5582965A8F /* Pods_monalxmpp.framework in Frameworks */, - 84DD7EFB2B28512C005AC131 /* CocoaLumberjackSwift in Frameworks */, - 84DD7EFD2B28512C005AC131 /* CocoaLumberjackSwiftLogBackend in Frameworks */, + 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */, + 849ADF432BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend in Frameworks */, 8414AE002A7ABC4300EFFCCC /* LibMonalRustSwiftBridge in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -999,20 +998,15 @@ 2644D4961FF29E1900F46AB5 /* Settings */ = { isa = PBXGroup; children = ( - 26B51C4618654D98002134C3 /* LogViewController.h */, - 26B51C4718654D98002134C3 /* LogViewController.m */, 2644D4971FF29E5600F46AB5 /* MLSettingsTableViewController.h */, 2644D4981FF29E5600F46AB5 /* MLSettingsTableViewController.m */, 26B0CA8721AE2E3C0080B133 /* MLSoundsTableViewController.h */, 26B0CA8821AE2E3C0080B133 /* MLSoundsTableViewController.m */, 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */, - C13EBB8C24DC685C008AADDA /* MLPrivacySettingsViewController.h */, - C13EBB8D24DC685C008AADDA /* MLPrivacySettingsViewController.m */, - 384E4EA525F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.h */, - 384E4EA625F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.m */, E8CF9CBF26249640001A1952 /* MLSettingsAboutViewController.h */, E8CF9CC026249640001A1952 /* MLSettingsAboutViewController.m */, 8441EFF82921B53500E851E9 /* BackgroundSettings.swift */, + 20ED55842BADDA5C0005783E /* PrivacySettings.swift */, ); name = Settings; sourceTree = ""; @@ -1072,6 +1066,7 @@ 846DF27B2937FAA600AAB9C0 /* ChatPlaceholder.swift */, 841898AB2957DBAC00FEC77D /* RichAlert.swift */, C114D13C2B15B903000FB99F /* ContactEntry.swift */, + 952EBC7F2BAF72F300183DBF /* DebugView.swift */, ); name = "View Controllers"; path = Classes; @@ -1302,6 +1297,7 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */, 842790842A32D16C005C18CC /* CallSounds */, 26B0CA8A21AE410E0080B133 /* AlertSounds */, 26158AF31FFA8A6A00E53BDC /* opensource.html */, @@ -1591,9 +1587,9 @@ name = monalxmpp; packageProductDependencies = ( 8414ADFF2A7ABC4300EFFCCC /* LibMonalRustSwiftBridge */, - 84DD7EF82B28512C005AC131 /* CocoaLumberjack */, - 84DD7EFA2B28512C005AC131 /* CocoaLumberjackSwift */, - 84DD7EFC2B28512C005AC131 /* CocoaLumberjackSwiftLogBackend */, + 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */, + 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */, + 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */, ); productName = monalxmpp; productReference = 26CC579223A0867400ABB92A /* monalxmpp.framework */; @@ -1743,7 +1739,7 @@ C1F5C7AD2777638B0001F295 /* XCRemoteSwiftPackageReference "swift-collections" */, C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */, - 84DD7EF72B28512C005AC131 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, + 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -1767,6 +1763,7 @@ 262E51931AD8CAC600788351 /* MLButtonCell.xib in Resources */, 2601D9CB0FBF25EF004DB939 /* sworim.sqlite in Resources */, 264A7E751AA7263600E860E3 /* MLSwitchCell.xib in Resources */, + 848C73E02BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */, 26B0CA8B21AE410E0080B133 /* AlertSounds in Resources */, 842790852A32D16D005C18CC /* CallSounds in Resources */, 26470F521835C4080069E3E0 /* Media.xcassets in Resources */, @@ -1787,6 +1784,7 @@ files = ( 845EFFBD2918721800C1E03E /* Localizable.strings in Resources */, C1D7D7B0283FB4E700401389 /* Media.xcassets in Resources */, + 848C73E22BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */, C1D7D7AF283FB4E500401389 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1795,6 +1793,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 848C73E12BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */, 845EFFBE2918723D00C1E03E /* Localizable.strings in Resources */, 26AA70182146BBB900598605 /* iosShare.storyboard in Resources */, ); @@ -2035,7 +2034,6 @@ files = ( 848717F3295ED64600B8D288 /* MLCall.m in Sources */, 26D89F061A890672009B147C /* MLSwitchCell.m in Sources */, - C13EBB8E24DC685C008AADDA /* MLPrivacySettingsViewController.m in Sources */, C18967C72B81F61B0073C7C5 /* ChannelMemberList.swift in Sources */, C114D13D2B15B903000FB99F /* ContactEntry.swift in Sources */, 841B6F1A297B18720074F9B7 /* AccountPicker.swift in Sources */, @@ -2045,9 +2043,9 @@ 1D60589B0D05DD56006BFB54 /* main.m in Sources */, 1D3623260D0F684500981E51 /* MonalAppDelegate.m in Sources */, 26158AF21FFA6E4500E53BDC /* MLWebViewController.m in Sources */, - 26B51C4918654D98002134C3 /* LogViewController.m in Sources */, C1943A4C25309A9D0036172F /* MLReloadCell.m in Sources */, 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, + 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, @@ -2059,7 +2057,6 @@ 26B0CA8921AE2E3C0080B133 /* MLSoundsTableViewController.m in Sources */, 84D31CE628653B83006D7926 /* WebRTCClient.swift in Sources */, 54F0B81C282316F5003664BD /* RegisterAccount.swift in Sources */, - 384E4EA725F1E21A00D384A3 /* MLAutoDownloadFiletransferSettingViewController.m in Sources */, 841B6F1C297B3CFC0074F9B7 /* AVCallUI.swift in Sources */, C15489B925680BBE00BBA2F0 /* MLQRCodeScanner.swift in Sources */, 2609B5291FD5B26800F09FA1 /* MLSplitViewDelegate.m in Sources */, @@ -2086,6 +2083,7 @@ 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */, C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */, 26D59D9320714F32006F1DEE /* UIColor+Theme.m in Sources */, + 952EBC802BAF72F300183DBF /* DebugView.swift in Sources */, 3D7D352328626CB80042C5E5 /* LoadingOverlay.swift in Sources */, 26D7C05E23D6AFD800CA123C /* MLChatInputContainer.m in Sources */, 262AEFE820AE756800498F82 /* MLMAMPrefTableViewController.m in Sources */, @@ -2659,7 +2657,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3026,7 +3024,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3188,7 +3186,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3464,7 +3462,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3817,7 +3815,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4231,7 +4229,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.2.0; + MARKETING_VERSION = 6.3.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -4619,12 +4617,12 @@ minimumVersion = 2.0.0; }; }; - 84DD7EF72B28512C005AC131 /* XCRemoteSwiftPackageReference "cocoalumberjack" */ = { + 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/cocoalumberjack/cocoalumberjack"; requirement = { - kind = revision; - revision = 5e6c2217d1ee7aa4795c411706e83adb3d414cb5; + kind = upToNextMajorVersion; + minimumVersion = 3.8.5; }; }; C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { @@ -4655,19 +4653,19 @@ package = 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */; productName = ViewExtractor; }; - 84DD7EF82B28512C005AC131 /* CocoaLumberjack */ = { + 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */ = { isa = XCSwiftPackageProductDependency; - package = 84DD7EF72B28512C005AC131 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; + package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjack; }; - 84DD7EFA2B28512C005AC131 /* CocoaLumberjackSwift */ = { + 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */ = { isa = XCSwiftPackageProductDependency; - package = 84DD7EF72B28512C005AC131 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; + package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjackSwift; }; - 84DD7EFC2B28512C005AC131 /* CocoaLumberjackSwiftLogBackend */ = { + 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */ = { isa = XCSwiftPackageProductDependency; - package = 84DD7EF72B28512C005AC131 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; + package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjackSwiftLogBackend; }; C1E1EC7A286A025F0097EC74 /* SwiftSoup */ = { diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 40d6de49f7..0eaf40f866 100644 --- a/Monal/NotificationService/NotificationService.m +++ b/Monal/NotificationService/NotificationService.m @@ -295,6 +295,8 @@ -(void) pushExpired //WARNING: we have to closely watch apple...if they remove this 5 second gap between this call to the expiration handler and the actual //appex freeze, this sleep will no longer be harmless and could even cause smacks state corruption (by not diconnecting cleanly and having stanzas //still in the TCP queue delivered on next appex unfreeze even if they have been handled by the mainapp already) + //NOTE: not sure if that really can happen since we use file based locking, because iOS will kill processes holding such a lock + //when trying to freeze the process (and we would still hold the MLProcessLock for the appex when the freeze happens --> process kill) usleep(500000); //this returns YES if we got new pushes in the meantime --> do nothing if so @@ -469,139 +471,143 @@ -(void) dealloc -(void) didReceiveNotificationRequest:(UNNotificationRequest*) request withContentHandler:(void (^)(UNNotificationContent* _Nonnull)) contentHandler { - DDLogInfo(@"Notification handler called (request id: %@)", request.identifier); - DDLogInfo(@"Push userInfo: %@", request.content.userInfo); - [handlers addObject:contentHandler]; - - //only show this notification once a day at maximum (and if a build number was given in our push) - NSDate* lastAppVersionAlert = [[HelperTools defaultsDB] objectForKey:@"lastAppVersionAlert"]; - if((lastAppVersionAlert == nil || [[NSDate date] timeIntervalSinceDate:lastAppVersionAlert] > 86400) && request.content.userInfo[@"firstGoodBuildNumber"] != nil) - { - NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; - long buildNumber = ((NSString*)[infoDict objectForKey:@"CFBundleVersion"]).integerValue; - long firstGoodBuildNumber = ((NSNumber*)request.content.userInfo[@"firstGoodBuildNumber"]).integerValue; - BOOL isKnownGoodBuild = NO; - for(NSNumber* allowed in request.content.userInfo[@"knownGoodBuildNumber"]) - if(buildNumber == allowed.integerValue) - isKnownGoodBuild = YES; - DDLogDebug(@"current build number: %ld, firstGoodBuildNumber: %ld, isKnownGoodBuild: %@", buildNumber, firstGoodBuildNumber, bool2str(isKnownGoodBuild)); - if(buildNumber < firstGoodBuildNumber && !isKnownGoodBuild) + //make sure to handle complete push and properly proxy it while not racing with expired handlers + @synchronized(handlers) { + DDLogInfo(@"Notification handler called (request id: %@)", request.identifier); + DDLogInfo(@"Push userInfo: %@", request.content.userInfo); + [handlers addObject:contentHandler]; + + //only show this notification once a day at maximum (and if a build number was given in our push) + NSDate* lastAppVersionAlert = [[HelperTools defaultsDB] objectForKey:@"lastAppVersionAlert"]; + if((lastAppVersionAlert == nil || [[NSDate date] timeIntervalSinceDate:lastAppVersionAlert] > 86400) && request.content.userInfo[@"firstGoodBuildNumber"] != nil) { - UNMutableNotificationContent* tooOldContent = [UNMutableNotificationContent new]; - tooOldContent.title = NSLocalizedString(@"Very old app version", @""); - tooOldContent.subtitle = NSLocalizedString(@"Please update!", @""); - tooOldContent.body = NSLocalizedString(@"This app is too old and can contain security bugs as well as suddenly cease operation. Please Upgrade!", @""); - tooOldContent.sound = [UNNotificationSound defaultSound]; - UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:tooOldContent trigger:nil]; + NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; + long buildNumber = ((NSString*)[infoDict objectForKey:@"CFBundleVersion"]).integerValue; + long firstGoodBuildNumber = ((NSNumber*)request.content.userInfo[@"firstGoodBuildNumber"]).integerValue; + BOOL isKnownGoodBuild = NO; + for(NSNumber* allowed in request.content.userInfo[@"knownGoodBuildNumber"]) + if(buildNumber == allowed.integerValue) + isKnownGoodBuild = YES; + DDLogDebug(@"current build number: %ld, firstGoodBuildNumber: %ld, isKnownGoodBuild: %@", buildNumber, firstGoodBuildNumber, bool2str(isKnownGoodBuild)); + if(buildNumber < firstGoodBuildNumber && !isKnownGoodBuild) + { + UNMutableNotificationContent* tooOldContent = [UNMutableNotificationContent new]; + tooOldContent.title = NSLocalizedString(@"Very old app version", @""); + tooOldContent.subtitle = NSLocalizedString(@"Please update!", @""); + tooOldContent.body = NSLocalizedString(@"This app is too old and can contain security bugs as well as suddenly cease operation. Please Upgrade!", @""); + tooOldContent.sound = [UNNotificationSound defaultSound]; + UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:tooOldContent trigger:nil]; + NSError* error = [HelperTools postUserNotificationRequest:errorRequest]; + if(error) + DDLogError(@"Error posting local app-too-old notification: %@", error); + [[HelperTools defaultsDB] setObject:[NSDate now] forKey:@"lastAppVersionAlert"]; + [[HelperTools defaultsDB] synchronize]; + } + } + + #ifdef DEBUG + if(warnUnclean) + { + UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; + errorContent.title = NSLocalizedString(@"Unclean appex shutown", @""); + errorContent.body = NSLocalizedString(@"This should never happen, please contact the developers and provide a logfile!", @""); + errorContent.sound = [UNNotificationSound defaultSound]; + UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:errorContent trigger:nil]; NSError* error = [HelperTools postUserNotificationRequest:errorRequest]; if(error) - DDLogError(@"Error posting local app-too-old notification: %@", error); - [[HelperTools defaultsDB] setObject:[NSDate now] forKey:@"lastAppVersionAlert"]; - [[HelperTools defaultsDB] synchronize]; + DDLogError(@"Error posting local appex unclean shutdown error notification: %@", error); + else + warnUnclean = NO; //try again on error } + #endif + + //proxy to push singleton + DDLogDebug(@"proxying to incomingPush"); + [DDLog flushLog]; + [[PushSingleton instance] incomingPush:contentHandler]; + DDLogDebug(@"incomingPush proxy completed"); + [DDLog flushLog]; } - -#ifdef DEBUG - if(warnUnclean) - { - UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; - errorContent.title = NSLocalizedString(@"Unclean appex shutown", @""); - errorContent.body = NSLocalizedString(@"This should never happen, please contact the developers and provide a logfile!", @""); - errorContent.sound = [UNNotificationSound defaultSound]; - UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:errorContent trigger:nil]; - NSError* error = [HelperTools postUserNotificationRequest:errorRequest]; - if(error) - DDLogError(@"Error posting local appex unclean shutdown error notification: %@", error); - else - warnUnclean = NO; //try again on error - } -#endif - - //proxy to push singleton - DDLogDebug(@"proxying to incomingPush"); - [DDLog flushLog]; - [[PushSingleton instance] incomingPush:contentHandler]; - DDLogDebug(@"incomingPush proxy completed"); - [DDLog flushLog]; } -(void) serviceExtensionTimeWillExpire { - DDLogError(@"notification handler expired, that should never happen!"); - + @synchronized(handlers) { + DDLogError(@"notification handler expired, that should never happen!"); + /* -#ifdef DEBUG - UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; - errorContent.title = @"Unexpected appex expiration"; - errorContent.body = @"This should never happen, please contact the developers and provide a logfile!"; - errorContent.sound = [UNNotificationSound defaultSound]; - UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:errorContent trigger:nil]; - NSError* error = [HelperTools postUserNotificationRequest:errorRequest]; - if(error) - DDLogError(@"Error posting local appex expiration error notification: %@", error); -#endif - - //It seems the iOS induced deadlock unlocks itself after this expiration handler got called and even new pushes - //can come in while this handler is still running - //--> we just wait for 1.8 seconds to make sure the unlocking can happen - // (this should be greater than the 1.5 seconds waiting time on last pushes and possibly smaller than 2 seconds, - // cause that could be the time apple will kill us after) - //NOTE: the unlocking of our deadlock will feed this expired handler and no killing should occur - //WARNING: if it's a real deadlock not unlocking itself, apple will kill us nontheless, - // but that's not different to us committing suicide like in the old code commented below - usleep(1800000); + #ifdef DEBUG + UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; + errorContent.title = @"Unexpected appex expiration"; + errorContent.body = @"This should never happen, please contact the developers and provide a logfile!"; + errorContent.sound = [UNNotificationSound defaultSound]; + UNNotificationRequest* errorRequest = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:errorContent trigger:nil]; + NSError* error = [HelperTools postUserNotificationRequest:errorRequest]; + if(error) + DDLogError(@"Error posting local appex expiration error notification: %@", error); + #endif + + //It seems the iOS induced deadlock unlocks itself after this expiration handler got called and even new pushes + //can come in while this handler is still running + //--> we just wait for 1.8 seconds to make sure the unlocking can happen + // (this should be greater than the 1.5 seconds waiting time on last pushes and possibly smaller than 2 seconds, + // cause that could be the time apple will kill us after) + //NOTE: the unlocking of our deadlock will feed this expired handler and no killing should occur + //WARNING: if it's a real deadlock not unlocking itself, apple will kill us nontheless, + // but that's not different to us committing suicide like in the old code commented below + usleep(1800000); */ #ifdef DEBUG - if([handlers count] > 0) - { - //we don't want two error notifications for the user - [NotificationService setAppexCleanShutdownStatus:YES]; - - //we feed all handlers, these shouldn't be silenced already, because we wouldn't see this expiration - for(void (^_handler)(UNNotificationContent* _Nonnull) in handlers) + if([handlers count] > 0) { - DDLogError(@"Feeding handler with error notification: %@", _handler); - UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; - errorContent.title = NSLocalizedString(@"Unexpected appex expiration", @""); - errorContent.body = NSLocalizedString(@"This should never happen, please contact the developers and provide a logfile!", @""); - errorContent.sound = [UNNotificationSound defaultSound]; - _handler(errorContent); + //we don't want two error notifications for the user + [NotificationService setAppexCleanShutdownStatus:YES]; + + //we feed all handlers, these shouldn't be silenced already, because we wouldn't see this expiration + for(void (^_handler)(UNNotificationContent* _Nonnull) in handlers) + { + DDLogError(@"Feeding handler with error notification: %@", _handler); + UNMutableNotificationContent* errorContent = [UNMutableNotificationContent new]; + errorContent.title = NSLocalizedString(@"Unexpected appex expiration", @""); + errorContent.body = NSLocalizedString(@"This should never happen, please contact the developers and provide a logfile!", @""); + errorContent.sound = [UNNotificationSound defaultSound]; + _handler(errorContent); + } } - } - else - [NotificationService setAppexCleanShutdownStatus:NO]; + else + [NotificationService setAppexCleanShutdownStatus:NO]; #else - if([handlers count] > 0) - { - //we don't want two error notifications for the user - [NotificationService setAppexCleanShutdownStatus:YES]; - - //we feed all handlers, these shouldn't be silenced already, because we wouldn't see this expiration - for(void (^_handler)(UNNotificationContent* _Nonnull) in handlers) + if([handlers count] > 0) { - DDLogError(@"Feeding handler with silent notification: %@", _handler); - UNMutableNotificationContent* emptyContent = [UNMutableNotificationContent new]; - _handler(emptyContent); + //we don't want two error notifications for the user + [NotificationService setAppexCleanShutdownStatus:YES]; + + //we feed all handlers, these shouldn't be silenced already, because we wouldn't see this expiration + for(void (^_handler)(UNNotificationContent* _Nonnull) in handlers) + { + DDLogError(@"Feeding handler with silent notification: %@", _handler); + UNMutableNotificationContent* emptyContent = [UNMutableNotificationContent new]; + _handler(emptyContent); + } } - } - else - [NotificationService setAppexCleanShutdownStatus:NO]; + else + [NotificationService setAppexCleanShutdownStatus:NO]; #endif - DDLogInfo(@"Committing suicide..."); - [DDLog flushLog]; - exit(0); + DDLogInfo(@"Committing suicide..."); + [DDLog flushLog]; + exit(0); /* - //proxy to push singleton - DDLogDebug(@"proxying to pushExpired"); - [DDLog flushLog]; - [[PushSingleton instance] pushExpired]; - DDLogDebug(@"pushExpired proxy completed"); - [DDLog flushLog]; + //proxy to push singleton + DDLogDebug(@"proxying to pushExpired"); + [DDLog flushLog]; + [[PushSingleton instance] pushExpired]; + DDLogDebug(@"pushExpired proxy completed"); + [DDLog flushLog]; */ - + } } @end diff --git a/Monal/Podfile.lock b/Monal/Podfile.lock index dd65f5952d..d67b4df377 100644 --- a/Monal/Podfile.lock +++ b/Monal/Podfile.lock @@ -2,26 +2,26 @@ PODS: - ASN1Decoder (1.10.0) - DZNEmptyDataSet (1.8.1) - FLAnimatedImage (1.0.17) - - KSCrash/Core (1.16.2): + - KSCrash/Core (1.17.0): - KSCrash/Reporting/Filters/Basic - - KSCrash/Recording (1.16.2): - - KSCrash/Recording/Tools (= 1.16.2) - - KSCrash/Recording/Tools (1.16.2) - - KSCrash/Reporting/Filters/AppleFmt (1.16.2): + - KSCrash/Recording (1.17.0): + - KSCrash/Recording/Tools (= 1.17.0) + - KSCrash/Recording/Tools (1.17.0) + - KSCrash/Reporting/Filters/AppleFmt (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Base (1.16.2): + - KSCrash/Reporting/Filters/Base (1.17.0): - KSCrash/Recording - - KSCrash/Reporting/Filters/Basic (1.16.2): + - KSCrash/Reporting/Filters/Basic (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/GZip (1.16.2): + - KSCrash/Reporting/Filters/GZip (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/JSON (1.16.2): + - KSCrash/Reporting/Filters/JSON (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Sets (1.16.2): + - KSCrash/Reporting/Filters/Sets (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/AppleFmt - KSCrash/Reporting/Filters/Base @@ -29,12 +29,12 @@ PODS: - KSCrash/Reporting/Filters/GZip - KSCrash/Reporting/Filters/JSON - KSCrash/Reporting/Filters/Stringify - - KSCrash/Reporting/Filters/Stringify (1.16.2): + - KSCrash/Reporting/Filters/Stringify (1.17.0): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Tools (1.16.2): + - KSCrash/Reporting/Filters/Tools (1.17.0): - KSCrash/Recording - - KSCrash/Reporting/Tools (1.16.2): + - KSCrash/Reporting/Tools (1.17.0): - KSCrash/Recording - MarqueeLabel (4.3.2) - MBProgressHUD (1.2.0) @@ -42,9 +42,9 @@ PODS: - MarqueeLabel (~> 4.3.0) - SnapKit (~> 5.6.0) - SAMKeychain (1.5.3) - - SDWebImage (5.19.0): - - SDWebImage/Core (= 5.19.0) - - SDWebImage/Core (5.19.0) + - SDWebImage (5.19.1): + - SDWebImage/Core (= 5.19.1) + - SDWebImage/Core (5.19.1) - SignalProtocolC (2.3.3) - SignalProtocolObjC (1.1.1): - SignalProtocolC (~> 2.3.3) @@ -53,7 +53,7 @@ PODS: - sqlite3/perf-threadsafe (3.45.1): - sqlite3/common - TOCropViewController (2.6.1) - - WebRTC-lib (122.0.0) + - WebRTC-lib (123.0.0) DEPENDENCIES: - ASN1Decoder @@ -110,18 +110,18 @@ SPEC CHECKSUMS: ASN1Decoder: 91cb1d781b5a178ea7375b2f1519e2bdaaa4c427 DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7 FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b - KSCrash: 469b9f982b97309acb719baaecbf9182f62ddf85 + KSCrash: 593ec373759e4c1bce381421a627326a20d2dc66 MarqueeLabel: 15e524a6762552bb279cb17438b8a94990269fb9 MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406 NotificationBannerSwift: dce54ded532b26e30cd8e7f4d80e124a0f2ba7d1 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SDWebImage: 981fd7e860af070920f249fd092420006014c3eb + SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb SignalProtocolC: 8092866e45b663a6bc3e45a8d13bad2571dbf236 SignalProtocolObjC: 1beb46b1d35733e7ab96a919f88bac20ec771c73 SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - WebRTC-lib: 5e9d21edbcf9ce5cc66b7ec7f94f0c15a2779113 + WebRTC-lib: bb973dd47acf5bc48d8a935a92ae836b70599bc1 PODFILE CHECKSUM: f415f317e34fd11a687374f84ee8dfb14ebb29a6 diff --git a/Monal/PrivacyInfo.xcprivacy b/Monal/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..191f1572f2 --- /dev/null +++ b/Monal/PrivacyInfo.xcprivacy @@ -0,0 +1,30 @@ + + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index 9e80f78eaa..16e176ab65 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -2268,7 +2268,7 @@ - + @@ -2547,6 +2547,9 @@ + + + @@ -2568,9 +2571,6 @@ - - - diff --git a/Monal/localization/Base.lproj/Settings.storyboard b/Monal/localization/Base.lproj/Settings.storyboard index 4ea6f4c0d5..091283cf0a 100644 --- a/Monal/localization/Base.lproj/Settings.storyboard +++ b/Monal/localization/Base.lproj/Settings.storyboard @@ -244,7 +244,6 @@ - @@ -500,14 +499,14 @@ - + - + diff --git a/monal.doap b/monal.doap index a9d3829007..7fc3229462 100644 --- a/monal.doap +++ b/monal.doap @@ -653,11 +653,20 @@ complete - 6.0 - 0.3.0 + 6.3 + 0.4.1 XEP-0424: Message Retraction + + + + complete + 6.3 + 0.3.0 + XEP-0425: Moderated Message Retraction + + @@ -706,5 +715,14 @@ XEP-0480: SASL Upgrade Tasks + + + + complete + 6.3 + 0.1.0 + XEP-0490: Message Displayed Synchronization + + diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 38f2bcc7d3..07067bcb68 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "cfg-if" @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "form_urlencoded" @@ -69,9 +69,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "monal-panic-handler" @@ -95,9 +95,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags", "errno", @@ -161,14 +161,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] name = "swift-bridge" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72aa49fc376893673e6b7ace589802877d2dad554da1fde5aa1445498e1929c" +checksum = "72088d7882bd9c900d194cbc6008222c876450f68ce97212ac764775307bfd74" dependencies = [ "swift-bridge-build", "swift-bridge-macro", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "swift-bridge-build" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4282dc94afa1b7d8f2445532a7c0b7508d470cee261132e26ec001b3185c3b" +checksum = "a0216c84c63a11fb704946f9c4843c9fad28aaf2431cbbd674a37d86d71f2100" dependencies = [ "proc-macro2", "swift-bridge-ir", @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "swift-bridge-ir" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2096a82fb42280699d9b5dde6674cb4f802931eca8623f1edc4587c50dc58da" +checksum = "183036306714fcb1a53192dd80b89694eef24389b034f3392109b3447006550f" dependencies = [ "proc-macro2", "quote", @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "swift-bridge-macro" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2f4d5b41976c351ecb735eb9336bdd3cbaaebc9990300023ea2b6874a2d68c" +checksum = "89560c6f6a3b65ec983c6fca5eb9d5e4c839ff41d8162c24339e258a20bf04a6" dependencies = [ "proc-macro2", "quote", @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote",