mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
commit
62bd3ece25
62 changed files with 879 additions and 529 deletions
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 6ea6f3fb1559694d5159dc282cb7842982fdb30b
|
||||
Subproject commit 25319b47801e3de4777084329f418fcfe36be04d
|
|
@ -874,7 +874,7 @@ static NSTimeInterval launchStartedAt;
|
|||
return;
|
||||
}
|
||||
|
||||
[SignalApp.sharedApp.homeViewController showNewConversationView];
|
||||
[SignalApp.sharedApp.homeViewController showNewConversationVC];
|
||||
|
||||
completionHandler(YES);
|
||||
}];
|
||||
|
|
|
@ -83,7 +83,7 @@ final class FriendRequestView : UIView {
|
|||
// MARK: Updating
|
||||
@objc private func handleFriendRequestStatusChangedNotification(_ notification: Notification) {
|
||||
let messageID = notification.object as! String
|
||||
guard messageID == message.uniqueId else { return }
|
||||
guard messageID == message.uniqueId && TSMessage.fetch(uniqueId: messageID) != nil else { return } // It's possible for the message to be deleted at this point
|
||||
message.reload()
|
||||
updateUI()
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ public final class LokiRSSFeedPoller : NSObject {
|
|||
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
|
||||
envelope.setContent(try! content.build().serializedData())
|
||||
OWSPrimaryStorage.shared().dbReadWriteConnection.readWrite { transaction in
|
||||
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction)
|
||||
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,10 +106,6 @@ final class NewConversationVC : OWSViewController, OWSQRScannerDelegate {
|
|||
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
} else if OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey == hexEncodedPublicKey {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Can't Start Conversation", comment: ""), message: NSLocalizedString("Please enter the public key of the person you'd like to message.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
} else {
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
|
||||
Analytics.shared.track("New Conversation Started")
|
||||
|
|
|
@ -5,7 +5,6 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
private var mode: Mode = .register { didSet { if mode != oldValue { handleModeChanged() } } }
|
||||
private var seed: Data! { didSet { updateMnemonic() } }
|
||||
private var mnemonic: String! { didSet { handleMnemonicChanged() } }
|
||||
private var linkingRequestMessageSendingTimer: Timer?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var registerStackView: UIStackView = {
|
||||
|
@ -50,7 +49,7 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
}()
|
||||
|
||||
private lazy var linkButton1: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device (Coming Soon)", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton1"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
|
@ -111,7 +110,7 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
}()
|
||||
|
||||
private lazy var linkButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device (Coming Soon)", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
|
@ -224,11 +223,6 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
restoreStackView.autoVCenterInSuperview()
|
||||
linkStackView.autoPinWidthToSuperview()
|
||||
linkStackView.autoVCenterInSuperview()
|
||||
// TODO: Enable this again later
|
||||
linkButton1.isUserInteractionEnabled = false
|
||||
linkButton1.alpha = 0.24
|
||||
linkButton2.isUserInteractionEnabled = false
|
||||
linkButton2.alpha = 0.24
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
|
@ -352,21 +346,13 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
present(deviceLinkingModal, animated: true, completion: nil)
|
||||
let masterHexEncodedPublicKey = masterHexEncodedPublicKeyTextField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
let linkingRequestMessage = DeviceLinkingUtilities.getLinkingRequestMessage(for: masterHexEncodedPublicKey)
|
||||
linkingRequestMessageSendingTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
||||
self?.sendLinkingRequestMessage(linkingRequestMessage)
|
||||
}
|
||||
sendLinkingRequestMessage(linkingRequestMessage)
|
||||
ThreadUtil.enqueue(linkingRequestMessage)
|
||||
} else {
|
||||
onboardingController.pushDisplayNameVC(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendLinkingRequestMessage(_ linkingRequestMessage: DeviceLinkMessage) {
|
||||
ThreadUtil.enqueue(linkingRequestMessage)
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
linkingRequestMessageSendingTimer?.invalidate()
|
||||
let userDefaults = UserDefaults.standard
|
||||
userDefaults.set(true, forKey: "didUpdateForMainnet")
|
||||
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
|
||||
|
@ -375,7 +361,6 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
}
|
||||
|
||||
func handleDeviceLinkingModalDismissed() {
|
||||
linkingRequestMessageSendingTimer?.invalidate()
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopLongPollerIfNeeded()
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
|
|
|
@ -138,7 +138,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
@objc private func authorizeDeviceLink() {
|
||||
let deviceLink = self.deviceLink!
|
||||
let linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(for: deviceLink)
|
||||
(0..<4).forEach { _ in ThreadUtil.enqueue(linkingAuthorizationMessage) }
|
||||
ThreadUtil.enqueue(linkingAuthorizationMessage)
|
||||
let session = DeviceLinkingSession.current!
|
||||
session.stopListeningForLinkingRequests()
|
||||
session.markLinkingRequestAsProcessed()
|
||||
|
@ -148,6 +148,9 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
LokiStorageAPI.addDeviceLink(signedDeviceLink).catch { error in
|
||||
print("[Loki] Failed to add device link due to error: \(error).")
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 8, repeats: false) { _ in
|
||||
let _ = SSKEnvironment.shared.syncManager.syncAllContacts()
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
|
|
|
@ -116,6 +116,8 @@ class ConversationViewItemActions: NSObject {
|
|||
class func mediaActions(conversationViewItem: ConversationViewItem, shouldAllowReply: Bool, delegate: MessageActionsDelegate) -> [MenuAction] {
|
||||
var actions: [MenuAction] = []
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
|
||||
if shouldAllowReply {
|
||||
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(replyAction)
|
||||
|
@ -132,8 +134,10 @@ class ConversationViewItemActions: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(deleteAction)
|
||||
if !isGroup || conversationViewItem.userCanDeleteGroupMessage {
|
||||
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(deleteAction)
|
||||
}
|
||||
|
||||
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(showDetailsAction)
|
||||
|
|
|
@ -250,11 +250,7 @@
|
|||
[section addItem:[OWSTableItem itemWithTitle:NSLocalizedString(@"Show QR Code", @"") actionBlock:^{ [weakSelf showQRCode]; }]];
|
||||
if (isMasterDevice) {
|
||||
[section addItem:[OWSTableItem itemWithTitle:NSLocalizedString(@"Link Device", @"") actionBlock:^{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Coming Soon" message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { }]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
// TODO: Enable this again later
|
||||
// [weakSelf linkDevice];
|
||||
[weakSelf linkDevice];
|
||||
}]];
|
||||
[section addItem:[OWSTableItem itemWithTitle:NSLocalizedString(@"Show Seed", @"") actionBlock:^{ [weakSelf showSeed]; }]];
|
||||
}
|
||||
|
|
|
@ -213,7 +213,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
if (self.message.isFriendRequest) {
|
||||
// Loki: Attach the friend request view if needed
|
||||
if ([self shouldShowFriendRequestUIForMessage:self.message]) {
|
||||
self.friendRequestView = [[LKFriendRequestView alloc] initWithMessage:self.message];
|
||||
self.friendRequestView.delegate = self.friendRequestViewDelegate;
|
||||
[self.contentView addSubview:self.friendRequestView];
|
||||
|
@ -298,6 +299,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
self.avatarView.image = authorAvatarImage;
|
||||
[self.contentView addSubview:self.avatarView];
|
||||
|
||||
// Loki: Show the moderator icon if needed
|
||||
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) {
|
||||
__block LKPublicChat *publicChat;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
|
@ -383,7 +385,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
cellSize.width += self.sendFailureBadgeSize + self.sendFailureBadgeSpacing;
|
||||
}
|
||||
|
||||
if (self.message.isFriendRequest) {
|
||||
// Loki: Include the friend request view if needed
|
||||
if ([self shouldShowFriendRequestUIForMessage:self.message]) {
|
||||
cellSize.height += [LKFriendRequestView calculateHeightWithMessage:self.message conversationStyle:self.conversationStyle];
|
||||
}
|
||||
|
||||
|
@ -468,7 +471,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
if (self.message.isFriendRequest) {
|
||||
if ([self shouldShowFriendRequestUIForMessage:self.message]) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -530,6 +533,39 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return location.y <= headerBottom.y;
|
||||
}
|
||||
|
||||
#pragma mark - Convenience
|
||||
|
||||
- (BOOL)shouldShowFriendRequestUIForMessage:(TSMessage *)message
|
||||
{
|
||||
if ([message isKindOfClass:TSOutgoingMessage.class]) {
|
||||
return message.isFriendRequest;
|
||||
} else {
|
||||
if (message.isFriendRequest) {
|
||||
// Only show the first friend request that was received
|
||||
NSString *senderID = ((TSIncomingMessage *)message).authorId;
|
||||
__block NSMutableSet<TSContactThread *> *linkedDeviceThreads;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction].mutableCopy;
|
||||
}];
|
||||
NSMutableArray<TSIncomingMessage *> *allFriendRequestMessages = @[].mutableCopy;
|
||||
for (TSContactThread *thread in linkedDeviceThreads) {
|
||||
[thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
|
||||
TSIncomingMessage *message = [interaction as:TSIncomingMessage.class];
|
||||
if (message != nil && message.isFriendRequest) {
|
||||
[allFriendRequestMessages addObject:message];
|
||||
}
|
||||
}];
|
||||
}
|
||||
[allFriendRequestMessages sortUsingComparator:^NSComparisonResult(TSIncomingMessage *lhs, TSIncomingMessage *rhs) {
|
||||
return [@(lhs.timestamp) compare:@(rhs.timestamp)] == NSOrderedDescending;
|
||||
}];
|
||||
return [message.uniqueId isEqual:allFriendRequestMessages.firstObject.uniqueId];
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -146,7 +146,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
break;
|
||||
}
|
||||
|
||||
if (statusIndicatorImage) {
|
||||
__block BOOL isNoteToSelf = NO;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSContactThread *thread = [outgoingMessage.thread as:TSContactThread.class];
|
||||
if (thread != nil) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction];
|
||||
}
|
||||
}];
|
||||
|
||||
if (statusIndicatorImage && !isNoteToSelf) {
|
||||
[self showStatusIndicatorWithIcon:statusIndicatorImage textColor:textColor];
|
||||
} else {
|
||||
[self hideStatusIndicator];
|
||||
|
@ -218,7 +226,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
timestampLabelText = [DateUtil formatMessageTimestamp:viewItem.interaction.timestamp];
|
||||
}
|
||||
|
||||
TSMessage *message = (TSMessage *)[viewItem.interaction as:TSMessage.class];
|
||||
TSMessage *message = [viewItem.interaction as:TSMessage.class];
|
||||
if (message != nil && message.isP2P) {
|
||||
NSString *string = [timestampLabelText.localizedUppercaseString stringByAppendingString:@" · P2P"];
|
||||
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];
|
||||
|
|
|
@ -1644,12 +1644,36 @@ typedef enum : NSUInteger {
|
|||
#pragma mark - Updating
|
||||
|
||||
- (void)updateInputToolbar {
|
||||
BOOL hasPendingFriendRequest = self.thread.hasPendingFriendRequest;
|
||||
[self.inputToolbar setUserInteractionEnabled:!hasPendingFriendRequest];
|
||||
NSString *placeholderText = hasPendingFriendRequest ? NSLocalizedString(@"Pending Friend Request...", "") : NSLocalizedString(@"New Message", "");
|
||||
BOOL isEnabled;
|
||||
BOOL isAttachmentButtonHidden;
|
||||
if ([self.thread isKindOfClass:TSContactThread.class]) {
|
||||
NSString *senderID = ((TSContactThread *)self.thread).contactIdentifier;
|
||||
__block NSSet<TSContactThread *> *linkedDeviceThreads;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction];
|
||||
}];
|
||||
if ([linkedDeviceThreads contains:^BOOL(TSContactThread *thread) {
|
||||
return thread.isContactFriend;
|
||||
}]) {
|
||||
isEnabled = true;
|
||||
isAttachmentButtonHidden = false;
|
||||
} else if (![linkedDeviceThreads contains:^BOOL(TSContactThread *thread) {
|
||||
return thread.hasPendingFriendRequest;
|
||||
}]) {
|
||||
isEnabled = true;
|
||||
isAttachmentButtonHidden = true;
|
||||
} else {
|
||||
isEnabled = false;
|
||||
isAttachmentButtonHidden = true;
|
||||
}
|
||||
} else {
|
||||
isEnabled = true;
|
||||
isAttachmentButtonHidden = false;
|
||||
}
|
||||
[self.inputToolbar setUserInteractionEnabled:isEnabled];
|
||||
NSString *placeholderText = isEnabled ? NSLocalizedString(@"New Message", "") : NSLocalizedString(@"Pending Friend Request...", "");
|
||||
[self.inputToolbar setPlaceholderText:placeholderText];
|
||||
BOOL isContactFriend = self.thread.isContactFriend;
|
||||
[self.inputToolbar setAttachmentButtonHidden:(!isContactFriend && !self.thread.isGroupThread)];
|
||||
[self.inputToolbar setAttachmentButtonHidden:isAttachmentButtonHidden];
|
||||
}
|
||||
|
||||
#pragma mark - Identity
|
||||
|
@ -3793,8 +3817,7 @@ typedef enum : NSUInteger {
|
|||
if (isBackspace) {
|
||||
self.currentMentionStartIndex = -1;
|
||||
[self.inputToolbar hideMentionCandidateSelectionView];
|
||||
NSArray *mentionsToRemove = [self.mentions filtered:^BOOL(NSObject *object) {
|
||||
LKMention *mention = (LKMention *)object;
|
||||
NSArray *mentionsToRemove = [self.mentions filtered:^BOOL(LKMention *mention) {
|
||||
return ![mention isContainedIn:newText];
|
||||
}];
|
||||
[self.mentions removeObjectsInArray:mentionsToRemove];
|
||||
|
@ -4428,10 +4451,26 @@ typedef enum : NSUInteger {
|
|||
|
||||
- (void)acceptFriendRequest:(TSIncomingMessage *)friendRequest
|
||||
{
|
||||
// Accept all outstanding friend requests associated with this user and try to establish sessions with the
|
||||
// subset of their devices that haven't sent a friend request.
|
||||
NSString *senderID = friendRequest.authorId;
|
||||
__block NSSet<TSContactThread *> *linkedDeviceThreads;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction];
|
||||
}];
|
||||
for (TSContactThread *thread in linkedDeviceThreads) {
|
||||
if (thread.hasPendingFriendRequest) {
|
||||
[ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:self.thread];
|
||||
} else {
|
||||
OWSMessageSender *messageSender = SSKEnvironment.shared.messageSender;
|
||||
OWSMessageSend *automatedFriendRequestMessage = [messageSender getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:thread.contactIdentifier];
|
||||
dispatch_async(OWSDispatch.sendingQueue, ^{
|
||||
[messageSender sendMessage:automatedFriendRequestMessage];
|
||||
});
|
||||
}
|
||||
}
|
||||
// Update the thread's friend request status
|
||||
[self.thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:nil];
|
||||
// Send a friend request accepted message
|
||||
[ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:self.thread];
|
||||
}
|
||||
|
||||
- (void)declineFriendRequest:(TSIncomingMessage *)friendRequest
|
||||
|
|
|
@ -1269,13 +1269,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
}];
|
||||
if (publicChat == nil) return false;
|
||||
|
||||
// Only allow deletion on incoming messages if the user has moderation permission
|
||||
if (interationType == OWSInteractionType_IncomingMessage) {
|
||||
BOOL isModerator = [LKPublicChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:publicChat.channel onServer:publicChat.server];
|
||||
if (!isModerator) return false;
|
||||
// Only allow deletion on incoming messages if the user has moderation permission
|
||||
return [LKPublicChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:publicChat.channel onServer:publicChat.server];
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// Only allow deletion on outgoing messages if the user was the sender (i.e. it was not sent from another linked device)
|
||||
return [self.interaction.actualSenderHexEncodedPublicKey isEqual:self.userHexEncodedPublicKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
animated:(BOOL)isAnimated;
|
||||
|
||||
// Used by force-touch Springboard icon shortcut
|
||||
- (void)showNewConversationView;
|
||||
- (void)showNewConversationVC;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -2624,7 +2624,6 @@
|
|||
"Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn't be used to transmit sensitive information." = "Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn't be used to transmit sensitive information.";
|
||||
"Copy Public Key" = "Copy Public Key";
|
||||
"Link Device" = "Link Device";
|
||||
"Link Device (Coming Soon)" = "Link Device (Coming Soon)";
|
||||
"Waiting for Device" = "Waiting for Device";
|
||||
"Waiting for Authorization" = "Waiting for Authorization";
|
||||
"Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Loki Messenger Account\" step to start the linking process" = "Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Loki Messenger Account\" step to start the linking process";
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import <SignalServiceKit/TSAccountManager.h>
|
||||
#import <SignalServiceKit/YapDatabaseConnection+OWS.h>
|
||||
#import <SignalServiceKit/TSContactThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -265,32 +266,27 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
|
|||
|
||||
- (AnyPromise *)syncAllContacts
|
||||
{
|
||||
return [self syncContactsForSignalAccounts:self.contactsManager.signalAccounts];
|
||||
NSMutableArray<SignalAccount *> *friends = @[].mutableCopy;
|
||||
[TSContactThread enumerateCollectionObjectsUsingBlock:^(TSContactThread *thread, BOOL *stop) {
|
||||
NSString *hexEncodedPublicKey = thread.contactIdentifier;
|
||||
if (hexEncodedPublicKey != nil) {
|
||||
[friends addObject:[[SignalAccount alloc] initWithRecipientId:hexEncodedPublicKey]];
|
||||
}
|
||||
}];
|
||||
return [self syncContactsForSignalAccounts:friends];
|
||||
}
|
||||
|
||||
- (AnyPromise *)syncContactsForSignalAccounts:(NSArray<SignalAccount *> *)signalAccounts
|
||||
{
|
||||
OWSSyncContactsMessage *syncContactsMessage =
|
||||
[[OWSSyncContactsMessage alloc] initWithSignalAccounts:signalAccounts
|
||||
identityManager:self.identityManager
|
||||
profileManager:self.profileManager];
|
||||
__block DataSource *dataSource;
|
||||
[self.readDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
dataSource = [DataSourceValue
|
||||
dataSourceWithSyncMessageData:[syncContactsMessage
|
||||
buildPlainTextAttachmentDataWithTransaction:transaction]];
|
||||
}];
|
||||
|
||||
OWSSyncContactsMessage *syncContactsMessage = [[OWSSyncContactsMessage alloc] initWithSignalAccounts:signalAccounts identityManager:self.identityManager profileManager:self.profileManager];
|
||||
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
[self.messageSender sendTemporaryAttachment:dataSource
|
||||
contentType:OWSMimeTypeApplicationOctetStream
|
||||
inMessage:syncContactsMessage
|
||||
[self.messageSender sendMessage:syncContactsMessage
|
||||
success:^{
|
||||
OWSLogInfo(@"Successfully sent contacts sync message.");
|
||||
resolve(@(1));
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to send contacts sync message with error: %@", error);
|
||||
OWSLogError(@"Failed to send contacts sync message with error: %@.", error);
|
||||
resolve(error);
|
||||
}];
|
||||
}];
|
||||
|
|
|
@ -17,6 +17,6 @@ files.
|
|||
|
||||
## Building Protobuf
|
||||
|
||||
cd ~/src/WhisperSystems/SignalServiceKit/protobuf
|
||||
cd SignalServiceKit/protobuf
|
||||
make
|
||||
|
||||
|
|
|
@ -102,6 +102,10 @@ typedef NS_ENUM(NSInteger, LKThreadFriendRequestStatus) {
|
|||
|
||||
#pragma mark Interactions
|
||||
|
||||
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, YapDatabaseReadTransaction *transaction))block;
|
||||
|
||||
- (void)enumerateInteractionsUsingBlock:(void (^)(TSInteraction *interaction))block;
|
||||
|
||||
/**
|
||||
* @return The number of interactions in this thread.
|
||||
*/
|
||||
|
|
|
@ -739,7 +739,7 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
|
|||
|
||||
// We want to remove any old incoming friend request messages which are pending
|
||||
if (interactionType == OWSInteractionType_IncomingMessage) {
|
||||
removeMessage = message.friendRequestStatus == LKMessageFriendRequestStatusPending;
|
||||
removeMessage = YES;
|
||||
} else {
|
||||
// Or if we're sending then remove any failed friend request messages
|
||||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message;
|
||||
|
|
|
@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
// Returns NO on error.
|
||||
- (BOOL)writeData:(NSData *)data;
|
||||
- (BOOL)writeUInt32:(UInt32)value;
|
||||
- (BOOL)writeVariableLengthUInt32:(UInt32)value;
|
||||
|
||||
@end
|
||||
|
|
|
@ -63,6 +63,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeUInt32:(UInt32)value {
|
||||
NSData *data = [[NSData alloc] initWithBytes:&value length:sizeof(value)];
|
||||
return [self writeData:data];
|
||||
}
|
||||
|
||||
- (BOOL)writeVariableLengthUInt32:(UInt32)value
|
||||
{
|
||||
while (YES) {
|
||||
|
|
|
@ -28,12 +28,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration *)disappearingMessagesConfiguration
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
OWSAssertDebug(signalAccount.contact);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
SSKProtoContactDetailsBuilder *contactBuilder =
|
||||
[SSKProtoContactDetails builderWithNumber:signalAccount.recipientId];
|
||||
[contactBuilder setName:signalAccount.contact.fullName];
|
||||
[contactBuilder setName:[LKDisplayNameUtilities getPrivateChatDisplayNameFor:signalAccount.recipientId] ?: signalAccount.recipientId];
|
||||
[contactBuilder setColor:conversationColorName];
|
||||
|
||||
if (recipientIdentity != nil) {
|
||||
|
@ -48,6 +47,7 @@ disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration
|
|||
contactBuilder.verified = verified;
|
||||
}
|
||||
|
||||
/*
|
||||
UIImage *_Nullable rawAvatar = [contactsManager avatarImageForCNContactId:signalAccount.contact.cnContactId];
|
||||
NSData *_Nullable avatarPng;
|
||||
if (rawAvatar) {
|
||||
|
@ -66,6 +66,7 @@ disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration
|
|||
[contactBuilder setAvatar:avatar];
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (profileKeyData) {
|
||||
OWSAssertDebug(profileKeyData.length == kAES256_KeyByteLength);
|
||||
|
@ -93,12 +94,14 @@ disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration
|
|||
}
|
||||
|
||||
uint32_t contactDataLength = (uint32_t)contactData.length;
|
||||
[self writeVariableLengthUInt32:contactDataLength];
|
||||
[self writeUInt32:contactDataLength];
|
||||
[self writeData:contactData];
|
||||
|
||||
/*
|
||||
if (avatarPng) {
|
||||
[self writeData:avatarPng];
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
+ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
|
||||
serverID:(uint64_t)serverID
|
||||
attachmentHandler:(void (^)(
|
||||
NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#import "TSQuotedMessage.h"
|
||||
#import "TSThread.h"
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import "OWSPrimaryStorage+Loki.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -60,6 +61,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
#pragma mark -
|
||||
|
||||
+ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)transcript
|
||||
serverID:(uint64_t)serverID
|
||||
attachmentHandler:(void (^)(
|
||||
NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
|
@ -102,6 +104,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
quotedMessage:transcript.quotedMessage
|
||||
contactShare:transcript.contact
|
||||
linkPreview:transcript.linkPreview];
|
||||
|
||||
if (serverID != 0) {
|
||||
outgoingMessage.groupChatServerID = serverID;
|
||||
}
|
||||
|
||||
NSArray<TSAttachmentPointer *> *attachmentPointers =
|
||||
[TSAttachmentPointer attachmentPointersFromProtos:transcript.attachmentPointerProtos
|
||||
|
@ -129,6 +135,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[outgoingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream];
|
||||
[outgoingMessage saveWithTransaction:transaction];
|
||||
if (serverID != 0) {
|
||||
[OWSPrimaryStorage.sharedManager setIDForMessageWithServerID:serverID to:outgoingMessage.uniqueId in:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
|
|
|
@ -63,7 +63,7 @@ public extension LokiAPI {
|
|||
]
|
||||
])
|
||||
print("[Loki] Invoking get_n_service_nodes on \(target).")
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { intermediate in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in
|
||||
let rawResponse = intermediate.responseObject
|
||||
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw "Failed to update random snode pool from: \(rawResponse)." }
|
||||
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
|
||||
|
@ -74,7 +74,7 @@ public extension LokiAPI {
|
|||
return LokiAPITarget(address: "https://\(address)", port: UInt16(port))
|
||||
})
|
||||
return randomSnodePool.randomElement()!
|
||||
}.recover { error -> Promise<LokiAPITarget> in
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiAPITarget> in
|
||||
print("[Loki] Failed to contact seed node at: \(target).")
|
||||
Analytics.shared.track("Seed Node Failed")
|
||||
throw error
|
||||
|
@ -91,7 +91,7 @@ public extension LokiAPI {
|
|||
return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) }
|
||||
} else {
|
||||
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
|
||||
return getRandomSnode().then { invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
|
||||
return getRandomSnode().then(on: DispatchQueue.global()) { invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ public extension LokiAPI {
|
|||
internal extension Promise {
|
||||
|
||||
internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
|
||||
return recover { error -> Promise<T> in
|
||||
return recover(on: DispatchQueue.global()) { error -> Promise<T> in
|
||||
if let error = error as? NetworkManagerError {
|
||||
switch error.statusCode {
|
||||
case 0, 400, 500, 503:
|
||||
|
|
|
@ -2,7 +2,7 @@ import PromiseKit
|
|||
|
||||
@objc(LKAPI)
|
||||
public final class LokiAPI : NSObject {
|
||||
private static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
|
||||
public static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
|
||||
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
|
||||
|
||||
// MARK: Convenience
|
||||
|
@ -14,12 +14,12 @@ public final class LokiAPI : NSObject {
|
|||
private static let maxRetryCount: UInt = 8
|
||||
private static let defaultTimeout: TimeInterval = 20
|
||||
private static let longPollingTimeout: TimeInterval = 40
|
||||
private static let deviceLinkUpdateInterval: TimeInterval = 8 * 60
|
||||
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
|
||||
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
|
||||
private static var userIDScanLimit: UInt = 4096
|
||||
internal static var powDifficulty: UInt = 4
|
||||
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
|
||||
public static let deviceLinkUpdateInterval: TimeInterval = 60
|
||||
|
||||
// MARK: Types
|
||||
public typealias RawResponse = Any
|
||||
|
@ -87,7 +87,7 @@ public final class LokiAPI : NSObject {
|
|||
let headers = request.allHTTPHeaderFields ?? [:]
|
||||
let headersDescription = headers.isEmpty ? "no custom headers specified" : headers.prettifiedDescription
|
||||
print("[Loki] Invoking \(method.rawValue) on \(target) with \(parameters.prettifiedDescription) (\(headersDescription)).")
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }
|
||||
.handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey).recoveringNetworkErrorsIfNeeded()
|
||||
}
|
||||
|
||||
|
@ -130,10 +130,10 @@ public final class LokiAPI : NSObject {
|
|||
if timeSinceLastUpdate > deviceLinkUpdateInterval {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
|
||||
LokiStorageAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done { _ in
|
||||
LokiStorageAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done(on: DispatchQueue.global()) { _ in
|
||||
getDestinations()
|
||||
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
|
||||
}.catch { error in
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
if case LokiDotNetAPI.Error.parsingFailed = error {
|
||||
// Don't immediately re-fetch in case of failure due to a parsing error
|
||||
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
|
||||
|
@ -157,7 +157,7 @@ public final class LokiAPI : NSObject {
|
|||
return invoke(.sendMessage, on: target, associatedWith: destination, parameters: parameters)
|
||||
}
|
||||
func sendLokiMessageUsingSwarmAPI() -> Promise<Set<RawResponsePromise>> {
|
||||
return lokiMessage.calculatePoW().then { lokiMessageWithPoW in
|
||||
return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW in
|
||||
return getTargetSnodes(for: destination).map { swarm in
|
||||
return Set(swarm.map { target in
|
||||
sendLokiMessage(lokiMessageWithPoW, to: target).map { rawResponse in
|
||||
|
@ -179,7 +179,7 @@ public final class LokiAPI : NSObject {
|
|||
return Promise.value([ target ]).mapValues { sendLokiMessage(lokiMessage, to: $0) }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount).get { _ in
|
||||
LokiP2PAPI.markOnline(destination)
|
||||
onP2PSuccess()
|
||||
}.recover { error -> Promise<Set<RawResponsePromise>> in
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<Set<RawResponsePromise>> in
|
||||
LokiP2PAPI.markOffline(destination)
|
||||
if lokiMessage.isPing {
|
||||
print("[Loki] Failed to ping \(destination); marking contact as offline.")
|
||||
|
@ -368,7 +368,7 @@ public final class LokiAPI : NSObject {
|
|||
private extension Promise {
|
||||
|
||||
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
|
||||
return recover() { error -> Promise<T> in
|
||||
return recover(on: DispatchQueue.global()) { error -> Promise<T> in
|
||||
switch error {
|
||||
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
|
||||
default: throw error
|
||||
|
|
|
@ -40,7 +40,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
|
||||
let isEncryptionRequired = (server == LokiStorageAPI.server)
|
||||
return Promise<Void>() { seal in
|
||||
getAuthToken(for: server).done { token in
|
||||
getAuthToken(for: server).done(on: DispatchQueue.global()) { token in
|
||||
let data: Data
|
||||
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
|
||||
print("[Loki] Couldn't read attachment data from disk.")
|
||||
|
@ -104,7 +104,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
return seal.fulfill(())
|
||||
})
|
||||
task.resume()
|
||||
}.catch { error in
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
print("[Loki] Couldn't upload attachment.")
|
||||
seal.reject(error)
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
if let token = getAuthTokenFromDatabase(for: server) {
|
||||
return Promise.value(token)
|
||||
} else {
|
||||
return requestNewAuthToken(for: server).then { submitAuthToken($0, for: server) }.map { token -> String in
|
||||
return requestNewAuthToken(for: server).then(on: DispatchQueue.global()) { submitAuthToken($0, for: server) }.map { token -> String in
|
||||
setAuthToken(for: server, to: token)
|
||||
return token
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
let queryParameters = "pubKey=\(userHexEncodedPublicKey)"
|
||||
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
|
||||
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
|
||||
throw Error.parsingFailed
|
||||
|
@ -153,7 +153,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
|
||||
let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ]
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { _ in token }
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in token }
|
||||
}
|
||||
|
||||
// MARK: Attachments (Public Obj-C API)
|
||||
|
|
|
@ -42,7 +42,7 @@ public final class LokiLongPoller : NSObject {
|
|||
// MARK: Private API
|
||||
private func openConnections() {
|
||||
guard !hasStopped else { return }
|
||||
LokiAPI.getSwarm(for: userHexEncodedPublicKey).then { [weak self] _ -> Guarantee<[Result<Void>]> in
|
||||
LokiAPI.getSwarm(for: userHexEncodedPublicKey).then(on: DispatchQueue.global()) { [weak self] _ -> Guarantee<[Result<Void>]> in
|
||||
guard let strongSelf = self else { return Guarantee.value([Result<Void>]()) }
|
||||
strongSelf.usedSnodes.removeAll()
|
||||
let connections: [Promise<Void>] = (0..<strongSelf.connectionCount).map { _ in
|
||||
|
@ -52,7 +52,7 @@ public final class LokiLongPoller : NSObject {
|
|||
}
|
||||
strongSelf.connections = Set(connections)
|
||||
return when(resolved: connections)
|
||||
}.ensure { [weak self] in
|
||||
}.ensure(on: DispatchQueue.global()) { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
Timer.scheduledTimer(withTimeInterval: strongSelf.retryInterval, repeats: false) { _ in
|
||||
guard let strongSelf = self else { return }
|
||||
|
@ -69,7 +69,7 @@ public final class LokiLongPoller : NSObject {
|
|||
let nextSnode = unusedSnodes.randomElement()!
|
||||
usedSnodes.insert(nextSnode)
|
||||
print("[Loki] Opening long polling connection to \(nextSnode).")
|
||||
longPoll(nextSnode, seal: seal).catch { [weak self] error in
|
||||
longPoll(nextSnode, seal: seal).catch(on: DispatchQueue.global()) { [weak self] error in
|
||||
print("[Loki] Long polling connection to \(nextSnode) failed; dropping it and switching to next snode.")
|
||||
LokiAPI.dropIfNeeded(nextSnode, hexEncodedPublicKey: userHexEncodedPublicKey)
|
||||
self?.openConnectionToNextSnode(seal: seal)
|
||||
|
@ -80,11 +80,16 @@ public final class LokiLongPoller : NSObject {
|
|||
}
|
||||
|
||||
private func longPoll(_ target: LokiAPITarget, seal: Resolver<Void>) -> Promise<Void> {
|
||||
return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then { [weak self] rawResponse -> Promise<Void> in
|
||||
return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then(on: DispatchQueue.global()) { [weak self] rawResponse -> Promise<Void> in
|
||||
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) }
|
||||
let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target)
|
||||
strongSelf.onMessagesReceived(messages)
|
||||
return strongSelf.longPoll(target, seal: seal)
|
||||
let hexEncodedPublicKeys = Set(messages.compactMap { $0.source })
|
||||
let promises = hexEncodedPublicKeys.map { LokiAPI.getDestinations(for: $0) }
|
||||
return when(resolved: promises).then(on: DispatchQueue.global()) { _ -> Promise<Void> in
|
||||
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) }
|
||||
strongSelf.onMessagesReceived(messages)
|
||||
return strongSelf.longPoll(target, seal: seal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ public struct LokiMessage {
|
|||
/// - Returns: The promise of a new message with its `timestamp` and `nonce` set.
|
||||
public func calculatePoW() -> Promise<LokiMessage> {
|
||||
return Promise<LokiMessage> { seal in
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
DispatchQueue.global().async {
|
||||
let now = NSDate.ows_millisecondTimeStamp()
|
||||
let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented
|
||||
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) {
|
||||
|
|
|
@ -18,50 +18,58 @@ public final class LokiStorageAPI : LokiDotNetAPI {
|
|||
/// Gets the device links associated with the given hex encoded public key from the
|
||||
/// server and stores and returns the valid ones.
|
||||
public static func getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> Promise<Set<DeviceLink>> {
|
||||
print("[Loki] Getting device links for: \(hexEncodedPublicKey).")
|
||||
return getAuthToken(for: server).then { token -> Promise<Set<DeviceLink>> in
|
||||
let queryParameters = "include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users/@\(hexEncodedPublicKey)?\(queryParameters)")!
|
||||
return getDeviceLinks(associatedWith: [ hexEncodedPublicKey ])
|
||||
}
|
||||
|
||||
/// Gets the device links associated with the given hex encoded public keys from the
|
||||
/// server and stores and returns the valid ones.
|
||||
public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>) -> Promise<Set<DeviceLink>> {
|
||||
print("[Loki] Getting device links for: \(hexEncodedPublicKeys).")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Set<DeviceLink>> in
|
||||
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse -> Set<DeviceLink> in
|
||||
guard let json = rawResponse as? JSON, let data = json["data"] as? JSON,
|
||||
let annotations = data["annotations"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse device links for user: \(hexEncodedPublicKey) from: \(rawResponse).")
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse -> Set<DeviceLink> in
|
||||
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
guard !annotations.isEmpty else { return [] }
|
||||
guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
|
||||
let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse device links for user: \(hexEncodedPublicKey) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return Set(rawDeviceLinks.flatMap { rawDeviceLink in
|
||||
guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
|
||||
let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
|
||||
print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
|
||||
return nil
|
||||
return Set(data.flatMap { data -> [DeviceLink] in
|
||||
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
|
||||
guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
|
||||
let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON],
|
||||
let hexEncodedPublicKey = data["username"] as? String else {
|
||||
print("[Loki] Couldn't parse device links from: \(rawResponse).")
|
||||
return []
|
||||
}
|
||||
let masterSignature: Data?
|
||||
if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
|
||||
masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
|
||||
} else {
|
||||
masterSignature = nil
|
||||
}
|
||||
let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
|
||||
let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature)
|
||||
let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature)
|
||||
let deviceLink = DeviceLink(between: master, and: slave)
|
||||
if let masterSignature = masterSignature {
|
||||
guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
|
||||
print("[Loki] Received a device link with an invalid master signature.")
|
||||
return rawDeviceLinks.compactMap { rawDeviceLink in
|
||||
guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
|
||||
let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
|
||||
print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
|
||||
return nil
|
||||
}
|
||||
let masterSignature: Data?
|
||||
if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
|
||||
masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
|
||||
} else {
|
||||
masterSignature = nil
|
||||
}
|
||||
let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
|
||||
let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature)
|
||||
let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature)
|
||||
let deviceLink = DeviceLink(between: master, and: slave)
|
||||
if let masterSignature = masterSignature {
|
||||
guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
|
||||
print("[Loki] Received a device link with an invalid master signature.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
|
||||
print("[Loki] Received a device link with an invalid slave signature.")
|
||||
return nil
|
||||
}
|
||||
return deviceLink
|
||||
}
|
||||
guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
|
||||
print("[Loki] Received a device link with an invalid slave signature.")
|
||||
return nil
|
||||
}
|
||||
return deviceLink
|
||||
})
|
||||
}.map { deviceLinks -> Set<DeviceLink> in
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
|
@ -74,7 +82,7 @@ public final class LokiStorageAPI : LokiDotNetAPI {
|
|||
|
||||
public static func setDeviceLinks(_ deviceLinks: Set<DeviceLink>) -> Promise<Void> {
|
||||
print("[Loki] Updating device links.")
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
let isMaster = deviceLinks.contains { $0.master.hexEncodedPublicKey == userHexEncodedPublicKey }
|
||||
let deviceLinksAsJSON = deviceLinks.map { $0.toJSON() }
|
||||
let value = !deviceLinksAsJSON.isEmpty ? [ "isPrimary" : isMaster ? 1 : 0, "authorisations" : deviceLinksAsJSON ] : nil
|
||||
|
@ -83,7 +91,7 @@ public final class LokiStorageAPI : LokiDotNetAPI {
|
|||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { _ in }.recover { error in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
|
||||
print("Couldn't update device links due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@ import PromiseKit
|
|||
@objc(LKPublicChatAPI)
|
||||
public final class LokiPublicChatAPI : LokiDotNetAPI {
|
||||
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
|
||||
public static var displayNameUpdatees: [String:Set<String>] = [:]
|
||||
|
||||
// MARK: Settings
|
||||
private static let fallbackBatchCount = 256
|
||||
private static let fallbackBatchCount = 64
|
||||
private static let maxRetryCount: UInt = 8
|
||||
|
||||
// MARK: Public Chat
|
||||
|
@ -83,7 +84,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
|
@ -129,6 +130,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
print("[Loki] Ignoring public chat message with invalid signature.")
|
||||
return nil
|
||||
}
|
||||
var existingMessageID: String? = nil
|
||||
storage.dbReadConnection.read { transaction in
|
||||
existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
|
||||
}
|
||||
guard existingMessageID == nil else {
|
||||
print("[Loki] Ignorning duplicate message.")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}.sorted { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
|
@ -136,14 +145,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
|
||||
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
|
||||
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
|
||||
return getAuthToken(for: server).then { token -> Promise<LokiPublicChatMessage> in
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<LokiPublicChatMessage> in
|
||||
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let displayName = userDisplayName
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
|
@ -155,7 +164,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
|
||||
}
|
||||
}.recover { error -> Promise<LokiPublicChatMessage> in
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiPublicChatMessage> in
|
||||
if let error = error as? NetworkManagerError, error.statusCode == 401 {
|
||||
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
|
||||
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
|
||||
|
@ -164,7 +173,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}.retryingIfNeeded(maxRetryCount: maxRetryCount).map { message in
|
||||
Analytics.shared.track("Group Message Sent")
|
||||
return message
|
||||
}.recover { error -> Promise<LokiPublicChatMessage> in
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiPublicChatMessage> in
|
||||
Analytics.shared.track("Failed to Send Group Message")
|
||||
throw error
|
||||
}
|
||||
|
@ -180,7 +189,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
|
@ -198,14 +207,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
let isModerationRequest = !isSentByUser
|
||||
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
|
||||
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
|
||||
let url = URL(string: urlAsString)!
|
||||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return TSNetworkManager.shared().makePromise(request: request).done { result -> Void in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
|
||||
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
|
||||
}
|
||||
|
@ -214,7 +223,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
|
||||
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
|
@ -229,6 +238,33 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
}
|
||||
|
||||
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
|
||||
displayNameUpdatees[publicChatID] = []
|
||||
print("[Loki] Getting display names for: \(hexEncodedPublicKeys).")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
data.forEach { data in
|
||||
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
|
||||
let endIndex = hexEncodedPublicKey.endIndex
|
||||
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
|
||||
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
|
||||
transaction.setObject(displayName, forKey: hexEncodedPublicKey, inCollection: "\(server).\(channel)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc (isUserModerator:forGroup:onServer:)
|
||||
public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool {
|
||||
return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false
|
||||
|
@ -236,12 +272,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
|
||||
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
|
||||
print("[Loki] Updating display name on server: \(server).")
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
|
||||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { _ in }.recover { error in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
|
||||
print("Couldn't update display name due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
|
@ -251,7 +287,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
public static func getInfo(for channel: UInt64, on server: String) -> Promise<LokiPublicChatInfo> {
|
||||
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse in
|
||||
guard let json = rawResponse as? JSON,
|
||||
let data = json["data"] as? JSON,
|
||||
let annotations = data["annotations"] as? [JSON],
|
||||
|
|
|
@ -54,7 +54,7 @@ public final class LokiPublicChatManager : NSObject {
|
|||
return Promise(error: Error.chatCreationFailed)
|
||||
}
|
||||
}
|
||||
return LokiPublicChatAPI.getAuthToken(for: server).then { token in
|
||||
return LokiPublicChatAPI.getAuthToken(for: server).then(on: DispatchQueue.global()) { token in
|
||||
return LokiPublicChatAPI.getInfo(for: channel, on: server)
|
||||
}.map { channelInfo -> LokiPublicChat in
|
||||
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
|
||||
// TODO: Move away from polling
|
||||
|
||||
@objc(LKPublicChatPoller)
|
||||
public final class LokiPublicChatPoller : NSObject {
|
||||
private let publicChat: LokiPublicChat
|
||||
private var pollForNewMessagesTimer: Timer? = nil
|
||||
private var pollForDeletedMessagesTimer: Timer? = nil
|
||||
private var pollForModeratorsTimer: Timer? = nil
|
||||
private var pollForDisplayNamesTimer: Timer? = nil
|
||||
private var hasStarted = false
|
||||
private let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
|
||||
|
@ -12,6 +15,7 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
private let pollForNewMessagesInterval: TimeInterval = 4
|
||||
private let pollForDeletedMessagesInterval: TimeInterval = 20
|
||||
private let pollForModeratorsInterval: TimeInterval = 10 * 60
|
||||
private let pollForDisplayNamesInterval: TimeInterval = 60
|
||||
|
||||
// MARK: Lifecycle
|
||||
@objc(initForPublicChat:)
|
||||
|
@ -25,10 +29,12 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForNewMessagesInterval, repeats: true) { [weak self] _ in self?.pollForNewMessages() }
|
||||
pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForDeletedMessagesInterval, repeats: true) { [weak self] _ in self?.pollForDeletedMessages() }
|
||||
pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: pollForModeratorsInterval, repeats: true) { [weak self] _ in self?.pollForModerators() }
|
||||
pollForDisplayNamesTimer = Timer.scheduledTimer(withTimeInterval: pollForDisplayNamesInterval, repeats: true) { [weak self] _ in self?.pollForDisplayNames() }
|
||||
// Perform initial updates
|
||||
pollForNewMessages()
|
||||
pollForDeletedMessages()
|
||||
pollForModerators()
|
||||
pollForDisplayNames()
|
||||
hasStarted = true
|
||||
}
|
||||
|
||||
|
@ -36,139 +42,158 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
pollForNewMessagesTimer?.invalidate()
|
||||
pollForDeletedMessagesTimer?.invalidate()
|
||||
pollForModeratorsTimer?.invalidate()
|
||||
pollForDisplayNamesTimer?.invalidate()
|
||||
hasStarted = false
|
||||
}
|
||||
|
||||
// MARK: Polling
|
||||
private func pollForNewMessages() {
|
||||
// Prepare
|
||||
let publicChat = self.publicChat
|
||||
let userHexEncodedPublicKey = self.userHexEncodedPublicKey
|
||||
// Processing logic for incoming messages
|
||||
func processIncomingMessage(_ message: LokiPublicChatMessage) {
|
||||
let senderHexEncodedPublicKey = message.hexEncodedPublicKey
|
||||
let endIndex = senderHexEncodedPublicKey.endIndex
|
||||
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
|
||||
let senderDisplayName = "\(message.displayName) (...\(senderHexEncodedPublicKey[cutoffIndex..<endIndex]))"
|
||||
let id = publicChat.idAsData
|
||||
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
|
||||
groupContext.setName(publicChat.displayName)
|
||||
let dataMessage = SSKProtoDataMessage.builder()
|
||||
let attachments: [SSKProtoAttachmentPointer] = message.attachments.compactMap { attachment in
|
||||
guard attachment.kind == .attachment else { return nil }
|
||||
let result = SSKProtoAttachmentPointer.builder(id: attachment.serverID)
|
||||
result.setContentType(attachment.contentType)
|
||||
result.setSize(UInt32(attachment.size))
|
||||
result.setFileName(attachment.fileName)
|
||||
result.setFlags(UInt32(attachment.flags))
|
||||
result.setWidth(UInt32(attachment.width))
|
||||
result.setHeight(UInt32(attachment.height))
|
||||
if let caption = attachment.caption {
|
||||
result.setCaption(caption)
|
||||
let _ = LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global()) { messages in
|
||||
let uniqueHexEncodedPublicKeys = Set(messages.map { $0.hexEncodedPublicKey })
|
||||
func proceed() {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
var newDisplayNameUpdatees: Set<String> = []
|
||||
storage.dbReadConnection.read { transaction in
|
||||
newDisplayNameUpdatees = Set(uniqueHexEncodedPublicKeys.filter { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) != $0 }.compactMap { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) })
|
||||
}
|
||||
result.setUrl(attachment.url)
|
||||
return try! result.build()
|
||||
}
|
||||
dataMessage.setAttachments(attachments)
|
||||
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
|
||||
let signalLinkPreview = SSKProtoDataMessagePreview.builder(url: linkPreview.linkPreviewURL!)
|
||||
signalLinkPreview.setTitle(linkPreview.linkPreviewTitle!)
|
||||
let attachment = SSKProtoAttachmentPointer.builder(id: linkPreview.serverID)
|
||||
attachment.setContentType(linkPreview.contentType)
|
||||
attachment.setSize(UInt32(linkPreview.size))
|
||||
attachment.setFileName(linkPreview.fileName)
|
||||
attachment.setFlags(UInt32(linkPreview.flags))
|
||||
attachment.setWidth(UInt32(linkPreview.width))
|
||||
attachment.setHeight(UInt32(linkPreview.height))
|
||||
if let caption = linkPreview.caption {
|
||||
attachment.setCaption(caption)
|
||||
if !newDisplayNameUpdatees.isEmpty {
|
||||
let displayNameUpdatees = LokiPublicChatAPI.displayNameUpdatees[publicChat.id] ?? []
|
||||
LokiPublicChatAPI.displayNameUpdatees[publicChat.id] = displayNameUpdatees.union(newDisplayNameUpdatees)
|
||||
}
|
||||
messages.forEach { message in
|
||||
var wasSentByCurrentUser = false
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
wasSentByCurrentUser = LokiDatabaseUtilities.isUserLinkedDevice(message.hexEncodedPublicKey, transaction: transaction)
|
||||
}
|
||||
var masterHexEncodedPublicKey: String? = nil
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: message.hexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
let senderHexEncodedPublicKey = masterHexEncodedPublicKey ?? message.hexEncodedPublicKey
|
||||
func generateDisplayName(from rawDisplayName: String) -> String {
|
||||
let endIndex = senderHexEncodedPublicKey.endIndex
|
||||
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
|
||||
return "\(rawDisplayName) (...\(senderHexEncodedPublicKey[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
var senderDisplayName = ""
|
||||
if let masterHexEncodedPublicKey = masterHexEncodedPublicKey {
|
||||
senderDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: senderHexEncodedPublicKey, in: publicChat.channel, on: publicChat.server) ?? generateDisplayName(from: NSLocalizedString("Anonymous", comment: ""))
|
||||
} else {
|
||||
senderDisplayName = generateDisplayName(from: message.displayName)
|
||||
}
|
||||
let id = publicChat.idAsData
|
||||
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
|
||||
groupContext.setName(publicChat.displayName)
|
||||
let dataMessage = SSKProtoDataMessage.builder()
|
||||
let attachments: [SSKProtoAttachmentPointer] = message.attachments.compactMap { attachment in
|
||||
guard attachment.kind == .attachment else { return nil }
|
||||
let result = SSKProtoAttachmentPointer.builder(id: attachment.serverID)
|
||||
result.setContentType(attachment.contentType)
|
||||
result.setSize(UInt32(attachment.size))
|
||||
result.setFileName(attachment.fileName)
|
||||
result.setFlags(UInt32(attachment.flags))
|
||||
result.setWidth(UInt32(attachment.width))
|
||||
result.setHeight(UInt32(attachment.height))
|
||||
if let caption = attachment.caption {
|
||||
result.setCaption(caption)
|
||||
}
|
||||
result.setUrl(attachment.url)
|
||||
return try! result.build()
|
||||
}
|
||||
dataMessage.setAttachments(attachments)
|
||||
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
|
||||
let signalLinkPreview = SSKProtoDataMessagePreview.builder(url: linkPreview.linkPreviewURL!)
|
||||
signalLinkPreview.setTitle(linkPreview.linkPreviewTitle!)
|
||||
let attachment = SSKProtoAttachmentPointer.builder(id: linkPreview.serverID)
|
||||
attachment.setContentType(linkPreview.contentType)
|
||||
attachment.setSize(UInt32(linkPreview.size))
|
||||
attachment.setFileName(linkPreview.fileName)
|
||||
attachment.setFlags(UInt32(linkPreview.flags))
|
||||
attachment.setWidth(UInt32(linkPreview.width))
|
||||
attachment.setHeight(UInt32(linkPreview.height))
|
||||
if let caption = linkPreview.caption {
|
||||
attachment.setCaption(caption)
|
||||
}
|
||||
attachment.setUrl(linkPreview.url)
|
||||
signalLinkPreview.setImage(try! attachment.build())
|
||||
dataMessage.setPreview([ try! signalLinkPreview.build() ])
|
||||
}
|
||||
dataMessage.setTimestamp(message.timestamp)
|
||||
dataMessage.setGroup(try! groupContext.build())
|
||||
if let quote = message.quote {
|
||||
let signalQuote = SSKProtoDataMessageQuote.builder(id: quote.quotedMessageTimestamp, author: quote.quoteeHexEncodedPublicKey)
|
||||
signalQuote.setText(quote.quotedMessageBody)
|
||||
dataMessage.setQuote(try! signalQuote.build())
|
||||
}
|
||||
let body = (message.body == message.timestamp.description) ? "" : message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
||||
dataMessage.setBody(body)
|
||||
if let messageServerID = message.serverID {
|
||||
let publicChatInfo = SSKProtoPublicChatInfo.builder()
|
||||
publicChatInfo.setServerID(messageServerID)
|
||||
dataMessage.setPublicChatInfo(try! publicChatInfo.build())
|
||||
}
|
||||
let content = SSKProtoContent.builder()
|
||||
if !wasSentByCurrentUser {
|
||||
content.setDataMessage(try! dataMessage.build())
|
||||
} else {
|
||||
let syncMessageSentBuilder = SSKProtoSyncMessageSent.builder()
|
||||
syncMessageSentBuilder.setMessage(try! dataMessage.build())
|
||||
syncMessageSentBuilder.setDestination(userHexEncodedPublicKey)
|
||||
syncMessageSentBuilder.setTimestamp(message.timestamp)
|
||||
let syncMessageSent = try! syncMessageSentBuilder.build()
|
||||
let syncMessageBuilder = SSKProtoSyncMessage.builder()
|
||||
syncMessageBuilder.setSent(syncMessageSent)
|
||||
content.setSyncMessage(try! syncMessageBuilder.build())
|
||||
}
|
||||
let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp)
|
||||
envelope.setSource(senderHexEncodedPublicKey)
|
||||
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
|
||||
envelope.setContent(try! content.build().serializedData())
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
transaction.setObject(senderDisplayName, forKey: senderHexEncodedPublicKey, inCollection: publicChat.id)
|
||||
let messageServerID = message.serverID
|
||||
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: messageServerID ?? 0)
|
||||
}
|
||||
}
|
||||
attachment.setUrl(linkPreview.url)
|
||||
signalLinkPreview.setImage(try! attachment.build())
|
||||
dataMessage.setPreview([ try! signalLinkPreview.build() ])
|
||||
}
|
||||
dataMessage.setTimestamp(message.timestamp)
|
||||
dataMessage.setGroup(try! groupContext.build())
|
||||
if let quote = message.quote {
|
||||
let signalQuote = SSKProtoDataMessageQuote.builder(id: quote.quotedMessageTimestamp, author: quote.quoteeHexEncodedPublicKey)
|
||||
signalQuote.setText(quote.quotedMessageBody)
|
||||
dataMessage.setQuote(try! signalQuote.build())
|
||||
}
|
||||
let body = (message.body == message.timestamp.description) ? "" : message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
||||
dataMessage.setBody(body)
|
||||
if let messageServerID = message.serverID {
|
||||
let publicChatInfo = SSKProtoPublicChatInfo.builder()
|
||||
publicChatInfo.setServerID(messageServerID)
|
||||
dataMessage.setPublicChatInfo(try! publicChatInfo.build())
|
||||
}
|
||||
let content = SSKProtoContent.builder()
|
||||
content.setDataMessage(try! dataMessage.build())
|
||||
let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp)
|
||||
envelope.setSource(senderHexEncodedPublicKey)
|
||||
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
|
||||
envelope.setContent(try! content.build().serializedData())
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
transaction.setObject(senderDisplayName, forKey: senderHexEncodedPublicKey, inCollection: publicChat.id)
|
||||
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction)
|
||||
}
|
||||
}
|
||||
// Processing logic for outgoing messages
|
||||
func processOutgoingMessage(_ message: LokiPublicChatMessage) {
|
||||
guard let messageServerID = message.serverID else { return }
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
var isDuplicate = false
|
||||
storage.dbReadConnection.read { transaction in
|
||||
let id = storage.getIDForMessage(withServerID: UInt(messageServerID), in: transaction)
|
||||
isDuplicate = id != nil
|
||||
}
|
||||
guard !isDuplicate else { return }
|
||||
let groupID = publicChat.idAsData
|
||||
let thread = TSGroupThread.getOrCreateThread(withGroupId: groupID)
|
||||
let signalQuote: TSQuotedMessage?
|
||||
if let quote = message.quote {
|
||||
signalQuote = TSQuotedMessage(timestamp: quote.quotedMessageTimestamp, authorId: quote.quoteeHexEncodedPublicKey, body: quote.quotedMessageBody, quotedAttachmentsForSending: [])
|
||||
} else {
|
||||
signalQuote = nil
|
||||
}
|
||||
var attachmentIDs: [String] = []
|
||||
// TODO: Restore attachments
|
||||
let signalLinkPreview: OWSLinkPreview?
|
||||
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
|
||||
let attachment = TSAttachmentPointer(serverId: linkPreview.serverID, encryptionKey: nil, byteCount: UInt32(linkPreview.size), contentType: linkPreview.contentType, sourceFilename: linkPreview.fileName, caption: linkPreview.caption, albumMessageId: nil)
|
||||
attachment.save()
|
||||
signalLinkPreview = OWSLinkPreview(urlString: linkPreview.linkPreviewURL!, title: linkPreview.linkPreviewTitle!, imageAttachmentId: attachment.uniqueId!, isDirectAttachment: false)
|
||||
} else {
|
||||
signalLinkPreview = nil
|
||||
}
|
||||
let body = (message.body == message.timestamp.description) ? "" : message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
||||
let message = TSOutgoingMessage(outgoingMessageWithTimestamp: message.timestamp, in: thread, messageBody: body, attachmentIds: NSMutableArray(array: attachmentIDs), expiresInSeconds: 0,
|
||||
expireStartedAt: 0, isVoiceMessage: false, groupMetaMessage: .deliver, quotedMessage: signalQuote, contactShare: nil, linkPreview: signalLinkPreview)
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
message.update(withSentRecipient: publicChat.server, wasSentByUD: false, transaction: transaction)
|
||||
message.saveGroupChatServerID(messageServerID, in: transaction)
|
||||
guard let messageID = message.uniqueId else { return print("[Loki] Failed to save public chat message.") }
|
||||
storage.setIDForMessageWithServerID(UInt(messageServerID), to: messageID, in: transaction)
|
||||
}
|
||||
if let linkPreviewURL = OWSLinkPreview.previewUrl(forMessageBodyText: message.body, selectedRange: nil) {
|
||||
message.generateLinkPreviewIfNeeded(fromURL: linkPreviewURL)
|
||||
}
|
||||
}
|
||||
// Poll
|
||||
let _ = LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: .main) { messages in
|
||||
messages.forEach { message in
|
||||
if message.hexEncodedPublicKey != userHexEncodedPublicKey {
|
||||
processIncomingMessage(message)
|
||||
let hexEncodedPublicKeysToUpdate = uniqueHexEncodedPublicKeys.filter { hexEncodedPublicKey in
|
||||
let timeSinceLastUpdate: TimeInterval
|
||||
if let lastDeviceLinkUpdate = LokiAPI.lastDeviceLinkUpdate[hexEncodedPublicKey] {
|
||||
timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate)
|
||||
} else {
|
||||
processOutgoingMessage(message)
|
||||
timeSinceLastUpdate = .infinity
|
||||
}
|
||||
return timeSinceLastUpdate > LokiAPI.deviceLinkUpdateInterval
|
||||
}
|
||||
if !hexEncodedPublicKeysToUpdate.isEmpty {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadConnection.read { transaction in
|
||||
LokiStorageAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global()) { _ in
|
||||
proceed()
|
||||
hexEncodedPublicKeysToUpdate.forEach {
|
||||
LokiAPI.lastDeviceLinkUpdate[$0] = Date()
|
||||
}
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
if case LokiDotNetAPI.Error.parsingFailed = error {
|
||||
// Don't immediately re-fetch in case of failure due to a parsing error
|
||||
hexEncodedPublicKeysToUpdate.forEach {
|
||||
LokiAPI.lastDeviceLinkUpdate[$0] = Date()
|
||||
}
|
||||
}
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func pollForDeletedMessages() {
|
||||
let publicChat = self.publicChat
|
||||
let _ = LokiPublicChatAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done { deletedMessageServerIDs in
|
||||
let _ = LokiPublicChatAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global()) { deletedMessageServerIDs in
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
let deletedMessageIDs = deletedMessageServerIDs.compactMap { storage.getIDForMessage(withServerID: UInt($0), in: transaction) }
|
||||
|
@ -182,4 +207,8 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
private func pollForModerators() {
|
||||
let _ = LokiPublicChatAPI.getModerators(for: publicChat.channel, on: publicChat.server)
|
||||
}
|
||||
|
||||
private func pollForDisplayNames() {
|
||||
let _ = LokiPublicChatAPI.getDisplayNames(for: publicChat.channel, on: publicChat.server)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,39 @@ public final class LokiDatabaseUtilities : NSObject {
|
|||
}
|
||||
|
||||
// MARK: Device Links
|
||||
@objc(getLinkedDeviceHexEncodedPublicKeysFor:in:)
|
||||
public static func getLinkedDeviceHexEncodedPublicKeys(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set<String> {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
|
||||
var result = Set(storage.getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction).flatMap { deviceLink in
|
||||
return [ deviceLink.master.hexEncodedPublicKey, deviceLink.slave.hexEncodedPublicKey ]
|
||||
})
|
||||
result.insert(hexEncodedPublicKey)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(getLinkedDeviceThreadsFor:in:)
|
||||
public static func getLinkedDeviceThreads(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set<TSContactThread> {
|
||||
return Set(getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction).compactMap { TSContactThread.getWithContactId($0, transaction: transaction) })
|
||||
}
|
||||
|
||||
@objc(isUserLinkedDevice:in:)
|
||||
public static func isUserLinkedDevice(_ hexEncodedPublicKey: String, transaction: YapDatabaseReadTransaction) -> Bool {
|
||||
let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
let userLinkedDeviceHexEncodedPublicKeys = getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
|
||||
return userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
@objc(getMasterHexEncodedPublicKeyFor:in:)
|
||||
public static func objc_getMasterHexEncodedPublicKey(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> String? {
|
||||
return OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: slaveHexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
|
||||
@objc(getDeviceLinksFor:in:)
|
||||
public static func objc_getDeviceLinks(for masterHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set<DeviceLink> {
|
||||
return OWSPrimaryStorage.shared().getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
|
||||
// MARK: Public Chats
|
||||
private static let publicChatCollection = "LokiPublicChatCollection"
|
||||
|
||||
|
|
24
SignalServiceKit/src/Loki/Messaging/ContactParser.swift
Normal file
24
SignalServiceKit/src/Loki/Messaging/ContactParser.swift
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
@objc public final class ContactParser : NSObject {
|
||||
private let data: Data
|
||||
|
||||
@objc public init(data: Data) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
@objc public func parseHexEncodedPublicKeys() -> [String] {
|
||||
var index = 0
|
||||
var result: [String] = []
|
||||
while index < data.endIndex {
|
||||
let uncheckedSize: UInt32? = try? data[index..<(index+4)].withUnsafeBytes { $0.pointee }
|
||||
guard let size = uncheckedSize else { break }
|
||||
let sizeAsInt = Int(size)
|
||||
index += 4
|
||||
let protoAsData = data[index..<(index+sizeAsInt)]
|
||||
guard let proto = try? SSKProtoContactDetails.parseData(protoAsData) else { break }
|
||||
index += sizeAsInt
|
||||
result.append(proto.number)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
@implementation LKAddressMessage
|
||||
|
||||
#pragma mark Initialization
|
||||
- (instancetype)initInThread:(nullable TSThread *)thread address:(NSString *)address port:(uint16_t)port isPing:(bool)isPing
|
||||
{
|
||||
self = [super initInThread:thread];
|
||||
|
@ -24,28 +25,25 @@
|
|||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Building
|
||||
- (SSKProtoContentBuilder *)prepareCustomContentBuilder:(SignalRecipient *)recipient {
|
||||
SSKProtoContentBuilder *contentBuilder = SSKProtoContent.builder;
|
||||
|
||||
SSKProtoLokiAddressMessageBuilder *addressBuilder = [SSKProtoLokiAddressMessage builder];
|
||||
[addressBuilder setPtpAddress:self.address];
|
||||
SSKProtoLokiAddressMessageBuilder *addressMessageBuilder = SSKProtoLokiAddressMessage.builder;
|
||||
[addressMessageBuilder setPtpAddress:self.address];
|
||||
uint32_t portAsUInt32 = self.port;
|
||||
[addressBuilder setPtpPort:portAsUInt32];
|
||||
|
||||
[addressMessageBuilder setPtpPort:portAsUInt32];
|
||||
NSError *error;
|
||||
SSKProtoLokiAddressMessage *addressMessage = [addressBuilder buildAndReturnError:&error];
|
||||
if (error || !addressMessage) {
|
||||
SSKProtoLokiAddressMessage *addressMessage = [addressMessageBuilder buildAndReturnError:&error];
|
||||
if (error || addressMessage == nil) {
|
||||
OWSFailDebug(@"Failed to build Loki address message for: %@ due to error: %@.", recipient.recipientId, error);
|
||||
return nil;
|
||||
} else {
|
||||
[contentBuilder setLokiAddressMessage:addressMessage];
|
||||
}
|
||||
|
||||
return contentBuilder;
|
||||
}
|
||||
|
||||
- (uint)ttl {
|
||||
// Address messages should only last 1 minute
|
||||
return 1 * kMinuteInMs;
|
||||
}
|
||||
#pragma mark Settings
|
||||
- (uint)ttl { return 1 * kMinuteInMs; }
|
||||
|
||||
@end
|
||||
|
|
|
@ -33,22 +33,18 @@
|
|||
#pragma mark Building
|
||||
- (SSKProtoContentBuilder *)prepareCustomContentBuilder:(SignalRecipient *)recipient {
|
||||
SSKProtoContentBuilder *contentBuilder = SSKProtoContent.builder;
|
||||
// If this is a request then we should attach a pre key bundle
|
||||
if (self.kind == LKDeviceLinkMessageKindRequest) {
|
||||
PreKeyBundle *bundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId];
|
||||
SSKProtoPrekeyBundleMessageBuilder *preKeyBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:bundle];
|
||||
// Build the pre key bundle message
|
||||
NSError *error;
|
||||
SSKProtoPrekeyBundleMessage *message = [preKeyBuilder buildAndReturnError:&error];
|
||||
if (error || !message) {
|
||||
OWSFailDebug(@"Failed to build pre key bundle for: %@ due to error: %@.", recipient.recipientId, error);
|
||||
return nil;
|
||||
} else {
|
||||
[contentBuilder setPrekeyBundleMessage:message];
|
||||
}
|
||||
NSError *error;
|
||||
// Build the pre key bundle message
|
||||
PreKeyBundle *preKeyBundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId];
|
||||
SSKProtoPrekeyBundleMessageBuilder *preKeyBundleMessageBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:preKeyBundle];
|
||||
SSKProtoPrekeyBundleMessage *preKeyBundleMessage = [preKeyBundleMessageBuilder buildAndReturnError:&error];
|
||||
if (error || preKeyBundleMessage == nil) {
|
||||
OWSFailDebug(@"Failed to build pre key bundle message for: %@ due to error: %@.", recipient.recipientId, error);
|
||||
return nil;
|
||||
} else {
|
||||
[contentBuilder setPrekeyBundleMessage:preKeyBundleMessage];
|
||||
}
|
||||
// Build the device link message
|
||||
NSError *error;
|
||||
SSKProtoLokiDeviceLinkMessageBuilder *deviceLinkMessageBuilder = [SSKProtoLokiDeviceLinkMessage builder];
|
||||
[deviceLinkMessageBuilder setMasterHexEncodedPublicKey:self.masterHexEncodedPublicKey];
|
||||
[deviceLinkMessageBuilder setSlaveHexEncodedPublicKey:self.slaveHexEncodedPublicKey];
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
@implementation LKEphemeralMessage
|
||||
|
||||
#pragma mark Initialization
|
||||
- (instancetype)initInThread:(nullable TSThread *)thread {
|
||||
return [self initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray<NSString *> new]
|
||||
expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil];
|
||||
}
|
||||
|
||||
#pragma mark Settings
|
||||
- (BOOL)shouldSyncTranscript { return NO; }
|
||||
- (BOOL)shouldBeSaved { return NO; }
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
#import "TSOutgoingMessage.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(FriendRequestMessage)
|
||||
@interface LKFriendRequestMessage : TSOutgoingMessage
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -6,24 +6,24 @@
|
|||
|
||||
@implementation LKFriendRequestMessage
|
||||
|
||||
#pragma mark Initialization
|
||||
- (SSKProtoContentBuilder *)prepareCustomContentBuilder:(SignalRecipient *)recipient {
|
||||
SSKProtoContentBuilder *contentBuilder = SSKProtoContent.builder;
|
||||
|
||||
PreKeyBundle *bundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId];
|
||||
SSKProtoPrekeyBundleMessageBuilder *preKeyBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:bundle];
|
||||
|
||||
// Build the pre key bundle message
|
||||
PreKeyBundle *preKeyBundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId];
|
||||
SSKProtoPrekeyBundleMessageBuilder *preKeyBundleMessageBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:preKeyBundle];
|
||||
NSError *error;
|
||||
SSKProtoPrekeyBundleMessage *_Nullable message = [preKeyBuilder buildAndReturnError:&error];
|
||||
if (error || !message) {
|
||||
OWSFailDebug(@"Failed to build pre key bundle for: %@ due to error: %@.", recipient.recipientId, error);
|
||||
SSKProtoPrekeyBundleMessage *preKeyBundleMessage = [preKeyBundleMessageBuilder buildAndReturnError:&error];
|
||||
if (error || preKeyBundleMessage == nil) {
|
||||
OWSFailDebug(@"Failed to build pre key bundle message for: %@ due to error: %@.", recipient.recipientId, error);
|
||||
return nil;
|
||||
} else {
|
||||
[contentBuilder setPrekeyBundleMessage:message];
|
||||
[contentBuilder setPrekeyBundleMessage:preKeyBundleMessage];
|
||||
}
|
||||
|
||||
return contentBuilder;
|
||||
}
|
||||
|
||||
#pragma mark Settings
|
||||
- (uint)ttl { return 4 * kDayInMs; }
|
||||
- (BOOL)shouldSyncTranscript { return NO; }
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
public enum LokiMessageWrapper {
|
||||
|
||||
public enum WrappingError : LocalizedError {
|
||||
public enum Error : LocalizedError {
|
||||
case failedToWrapData
|
||||
case failedToWrapMessageInEnvelope
|
||||
case failedToWrapEnvelopeInWebSocketMessage
|
||||
|
@ -24,7 +24,7 @@ public enum LokiMessageWrapper {
|
|||
let webSocketMessage = try createWebSocketMessage(around: envelope)
|
||||
return try webSocketMessage.serializedData()
|
||||
} catch let error {
|
||||
throw error as? WrappingError ?? WrappingError.failedToWrapData
|
||||
throw error as? Error ?? Error.failedToWrapData
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,12 +36,12 @@ public enum LokiMessageWrapper {
|
|||
if let content = try Data(base64Encoded: message.content) {
|
||||
builder.setContent(content)
|
||||
} else {
|
||||
throw WrappingError.failedToWrapMessageInEnvelope
|
||||
throw Error.failedToWrapMessageInEnvelope
|
||||
}
|
||||
return try builder.build()
|
||||
} catch let error {
|
||||
print("[Loki] Failed to wrap message in envelope: \(error).")
|
||||
throw WrappingError.failedToWrapMessageInEnvelope
|
||||
throw Error.failedToWrapMessageInEnvelope
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ public enum LokiMessageWrapper {
|
|||
return try messageBuilder.build()
|
||||
} catch let error {
|
||||
print("[Loki] Failed to wrap envelope in web socket message: \(error).")
|
||||
throw WrappingError.failedToWrapEnvelopeInWebSocketMessage
|
||||
throw Error.failedToWrapEnvelopeInWebSocketMessage
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ public enum LokiMessageWrapper {
|
|||
return try SSKProtoEnvelope.parseData(envelope)
|
||||
} catch let error {
|
||||
print("[Loki] Failed to unwrap data: \(error).")
|
||||
throw WrappingError.failedToUnwrapData
|
||||
throw Error.failedToUnwrapData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ public extension Notification.Name {
|
|||
public static let threadDeleted = Notification.Name("threadDeleted")
|
||||
}
|
||||
|
||||
// MARK: - Obj-C
|
||||
|
||||
@objc public extension NSNotification {
|
||||
@objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString
|
||||
@objc public static let newMessagesReceived = Notification.Name.newMessagesReceived.rawValue as NSString
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
@objc public extension SSKProtoPrekeyBundleMessage {
|
||||
|
||||
@objc public static func builder(fromPreKeyBundle preKeyBundle: PreKeyBundle) -> SSKProtoPrekeyBundleMessageBuilder {
|
||||
@objc(builderFromPreKeyBundle:)
|
||||
public static func builder(from preKeyBundle: PreKeyBundle) -> SSKProtoPrekeyBundleMessageBuilder {
|
||||
let builder = self.builder()
|
||||
|
||||
builder.setIdentityKey(preKeyBundle.identityKey)
|
||||
builder.setDeviceID(UInt32(preKeyBundle.deviceId))
|
||||
builder.setPrekeyID(UInt32(preKeyBundle.preKeyId))
|
||||
|
@ -11,11 +11,11 @@
|
|||
builder.setSignedKeyID(UInt32(preKeyBundle.signedPreKeyId))
|
||||
builder.setSignedKey(preKeyBundle.signedPreKeyPublic)
|
||||
builder.setSignature(preKeyBundle.signedPreKeySignature)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
@objc public func createPreKeyBundle(withTransaction transaction: YapDatabaseReadWriteTransaction) -> PreKeyBundle? {
|
||||
@objc(getPreKeyBundleWithTransaction:)
|
||||
public func getPreKeyBundle(with transaction: YapDatabaseReadWriteTransaction) -> PreKeyBundle? {
|
||||
let registrationId = TSAccountManager.sharedInstance().getOrGenerateRegistrationId(transaction)
|
||||
return PreKeyBundle(registrationId: Int32(registrationId), deviceId: Int32(deviceID), preKeyId: Int32(prekeyID), preKeyPublic: prekey,
|
||||
signedPreKeyPublic: signedKey, signedPreKeyId: Int32(signedKeyID), signedPreKeySignature: signature, identityKey: identityKey)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
@interface NSArray (Functional)
|
||||
|
||||
- (BOOL)contains:(BOOL (^)(NSObject *))predicate;
|
||||
- (NSArray *)filtered:(BOOL (^)(NSObject *))isIncluded;
|
||||
- (NSArray *)map:(NSObject *(^)(NSObject *))transform;
|
||||
- (BOOL)contains:(BOOL (^)(id))predicate;
|
||||
- (NSArray *)filtered:(BOOL (^)(id))isIncluded;
|
||||
- (NSArray *)map:(id (^)(id))transform;
|
||||
|
||||
@end
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
@implementation NSArray (Functional)
|
||||
|
||||
- (BOOL)contains:(BOOL (^)(NSObject *))predicate {
|
||||
for (NSObject *object in self) {
|
||||
- (BOOL)contains:(BOOL (^)(id))predicate {
|
||||
for (id object in self) {
|
||||
BOOL isPredicateSatisfied = predicate(object);
|
||||
if (isPredicateSatisfied) { return YES; }
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSArray *)filtered:(BOOL (^)(NSObject *))isIncluded {
|
||||
- (NSArray *)filtered:(BOOL (^)(id))isIncluded {
|
||||
NSMutableArray *result = [NSMutableArray new];
|
||||
for (NSObject *object in self) {
|
||||
for (id object in self) {
|
||||
if (isIncluded(object)) {
|
||||
[result addObject:object];
|
||||
}
|
||||
|
@ -20,10 +20,10 @@
|
|||
return result;
|
||||
}
|
||||
|
||||
- (NSArray *)map:(NSObject *(^)(NSObject *))transform {
|
||||
- (NSArray *)map:(id (^)(id))transform {
|
||||
NSMutableArray *result = [NSMutableArray new];
|
||||
for (NSObject *object in self) {
|
||||
NSObject *transformedObject = transform(object);
|
||||
for (id object in self) {
|
||||
id transformedObject = transform(object);
|
||||
[result addObject:transformedObject];
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
@interface NSObject (Casting)
|
||||
|
||||
- (instancetype)as:(Class)cls;
|
||||
- (id)as:(Class)cls;
|
||||
|
||||
@end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@implementation NSObject (Casting)
|
||||
|
||||
- (instancetype)as:(Class)cls {
|
||||
- (id)as:(Class)cls {
|
||||
if ([self isKindOfClass:cls]) { return self; }
|
||||
return nil;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
@interface NSSet (Functional)
|
||||
|
||||
- (BOOL)contains:(BOOL (^)(NSObject *))predicate;
|
||||
- (NSSet *)filtered:(BOOL (^)(NSObject *))isIncluded;
|
||||
- (BOOL)contains:(BOOL (^)(id))predicate;
|
||||
- (NSSet *)filtered:(BOOL (^)(id))isIncluded;
|
||||
- (NSSet *)map:(id (^)(id))transform;
|
||||
|
||||
@end
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
@implementation NSSet (Functional)
|
||||
|
||||
- (BOOL)contains:(BOOL (^)(NSObject *))predicate {
|
||||
for (NSObject *object in self) {
|
||||
- (BOOL)contains:(BOOL (^)(id))predicate {
|
||||
for (id object in self) {
|
||||
BOOL isPredicateSatisfied = predicate(object);
|
||||
if (isPredicateSatisfied) { return YES; }
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSSet *)filtered:(BOOL (^)(NSObject *))isIncluded {
|
||||
- (NSSet *)filtered:(BOOL (^)(id))isIncluded {
|
||||
NSMutableSet *result = [NSMutableSet new];
|
||||
for (NSObject *object in self) {
|
||||
for (id object in self) {
|
||||
if (isIncluded(object)) {
|
||||
[result addObject:object];
|
||||
}
|
||||
|
@ -20,4 +20,13 @@
|
|||
return result;
|
||||
}
|
||||
|
||||
- (NSSet *)map:(id (^)(id))transform {
|
||||
NSMutableSet *result = [NSMutableSet new];
|
||||
for (id object in self) {
|
||||
id transformedObject = transform(object);
|
||||
[result addObject:transformedObject];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -5,7 +5,7 @@ internal extension Promise {
|
|||
internal func retryingIfNeeded(maxRetryCount: UInt) -> Promise<T> {
|
||||
var retryCount = 0
|
||||
func retryIfNeeded() -> Promise<T> {
|
||||
return recover { error -> Promise<T> in
|
||||
return recover(on: DispatchQueue.global()) { error -> Promise<T> in
|
||||
guard retryCount != maxRetryCount else { throw error }
|
||||
retryCount += 1
|
||||
return retryIfNeeded()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#import "TSContactThread.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import "OWSPrimaryStorage.h"
|
||||
|
||||
@import Contacts;
|
||||
|
||||
|
@ -66,25 +67,34 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder
|
||||
{
|
||||
if (self.attachmentIds.count != 1) {
|
||||
OWSLogError(@"expected sync contact message to have exactly one attachment, but found %lu",
|
||||
(unsigned long)self.attachmentIds.count);
|
||||
}
|
||||
|
||||
SSKProtoAttachmentPointer *_Nullable attachmentProto =
|
||||
[TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject];
|
||||
if (!attachmentProto) {
|
||||
OWSFailDebug(@"could not build protobuf.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
SSKProtoSyncMessageContactsBuilder *contactsBuilder = [SSKProtoSyncMessageContacts builderWithBlob:attachmentProto];
|
||||
[contactsBuilder setIsComplete:YES];
|
||||
|
||||
NSError *error;
|
||||
SSKProtoSyncMessageContacts *_Nullable contactsProto = [contactsBuilder buildAndReturnError:&error];
|
||||
if (error || !contactsProto) {
|
||||
OWSFailDebug(@"could not build protobuf: %@", error);
|
||||
if (self.attachmentIds.count > 1) {
|
||||
OWSLogError(@"Expected sync contact message to have one or zero attachments, but found %lu.", (unsigned long)self.attachmentIds.count);
|
||||
}
|
||||
|
||||
SSKProtoSyncMessageContactsBuilder *contactsBuilder;
|
||||
if (self.attachmentIds.count == 0) {
|
||||
SSKProtoAttachmentPointerBuilder *attachmentProtoBuilder = [SSKProtoAttachmentPointer builderWithId:0];
|
||||
SSKProtoAttachmentPointer *attachmentProto = [attachmentProtoBuilder buildAndReturnError:&error];
|
||||
contactsBuilder = [SSKProtoSyncMessageContacts builderWithBlob:attachmentProto];
|
||||
__block NSData *data;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
data = [self buildPlainTextAttachmentDataWithTransaction:transaction];
|
||||
}];
|
||||
[contactsBuilder setData:data];
|
||||
} else {
|
||||
SSKProtoAttachmentPointer *attachmentProto = [TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject];
|
||||
if (attachmentProto == nil) {
|
||||
OWSFailDebug(@"Couldn't build protobuf.");
|
||||
return nil;
|
||||
}
|
||||
contactsBuilder = [SSKProtoSyncMessageContacts builderWithBlob:attachmentProto];
|
||||
}
|
||||
[contactsBuilder setIsComplete:YES];
|
||||
|
||||
SSKProtoSyncMessageContacts *contactsProto = [contactsBuilder buildAndReturnError:&error];
|
||||
if (error || contactsProto == nil) {
|
||||
OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error);
|
||||
return nil;
|
||||
}
|
||||
SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder];
|
||||
|
|
|
@ -39,6 +39,9 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value);
|
|||
@property (nonatomic, readonly) uint64_t timestamp;
|
||||
@property (nonatomic, readonly) uint64_t sortId;
|
||||
@property (nonatomic, readonly) uint64_t receivedAtTimestamp;
|
||||
/// Used for public chats where a message sent from a slave device is interpreted as having been sent from the master device.
|
||||
@property (nonatomic) NSString *actualSenderHexEncodedPublicKey;
|
||||
|
||||
- (NSDate *)receivedAtDate;
|
||||
|
||||
- (OWSInteractionType)interactionType;
|
||||
|
|
|
@ -44,6 +44,8 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
|
|||
@property (nonatomic) uint64_t friendRequestExpiresAt;
|
||||
@property (nonatomic, readonly) BOOL isFriendRequest;
|
||||
@property (nonatomic, readonly) BOOL hasFriendRequestStatusMessage;
|
||||
@property BOOL skipSave;
|
||||
// P2P
|
||||
@property (nonatomic) BOOL isP2P;
|
||||
// Group chat
|
||||
@property (nonatomic) uint64_t groupChatServerID; // Should ideally be publicChatServerID
|
||||
|
|
|
@ -445,10 +445,11 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
|
|||
[self.messageManager throws_processEnvelope:envelope
|
||||
plaintextData:job.plaintextData
|
||||
wasReceivedByUD:job.wasReceivedByUD
|
||||
transaction:transaction];
|
||||
transaction:transaction
|
||||
serverID:0];
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
OWSFailDebug(@"Received an invalid envelope: %@", exception.debugDescription);
|
||||
// OWSFailDebug(@"Received an invalid envelope: %@", exception.debugDescription);
|
||||
reportFailure(transaction);
|
||||
}
|
||||
[processedJobs addObject:job];
|
||||
|
|
|
@ -170,8 +170,9 @@ NSString *envelopeAddress(SSKProtoEnvelope *envelope)
|
|||
NSString *verifiedString =
|
||||
[NSString stringWithFormat:@"Verification for: %@", syncMessage.verified.destination];
|
||||
[description appendString:verifiedString];
|
||||
} else if (syncMessage.contacts) {
|
||||
[description appendString:@"Contacts"];
|
||||
} else {
|
||||
OWSFailDebug(@"Unknown sync message type");
|
||||
[description appendString:@"Unknown"];
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (void)throws_processEnvelope:(SSKProtoEnvelope *)envelope
|
||||
plaintextData:(NSData *_Nullable)plaintextData
|
||||
wasReceivedByUD:(BOOL)wasReceivedByUD
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
serverID:(uint64_t)serverID;
|
||||
|
||||
// This should be invoked by the main app when the app is ready.
|
||||
- (void)startObserving;
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import "OWSDispatch.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -241,6 +242,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
plaintextData:(NSData *_Nullable)plaintextData
|
||||
wasReceivedByUD:(BOOL)wasReceivedByUD
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
serverID:(uint64_t)serverID
|
||||
{
|
||||
if (!envelope) {
|
||||
OWSFailDebug(@"Missing envelope.");
|
||||
|
@ -286,7 +288,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self throws_handleEnvelope:envelope
|
||||
plaintextData:plaintextData
|
||||
wasReceivedByUD:wasReceivedByUD
|
||||
transaction:transaction];
|
||||
transaction:transaction
|
||||
serverID:serverID];
|
||||
break;
|
||||
case SSKProtoEnvelopeTypeReceipt:
|
||||
OWSAssertDebug(!plaintextData);
|
||||
|
@ -379,6 +382,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
plaintextData:(NSData *)plaintextData
|
||||
wasReceivedByUD:(BOOL)wasReceivedByUD
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
serverID:(uint64_t)serverID
|
||||
{
|
||||
if (!envelope) {
|
||||
OWSFailDebug(@"Missing envelope.");
|
||||
|
@ -439,7 +443,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSLogInfo(@"[Loki] Received a device linking authorization from: %@", envelope.source); // Not masterHexEncodedPublicKey
|
||||
[LKDeviceLinkingSession.current processLinkingAuthorizationFrom:masterHexEncodedPublicKey for:slaveHexEncodedPublicKey masterSignature:masterSignature slaveSignature:slaveSignature];
|
||||
} else if (slaveSignature != nil) { // Request
|
||||
OWSLogInfo(@"[Loki] Received a device linking request from: %@", envelope.source); // Not slaveHexEncodedPublicKey }
|
||||
OWSLogInfo(@"[Loki] Received a device linking request from: %@", envelope.source); // Not slaveHexEncodedPublicKey
|
||||
[LKDeviceLinkingSession.current processLinkingRequestFrom:slaveHexEncodedPublicKey to:masterHexEncodedPublicKey with:slaveSignature];
|
||||
}
|
||||
}
|
||||
|
@ -447,7 +451,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// Loki: Handle pre key bundle message if needed
|
||||
if (contentProto.prekeyBundleMessage != nil) {
|
||||
OWSLogInfo(@"[Loki] Received a pre key bundle message from: %@.", envelope.source);
|
||||
PreKeyBundle *_Nullable bundle = [contentProto.prekeyBundleMessage createPreKeyBundleWithTransaction:transaction];
|
||||
PreKeyBundle *_Nullable bundle = [contentProto.prekeyBundleMessage getPreKeyBundleWithTransaction:transaction];
|
||||
if (bundle == nil) {
|
||||
OWSFailDebug(@"Failed to create a pre key bundle.");
|
||||
}
|
||||
|
@ -464,7 +468,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (contentProto.syncMessage) {
|
||||
[self throws_handleIncomingEnvelope:envelope
|
||||
withSyncMessage:contentProto.syncMessage
|
||||
transaction:transaction];
|
||||
transaction:transaction
|
||||
serverID:serverID];
|
||||
|
||||
[[OWSDeviceManager sharedManager] setHasReceivedSyncMessage];
|
||||
} else if (contentProto.dataMessage) {
|
||||
|
@ -887,6 +892,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (void)throws_handleIncomingEnvelope:(SSKProtoEnvelope *)envelope
|
||||
withSyncMessage:(SSKProtoSyncMessage *)syncMessage
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
serverID:(uint64_t)serverID
|
||||
{
|
||||
if (!envelope) {
|
||||
OWSFailDebug(@"Missing envelope.");
|
||||
|
@ -901,9 +907,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return;
|
||||
}
|
||||
|
||||
NSString *localNumber = self.tsAccountManager.localNumber;
|
||||
if (![localNumber isEqualToString:envelope.source]) {
|
||||
// Sync messages should only come from linked devices.
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction];
|
||||
if (![linkedDeviceHexEncodedPublicKeys contains:^BOOL(NSString *hexEncodedPublicKey) {
|
||||
return [hexEncodedPublicKey isEqual:envelope.source];
|
||||
}]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorSyncMessageFromUnknownSource], envelope);
|
||||
return;
|
||||
}
|
||||
|
@ -931,6 +939,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if ([self isDataMessageGroupAvatarUpdate:syncMessage.sent.message] && !syncMessage.sent.isRecipientUpdate) {
|
||||
[OWSRecordTranscriptJob
|
||||
processIncomingSentMessageTranscript:transcript
|
||||
serverID:0
|
||||
attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
|
||||
OWSAssertDebug(attachmentStreams.count == 1);
|
||||
TSAttachmentStream *attachmentStream = attachmentStreams.firstObject;
|
||||
|
@ -952,6 +961,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
} else {
|
||||
[OWSRecordTranscriptJob
|
||||
processIncomingSentMessageTranscript:transcript
|
||||
serverID:serverID
|
||||
attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
|
||||
OWSLogDebug(@"successfully fetched transcript attachments: %lu",
|
||||
(unsigned long)attachmentStreams.count);
|
||||
|
@ -1005,6 +1015,35 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
} else if (syncMessage.verified) {
|
||||
OWSLogInfo(@"Received verification state for %@", syncMessage.verified.destination);
|
||||
[self.identityManager throws_processIncomingSyncMessage:syncMessage.verified transaction:transaction];
|
||||
} else if (syncMessage.contacts) {
|
||||
NSLog(@"[Loki] Received contact sync message.");
|
||||
NSData *data = syncMessage.contacts.data;
|
||||
ContactParser *parser = [[ContactParser alloc] initWithData:data];
|
||||
NSArray<NSString *> *hexEncodedPublicKeys = [parser parseHexEncodedPublicKeys];
|
||||
// Try to establish sessions
|
||||
for (NSString *hexEncodedPublicKey in hexEncodedPublicKeys) {
|
||||
TSContactThread *thread = [TSContactThread getThreadWithContactId:hexEncodedPublicKey transaction:transaction];
|
||||
if (thread == nil) { return; }
|
||||
LKThreadFriendRequestStatus friendRequestStatus = thread.friendRequestStatus;
|
||||
switch (friendRequestStatus) {
|
||||
case LKThreadFriendRequestStatusNone: {
|
||||
OWSMessageSender *messageSender = SSKEnvironment.shared.messageSender;
|
||||
OWSMessageSend *automatedFriendRequestMessage = [messageSender getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:hexEncodedPublicKey];
|
||||
dispatch_async(OWSDispatch.sendingQueue, ^{
|
||||
[messageSender sendMessage:automatedFriendRequestMessage];
|
||||
});
|
||||
break;
|
||||
}
|
||||
case LKThreadFriendRequestStatusRequestReceived: {
|
||||
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];
|
||||
// The two lines below are equivalent to calling [ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:thread]
|
||||
LKEphemeralMessage *backgroundMessage = [[LKEphemeralMessage alloc] initInThread:thread];
|
||||
[self.messageSenderJobQueue addMessage:backgroundMessage transaction:transaction];
|
||||
break;
|
||||
}
|
||||
default: break; // Do nothing
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OWSLogWarn(@"Ignoring unsupported sync message.");
|
||||
}
|
||||
|
@ -1415,11 +1454,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Loki: Cache the user hex encoded public key (for mentions)
|
||||
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
|
||||
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
|
||||
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
|
||||
}];
|
||||
});
|
||||
|
||||
[self finalizeIncomingMessage:incomingMessage
|
||||
thread:oldGroupThread
|
||||
masterThread:oldGroupThread
|
||||
envelope:envelope
|
||||
transaction:transaction];
|
||||
|
||||
|
@ -1437,24 +1481,25 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
} else {
|
||||
|
||||
// TODO: Do we need to fetch the device mapping here?
|
||||
// Loki: A message from a secondary device should appear as if it came from the primary device; the underlying
|
||||
// friend request logic, however, should still be specific to the secondary device.
|
||||
|
||||
// Loki: Get the master hex encoded public key
|
||||
NSString *hexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
|
||||
OWSLogDebug(
|
||||
@"incoming message from: %@ with timestamp: %lu", hexEncodedPublicKey, (unsigned long)timestamp);
|
||||
TSContactThread *thread =
|
||||
[TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
|
||||
// Loki: Get the master hex encoded public key and thread
|
||||
NSString *hexEncodedPublicKey = envelope.source;
|
||||
NSString *masterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
|
||||
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterHexEncodedPublicKey transaction:transaction];
|
||||
|
||||
OWSLogDebug(@"incoming message from: %@ with timestamp: %lu", hexEncodedPublicKey, (unsigned long)timestamp);
|
||||
|
||||
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
|
||||
thread:thread
|
||||
thread:masterThread
|
||||
createdByRemoteRecipientId:hexEncodedPublicKey
|
||||
createdInExistingGroup:NO
|
||||
transaction:transaction];
|
||||
|
||||
TSQuotedMessage *_Nullable quotedMessage = [TSQuotedMessage quotedMessageForDataMessage:dataMessage
|
||||
thread:thread
|
||||
thread:masterThread
|
||||
transaction:transaction];
|
||||
|
||||
NSError *linkPreviewError;
|
||||
|
@ -1470,8 +1515,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// Legit usage of senderTimestamp when creating incoming message from received envelope
|
||||
TSIncomingMessage *incomingMessage =
|
||||
[[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp
|
||||
inThread:thread
|
||||
authorId:[thread contactIdentifier]
|
||||
inThread:masterThread
|
||||
authorId:masterThread.contactIdentifier
|
||||
sourceDeviceId:envelope.sourceDevice
|
||||
messageBody:body
|
||||
attachmentIds:@[]
|
||||
|
@ -1486,9 +1531,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSString *rawDisplayName = dataMessage.profile.displayName;
|
||||
if (rawDisplayName != nil && rawDisplayName.length > 0) {
|
||||
NSString *displayName = [NSString stringWithFormat:@"%@ (...%@)", rawDisplayName, [incomingMessage.authorId substringFromIndex:incomingMessage.authorId.length - 8]];
|
||||
[self.profileManager setDisplayNameForContactWithID:thread.contactIdentifier to:displayName with:transaction];
|
||||
[self.profileManager setDisplayNameForContactWithID:masterThread.contactIdentifier to:displayName with:transaction];
|
||||
} else {
|
||||
[self.profileManager setDisplayNameForContactWithID:thread.contactIdentifier to:nil with:transaction];
|
||||
[self.profileManager setDisplayNameForContactWithID:masterThread.contactIdentifier to:nil with:transaction];
|
||||
}
|
||||
|
||||
// Loki: Parse Loki specific properties if needed
|
||||
|
@ -1522,6 +1567,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[self finalizeIncomingMessage:incomingMessage
|
||||
thread:thread
|
||||
masterThread:thread
|
||||
envelope:envelope
|
||||
transaction:transaction];
|
||||
|
||||
|
@ -1529,8 +1575,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
- (void)handleFriendRequestMessageIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope data:(SSKProtoDataMessage *)data message:(TSIncomingMessage *)message thread:(TSThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
if (envelope.isGroupChatMessage || envelope.type != SSKProtoEnvelopeTypeFriendRequest) return;
|
||||
- (BOOL)canFriendRequestBeAutoAcceptedForThread:(TSContactThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
NSString *senderHexEncodedPublicKey = thread.contactIdentifier;
|
||||
if (thread.hasCurrentUserSentFriendRequest) {
|
||||
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
|
||||
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
|
||||
|
@ -1542,28 +1589,71 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// before updating Alice's thread's friend request status to LKThreadFriendRequestStatusFriends,
|
||||
// we can end up in a deadlock where both users' threads' friend request statuses are
|
||||
// LKThreadFriendRequestStatusRequestSent.
|
||||
return YES;
|
||||
}
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSSet<NSString *> *userLinkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction];
|
||||
if ([userLinkedDeviceHexEncodedPublicKeys containsObject:senderHexEncodedPublicKey]) {
|
||||
// Auto-accept any friend requests from the user's own linked devices
|
||||
return YES;
|
||||
}
|
||||
NSSet<TSContactThread *> *senderLinkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderHexEncodedPublicKey in:transaction];
|
||||
if ([senderLinkedDeviceThreads contains:^BOOL(TSContactThread *thread) {
|
||||
return thread.isContactFriend;
|
||||
}]) {
|
||||
// Auto-accept if the user is friends with any of the sender's linked devices.
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)handleFriendRequestMessageIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope data:(SSKProtoDataMessage *)data message:(TSIncomingMessage *)message thread:(TSContactThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
if (envelope.isGroupChatMessage) {
|
||||
return NSLog(@"[Loki] Ignoring friend request in group chat.", @"");
|
||||
}
|
||||
if (envelope.type != SSKProtoEnvelopeTypeFriendRequest) {
|
||||
return NSLog(@"[Loki] handleFriendRequestMessageIfNeededWithEnvelope:data:message:thread:transaction was called with an envelope that isn't of type SSKProtoEnvelopeTypeFriendRequest.");
|
||||
}
|
||||
if ([self canFriendRequestBeAutoAcceptedForThread:thread transaction:transaction]) {
|
||||
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];
|
||||
TSOutgoingMessage *existingFriendRequestMessage = (TSOutgoingMessage *)[thread.lastInteraction as:TSOutgoingMessage.class];
|
||||
if (existingFriendRequestMessage != nil && existingFriendRequestMessage.isFriendRequest) {
|
||||
__block TSOutgoingMessage *existingFriendRequestMessage;
|
||||
[thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) {
|
||||
if ([interaction isKindOfClass:TSOutgoingMessage.class] && ((TSOutgoingMessage *)interaction).isFriendRequest) {
|
||||
existingFriendRequestMessage = (TSOutgoingMessage *)interaction;
|
||||
}
|
||||
}];
|
||||
if (existingFriendRequestMessage != nil) {
|
||||
[existingFriendRequestMessage saveFriendRequestStatus:LKMessageFriendRequestStatusAccepted withTransaction:transaction];
|
||||
}
|
||||
// The two lines below are equivalent to calling [ThreadUtil enqueueFriendRequestAcceptanceMessageInThread:thread]
|
||||
LKEphemeralMessage *backgroundMessage = [[LKEphemeralMessage alloc] initInThread:thread];
|
||||
[self.messageSenderJobQueue addMessage:backgroundMessage transaction:transaction];
|
||||
} else {
|
||||
if (!thread.isContactFriend) {
|
||||
// Checking that the sender of the message isn't already a friend is necessary because otherwise
|
||||
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
|
||||
// friend request status is reset to LKThreadFriendRequestStatusNone. Bob now sends Alice a friend
|
||||
// request. Alice's thread's friend request status is reset to
|
||||
// LKThreadFriendRequestStatusRequestReceived.
|
||||
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestReceived withTransaction:transaction];
|
||||
message.friendRequestStatus = LKMessageFriendRequestStatusPending; // Don't save yet. This is done in finalizeIncomingMessage:thread:envelope:transaction.
|
||||
} else {
|
||||
// This can happen if Alice and Bob have a session, Bob deletes his app, restores from seed, and then sends a friend request to Alice again.
|
||||
// TODO: Re-enable when seed restoration is done
|
||||
// [self handleEndSessionMessageWithEnvelope:envelope dataMessage:data transaction:transaction];
|
||||
} else if (!thread.isContactFriend) {
|
||||
// Checking that the sender of the message isn't already a friend is necessary because otherwise
|
||||
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
|
||||
// friend request status is reset to LKThreadFriendRequestStatusNone. Bob now sends Alice a friend
|
||||
// request. Alice's thread's friend request status is reset to
|
||||
// LKThreadFriendRequestStatusRequestReceived.
|
||||
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestReceived withTransaction:transaction];
|
||||
// Except for the message.friendRequestStatus = LKMessageFriendRequestStatusPending line below, all of this is to ensure that
|
||||
// there's only ever one message with status LKMessageFriendRequestStatusPending in a thread (where a thread is the combination
|
||||
// of all threads belonging to the linked devices of a user).
|
||||
NSString *senderID = ((TSIncomingMessage *)message).authorId;
|
||||
NSSet<TSContactThread *> *linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction];
|
||||
for (TSContactThread *thread in linkedDeviceThreads) {
|
||||
[thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) {
|
||||
if (![interaction isKindOfClass:TSIncomingMessage.class]) { return; }
|
||||
TSIncomingMessage *message = (TSIncomingMessage *)interaction;
|
||||
if (message.friendRequestStatus != LKMessageFriendRequestStatusNone) {
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusNone withTransaction:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
message.friendRequestStatus = LKMessageFriendRequestStatusPending; // Don't save yet. This is done in finalizeIncomingMessage:thread:masterThread:envelope:transaction.
|
||||
} else {
|
||||
// This can happen if Alice and Bob have a session, Bob deletes his app, restores from seed, and then sends a friend request to Alice again.
|
||||
// TODO: Re-enable when seed restoration is done
|
||||
// [self handleEndSessionMessageWithEnvelope:envelope dataMessage:data transaction:transaction];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1578,7 +1668,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (thread.isContactFriend) return;
|
||||
// Become happy friends and go on great adventures
|
||||
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];
|
||||
TSOutgoingMessage *existingFriendRequestMessage = (TSOutgoingMessage *)[thread.lastInteraction as:TSOutgoingMessage.class];
|
||||
TSOutgoingMessage *existingFriendRequestMessage = [thread.lastInteraction as:TSOutgoingMessage.class];
|
||||
if (existingFriendRequestMessage != nil && existingFriendRequestMessage.isFriendRequest) {
|
||||
[existingFriendRequestMessage saveFriendRequestStatus:LKMessageFriendRequestStatusAccepted withTransaction:transaction];
|
||||
}
|
||||
|
@ -1591,6 +1681,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)finalizeIncomingMessage:(TSIncomingMessage *)incomingMessage
|
||||
thread:(TSThread *)thread
|
||||
masterThread:(TSThread *)masterThread
|
||||
envelope:(SSKProtoEnvelope *)envelope
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
|
@ -1619,7 +1710,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Any messages sent from the current user - from this device or another - should be automatically marked as read.
|
||||
if ([(thread.contactIdentifier ?: envelope.source) isEqualToString:self.tsAccountManager.localNumber]) {
|
||||
if ([(masterThread.contactIdentifier ?: envelope.source) isEqualToString:self.tsAccountManager.localNumber]) {
|
||||
// Don't send a read receipt for messages sent by ourselves.
|
||||
[incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction];
|
||||
}
|
||||
|
@ -1673,15 +1764,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
transaction:transaction];
|
||||
|
||||
// Update thread preview in inbox
|
||||
[thread touchWithTransaction:transaction];
|
||||
[masterThread touchWithTransaction:transaction];
|
||||
|
||||
[SSKEnvironment.shared.notificationsManager notifyUserForIncomingMessage:incomingMessage
|
||||
inThread:thread
|
||||
inThread:masterThread
|
||||
transaction:transaction];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.typingIndicators didReceiveIncomingMessageInThread:thread
|
||||
recipientId:(thread.contactIdentifier ?: envelope.source)
|
||||
[self.typingIndicators didReceiveIncomingMessageInThread:masterThread
|
||||
recipientId:(masterThread.contactIdentifier ?: envelope.source)
|
||||
deviceId:envelope.sourceDevice];
|
||||
});
|
||||
}
|
||||
|
@ -1752,7 +1843,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
SignalRecipient *_Nullable recipient =
|
||||
[SignalRecipient registeredRecipientForRecipientId:localNumber mustHaveDevices:NO transaction:transaction];
|
||||
if (!recipient) {
|
||||
OWSFailDebug(@"No local SignalRecipient.");
|
||||
// OWSFailDebug(@"No local SignalRecipient.");
|
||||
} else {
|
||||
BOOL isRecipientDevice = [recipient.devices containsObject:@(envelope.sourceDevice)];
|
||||
if (!isRecipientDevice) {
|
||||
|
|
|
@ -16,6 +16,7 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
|
|||
@class TSOutgoingMessage;
|
||||
@class TSThread;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
@class OWSMessageSend;
|
||||
|
||||
@protocol ContactsManagerProtocol;
|
||||
|
||||
|
@ -96,6 +97,9 @@ NS_SWIFT_NAME(MessageSender)
|
|||
success:(void (^)(void))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler;
|
||||
|
||||
- (OWSMessageSend *)getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:(NSString *)hexEncodedPublicKey;
|
||||
- (void)sendMessage:(OWSMessageSend *)messageSend;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -232,7 +232,7 @@ void AssertIsOnSendingQueue()
|
|||
- (void)didSucceed
|
||||
{
|
||||
if (self.message.messageState != TSOutgoingMessageStateSent) {
|
||||
OWSFailDebug(@"unexpected message status: %@", self.message.statusDescription);
|
||||
// OWSFailDebug(@"unexpected message status: %@", self.message.statusDescription);
|
||||
}
|
||||
|
||||
self.successHandler();
|
||||
|
@ -498,9 +498,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
OWSAssertDebug(message);
|
||||
OWSAssertDebug(errorHandle);
|
||||
|
||||
NSMutableSet<NSString *> *recipientIds = [NSMutableSet new];
|
||||
__block NSMutableSet<NSString *> *recipientIds = [NSMutableSet new];
|
||||
if ([message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
|
||||
[recipientIds addObject:self.tsAccountManager.localNumber];
|
||||
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction] ?: userHexEncodedPublicKey;
|
||||
recipientIds = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction].mutableCopy;
|
||||
}];
|
||||
} else if (thread.isGroupThread) {
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
LKPublicChat *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
|
||||
|
@ -597,7 +601,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
}
|
||||
resolve(error);
|
||||
}];
|
||||
[self sendMessageToRecipient:messageSend];
|
||||
[self sendMessageToDestinationAndLinkedDevices:messageSend];
|
||||
}];
|
||||
[sendPromises addObject:sendPromise];
|
||||
}
|
||||
|
@ -668,14 +672,17 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
return failureHandler(error);
|
||||
}
|
||||
|
||||
// In the "self-send" special case, we ony need to send a sync message with a delivery receipt.
|
||||
if ([thread isKindOfClass:[TSContactThread class]] &&
|
||||
[((TSContactThread *)thread).contactIdentifier isEqualToString:self.tsAccountManager.localNumber]) {
|
||||
// Send to self.
|
||||
OWSAssertDebug(message.recipientIds.count == 1);
|
||||
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
|
||||
successHandler();
|
||||
return;
|
||||
// Loki: Handle note to self case
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
__block BOOL isNoteToSelf;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:((TSContactThread *)thread).contactIdentifier in:transaction];
|
||||
}];
|
||||
if (isNoteToSelf) {
|
||||
[self sendSyncTranscriptForMessage:message isRecipientUpdate:NO success:^{ } failure:^(NSError *error) { }];
|
||||
successHandler();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (thread.isGroupThread) {
|
||||
|
@ -919,31 +926,29 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
thread.isForceHidden = YES;
|
||||
[thread saveWithTransaction:transaction];
|
||||
}];
|
||||
LKFriendRequestMessage *message = [[LKFriendRequestMessage alloc] initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"Accept this friend request to enable messages to be synced across devices" attachmentIds:[NSMutableArray new]
|
||||
LKFriendRequestMessage *message = [[LKFriendRequestMessage alloc] initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"Please accept to enable messages to be synced across devices" attachmentIds:[NSMutableArray new]
|
||||
expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil];
|
||||
message.skipSave = YES;
|
||||
SignalRecipient *recipient = [[SignalRecipient alloc] initWithUniqueId:hexEncodedPublicKey];
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
return [[OWSMessageSend alloc] initWithMessage:message thread:thread recipient:recipient senderCertificate:nil udAccess:nil localNumber:userHexEncodedPublicKey success:^{ } failure:^(NSError *error) { }];
|
||||
}
|
||||
|
||||
- (void)sendMessageToRecipient:(OWSMessageSend *)messageSend
|
||||
- (void)sendMessageToDestinationAndLinkedDevices:(OWSMessageSend *)messageSend
|
||||
{
|
||||
TSOutgoingMessage *message = messageSend.message;
|
||||
NSString *contactID = messageSend.recipient.recipientId;
|
||||
BOOL isGroupMessage = messageSend.thread.isGroupThread;
|
||||
BOOL isDeviceLinkMessage = [message isKindOfClass:LKDeviceLinkMessage.class];
|
||||
BOOL isMessageToSelf = (contactID == OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey);
|
||||
BOOL isSyncMessage = [message isKindOfClass:OWSOutgoingSyncMessage.class];
|
||||
if (isGroupMessage || isDeviceLinkMessage || isMessageToSelf || isSyncMessage) {
|
||||
if (isGroupMessage || isDeviceLinkMessage) {
|
||||
[self sendMessage:messageSend];
|
||||
} else {
|
||||
BOOL isSilentMessage = message.isSilent;
|
||||
BOOL isSilentMessage = message.isSilent || [message isKindOfClass:LKEphemeralMessage.class] || [message isKindOfClass:OWSOutgoingSyncMessage.class];
|
||||
BOOL isFriendRequestMessage = [message isKindOfClass:LKFriendRequestMessage.class];
|
||||
[[LKAPI getDestinationsFor:contactID]
|
||||
.thenOn(OWSDispatch.sendingQueue, ^(NSArray<LKDestination *> *destinations) {
|
||||
// Get master destination
|
||||
LKDestination *masterDestination = [destinations filtered:^BOOL(NSObject *object) {
|
||||
LKDestination *destination = [object as:LKDestination.class];
|
||||
LKDestination *masterDestination = [destinations filtered:^BOOL(LKDestination *destination) {
|
||||
return [destination.kind isEqual:@"master"];
|
||||
}].firstObject;
|
||||
// Send to master destination
|
||||
|
@ -958,8 +963,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
}
|
||||
}
|
||||
// Get slave destinations
|
||||
NSArray *slaveDestinations = [destinations filtered:^BOOL(NSObject *object) {
|
||||
LKDestination *destination = [object as:LKDestination.class];
|
||||
NSArray *slaveDestinations = [destinations filtered:^BOOL(LKDestination *destination) {
|
||||
return [destination.kind isEqual:@"slave"];
|
||||
}];
|
||||
// Send to slave destinations (using a best attempt approach (i.e. ignoring the message send result) for now)
|
||||
|
@ -1164,9 +1168,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
// so that we can learn from the service whether or not there are
|
||||
// linked devices that we don't know about.
|
||||
OWSLogWarn(@"Sending a message with no device messages.");
|
||||
|
||||
// Loki: We don't handle linked devices yet so it's better to just exit early
|
||||
// This would only occur if we're sending a message to ourself
|
||||
|
||||
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
||||
[error setIsRetryable:NO];
|
||||
return messageSend.failure(error);
|
||||
|
@ -1225,6 +1227,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
NSUInteger height = attachment.shouldHaveImageSize ? @(attachment.imageSize.height).unsignedIntegerValue : 0;
|
||||
[groupMessage addAttachmentWithKind:@"attachment" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:width height:height caption:attachment.caption url:attachment.downloadURL linkPreviewURL:nil linkPreviewTitle:nil];
|
||||
}
|
||||
message.actualSenderHexEncodedPublicKey = userHexEncodedPublicKey;
|
||||
[[LKPublicChatAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server]
|
||||
.thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
|
@ -1247,27 +1250,31 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
uint64_t ttl = ((NSNumber *)signalMessageInfo[@"ttl"]).unsignedIntegerValue;
|
||||
BOOL isPing = ((NSNumber *)signalMessageInfo[@"isPing"]).boolValue;
|
||||
LKSignalMessage *signalMessage = [[LKSignalMessage alloc] initWithType:type timestamp:timestamp senderID:senderID senderDeviceID:senderDeviceID content:content recipientID:recipientID ttl:ttl isPing:isPing];
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the PoW calculation status
|
||||
[message saveIsCalculatingProofOfWork:YES withTransaction:transaction];
|
||||
// Update the message and thread if needed
|
||||
if (signalMessage.type == TSFriendRequestMessageType) {
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSending withTransaction:transaction];
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
|
||||
}
|
||||
}];
|
||||
if (!message.skipSave) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the PoW calculation status
|
||||
[message saveIsCalculatingProofOfWork:YES withTransaction:transaction];
|
||||
// Update the message and thread if needed
|
||||
if (signalMessage.type == TSFriendRequestMessageType) {
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSending withTransaction:transaction];
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
// Convenience
|
||||
void (^onP2PSuccess)() = ^() { message.isP2P = YES; };
|
||||
void (^handleError)(NSError *error) = ^(NSError *error) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the message and thread if needed
|
||||
if (signalMessage.type == TSFriendRequestMessageType) {
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusNone withTransaction:transaction];
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
|
||||
}
|
||||
// Update the PoW calculation status
|
||||
[message saveIsCalculatingProofOfWork:NO withTransaction:transaction];
|
||||
}];
|
||||
if (!message.skipSave) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the message and thread if needed
|
||||
if (signalMessage.type == TSFriendRequestMessageType) {
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusNone withTransaction:transaction];
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
|
||||
}
|
||||
// Update the PoW calculation status
|
||||
[message saveIsCalculatingProofOfWork:NO withTransaction:transaction];
|
||||
}];
|
||||
}
|
||||
// Handle the error
|
||||
failedMessageSend(error);
|
||||
};
|
||||
|
@ -1285,16 +1292,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
[LKAnalytics.shared track:@"Sent Message Using Swarm API"];
|
||||
isSuccess = YES;
|
||||
if (signalMessage.type == TSFriendRequestMessageType) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the thread
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSent withTransaction:transaction];
|
||||
[message.thread removeOldOutgoingFriendRequestMessagesIfNeededWithTransaction:transaction];
|
||||
// Update the message
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusPending withTransaction:transaction];
|
||||
NSTimeInterval expirationInterval = 72 * kHourInterval;
|
||||
NSDate *expirationDate = [[NSDate new] dateByAddingTimeInterval:expirationInterval];
|
||||
[message saveFriendRequestExpiresAt:[NSDate ows_millisecondsSince1970ForDate:expirationDate] withTransaction:transaction];
|
||||
}];
|
||||
if (!message.skipSave) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Update the thread
|
||||
[message.thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSent withTransaction:transaction];
|
||||
[message.thread removeOldOutgoingFriendRequestMessagesIfNeededWithTransaction:transaction];
|
||||
// Update the message
|
||||
[message saveFriendRequestStatus:LKMessageFriendRequestStatusPending withTransaction:transaction];
|
||||
NSTimeInterval expirationInterval = 72 * kHourInterval;
|
||||
NSDate *expirationDate = [[NSDate new] dateByAddingTimeInterval:expirationInterval];
|
||||
[message saveFriendRequestExpiresAt:[NSDate ows_millisecondsSince1970ForDate:expirationDate] withTransaction:transaction];
|
||||
}];
|
||||
}
|
||||
}
|
||||
// Invoke the completion handler
|
||||
[self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:false wasSentByWebsocket:false];
|
||||
|
@ -1348,15 +1357,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
// have a valid Signal account.
|
||||
[SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction];
|
||||
}];
|
||||
|
||||
// Loki: Check if we need to generate a link preview
|
||||
TSMessage *message = messageSend.message;
|
||||
if (message.linkPreview == nil && !message.hasAttachments) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString *linkPreviewURL = [OWSLinkPreview previewURLForRawBodyText:message.body];
|
||||
if (linkPreviewURL) { [message generateLinkPreviewIfNeededFromURL:linkPreviewURL]; }
|
||||
});
|
||||
}
|
||||
|
||||
messageSend.success();
|
||||
});
|
||||
|
@ -1388,7 +1388,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
|
||||
dispatch_async([OWSDispatch sendingQueue], ^{
|
||||
OWSLogDebug(@"Retrying: %@", message.debugDescription);
|
||||
[self sendMessageToRecipient:messageSend];
|
||||
[self sendMessage:messageSend];
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1543,19 +1543,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
failure:(RetryableFailureHandler)failure
|
||||
{
|
||||
dispatch_block_t success = ^{
|
||||
TSThread *_Nullable thread = message.thread;
|
||||
if (thread && [thread isKindOfClass:[TSContactThread class]] &&
|
||||
[thread.contactIdentifier isEqualToString:self.tsAccountManager.localNumber]) {
|
||||
OWSAssertDebug(message.recipientIds.count == 1);
|
||||
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
TSThread *thread = message.thread;
|
||||
// Loki: Handle note to self case
|
||||
if (thread && [thread isKindOfClass:[TSContactThread class]] && [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction]) {
|
||||
for (NSString *recipientId in message.sendingRecipientIds) {
|
||||
[message updateWithReadRecipientId:recipientId
|
||||
readTimestamp:message.timestamp
|
||||
transaction:transaction];
|
||||
[message updateWithReadRecipientId:recipientId readTimestamp:message.timestamp transaction:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
successParam();
|
||||
};
|
||||
|
@ -1570,7 +1566,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
return success();
|
||||
}
|
||||
|
||||
BOOL shouldSendTranscript = (AreRecipientUpdatesEnabled() || !message.hasSyncedTranscript);
|
||||
// Loki: Handle note to self case
|
||||
__block BOOL isNoteToSelf = NO;
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSThread *thread = message.thread;
|
||||
if (thread && [thread isKindOfClass:[TSContactThread class]] && [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction]) {
|
||||
isNoteToSelf = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
BOOL isPublicChatMessage = message.thread.isGroupThread;
|
||||
|
||||
BOOL shouldSendTranscript = (AreRecipientUpdatesEnabled() || !message.hasSyncedTranscript) && !isNoteToSelf && !isPublicChatMessage;
|
||||
if (!shouldSendTranscript) {
|
||||
return success();
|
||||
}
|
||||
|
@ -1619,7 +1626,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
|
||||
failure(error);
|
||||
}];
|
||||
[self sendMessageToRecipient:messageSend];
|
||||
[self sendMessage:messageSend];
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary *> *)throws_deviceMessagesForMessageSend:(OWSMessageSend *)messageSend
|
||||
|
@ -1644,20 +1651,21 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
messageSend.isLocalNumber,
|
||||
messageSend.isUDSend);
|
||||
|
||||
// Loki: Since we don't support multi-device sending yet, just send it to the primary device
|
||||
NSMutableArray<NSNumber *> *deviceIds = [NSMutableArray arrayWithObject:@(OWSDevicePrimaryDeviceId)];
|
||||
|
||||
/* Loki: Original code
|
||||
NSMutableArray<NSNumber *> *deviceIds = [recipient.devices mutableCopy];
|
||||
*/
|
||||
|
||||
OWSAssertDebug(deviceIds);
|
||||
|
||||
if (messageSend.isLocalNumber) {
|
||||
[deviceIds removeObject:@(OWSDevicePrimaryDeviceId)];
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
|
||||
__block NSMutableSet<NSString *> *recipientIDs = [NSSet setWithObject:recipient.uniqueId];
|
||||
if ([messageSend.message isKindOfClass:OWSOutgoingSyncMessage.class]) {
|
||||
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction] ?: userHexEncodedPublicKey;
|
||||
recipientIDs = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction].mutableCopy;
|
||||
}];
|
||||
}
|
||||
|
||||
for (NSNumber *deviceId in deviceIds) {
|
||||
if (messageSend.isLocalNumber) {
|
||||
[recipientIDs removeObject:userHexEncodedPublicKey];
|
||||
}
|
||||
|
||||
for (NSString *recipientID in recipientIDs) {
|
||||
@try {
|
||||
// This may involve blocking network requests, so we do it _before_
|
||||
// we open a transaction.
|
||||
|
@ -1666,7 +1674,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
BOOL isFriendRequest = [messageSend.message isKindOfClass:LKFriendRequestMessage.class];
|
||||
BOOL isDeviceLinkMessage = [messageSend.message isKindOfClass:LKDeviceLinkMessage.class];
|
||||
if (!isFriendRequest && !isDeviceLinkMessage) {
|
||||
[self throws_ensureRecipientHasSessionForMessageSend:messageSend deviceId:deviceId];
|
||||
[self throws_ensureRecipientHasSessionForMessageSend:messageSend recipientID:recipientID deviceId:@(OWSDevicePrimaryDeviceId)];
|
||||
}
|
||||
|
||||
__block NSDictionary *_Nullable messageDict;
|
||||
|
@ -1675,7 +1683,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
@try {
|
||||
messageDict = [self throws_encryptedMessageForMessageSend:messageSend
|
||||
deviceId:deviceId
|
||||
recipientID:recipientID
|
||||
plainText:plainText
|
||||
transaction:transaction];
|
||||
} @catch (NSException *exception) {
|
||||
|
@ -1697,7 +1705,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[recipient updateRegisteredRecipientWithDevicesToAdd:nil
|
||||
devicesToRemove:@[ deviceId ]
|
||||
devicesToRemove:@[ @(OWSDevicePrimaryDeviceId) ]
|
||||
transaction:transaction];
|
||||
}];
|
||||
} else {
|
||||
|
@ -1709,19 +1717,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
return [messagesArray copy];
|
||||
}
|
||||
|
||||
- (void)throws_ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend deviceId:(NSNumber *)deviceId
|
||||
- (void)throws_ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend recipientID:(NSString *)recipientID deviceId:(NSNumber *)deviceId
|
||||
{
|
||||
OWSAssertDebug(messageSend);
|
||||
OWSAssertDebug(deviceId);
|
||||
|
||||
OWSPrimaryStorage *storage = self.primaryStorage;
|
||||
SignalRecipient *recipient = messageSend.recipient;
|
||||
NSString *recipientId = recipient.recipientId;
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug(recipientID.length > 0);
|
||||
|
||||
__block BOOL hasSession;
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
hasSession = [storage containsSession:recipientId deviceId:[deviceId intValue] protocolContext:transaction];
|
||||
hasSession = [storage containsSession:recipientID deviceId:[deviceId intValue] protocolContext:transaction];
|
||||
}];
|
||||
if (hasSession) {
|
||||
return;
|
||||
|
@ -1732,8 +1739,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
OWSRaiseException(NoSessionForTransientMessageException, @"No session for transient message.");
|
||||
}
|
||||
|
||||
PreKeyBundle *_Nullable bundle = [storage getPreKeyBundleForContact:recipientId];
|
||||
__block NSException *_Nullable exception;
|
||||
PreKeyBundle *_Nullable bundle = [storage getPreKeyBundleForContact:recipientID];
|
||||
__block NSException *exception;
|
||||
|
||||
/** Loki: Original code
|
||||
* ================
|
||||
|
@ -1776,14 +1783,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
preKeyStore:storage
|
||||
signedPreKeyStore:storage
|
||||
identityKeyStore:self.identityManager
|
||||
recipientId:recipientId
|
||||
recipientId:recipientID
|
||||
deviceId:[deviceId intValue]];
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
@try {
|
||||
[builder throws_processPrekeyBundle:bundle protocolContext:transaction];
|
||||
|
||||
// Loki: Discard the prekey bundle here since the session is initiated
|
||||
[storage removePreKeyBundleForContact:recipientId transaction:transaction];
|
||||
// Loki: Discard the pre key bundle here since the session has been established
|
||||
[storage removePreKeyBundleForContact:recipientID transaction:transaction];
|
||||
} @catch (NSException *caughtException) {
|
||||
exception = caughtException;
|
||||
}
|
||||
|
@ -1791,7 +1798,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
if (exception) {
|
||||
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
|
||||
OWSRaiseExceptionWithUserInfo(UntrustedIdentityKeyException,
|
||||
(@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientId }),
|
||||
(@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientID }),
|
||||
@"");
|
||||
}
|
||||
@throw exception;
|
||||
|
@ -1896,43 +1903,40 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
}
|
||||
|
||||
- (nullable NSDictionary *)throws_encryptedMessageForMessageSend:(OWSMessageSend *)messageSend
|
||||
deviceId:(NSNumber *)deviceId
|
||||
recipientID:(NSString *)recipientID
|
||||
plainText:(NSData *)plainText
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(messageSend);
|
||||
OWSAssertDebug(deviceId);
|
||||
OWSAssertDebug(recipientID);
|
||||
OWSAssertDebug(plainText);
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
OWSPrimaryStorage *storage = self.primaryStorage;
|
||||
TSOutgoingMessage *message = messageSend.message;
|
||||
SignalRecipient *recipient = messageSend.recipient;
|
||||
NSString *recipientId = recipient.recipientId;
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
// Loki: Both for friend request messages and device link messages we use fallback encryption as we don't necessarily have a session yet
|
||||
BOOL isFriendRequest = [messageSend.message isKindOfClass:LKFriendRequestMessage.class];
|
||||
BOOL isDeviceLinkMessage = [messageSend.message isKindOfClass:LKDeviceLinkMessage.class];
|
||||
if (isFriendRequest || isDeviceLinkMessage) {
|
||||
return [self throws_encryptedFriendRequestOrDeviceLinkMessageForMessageSend:messageSend deviceId:deviceId plainText:plainText];
|
||||
return [self throws_encryptedFriendRequestOrDeviceLinkMessageForMessageSend:messageSend deviceId:@(OWSDevicePrimaryDeviceId) plainText:plainText];
|
||||
}
|
||||
|
||||
// This may throw an exception.
|
||||
if (![storage containsSession:recipientId deviceId:[deviceId intValue] protocolContext:transaction]) {
|
||||
if (![storage containsSession:recipientID deviceId:@(OWSDevicePrimaryDeviceId).intValue protocolContext:transaction]) {
|
||||
NSString *missingSessionException = @"missingSessionException";
|
||||
OWSRaiseException(missingSessionException,
|
||||
@"Unexpectedly missing session for recipient: %@, device: %@",
|
||||
recipientId,
|
||||
deviceId);
|
||||
recipientID,
|
||||
@(OWSDevicePrimaryDeviceId));
|
||||
}
|
||||
|
||||
SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage
|
||||
preKeyStore:storage
|
||||
signedPreKeyStore:storage
|
||||
identityKeyStore:self.identityManager
|
||||
recipientId:recipientId
|
||||
deviceId:[deviceId intValue]];
|
||||
recipientId:recipientID
|
||||
deviceId:@(OWSDevicePrimaryDeviceId).intValue];
|
||||
|
||||
NSData *_Nullable serializedMessage;
|
||||
TSWhisperMessageType messageType;
|
||||
|
@ -1948,8 +1952,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher.");
|
||||
}
|
||||
|
||||
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientId
|
||||
deviceId:deviceId.intValue
|
||||
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
|
||||
deviceId:@(OWSDevicePrimaryDeviceId).intValue
|
||||
paddedPlaintext:[plainText paddedMessageBody]
|
||||
senderCertificate:messageSend.senderCertificate
|
||||
protocolContext:transaction
|
||||
|
@ -1971,12 +1975,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
BOOL isSilent = message.isSilent;
|
||||
BOOL isOnline = message.isOnline;
|
||||
|
||||
LKAddressMessage *_Nullable addressMessage = [message as:[LKAddressMessage class]];
|
||||
LKAddressMessage *addressMessage = [message as:[LKAddressMessage class]];
|
||||
BOOL isPing = addressMessage != nil && addressMessage.isPing;
|
||||
OWSMessageServiceParams *messageParams =
|
||||
[[OWSMessageServiceParams alloc] initWithType:messageType
|
||||
recipientId:recipientId
|
||||
device:[deviceId intValue]
|
||||
recipientId:recipientID
|
||||
device:@(OWSDevicePrimaryDeviceId).intValue
|
||||
content:serializedMessage
|
||||
isSilent:isSilent
|
||||
isOnline:isOnline
|
||||
|
|
|
@ -286,7 +286,12 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
|||
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
|
||||
}
|
||||
|
||||
if ([message.authorId isEqualToString:[TSAccountManager localNumber]]) {
|
||||
__block BOOL isNoteToSelf;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:message.authorId in:transaction];
|
||||
}];
|
||||
|
||||
if (isNoteToSelf) {
|
||||
OWSLogVerbose(@"Ignoring read receipt for self-sender.");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -29,10 +29,15 @@ extension NetworkManagerError {
|
|||
extension TSNetworkManager {
|
||||
public typealias NetworkManagerResult = (task: URLSessionDataTask, responseObject: Any?)
|
||||
|
||||
public func makePromise(request: TSRequest) -> Promise<NetworkManagerResult> {
|
||||
public func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise<NetworkManagerResult> {
|
||||
return makePromise(request: request, queue: queue)
|
||||
}
|
||||
|
||||
public func makePromise(request: TSRequest, queue: DispatchQueue = DispatchQueue.main) -> Promise<NetworkManagerResult> {
|
||||
let (promise, resolver) = Promise<NetworkManagerResult>.pending()
|
||||
|
||||
self.makeRequest(request,
|
||||
completionQueue: queue,
|
||||
success: { task, responseObject in
|
||||
resolver.fulfill((task: task, responseObject: responseObject))
|
||||
},
|
||||
|
|
|
@ -323,8 +323,16 @@ public class TypingIndicatorsImpl: NSObject, TypingIndicators {
|
|||
return
|
||||
}
|
||||
|
||||
// Don't send any typing indicators for groups
|
||||
guard !thread.isGroupThread() else { return }
|
||||
// Loki: Don't send typing indicators in group or note to self threads
|
||||
if (thread.isGroupThread()) {
|
||||
return
|
||||
} else {
|
||||
var isNoteToSelf = false
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
isNoteToSelf = LokiDatabaseUtilities.isUserLinkedDevice(thread.contactIdentifier()!, transaction: transaction)
|
||||
}
|
||||
if isNoteToSelf { return }
|
||||
}
|
||||
|
||||
let message = TypingIndicatorMessage(thread: thread, action: action)
|
||||
messageSender.sendPromise(message: message).retainUntilComplete()
|
||||
|
|
Loading…
Reference in a new issue