Merge pull request #175 from loki-project/friend-request-refactor

Friend Request Refactor
pull/183/head
Niels Andriesse 5 years ago committed by GitHub
commit e108a3db96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit bfa486d0a82a6526220e7060fd87618a9cc28e85
Subproject commit cd26ded27108becc8ea6660f3d8e6b58ee2bd6c7

@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
2400888E239F30A600305217 /* SessionRestorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2400888D239F30A600305217 /* SessionRestorationView.swift */; };
241C1192245F8878005CB2F4 /* LK001UpdateFriendRequestStatusStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C1191245F8878005CB2F4 /* LK001UpdateFriendRequestStatusStorage.swift */; };
241C1194245F8CE2005CB2F4 /* LK001UpdateFriendRequestStatusStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C1193245F8C95005CB2F4 /* LK001UpdateFriendRequestStatusStorageTest.swift */; };
241C6314231F64C000B4198E /* JazzIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C630E231F5AAC00B4198E /* JazzIcon.swift */; };
241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */; };
241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6310231F5C4400B4198E /* UIColor+Helper.swift */; };
@ -740,6 +742,8 @@
1C93CF3971B64E8B6C1F9AC1 /* Pods-SignalShareExtension.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.test.xcconfig"; sourceTree = "<group>"; };
1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = "<group>"; };
2400888D239F30A600305217 /* SessionRestorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRestorationView.swift; sourceTree = "<group>"; };
241C1191245F8878005CB2F4 /* LK001UpdateFriendRequestStatusStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LK001UpdateFriendRequestStatusStorage.swift; sourceTree = "<group>"; };
241C1193245F8C95005CB2F4 /* LK001UpdateFriendRequestStatusStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LK001UpdateFriendRequestStatusStorageTest.swift; sourceTree = "<group>"; };
241C630E231F5AAC00B4198E /* JazzIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JazzIcon.swift; sourceTree = "<group>"; };
241C6310231F5C4400B4198E /* UIColor+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helper.swift"; sourceTree = "<group>"; };
241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Rounding.swift"; sourceTree = "<group>"; };
@ -1621,6 +1625,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
241C1190245F8765005CB2F4 /* Migrations */ = {
isa = PBXGroup;
children = (
241C1191245F8878005CB2F4 /* LK001UpdateFriendRequestStatusStorage.swift */,
241C1193245F8C95005CB2F4 /* LK001UpdateFriendRequestStatusStorageTest.swift */,
);
path = Migrations;
sourceTree = "<group>";
};
34074F54203D0722004596AE /* Sounds */ = {
isa = PBXGroup;
children = (
@ -2776,6 +2789,7 @@
B846365922B7417900AF1514 /* Loki */ = {
isa = PBXGroup;
children = (
241C1190245F8765005CB2F4 /* Migrations */,
B8C9689223FA1B05005F64E0 /* Redesign */,
B8544E3623D520F600299F14 /* Jazz Icon */,
);
@ -3970,6 +3984,7 @@
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */,
34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */,
459B775C207BA46C0071D0AB /* OWSQuotedReplyModel.m in Sources */,
241C1192245F8878005CB2F4 /* LK001UpdateFriendRequestStatusStorage.swift in Sources */,
340872D622397E6800CB25B0 /* AttachmentCaptionToolbar.swift in Sources */,
34ABB2C42090C59700C727A6 /* OWSResaveCollectionDBMigration.m in Sources */,
4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */,
@ -4263,6 +4278,7 @@
files = (
456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */,
3491D9A121022DB7001EF5A1 /* CDSSigningCertificateTest.m in Sources */,
241C1194245F8CE2005CB2F4 /* LK001UpdateFriendRequestStatusStorageTest.swift in Sources */,
34BBC861220E883300857249 /* ImageEditorModelTest.swift in Sources */,
340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */,
458E383A1D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m in Sources */,

@ -9,7 +9,7 @@ final class FriendRequestView : UIView {
return isIncoming ? .incoming : .outgoing
}
// MARK: Types
// MARK: Kind
enum Kind : String { case incoming, outgoing }
// MARK: Components
@ -90,7 +90,7 @@ final class FriendRequestView : UIView {
mainStackView.pin(to: self)
updateUI()
// Observe friend request status changes
NotificationCenter.default.addObserver(self, selector: #selector(handleFriendRequestStatusChangedNotification), name: .messageFriendRequestStatusChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleFriendRequestStatusChangedNotification), name: .userFriendRequestStatusChanged, object: nil)
}
deinit {
@ -100,63 +100,51 @@ final class FriendRequestView : UIView {
// MARK: Updating
@objc private func handleFriendRequestStatusChangedNotification(_ notification: Notification) {
let messageID = notification.object as! String
guard messageID == message.uniqueId && TSMessage.fetch(uniqueId: messageID) != nil else { return } // It's possible for the message to be deleted at this point
// It's possible for the message to be deleted at this point
guard messageID == message.uniqueId && TSMessage.fetch(uniqueId: messageID) != nil else { return }
message.reload()
updateUI()
}
private func updateUI() {
let thread = message.thread
let friendRequestStatus = FriendRequestProtocol.getFriendRequestUIStatus(for: thread)
guard friendRequestStatus != .none, let contactID = thread.contactIdentifier() else { return }
let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
switch kind {
case .incoming:
guard let message = message as? TSIncomingMessage else { preconditionFailure() }
buttonStackView.isHidden = message.friendRequestStatus != .pending
buttonStackView.isHidden = friendRequestStatus != .received
spacer2.isHidden = buttonStackView.isHidden
let format: String = {
switch (message.friendRequestStatus) {
case .none, .sendingOrFailed: preconditionFailure()
case .pending: return NSLocalizedString("%@ sent you a session request", comment: "")
case .accepted: return NSLocalizedString("You've accepted %@'s session request", comment: "")
case .declined: return NSLocalizedString("You've declined %@'s session request", comment: "")
case .expired: return NSLocalizedString("%@'s session request has expired", comment: "")
default: preconditionFailure()
let format: String
switch friendRequestStatus {
case .none: format = NSLocalizedString("You've declined %@'s session request", comment: "")
case .friends: format = NSLocalizedString("You've accepted %@'s session request", comment: "")
case .received: format = NSLocalizedString("%@ sent you a session request", comment: "")
case .sent: return // Should never occur
case .expired: format = NSLocalizedString("%@'s session request has expired", comment: "")
}
}()
let contactID = message.authorId
let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
label.text = String(format: format, displayName)
case .outgoing:
guard let message = message as? TSOutgoingMessage else { preconditionFailure() }
let format: String? = {
switch (message.friendRequestStatus) {
case .none: preconditionFailure()
case .sendingOrFailed: return nil
case .pending: return NSLocalizedString("You've sent %@ a session request", comment: "")
case .accepted: return NSLocalizedString("%@ accepted your session request", comment: "")
case .declined: preconditionFailure()
case .expired: return NSLocalizedString("Your session request to %@ has expired", comment: "")
default: preconditionFailure()
let format: String
switch friendRequestStatus {
case .none: return // Should never occur
case .friends: format = NSLocalizedString("%@ accepted your session request", comment: "")
case .received: return // Should never occur
case .sent: format = NSLocalizedString("You've sent %@ a session request", comment: "")
case .expired: format = NSLocalizedString("Your session request to %@ has expired", comment: "")
}
}()
if let format = format {
let contactID = message.thread.contactIdentifier()!
let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
label.text = String(format: format, displayName)
}
label.isHidden = (format == nil)
spacer1.isHidden = (label.isHidden)
}
}
// MARK: Interaction
@objc private func accept() {
guard let message = message as? TSIncomingMessage else { preconditionFailure() }
message.saveFriendRequestStatus(.accepted, with: nil)
delegate?.acceptFriendRequest(message)
}
@objc private func decline() {
guard let message = message as? TSIncomingMessage else { preconditionFailure() }
message.saveFriendRequestStatus(.declined, with: nil)
delegate?.declineFriendRequest(message)
}

@ -184,13 +184,18 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
let signedDeviceLink = DeviceLink(between: master, and: deviceLink.slave)
LokiFileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
SSKEnvironment.shared.messageSender.send(linkingAuthorizationMessage, success: {
let thread = TSContactThread.getOrCreateThread(contactId: deviceLink.slave.hexEncodedPublicKey)
thread.save()
let storage = OWSPrimaryStorage.shared()
let slaveHexEncodedPublicKey = deviceLink.slave.hexEncodedPublicKey
storage.dbReadWriteConnection.readWrite { transaction in
let thread = TSContactThread.getOrCreateThread(withContactId: slaveHexEncodedPublicKey, transaction: transaction)
thread.save(with: transaction)
}
let _ = SSKEnvironment.shared.syncManager.syncAllContacts()
let _ = SSKEnvironment.shared.syncManager.syncAllGroups()
let _ = SSKEnvironment.shared.syncManager.syncAllOpenGroups()
thread.friendRequestStatus = .friends
thread.save()
storage.dbReadWriteConnection.readWrite { transaction in
storage.setFriendRequestStatus(.friends, for: slaveHexEncodedPublicKey, transaction: transaction)
}
DispatchQueue.main.async {
self?.dismiss(animated: true, completion: nil)
self?.delegate?.handleDeviceLinkAuthorized(signedDeviceLink)

@ -539,12 +539,12 @@ NS_ASSUME_NONNULL_BEGIN
// Only show the first friend request that was received
NSString *senderID = ((TSIncomingMessage *)message).authorId;
__block NSMutableSet<TSContactThread *> *linkedDeviceThreads;
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction].mutableCopy;
}];
NSMutableArray<TSIncomingMessage *> *allFriendRequestMessages = @[].mutableCopy;
for (TSContactThread *thread in linkedDeviceThreads) {
[thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
[thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { // Starts a new write transaction internally
TSIncomingMessage *message = [interaction as:TSIncomingMessage.class];
if (message != nil && message.isFriendRequest) {
[allFriendRequestMessages addObject:message];

@ -421,8 +421,8 @@ typedef enum : NSUInteger {
name:UIKeyboardDidChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleThreadFriendRequestStatusChangedNotification:)
name:NSNotification.threadFriendRequestStatusChanged
selector:@selector(handleUserFriendRequestStatusChangedNotification:)
name:NSNotification.userFriendRequestStatusChanged
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleThreadSessionRestoreDevicesChangedNotifiaction:)
@ -529,14 +529,21 @@ typedef enum : NSUInteger {
[self resetContentAndLayout];
}
- (void)handleThreadFriendRequestStatusChangedNotification:(NSNotification *)notification
- (void)handleUserFriendRequestStatusChangedNotification:(NSNotification *)notification
{
// Check thread
NSString *threadID = (NSString *)notification.object;
if (![threadID isEqualToString:self.thread.uniqueId]) { return; }
// Ensure thread instance is up to date
// Friend request status doesn't apply to group threads
if (self.thread.isGroupThread) { return; }
NSString *hexEncodedPublicKey = (NSString *)notification.object;
// Check if we should update the UI
__block BOOL needsUpdate;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSSet<NSString *> *linkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:self.thread.contactIdentifier in:transaction];
needsUpdate = [linkedDevices containsObject:hexEncodedPublicKey];
}];
if (!needsUpdate) { return; }
// Ensure the thread instance is up to date
[self.thread reload];
// Update UI
// Update the UI
[self.viewItems.lastObject clearCachedLayoutState];
[self updateInputToolbar];
[self resetContentAndLayout];
@ -4471,15 +4478,17 @@ typedef enum : NSUInteger {
- (void)acceptFriendRequest:(TSIncomingMessage *)friendRequest
{
if (self.thread.isGroupThread || self.thread.contactIdentifier == nil) { return; }
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKFriendRequestProtocol acceptFriendRequestFrom:friendRequest.authorId in:self.thread using:transaction];
[LKFriendRequestProtocol acceptFriendRequestFromHexEncodedPublicKey:self.thread.contactIdentifier using:transaction];
}];
}
- (void)declineFriendRequest:(TSIncomingMessage *)friendRequest
{
if (self.thread.isGroupThread || self.thread.contactIdentifier == nil) { return; }
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKFriendRequestProtocol declineFriendRequest:friendRequest in:self.thread using:transaction];
[LKFriendRequestProtocol declineFriendRequestFromHexEncodedPublicKey:self.thread.contactIdentifier using:transaction];
}];
}

@ -0,0 +1,37 @@
import Foundation
import SignalServiceKit
@objc
public class LK001UpdateFriendRequestStatusStorage : OWSDatabaseMigration {
// MARK: -
// Increment a similar constant for each migration.
// 100-114 are reserved for Signal migrations
@objc
class func migrationId() -> String {
return "001"
}
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
self.doMigrationAsync(completion: completion)
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
DispatchQueue.global().async {
self.dbReadWriteConnection().readWrite { transaction in
guard let threads = TSThread.allObjectsInCollection() as? [TSThread] else {
owsFailDebug("Failed to convert objects to TSThread.")
return
}
for thread in threads {
guard let thread = thread as? TSContactThread,
let friendRequestStatus = LKFriendRequestStatus(rawValue: thread.friendRequestStatus) else { continue }
OWSPrimaryStorage.shared().setFriendRequestStatus(friendRequestStatus, for: thread.contactIdentifier(), transaction: transaction)
}
}
completion()
}
}
}

@ -0,0 +1,66 @@
@testable import SignalServiceKit
import XCTest
import Curve25519Kit
class LK001UpdateFriendRequestStatusStorageTest : XCTestCase {
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
override func setUp() {
super.setUp()
ClearCurrentAppContextForTests()
SetCurrentAppContext(TestAppContext())
MockSSKEnvironment.activate()
let identityManager = OWSIdentityManager.shared()
let seed = Randomness.generateRandomBytes(16)!
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
TSAccountManager.sharedInstance().didRegister()
}
func test_shouldMigrateFriendRequestStatusCorrectly() {
typealias ThreadFriendRequestStatus = NSInteger
let friendRequestMappings: [ThreadFriendRequestStatus:LKFriendRequestStatus] = [
0 : .none,
1 : .requestSending,
2 : .requestSent,
3 : .requestReceived,
4 : .friends,
5 : .requestExpired
]
var hexEncodedPublicKeyMapping: [String:ThreadFriendRequestStatus] = [:]
for (threadFriendRequestStatus, _) in friendRequestMappings {
let hexEncodedPublicKey = Curve25519.generateKeyPair().hexEncodedPublicKey
hexEncodedPublicKeyMapping[hexEncodedPublicKey] = threadFriendRequestStatus
}
storage.dbReadWriteConnection.readWrite { transaction in
for (hexEncodedPublicKey, friendRequestStatus) in hexEncodedPublicKeyMapping {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
thread.friendRequestStatus = friendRequestStatus
thread.save(with: transaction)
}
}
// Wait for the migration to complete
let migration = self.expectation(description: "Migration")
LK001UpdateFriendRequestStatusStorage().runUp {
migration.fulfill()
}
wait(for: [ migration ], timeout: 5)
storage.dbReadWriteConnection.readWrite { transaction in
for (hexEncodedPublicKey, threadFriendRequestStatus) in hexEncodedPublicKeyMapping {
let expectedFriendRequestStatus = friendRequestMappings[threadFriendRequestStatus]!
let friendRequestStatus = self.storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
XCTAssertEqual(friendRequestStatus, expectedFriendRequestStatus, "Expected friend request status \(friendRequestStatus.rawValue) to match \(expectedFriendRequestStatus.rawValue).")
}
}
}
}

@ -10,10 +10,12 @@
#import <SignalServiceKit/AppContext.h>
#import <SignalServiceKit/AppVersion.h>
#import <SignalServiceKit/NSUserDefaults+OWS.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/OWSPrimaryStorage+Loki.h>
#import <SignalServiceKit/OWSRequestFactory.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSNetworkManager.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/TSGroupThread.h>
#import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN

@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN
[[OWS112TypingIndicatorsMigration alloc] init],
[[OWS113MultiAttachmentMediaMessages alloc] init],
[[OWS114RemoveDynamicInteractions alloc] init],
[[LK001UpdateFriendRequestStatusStorage alloc] init]
];
}

@ -40,7 +40,6 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
- (nullable UIImage *)localProfileAvatarImage;
- (nullable NSData *)localProfileAvatarData;
- (nullable NSString *)profilePictureURL;
- (void)ensureLocalProfileCached;
// This method is used to update the "local profile" state on the client
// and the service. Client state is only updated if service state is

@ -19,7 +19,7 @@
#import <SignalServiceKit/OWSBlockingManager.h>
#import <SignalServiceKit/OWSFileSystem.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/OWSPrimaryStorage+Loki.h>
#import <SignalServiceKit/OWSProfileKeyMessage.h>
#import <SignalServiceKit/OWSRequestBuilder.h>
#import <SignalServiceKit/OWSSignalService.h>
@ -920,7 +920,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
TSGroupThread *groupThread = (TSGroupThread *)thread;
NSData *groupId = groupThread.groupModel.groupId;
return [self isGroupIdInProfileWhitelist:groupId];
} else if (thread.friendRequestStatus == LKThreadFriendRequestStatusFriends) {
} else if ([LKFriendRequestProtocol isFriendsWithAnyLinkedDeviceOfHexEncodedPublicKey:thread.contactIdentifier]) {
return true;
} else {
NSString *recipientId = thread.contactIdentifier;

@ -182,8 +182,7 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
// Loki: If we're not friends then always set the message to a friend request message.
// If we're friends then the assumption is that we have the other user's pre key bundle.
BOOL isNoteToSelf = [LKSessionMetaProtocol isMessageNoteToSelf:thread];
NSString *messageClassAsString = (thread.isContactFriend || thread.isGroupThread || isNoteToSelf) ? @"TSOutgoingMessage" : @"LKFriendRequestMessage";
NSString *messageClassAsString = (thread.isContactFriend || thread.isGroupThread || thread.isNoteToSelf) ? @"TSOutgoingMessage" : @"LKFriendRequestMessage";
Class messageClass = NSClassFromString(messageClassAsString);
TSOutgoingMessage *message =

@ -29,21 +29,6 @@ extern ConversationColorName const ConversationColorNameSteel;
extern ConversationColorName const kConversationColorName_Default;
typedef NS_ENUM(NSInteger, LKThreadFriendRequestStatus) {
/// New conversation; no messages sent or received.
LKThreadFriendRequestStatusNone,
/// This state is used to lock the input early while sending.
LKThreadFriendRequestStatusRequestSending,
/// Friend request sent; awaiting response.
LKThreadFriendRequestStatusRequestSent,
/// Friend request received; awaiting user input.
LKThreadFriendRequestStatusRequestReceived,
/// We are friends with the other user in this thread.
LKThreadFriendRequestStatusFriends,
/// A friend request was sent, but it timed out (i.e. the other user didn't accept within the allocated time).
LKThreadFriendRequestStatusRequestExpired
};
/**
* TSThread is the superclass of TSContactThread and TSGroupThread
*/
@ -55,7 +40,7 @@ typedef NS_ENUM(NSInteger, LKThreadFriendRequestStatus) {
@property (nonatomic, readonly) TSInteraction *lastInteraction;
// Loki friend request handling
// ========
@property (nonatomic) LKThreadFriendRequestStatus friendRequestStatus;
@property (nonatomic) NSInteger friendRequestStatus __deprecated_msg("use OWSPrimaryStorage.getFriendRequestStatusForContact:transaction: instead");
@property (nonatomic, readonly) NSString *friendRequestStatusDescription;
/// Shorthand for checking that `friendRequestStatus` is `LKThreadFriendRequestStatusRequestSending`, `LKThreadFriendRequestStatusRequestSent`
/// or `LKThreadFriendRequestStatusRequestReceived`.
@ -204,8 +189,6 @@ typedef NS_ENUM(NSInteger, LKThreadFriendRequestStatus) {
#pragma mark - Loki Friend Request Handling
- (void)saveFriendRequestStatus:(LKThreadFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
/**
Remove any outgoing friend request message which failed to send
*/

@ -92,13 +92,6 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
} else {
_conversationColorName = [self.class stableColorNameForNewConversationWithString:self.uniqueId];
}
// Loki: Friend request logic doesn't apply to group chats
if (self.isGroupThread) {
_friendRequestStatus = LKThreadFriendRequestStatusFriends;
} else {
_friendRequestStatus = LKThreadFriendRequestStatusNone;
}
}
return self;
@ -216,7 +209,7 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
if (!IsNoteToSelfEnabled()) {
return NO;
}
return [LKSessionMetaProtocol isMessageNoteToSelf:self];
return [LKSessionMetaProtocol isThreadNoteToSelf:self];
}
#pragma mark - To be subclassed.
@ -730,7 +723,8 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
- (void)removeOldFriendRequestMessagesIfNeeded:(OWSInteractionType)interactionType withTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
// If we're friends with the person then we don't need to remove any friend request messages
if (self.friendRequestStatus == LKThreadFriendRequestStatusFriends) { return; }
if (self.isGroupThread) { return; }
if ([LKFriendRequestProtocol isFriendsWithAnyLinkedDeviceOfHexEncodedPublicKey:self.contactIdentifier]) { return; }
NSMutableArray<NSString *> *idsToRemove = [NSMutableArray new];
__block TSMessage *_Nullable messageToKeep = nil; // We want to keep this interaction and not remove it
@ -774,55 +768,6 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
}
}
- (void)saveFriendRequestStatus:(LKThreadFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction
{
self.friendRequestStatus = friendRequestStatus;
[LKLogger print:[NSString stringWithFormat:@"[Loki] Setting thread friend request status to %@.", self.friendRequestStatusDescription]];
void (^postNotification)() = ^() {
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.threadFriendRequestStatusChanged object:self.uniqueId];
};
if (transaction == nil) {
[self save];
[self.dbReadWriteConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ postNotification(); }];
} else {
[self saveWithTransaction:transaction];
[transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ postNotification(); }];
}
}
- (NSString *)friendRequestStatusDescription
{
switch (self.friendRequestStatus) {
case LKThreadFriendRequestStatusNone: return @"none";
case LKThreadFriendRequestStatusRequestSending: return @"sending";
case LKThreadFriendRequestStatusRequestSent: return @"sent";
case LKThreadFriendRequestStatusRequestReceived: return @"received";
case LKThreadFriendRequestStatusFriends: return @"friends";
case LKThreadFriendRequestStatusRequestExpired: return @"expired";
}
}
- (BOOL)hasPendingFriendRequest
{
return self.friendRequestStatus == LKThreadFriendRequestStatusRequestSending || self.friendRequestStatus == LKThreadFriendRequestStatusRequestSent
|| self.friendRequestStatus == LKThreadFriendRequestStatusRequestReceived;
}
- (BOOL)isContactFriend
{
return self.friendRequestStatus == LKThreadFriendRequestStatusFriends;
}
- (BOOL)hasCurrentUserSentFriendRequest
{
return self.friendRequestStatus == LKThreadFriendRequestStatusRequestSent;
}
- (BOOL)hasCurrentUserReceivedFriendRequest
{
return self.friendRequestStatus == LKThreadFriendRequestStatusRequestReceived;
}
@end
NS_ASSUME_NONNULL_END

