Merge pull request #63 from loki-project/multi-device

Multi Device V2
This commit is contained in:
gmbnt 2019-11-15 14:46:15 +11:00 committed by GitHub
commit 62bd3ece25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 879 additions and 529 deletions

2
Pods

@ -1 +1 @@
Subproject commit 6ea6f3fb1559694d5159dc282cb7842982fdb30b
Subproject commit 25319b47801e3de4777084329f418fcfe36be04d

View file

@ -874,7 +874,7 @@ static NSTimeInterval launchStartedAt;
return;
}
[SignalApp.sharedApp.homeViewController showNewConversationView];
[SignalApp.sharedApp.homeViewController showNewConversationVC];
completionHandler(YES);
}];

View file

@ -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()
}

View file

@ -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)
}
}
}

View file

@ -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")

View file

@ -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()

View file

@ -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) {

View file

@ -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)

View file

@ -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]; }]];
}

View file

@ -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

View file

@ -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];

View file

@ -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

View file

@ -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

View file

@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
animated:(BOOL)isAnimated;
// Used by force-touch Springboard icon shortcut
- (void)showNewConversationView;
- (void)showNewConversationVC;
@end

View file

@ -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";

View file

@ -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);
}];
}];

View file

@ -17,6 +17,6 @@ files.
## Building Protobuf
cd ~/src/WhisperSystems/SignalServiceKit/protobuf
cd SignalServiceKit/protobuf
make

View file

@ -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.
*/

View file

@ -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;

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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;

View file

@ -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) {

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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)
}
}
}
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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],

View file

@ -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 }

View file

@ -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)
}
}

View file

@ -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"

View 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
}
}

View file

@ -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

View file

@ -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];

View file

@ -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; }

View file

@ -1,10 +1,6 @@
#import "TSOutgoingMessage.h"
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(FriendRequestMessage)
@interface LKFriendRequestMessage : TSOutgoingMessage
@end
NS_ASSUME_NONNULL_END

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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;

View file

@ -1,6 +1,6 @@
@interface NSObject (Casting)
- (instancetype)as:(Class)cls;
- (id)as:(Class)cls;
@end

View file

@ -2,7 +2,7 @@
@implementation NSObject (Casting)
- (instancetype)as:(Class)cls {
- (id)as:(Class)cls {
if ([self isKindOfClass:cls]) { return self; }
return nil;
}

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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];

View file

@ -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;

View file

@ -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

View file

@ -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];

View file

@ -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"];
}

View file

@ -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;

View file

@ -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) {

View file

@ -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 -

View file

@ -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

View file

@ -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;
}

View file

@ -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))
},

View file

@ -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()