@ -8,6 +8,21 @@
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, LKFriendRequestStatus) {
/// New conversation; no messages sent or received.
LKFriendRequestStatusNone,
/// This state is used to lock the input early while sending.
LKFriendRequestStatusRequestSending,
/// Friend request sent; awaiting response.
LKFriendRequestStatusRequestSent,
/// Friend request received; awaiting user input.
LKFriendRequestStatusRequestReceived,
/// We're friends with the other user.
LKFriendRequestStatusFriends,
/// A friend request was sent, but it timed out (i.e. the other user didn't accept within the allocated time).
LKFriendRequestStatusRequestExpired
};
@interface OWSPrimaryStorage (Loki)
# pragma mark - Pre Key Record Management
@ -45,6 +60,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setRestorationTime:(NSTimeInterval)time;
- (NSTimeInterval)getRestorationTime;
# pragma mark - Friend Requests
- (LKFriendRequestStatus)getFriendRequestStatusForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(getFriendRequestStatus(for:transaction:));
- (void)setFriendRequestStatus:(LKFriendRequestStatus)friendRequestStatus forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setFriendRequestStatus(_:for:transaction:));
@end
NS_ASSUME_NONNULL_END

@ -222,4 +222,21 @@
return [self.dbReadConnection doubleForKey:@"restoration_time" inCollection:LKGeneralCollection defaultValue:0];
}
# pragma mark - Friend Requests
#define LKFriendRequestCollection @"LKFriendRequestCollection"
- (LKFriendRequestStatus)getFriendRequestStatusForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction {
NSNumber *_Nullable status = [transaction objectForKey:hexEncodedPublicKey inCollection:LKFriendRequestCollection];
if (status == nil) { return LKFriendRequestStatusNone; }
return [status integerValue];
}
- (void)setFriendRequestStatus:(LKFriendRequestStatus)friendRequestStatus forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction setObject:@(friendRequestStatus) forKey:hexEncodedPublicKey inCollection:LKFriendRequestCollection];
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.userFriendRequestStatusChanged object:hexEncodedPublicKey];
}];
}
@end

@ -6,6 +6,7 @@
public final class FriendRequestExpirationJob : NSObject {
private let databaseConnection: YapDatabaseConnection
private let messageFinder = ExpiringFriendRequestFinder()
private let storage: OWSPrimaryStorage
// These properties should only be accessed on the main thread.
private var hasStarted = false
@ -19,6 +20,7 @@ public final class FriendRequestExpirationJob : NSObject {
/// Create a `FriendRequestExpireJob`.
/// This will create an auto-running job which will set friend requests to expired.
@objc public init(withPrimaryStorage primaryStorage: OWSPrimaryStorage) {
storage = primaryStorage
databaseConnection = primaryStorage.newDatabaseConnection()
super.init()
@ -108,6 +110,7 @@ public final class FriendRequestExpirationJob : NSObject {
strongSelf.databaseConnection.readWrite { transaction in
strongSelf.messageFinder.enumurateMessagesPendingExpiration(with: { message in
guard message.thread is TSContactThread else { return }
// Sanity check
guard message.friendRequestExpiresAt <= now else {
@ -123,8 +126,7 @@ public final class FriendRequestExpirationJob : NSObject {
}
// Loki: Expire the friend request message
message.thread.saveFriendRequestStatus(.requestExpired, with: transaction)
message.saveFriendRequestStatus(.expired, with: transaction)
strongSelf.storage.setFriendRequestStatus(.requestExpired, for: message.thread.contactIdentifier()!, transaction: transaction)
message.saveFriendRequestExpires(at: 0, with: transaction)
}, transaction: transaction)
}

@ -15,22 +15,44 @@ public final class FriendRequestProtocol : NSObject {
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: - Friend Request UI Status
@objc public enum FriendRequestUIStatus : Int {
case friends, received, sent, none, expired
}
// MARK: - General
@objc(isFriendsWithAnyLinkedDeviceOfHexEncodedPublicKey:)
public static func isFriendsWithAnyLinkedDevice(of hexEncodedPublicKey: String) -> Bool {
var result = false
storage.dbReadConnection.read { transaction in
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let friendRequestStatuses = linkedDevices.map {
storage.getFriendRequestStatus(for: $0, transaction: transaction)
}
result = friendRequestStatuses.contains { $0 == .friends }
}
return result
}
@objc(shouldInputBarBeEnabledForThread:)
public static func shouldInputBarBeEnabled(for thread: TSThread) -> Bool {
// Friend requests have nothing to do with groups, so if this isn't a contact thread the input bar should be enabled
guard let thread = thread as? TSContactThread else { return true }
// If this is a note to self, the input bar should be enabled
if SessionMetaProtocol.isMessageNoteToSelf(thread) { return true }
// If this is a note to self the input bar should be enabled
if thread.isNoteToSelf() { return true }
// Gather friend request statuses
let contactID = thread.contactIdentifier()
var linkedDeviceThreads: Set<TSContactThread> = []
var linkedDeviceFriendRequestStatuses: [LKFriendRequestStatus] = []
storage.dbReadConnection.read { transaction in
linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction)
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction)
linkedDeviceFriendRequestStatuses = linkedDevices.map {
storage.getFriendRequestStatus(for: $0, transaction: transaction)
}
}
// If the current user is friends with any of the other user's devices, the input bar should be enabled
if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true }
if linkedDeviceFriendRequestStatuses.contains(where: { $0 == .friends }) { return true }
// If no friend request has been sent, the input bar should be enabled
if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return true }
if linkedDeviceFriendRequestStatuses.allSatisfy({ $0 == .none || $0 == .requestExpired }) { return true }
// There must be a pending friend request
return false
}
@ -40,33 +62,64 @@ public final class FriendRequestProtocol : NSObject {
// Friend requests have nothing to do with groups, so if this isn't a contact thread the attachment button should be enabled
guard let thread = thread as? TSContactThread else { return true }
// If this is a note to self, the attachment button should be enabled
if SessionMetaProtocol.isMessageNoteToSelf(thread) { return true }
if thread.isNoteToSelf() { return true }
/// Gather friend request statuses
let contactID = thread.contactIdentifier()
var linkedDeviceThreads: Set<TSContactThread> = []
var linkedDeviceFriendRequestStatuses: [LKFriendRequestStatus] = []
storage.dbReadConnection.read { transaction in
linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction)
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction)
linkedDeviceFriendRequestStatuses = linkedDevices.map {
storage.getFriendRequestStatus(for: $0, transaction: transaction)
}
}
// If the current user is friends with any of the other user's devices, the attachment button should be enabled
if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true }
// If no friend request has been sent, the attachment button should be disabled
if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return false }
// There must be a pending friend request
if linkedDeviceFriendRequestStatuses.contains(where: { $0 == .friends }) { return true }
// Otherwise don't allow attachments
return false
}
@objc(getFriendRequestUIStatusForThread:)
public static func getFriendRequestUIStatus(for thread: TSThread) -> FriendRequestUIStatus {
// Friend requests have nothing to do with groups
guard let thread = thread as? TSContactThread else { return .none }
// If this is a note to self then we don't want to show the friend request UI
guard !thread.isNoteToSelf() else { return .none }
// Gather friend request statuses for all linked devices
var friendRequestStatuses: [LKFriendRequestStatus] = []
storage.dbReadConnection.read { transaction in
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: thread.contactIdentifier(), in: transaction)
friendRequestStatuses = linkedDevices.map {
storage.getFriendRequestStatus(for: $0, transaction: transaction)
}
}
// Return
if friendRequestStatuses.contains(where: { $0 == .friends }) { return .friends }
if friendRequestStatuses.contains(where: { $0 == .requestReceived }) { return .received }
if friendRequestStatuses.contains(where: { $0 == .requestSent || $0 == .requestSending }) { return .sent }
if friendRequestStatuses.contains(where: { $0 == .requestExpired }) { return .expired }
return .none
}
// MARK: - Sending
@objc(acceptFriendRequestFrom:in:using:)
public static func acceptFriendRequest(from hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
@objc(acceptFriendRequestFromHexEncodedPublicKey:using:)
public static func acceptFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
return
}
// 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.
let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction) // This doesn't create new threads if they don't exist yet
// FIXME: Capture send failures
for thread in linkedDeviceThreads {
if thread.hasPendingFriendRequest {
sendFriendRequestAcceptanceMessage(to: thread.contactIdentifier(), in: thread, using: transaction) // NOT hexEncodedPublicKey
thread.saveFriendRequestStatus(.friends, with: transaction)
} else {
MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: thread.contactIdentifier(), in: transaction) // NOT hexEncodedPublicKey
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
for device in linkedDevices {
let friendRequestStatus = storage.getFriendRequestStatus(for: device, transaction: transaction)
if friendRequestStatus == .requestReceived {
storage.setFriendRequestStatus(.friends, for: device, transaction: transaction)
sendFriendRequestAcceptanceMessage(to: device, using: transaction)
} else if friendRequestStatus == .requestSent {
// We sent a friend request to this device before, how can we be sure that it hasn't expired?
} else if friendRequestStatus == .none || friendRequestStatus == .requestExpired {
// TODO: We should track these so that we can expire them and resend if needed
MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: device, in: transaction)
.done(on: OWSDispatch.sendingQueue()) { autoGeneratedFRMessageSend in
let messageSender = SSKEnvironment.shared.messageSender
messageSender.sendMessage(autoGeneratedFRMessageSend)
@ -75,20 +128,56 @@ public final class FriendRequestProtocol : NSObject {
}
}
@objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:in:using:)
public static func sendFriendRequestAcceptanceMessage(to hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
@objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:using:)
public static func sendFriendRequestAcceptanceMessage(to hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
return
}
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let ephemeralMessage = EphemeralMessage(in: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
@objc(declineFriendRequest:in:using:)
public static func declineFriendRequest(_ friendRequest: TSIncomingMessage, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
thread.saveFriendRequestStatus(.none, with: transaction)
@objc(declineFriendRequestFromHexEncodedPublicKey:using:)
public static func declineFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
return
}
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
for device in linkedDevices {
let friendRequestStatus = storage.getFriendRequestStatus(for: device, transaction: transaction)
// We only want to decline incoming requests
if friendRequestStatus == .requestReceived {
// Delete the pre key bundle for the given contact. This ensures that if we send a
// new message after this, it restarts the friend request process from scratch.
let senderID = friendRequest.authorId
storage.removePreKeyBundle(forContact: senderID, transaction: transaction)
storage.removePreKeyBundle(forContact: device, transaction: transaction)
storage.setFriendRequestStatus(.none, for: device, transaction: transaction)
}
}
}
@objc(setFriendRequestStatusToSendingIfNeededForHexEncodedPublicKey:transaction:)
public static func setFriendRequestStatusToSendingIfNeeded(for hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
guard friendRequestStatus == .none || friendRequestStatus == .requestExpired else { return }
storage.setFriendRequestStatus(.requestSending, for: hexEncodedPublicKey, transaction: transaction)
}
@objc(setFriendRequestStatusToSentIfNeededForHexEncodedPublicKey:transaction:)
public static func setFriendRequestStatusToSentIfNeeded(for hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
guard friendRequestStatus == .none || friendRequestStatus == .requestExpired || friendRequestStatus == .requestSending else { return }
storage.setFriendRequestStatus(.requestSent, for: hexEncodedPublicKey, transaction: transaction)
}
@objc(setFriendRequestStatusToFailedIfNeededForHexEncodedPublicKey:transaction:)
public static func setFriendRequestStatusToFailedIfNeeded(for hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
guard friendRequestStatus == .requestSending else { return }
storage.setFriendRequestStatus(.none, for: hexEncodedPublicKey, transaction: transaction)
}
// MARK: - Receiving
@ -99,9 +188,9 @@ public final class FriendRequestProtocol : NSObject {
return (envelope.type == .friendRequest && envelope.timestamp < restorationTimeInMs)
}
@objc(canFriendRequestBeAutoAcceptedForHexEncodedPublicKey:in:using:)
public static func canFriendRequestBeAutoAccepted(for hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadTransaction) -> Bool {
if thread.hasCurrentUserSentFriendRequest {
@objc(canFriendRequestBeAutoAcceptedForHexEncodedPublicKey:using:)
public static func canFriendRequestBeAutoAccepted(for hexEncodedPublicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool {
if storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction) == .requestSent {
// 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
// and send a friend request accepted message back to Bob. We don't check that sending the
@ -118,8 +207,12 @@ public final class FriendRequestProtocol : NSObject {
let userLinkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
if userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey) { return true }
// Auto-accept if the user is friends with any of the sender's linked devices.
let senderLinkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction)
if senderLinkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true }
let senderLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
if senderLinkedDevices.contains(where: {
storage.getFriendRequestStatus(for: $0, transaction: transaction) == .friends
}) {
return true
}
// We can't auto-accept
return false
}
@ -132,26 +225,15 @@ public final class FriendRequestProtocol : NSObject {
guard !envelope.isGroupChatMessage && envelope.type != .friendRequest else { return }
// If we get an envelope that isn't a friend request, then we can infer that we had to use
// Signal cipher decryption and thus that we have a session with the other person.
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction);
// We shouldn't be able to skip from none to friends
guard thread.friendRequestStatus != .none else { return }
guard friendRequestStatus != .none else { return }
// Become friends
thread.saveFriendRequestStatus(.friends, with: transaction)
if let existingFriendRequestMessage = thread.getLastInteraction(with: transaction) as? TSOutgoingMessage,
existingFriendRequestMessage.isFriendRequest {
existingFriendRequestMessage.saveFriendRequestStatus(.accepted, with: transaction)
}
/*
// Send our P2P details
if let addressMessage = LokiP2PAPI.onlineBroadcastMessage(forThread: thread) {
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: addressMessage, transaction: transaction)
}
*/
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
}
@objc(handleFriendRequestMessageIfNeeded:associatedWith:wrappedIn:in:using:)
public static func handleFriendRequestMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, associatedWith message: TSIncomingMessage, wrappedIn envelope: SSKProtoEnvelope, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
@objc(handleFriendRequestMessageIfNeededFromEnvelope:using:)
public static func handleFriendRequestMessageIfNeeded(from envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
guard !envelope.isGroupChatMessage else {
print("[Loki] Ignoring friend request in group chat.")
return
@ -163,37 +245,16 @@ public final class FriendRequestProtocol : NSObject {
print("[Loki] Ignoring friend request logic for non friend request type envelope.")
return
}
if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, in: thread, using: transaction) {
thread.saveFriendRequestStatus(.friends, with: transaction)
var existingFriendRequestMessage: TSOutgoingMessage?
thread.enumerateInteractions(with: transaction) { interaction, _ in
if let outgoingMessage = interaction as? TSOutgoingMessage, outgoingMessage.isFriendRequest {
existingFriendRequestMessage = outgoingMessage
}
}
if let existingFriendRequestMessage = existingFriendRequestMessage {
existingFriendRequestMessage.saveFriendRequestStatus(.accepted, with: transaction)
}
sendFriendRequestAcceptanceMessage(to: hexEncodedPublicKey, in: thread, using: transaction)
} else if !thread.isContactFriend {
if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, using: transaction) {
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
sendFriendRequestAcceptanceMessage(to: hexEncodedPublicKey, using: transaction)
} else if storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction) != .friends {
// 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(.requestReceived, with: 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).
let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction)
for thread in linkedDeviceThreads {
thread.enumerateInteractions(with: transaction) { interaction, _ in
guard let incomingMessage = interaction as? TSIncomingMessage, incomingMessage.friendRequestStatus != .none else { return }
incomingMessage.saveFriendRequestStatus(.none, with: transaction)
}
}
message.friendRequestStatus = .pending
// Don't save yet. This is done in finalizeIncomingMessage:thread:masterThread:envelope:transaction.
storage.setFriendRequestStatus(.requestReceived, for: hexEncodedPublicKey, transaction: transaction)
}
}
}

@ -2,18 +2,21 @@ import CryptoSwift
import PromiseKit
@testable import SignalServiceKit
import XCTest
import Curve25519Kit
class FriendRequestProtocolTests : XCTestCase {
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
private var messageSender: OWSFakeMessageSender { MockSSKEnvironment.shared.messageSender as! OWSFakeMessageSender }
// MARK: - Setup
override func setUp() {
super.setUp()
// Activate the mock environment
ClearCurrentAppContextForTests()
SetCurrentAppContext(TestAppContext())
MockSSKEnvironment.activate()
// Register a mock user
let identityManager = OWSIdentityManager.shared()
let seed = Randomness.generateRandomBytes(16)!
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
@ -23,46 +26,613 @@ class FriendRequestProtocolTests : XCTestCase {
TSAccountManager.sharedInstance().didRegister()
}
func testMultiDeviceFriendRequestAcceptance() {
// When Alice accepts Bob's friend request, she should accept all outstanding friend requests with Bob's
// linked devices and try to establish sessions with the subset of Bob's devices that haven't sent a friend request.
func getDevice() -> DeviceLink.Device? {
guard let publicKey = Data.getSecureRandomData(ofSize: 64) else { return nil }
let hexEncodedPublicKey = "05" + publicKey.toHexString()
// MARK: - Helpers
func isFriendRequestStatus(oneOf values: [LKFriendRequestStatus], for hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Bool {
let status = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
return values.contains(status)
}
func isFriendRequestStatus(_ value: LKFriendRequestStatus, for hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Bool {
return isFriendRequestStatus(oneOf: [ value ], for: hexEncodedPublicKey, transaction: transaction)
}
func generateHexEncodedPublicKey() -> String {
return Curve25519.generateKeyPair().hexEncodedPublicKey
}
func getDevice(for hexEncodedPublicKey: String) -> DeviceLink.Device? {
guard let signature = Data.getSecureRandomData(ofSize: 64) else { return nil }
return DeviceLink.Device(hexEncodedPublicKey: hexEncodedPublicKey, signature: signature)
}
func createThread(for hexEncodedPublicKey: String) -> TSContactThread {
func createContactThread(for hexEncodedPublicKey: String) -> TSContactThread {
var result: TSContactThread!
storage.dbReadWriteConnection.readWrite { transaction in
result = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
}
return result
}
// Get devices
guard let bobMasterDevice = getDevice() else { return XCTFail() }
guard let bobSlaveDevice = getDevice() else { return XCTFail() }
// Create device link
let bobDeviceLink = DeviceLink(between: bobMasterDevice, and: bobSlaveDevice)
func createGroupThread(groupType: GroupType) -> TSGroupThread? {
let hexEncodedGroupID = Randomness.generateRandomBytes(kGroupIdLength)!.toHexString()
let groupID: Data
switch groupType {
case .closedGroup: groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(hexEncodedGroupID)
case .openGroup: groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(hexEncodedGroupID)
case .rssFeed: groupID = LKGroupUtilities.getEncodedRSSFeedIDAsData(hexEncodedGroupID)
default: return nil
}
return TSGroupThread.getOrCreateThread(withGroupId: groupID, groupType: groupType)
}
// MARK: - shouldInputBarBeEnabled
func test_shouldInputBarBeEnabledReturnsTrueOnGroupThread() {
let allGroupTypes: [GroupType] = [ .closedGroup, .openGroup, .rssFeed ]
for groupType in allGroupTypes {
guard let groupThread = createGroupThread(groupType: groupType) else { return XCTFail() }
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: groupThread))
}
}
func test_shouldInputBarBeEnabledReturnsTrueOnNoteToSelf() {
guard let master = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else { return XCTFail() }
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread))
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: slaveThread))
}
func test_shouldInputBarBeEnabledReturnsTrueWhenStatusIsNotPending() {
let statuses: [LKFriendRequestStatus] = [ .none, .requestExpired, .friends ]
let device = generateHexEncodedPublicKey()
let thread = createContactThread(for: device)
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: device, transaction: transaction)
}
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: thread))
}
}
func test_shouldInputBarBeEnabledReturnsFalseWhenStatusIsPending() {
let statuses: [LKFriendRequestStatus] = [ .requestSending, .requestSent, .requestReceived ]
let device = generateHexEncodedPublicKey()
let thread = createContactThread(for: device)
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: device, transaction: transaction)
}
XCTAssertFalse(FriendRequestProtocol.shouldInputBarBeEnabled(for: thread))
}
}
func test_shouldInputBarBeEnabledReturnsTrueWhenFriendsWithOneLinkedDevice() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.friends, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread))
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: slaveThread))
}
func test_shouldInputBarBeEnabledReturnsFalseWhenOneLinkedDeviceIsPending() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
let statuses: [LKFriendRequestStatus] = [ .requestSending, .requestSent, .requestReceived ]
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: slave, transaction: transaction)
}
XCTAssertFalse(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread))
XCTAssertFalse(FriendRequestProtocol.shouldInputBarBeEnabled(for: slaveThread))
}
}
func test_shouldInputBarBeEnabledReturnsTrueWhenAllLinkedDevicesAreNotPendingAndNotFriends() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.none, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
let statuses: [LKFriendRequestStatus] = [ .requestExpired, .none ]
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: slave, transaction: transaction)
}
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread))
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: slaveThread))
}
}
func test_shouldInputBarEnabledShouldStillWorkIfLinkedDeviceThreadDoesNotExist() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread))
}
// MARK: - shouldAttachmentButtonBeEnabled
func test_shouldAttachmentButtonBeEnabledReturnsTrueOnGroupThread() {
let allGroupTypes: [GroupType] = [ .closedGroup, .openGroup, .rssFeed ]
for groupType in allGroupTypes {
guard let groupThread = createGroupThread(groupType: groupType) else { return XCTFail() }
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: groupThread))
}
}
func test_shouldAttachmentButtonBeEnabledReturnsTrueOnNoteToSelf() {
guard let master = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else { return XCTFail() }
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: masterThread))
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: slaveThread))
}
func test_shouldAttachmentButtonBeEnabledReturnsTrueWhenFriends() {
let device = generateHexEncodedPublicKey()
let thread = createContactThread(for: device)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(.friends, for: device, transaction: transaction)
}
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: thread))
}
func test_shouldAttachmentButtonBeEnabledReturnsFalseWhenNotFriends() {
let statuses: [LKFriendRequestStatus] = [ .requestSending, .requestSent, .requestReceived, .none, .requestExpired ]
let device = generateHexEncodedPublicKey()
let thread = createContactThread(for: device)
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: device, transaction: transaction)
}
XCTAssertFalse(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: thread))
}
}
func test_shouldAttachmentButtonBeEnabledReturnsTrueWhenFriendsWithOneLinkedDevice() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.friends, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: masterThread))
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: slaveThread))
}
func test_shouldAttachmentButtonBeEnabledShouldStillWorkIfLinkedDeviceThreadDoesNotExist() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: masterThread))
}
// MARK: - getFriendRequestUIState
func test_getFriendRequestUIStateShouldReturnNoneForGroupThreads() {
let allGroupTypes: [GroupType] = [ .closedGroup, .openGroup, .rssFeed ]
for groupType in allGroupTypes {
guard let groupThread = createGroupThread(groupType: groupType) else { return XCTFail() }
XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIStatus(for: groupThread) == .none)
}
}
func test_getFriendRequestUIStateShouldReturnNoneOnNoteToSelf() {
guard let master = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else { return XCTFail() }
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.friends, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIStatus(for: masterThread) == .none)
XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIStatus(for: slaveThread) == .none )
}
func test_getFriendRequestUIStateShouldReturnTheCorrectStates() {
let bob = generateHexEncodedPublicKey()
let bobThread = createContactThread(for: bob)
let expectedStatuses: [LKFriendRequestStatus:FriendRequestProtocol.FriendRequestUIStatus] = [
.none: .none,
.requestExpired: .expired,
.requestSending: .sent,
.requestSent: .sent,
.requestReceived: .received,
.friends: .friends,
]
for (friendRequestStatus, uiState) in expectedStatuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(friendRequestStatus, for: bob, transaction: transaction)
}
XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIStatus(for: bobThread), uiState, "Expected FriendRequestUIStatus to be \(uiState).")
}
}
func test_getFriendRequestUIStateShouldWorkWithMultiDevice() {
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction)
}
let masterThread = createContactThread(for: master)
let slaveThread = createContactThread(for: slave)
let expectedStatuses: [LKFriendRequestStatus:FriendRequestProtocol.FriendRequestUIStatus] = [
.none: .none,
.requestExpired: .expired,
.requestSending: .sent,
.requestSent: .sent,
.requestReceived: .received,
.friends: .friends,
]
for (friendRequestStatus, uiState) in expectedStatuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(friendRequestStatus, for: slave, transaction: transaction)
}
XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIStatus(for: masterThread), uiState, "Expected FriendRequestUIStatus to be \(uiState.rawValue).")
XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIStatus(for: slaveThread), uiState, "Expected FriendRequestUIStatus to be \(uiState.rawValue).")
}
}
func test_getFriendRequestUIStateShouldPreferFriendsOverRequestReceived() {
// Case: We don't want to confuse the user by showing a friend request box when they're already friends.
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let masterThread = createContactThread(for: master)
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction)
}
XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIStatus(for: masterThread) == .friends)
}
func test_getFriendRequestUIStateShouldPreferReceivedOverSent() {
// Case: We sent Bob a friend request and he sent one back to us through another device.
// If something went wrong then we should be able to fall back to manually accepting the friend request even if we sent one.
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
let masterThread = createContactThread(for: master)
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(deviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: slave, transaction: transaction)
}
XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIStatus(for: masterThread) == .received)
}
// MARK: - acceptFriendRequest
func test_acceptFriendRequestShouldSetStatusToFriendsIfWeReceivedAFriendRequest() {
// Case: Bob sent us a friend request, we should become friends with him on accepting.
let bob = generateHexEncodedPublicKey()
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(bobDeviceLink, in: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: bob, transaction: transaction)
}
// Create threads
let bobMasterThread = createThread(for: bobMasterDevice.hexEncodedPublicKey)
let bobSlaveThread = createThread(for: bobSlaveDevice.hexEncodedPublicKey)
// Scenario 1: Alice has a pending friend request from Bob's master device, and nothing
// from his slave device. After accepting the pending friend request we'd expect the
// friend request status for Bob's master thread to be `friends`, and that of Bob's
// slave thread to be `requestSent`.
storage.dbReadWriteConnection.readWrite { transaction in
bobMasterThread.saveFriendRequestStatus(.requestReceived, with: transaction)
bobSlaveThread.saveFriendRequestStatus(.none, with: transaction)
FriendRequestProtocol.acceptFriendRequest(from: bob, using: transaction)
XCTAssertTrue(self.storage.getFriendRequestStatus(for: bob, transaction: transaction) == .friends)
}
}
// TODO: Add test to see if an accept message is sent out
func test_acceptFriendRequestShouldSendAFriendRequestMessageIfStatusIsNoneOrExpired() {
// Case: Somehow our friend request status doesn't match the UI.
// Since user accepted then we should send a friend request message.
let statuses: [LKFriendRequestStatus] = [ .none, .requestExpired ]
for status in statuses {
let bob = generateHexEncodedPublicKey()
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: bob, transaction: transaction)
}
let expectation = self.expectation(description: "sent message")
let messageSender = self.messageSender
messageSender.sendMessageWasCalledBlock = { sentMessage in
guard sentMessage is FriendRequestMessage else {
return XCTFail("Expected a friend request to be sent, but found: \(sentMessage).")
}
expectation.fulfill()
messageSender.sendMessageWasCalledBlock = nil
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: bob, using: transaction)
}
wait(for: [ expectation ], timeout: 1)
}
}
func test_acceptFriendRequestShouldNotDoAnythingIfRequestHasBeenSent() {
// Case: We sent Bob a friend request.
// We can't accept because we don't have keys to communicate with Bob.
let bob = generateHexEncodedPublicKey()
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(.requestSent, for: bob, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: bob, using: transaction)
XCTAssertTrue(self.isFriendRequestStatus(.requestSent, for: bob, transaction: transaction))
}
}
func test_acceptFriendRequestShouldWorkWithMultiDevice() {
// Case: Bob sent a friend request from his slave device.
// Accepting the friend request should set it to friends.
// We should also send out a friend request to Bob's other devices if possible.
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
let otherSlave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
guard let otherSlaveDevice = getDevice(for: otherSlave) else { return XCTFail() }
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(DeviceLink(between: masterDevice, and: slaveDevice), in: transaction)
self.storage.addDeviceLink(DeviceLink(between: masterDevice, and: otherSlaveDevice), in: transaction)
self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: slave, transaction: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: otherSlave, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: master, using: transaction)
}
eventually {
self.storage.dbReadWriteConnection.readWrite { transaction in
// TODO: Re-enable this case when we split friend request logic from OWSMessageSender
// XCTAssertTrue(self.isFriendRequestStatus([ .requestSending, .requestSent ], for: master, transaction: transaction))
XCTAssertTrue(self.isFriendRequestStatus(.friends, for: slave, transaction: transaction))
XCTAssertTrue(self.isFriendRequestStatus(.requestSent, for: otherSlave, transaction: transaction))
}
}
}
func test_acceptFriendRequestShouldNotChangeStatusIfDevicesAreNotLinked() {
let alice = generateHexEncodedPublicKey()
let bob = generateHexEncodedPublicKey()
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(.requestReceived, for: alice, transaction: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: bob, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: alice, using: transaction)
XCTAssertTrue(self.isFriendRequestStatus(.friends, for: alice, transaction: transaction))
XCTAssertTrue(self.isFriendRequestStatus(.requestReceived, for: bob, transaction: transaction))
}
}
// MARK: - declineFriendRequest
func test_declineFriendRequestShouldChangeStatusFromReceivedToNone() {
let bob = generateHexEncodedPublicKey()
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(.requestReceived, for: bob, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.declineFriendRequest(from: bob, using: transaction)
XCTAssertTrue(self.isFriendRequestStatus(.none, for: bob, transaction: transaction))
}
}
func test_declineFriendRequestShouldNotChangeStatusToNoneFromOtherStatuses() {
let statuses: [LKFriendRequestStatus] = [ .none, .requestSending, .requestSent, .requestExpired, .friends ]
let bob = generateHexEncodedPublicKey()
for status in statuses {
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setFriendRequestStatus(status, for: bob, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.declineFriendRequest(from: bob, using: transaction)
XCTAssertTrue(self.isFriendRequestStatus(status, for: bob, transaction: transaction))
}
}
}
func test_declineFriendRequestShouldDeletePreKeyBundleIfNeeded() {
let shouldExpectDeletedPreKeyBundle: (LKFriendRequestStatus) -> Bool = { status in
return status == .requestReceived
}
let statuses: [LKFriendRequestStatus] = [ .none, .requestSending, .requestSent, .requestReceived, .requestExpired, .friends ]
for status in statuses {
let bob = generateHexEncodedPublicKey()
let bundle = storage.generatePreKeyBundle(forContact: bob)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.setPreKeyBundle(bundle, forContact: bob, transaction: transaction)
self.storage.setFriendRequestStatus(status, for: bob, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.declineFriendRequest(from: bob, using: transaction)
}
let storedBundle = storage.getPreKeyBundle(forContact: bob)
if (shouldExpectDeletedPreKeyBundle(status)) {
XCTAssertNil(storedBundle, "Expected PreKeyBundle to be deleted for friend request status \(status.rawValue).")
} else {
XCTAssertNotNil(storedBundle, "Expected PreKeyBundle to not be deleted for friend request status \(status.rawValue).")
}
}
}
func test_declineFriendRequestShouldWorkWithMultipleLinkedDevices() {
// Case: Bob sends 2 friend requests to Alice.
// When Alice declines, it should change the statuses from requestReceived to none so friend request logic can be re-triggered.
let master = generateHexEncodedPublicKey()
let slave = generateHexEncodedPublicKey()
let otherSlave = generateHexEncodedPublicKey()
guard let masterDevice = getDevice(for: master) else { return XCTFail() }
guard let slaveDevice = getDevice(for: slave) else { return XCTFail() }
guard let otherSlaveDevice = getDevice(for: otherSlave) else { return XCTFail() }
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(DeviceLink(between: masterDevice, and: slaveDevice), in: transaction)
self.storage.addDeviceLink(DeviceLink(between: masterDevice, and: otherSlaveDevice), in: transaction)
self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: slave, transaction: transaction)
self.storage.setFriendRequestStatus(.requestReceived, for: otherSlave, transaction: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: bobMasterDevice.hexEncodedPublicKey, in: bobMasterThread, using: transaction)
FriendRequestProtocol.declineFriendRequest(from: master, using: transaction)
XCTAssertTrue(self.isFriendRequestStatus(.requestSent, for: master, transaction: transaction))
XCTAssertTrue(self.isFriendRequestStatus(.none, for: slave, transaction: transaction))
XCTAssertTrue(self.isFriendRequestStatus(.none, for: otherSlave, transaction: transaction))
}
XCTAssert(bobMasterThread.friendRequestStatus == .friends)
XCTAssert(bobSlaveThread.friendRequestStatus == .requestSent)
// TODO: Add other scenarios
}
}

@ -59,8 +59,9 @@ public final class SessionMetaProtocol : NSObject {
}
// MARK: Note to Self
@objc(isMessageNoteToSelf:)
public static func isMessageNoteToSelf(_ thread: TSThread) -> Bool {
@objc(isThreadNoteToSelf:)
public static func isThreadNoteToSelf(_ thread: TSThread) -> Bool {
guard let thread = thread as? TSContactThread else { return false }
var isNoteToSelf = false
storage.dbReadConnection.read { transaction in
@ -82,13 +83,23 @@ public final class SessionMetaProtocol : NSObject {
/// send them if certain conditions are met.
@objc(shouldSendTypingIndicatorForThread:)
public static func shouldSendTypingIndicator(for thread: TSThread) -> Bool {
return thread.friendRequestStatus == .friends && !thread.isGroupThread()
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
var isContactFriend = false
storage.dbReadConnection.read { transaction in
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
}
return isContactFriend
}
// MARK: Receipts
@objc(shouldSendReceiptForThread:)
public static func shouldSendReceipt(for thread: TSThread) -> Bool {
return thread.friendRequestStatus == .friends && !thread.isGroupThread()
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
var isContactFriend = false
storage.dbReadConnection.read { transaction in
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
}
return isContactFriend
}
// MARK: - Receiving

@ -171,10 +171,9 @@ public final class MultiDeviceProtocol : NSObject {
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
public static func getAutoGeneratedMultiDeviceFRMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction, seal externalSeal: Resolver<Void>? = nil) -> Promise<OWSMessageSend> {
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let message = getAutoGeneratedMultiDeviceFRMessage(for: hexEncodedPublicKey, in: transaction)
thread.friendRequestStatus = .requestSending
thread.save(with: transaction)
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: hexEncodedPublicKey, transaction: transaction)
let udManager = SSKEnvironment.shared.udManager
let senderCertificate = udManager.getSenderCertificate()
@ -188,18 +187,8 @@ public final class MultiDeviceProtocol : NSObject {
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
externalSeal?.fulfill(())
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
thread.friendRequestStatus = .requestSent
thread.save()
}
}, failure: { error in
externalSeal?.reject(error)
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
thread.friendRequestStatus = .none
thread.save()
}
})
seal.fulfill(messageSend)
}

@ -167,13 +167,14 @@ public final class SyncMessagesProtocol : NSObject {
let hexEncodedPublicKeys = parser.parseHexEncodedPublicKeys()
// Try to establish sessions
for hexEncodedPublicKey in hexEncodedPublicKeys {
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let friendRequestStatus = thread.friendRequestStatus
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
switch friendRequestStatus {
case .none:
case .none, .requestExpired:
let messageSender = SSKEnvironment.shared.messageSender
// We need to send the FR message to all of the user's devices as the contact sync message excludes slave devices
let autoGeneratedFRMessage = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessage(for: hexEncodedPublicKey, in: transaction)
thread.friendRequestStatus = .requestSending
thread.isForceHidden = true
thread.save(with: transaction)
// This takes into account multi device
@ -181,7 +182,6 @@ public final class SyncMessagesProtocol : NSObject {
DispatchQueue.main.async {
storage.dbReadWriteConnection.readWrite { transaction in
autoGeneratedFRMessage.remove(with: transaction)
thread.friendRequestStatus = .requestSent
thread.isForceHidden = false
thread.save(with: transaction)
}
@ -190,16 +190,15 @@ public final class SyncMessagesProtocol : NSObject {
DispatchQueue.main.async {
storage.dbReadWriteConnection.readWrite { transaction in
autoGeneratedFRMessage.remove(with: transaction)
thread.friendRequestStatus = .none
thread.isForceHidden = false
thread.save(with: transaction)
}
}
})
case .requestReceived:
thread.saveFriendRequestStatus(.friends, with: transaction)
// Not sendFriendRequestAcceptanceMessage(to:in:using:) to take into account multi device
FriendRequestProtocol.acceptFriendRequest(from: hexEncodedPublicKey, in: thread, using: transaction)
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
// Not sendFriendRequestAcceptanceMessage(to:using:) to take into account multi device
FriendRequestProtocol.acceptFriendRequest(from: hexEncodedPublicKey, using: transaction)
default: break
}
}

@ -5,6 +5,7 @@ import XCTest
class SyncMessagesProtocolTests : XCTestCase {
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
private var messageSender: OWSFakeMessageSender { MockSSKEnvironment.shared.messageSender as! OWSFakeMessageSender }
override func setUp() {
super.setUp()
@ -34,16 +35,31 @@ class SyncMessagesProtocolTests : XCTestCase {
let contactData = Data(base64Encoded: base64EncodedContactData)!
let parser = ContactParser(data: contactData)
let hexEncodedPublicKeys = parser.parseHexEncodedPublicKeys()
let expectation = self.expectation(description: "Send friend request messages")
var messageCount = 0
let messageSender = self.messageSender
messageSender.sendMessageWasCalledBlock = { sentMessage in
messageCount += 1
guard sentMessage is FriendRequestMessage else {
return XCTFail("Expected a friend request to be sent, but found: \(sentMessage).")
}
guard messageCount == hexEncodedPublicKeys.count else { return }
expectation.fulfill()
messageSender.sendMessageWasCalledBlock = nil
}
storage.dbReadWriteConnection.readWrite { transaction in
SyncMessagesProtocol.handleContactSyncMessageData(contactData, using: transaction)
}
wait(for: [ expectation ], timeout: 1)
/* TODO: Re-enable when we've split friend request logic from OWSMessageSender
hexEncodedPublicKeys.forEach { hexEncodedPublicKey in
var thread: TSContactThread!
var friendRequestStatus: LKFriendRequestStatus!
storage.dbReadWriteConnection.readWrite { transaction in
thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
friendRequestStatus = self.storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
}
XCTAssert(thread.friendRequestStatus == .requestSent)
XCTAssert(friendRequestStatus == .requestSent)
}
*/
// TODO: Test the case where Bob has multiple devices
}
}

@ -210,16 +210,16 @@ public class LokiP2PAPI : NSObject {
}
private static func getAllFriendThreads() -> [TSContactThread] {
var friendThreadIds = [String]()
TSContactThread.enumerateCollectionObjects { (object, _) in
guard let thread = object as? TSContactThread, let uniqueId = thread.uniqueId else { return }
if thread.friendRequestStatus == .friends && thread.contactIdentifier() != ourHexEncodedPubKey {
friendThreadIds.append(thread.uniqueId!)
var friendThreadIDs: [String] = []
storage.dbReadConnection.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSContactThread, let uniqueID = thread.uniqueId, thread.contactIdentifier() != ourHexEncodedPubKey else { return }
let status = storage.getFriendRequestStatus(for: thread.contactIdentifier(), transaction: transaction)
guard status == .friends else { return }
friendThreadIDs.append(uniqueID)
}
}
return friendThreadIds.compactMap { TSContactThread.fetch(uniqueId: $0) }
return friendThreadIDs.compactMap { TSContactThread.fetch(uniqueId: $0) }
}
private static func createLokiAddressMessage(for thread: TSThread, isPing: Bool) -> LokiAddressMessage? {

@ -3,7 +3,7 @@ public extension Notification.Name {
// State changes
public static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged")
public static let threadFriendRequestStatusChanged = Notification.Name("threadFriendRequestStatusChanged")
public static let userFriendRequestStatusChanged = Notification.Name("userFriendRequestStatusChanged")
public static let groupThreadUpdated = Notification.Name("groupThreadUpdated")
public static let messageFriendRequestStatusChanged = Notification.Name("messageFriendRequestStatusChanged")
public static let threadDeleted = Notification.Name("threadDeleted")
@ -26,7 +26,7 @@ public extension Notification.Name {
// State changes
@objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString
@objc public static let threadFriendRequestStatusChanged = Notification.Name.threadFriendRequestStatusChanged.rawValue as NSString
@objc public static let userFriendRequestStatusChanged = Notification.Name.userFriendRequestStatusChanged.rawValue as NSString
@objc public static let groupThreadUpdated = Notification.Name.groupThreadUpdated.rawValue as NSString
@objc public static let messageFriendRequestStatusChanged = Notification.Name.messageFriendRequestStatusChanged.rawValue as NSString
@objc public static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString

@ -0,0 +1,47 @@
import XCTest
extension XCTestCase {
/// A helper for asynchronous testing.
///
/// Usage example:
///
/// ```
/// func testSomething() {
/// doAsyncThings()
/// eventually {
/// /* XCTAssert goes here... */
/// }
/// }
/// ```
///
/// The provided closure won't execute until `timeout` seconds have passed. Pass
/// in a timeout long enough for your asynchronous process to finish if it's
/// expected to take more than the default 0.1 second.
///
/// - Parameters:
/// - timeout: number of seconds to wait before executing `closure`.
/// - closure: a closure to execute when `timeout` seconds have passed.
///
/// - Note: `timeout` must be less than 60 seconds.
func eventually(timeout: TimeInterval = 0.1, closure: @escaping () -> Void) {
assert(timeout < 60)
let expectation = self.expectation(description: "")
expectation.fulfillAfter(timeout)
self.waitForExpectations(timeout: 60) { _ in
closure()
}
}
}
extension XCTestExpectation {
/// Call `fulfill()` after some time.
///
/// - Parameter time: number of seconds after which `fulfill()` will be called.
func fulfillAfter(_ time: TimeInterval) {
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
self.fulfill()
}
}
}

@ -25,7 +25,7 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
LKMessageFriendRequestStatusAccepted,
LKMessageFriendRequestStatusDeclined,
LKMessageFriendRequestStatusExpired
};
} __deprecated_enum_msg("no longer used as of version 1.1.2");
@interface TSMessage : TSInteraction <OWSPreviewText>
@ -39,7 +39,7 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
@property (nonatomic, readonly, nullable) OWSContact *contactShare;
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
// Loki friend request handling
@property (nonatomic) LKMessageFriendRequestStatus friendRequestStatus;
@property (nonatomic) LKMessageFriendRequestStatus friendRequestStatus __deprecated_msg("no longer used as of version 1.1.2");
@property (nonatomic, readonly) NSString *friendRequestStatusDescription;
@property (nonatomic) uint64_t friendRequestExpiresAt;
@property (nonatomic, readonly) BOOL isFriendRequest;
@ -93,7 +93,6 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
#pragma mark - Loki Friend Request Handling
- (void)saveFriendRequestStatus:(LKMessageFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
- (void)saveFriendRequestExpiresAt:(u_int64_t)expiresAt withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
#pragma mark - Open Groups

@ -1549,7 +1549,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Do this before the check below
[LKFriendRequestProtocol handleFriendRequestMessageIfNeeded:dataMessage associatedWith:incomingMessage wrappedIn:envelope in:thread using:transaction];
[LKFriendRequestProtocol handleFriendRequestMessageIfNeededFromEnvelope:envelope using:transaction];
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.",

@ -360,7 +360,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSAssertDebug([message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold);
}
if (!message.thread.isGroupThread && ![LKSessionMetaProtocol isMessageNoteToSelf:message.thread]) {
if (!message.thread.isGroupThread && ![LKSessionMetaProtocol isThreadNoteToSelf:message.thread]) {
// Not really true but better from a UI point of view
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.calculatingPoW object:[[NSNumber alloc] initWithUnsignedLongLong:message.timestamp]];
}
@ -672,7 +672,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// In the "self-send" special case, we ony need to send a sync message with a delivery receipt
// Loki: Take into account multi device
if ([LKSessionMetaProtocol isMessageNoteToSelf:thread] && !([message isKindOfClass:LKDeviceLinkMessage.class])) {
if ([LKSessionMetaProtocol isThreadNoteToSelf:thread] && !([message isKindOfClass:LKDeviceLinkMessage.class])) {
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent
successHandler();
return;
@ -1179,33 +1179,27 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
BOOL isPing = ((NSNumber *)signalMessageInfo[@"isPing"]).boolValue;
BOOL isFriendRequest = ((NSNumber *)signalMessageInfo[@"isFriendRequest"]).boolValue;
LKSignalMessage *signalMessage = [[LKSignalMessage alloc] initWithType:type timestamp:timestamp senderID:senderID senderDeviceID:senderDeviceID content:content recipientID:recipientID ttl:ttl isPing:isPing isFriendRequest:isFriendRequest];
if (!message.skipSave) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (!message.skipSave) {
// Update the PoW calculation status
[message saveIsCalculatingProofOfWork:YES withTransaction:transaction];
// Update the message and thread if needed
}
if (signalMessage.isFriendRequest) {
TSContactThread *thread = [TSContactThread getThreadWithContactId:recipientID transaction:transaction]; // Take into account multi device
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSending withTransaction:transaction];
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
[LKFriendRequestProtocol setFriendRequestStatusToSendingIfNeededForHexEncodedPublicKey:recipientID transaction:transaction];
}
}];
}
// Convenience
void (^onP2PSuccess)() = ^() { message.isP2P = YES; };
void (^handleError)(NSError *error) = ^(NSError *error) {
if (!message.skipSave) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// Update the message and thread if needed
if (signalMessage.isFriendRequest) {
TSContactThread *thread = [TSContactThread getThreadWithContactId:recipientID transaction:transaction]; // Take into account multi device
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusNone withTransaction:transaction];
[message saveFriendRequestStatus:LKMessageFriendRequestStatusSendingOrFailed withTransaction:transaction];
}
if (!message.skipSave) {
// Update the PoW calculation status
[message saveIsCalculatingProofOfWork:NO withTransaction:transaction];
}];
}
if (signalMessage.isFriendRequest) {
[LKFriendRequestProtocol setFriendRequestStatusToFailedIfNeededForHexEncodedPublicKey:recipientID transaction:transaction];
}
}];
// Handle the error
failedMessageSend(error);
};
@ -1223,22 +1217,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageSent object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]];
isSuccess = YES;
if (signalMessage.isFriendRequest) {
if (!message.skipSave) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// Update the thread
TSContactThread *thread = [TSContactThread getThreadWithContactId:recipientID transaction:transaction]; // Take into account multi device
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusRequestSent withTransaction:transaction];
if (!message.skipSave) {
[message.thread removeOldOutgoingFriendRequestMessagesIfNeededWithTransaction:transaction];
if ([message.thread isKindOfClass:[TSContactThread class]]) {
[((TSContactThread *) message.thread) removeAllSessionRestoreDevicesWithTransaction: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];
}];
}
[LKFriendRequestProtocol setFriendRequestStatusToSentIfNeededForHexEncodedPublicKey:recipientID transaction:transaction];
}];
}
// Invoke the completion handler
[self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false];
@ -1485,7 +1476,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
dispatch_block_t success = ^{
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent
// Loki: Take into account multi device
BOOL isNoteToSelf = [LKSessionMetaProtocol isMessageNoteToSelf:message.thread];
BOOL isNoteToSelf = [LKSessionMetaProtocol isThreadNoteToSelf:message.thread];
if (isNoteToSelf && !([message isKindOfClass:LKDeviceLinkMessage.class])) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *recipientId in message.sendingRecipientIds) {

@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateProfileForContactWithID:(NSString *)contactID displayName:(NSString *)displayName with:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL;
- (void)ensureLocalProfileCached;
@end
NS_ASSUME_NONNULL_END

@ -3,6 +3,7 @@
//
#import "OWSFakeMessageSender.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -24,6 +25,18 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)sendMessage:(OWSMessageSend *)messageSend
{
if (self.stubbedFailingError) {
messageSend.failure(self.stubbedFailingError);
} else {
messageSend.success();
}
if (self.sendMessageWasCalledBlock) {
self.sendMessageWasCalledBlock(messageSend.message);
}
}
- (void)sendAttachment:(DataSource *)dataSource
contentType:(NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename

@ -90,6 +90,11 @@ NS_ASSUME_NONNULL_BEGIN
// Do nothing.
}
- (void)ensureLocalProfileCached
{
// Do nothing.
}
@end
#endif

Loading…
Cancel
Save