mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
283 lines
18 KiB
Swift
283 lines
18 KiB
Swift
import PromiseKit
|
|
|
|
// A few notes about making changes in this file:
|
|
//
|
|
// • Don't use a database transaction if you can avoid it.
|
|
// • If you do need to use a database transaction, use a read transaction if possible.
|
|
// • Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
|
|
// • Think carefully about adding a function; there might already be one for what you need.
|
|
// • Document the expected cases for everything.
|
|
// • Express those cases in tests.
|
|
|
|
/// See [The Session Friend Request Protocol](https://github.com/loki-project/session-protocol-docs/wiki/Friend-Requests) for more information.
|
|
@objc(LKFriendRequestProtocol)
|
|
public final class FriendRequestProtocol : NSObject {
|
|
|
|
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
|
|
|
// MARK: - Enums
|
|
// FIXME: Better naming :(
|
|
@objc public enum FriendRequestUIState : Int {
|
|
case friends, received, sent, none
|
|
}
|
|
|
|
// MARK: - General
|
|
@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 thread.isNoteToSelf() { return true }
|
|
let contactID = thread.contactIdentifier()
|
|
var friendRequestStatuses: [LKFriendRequestStatus] = []
|
|
storage.dbReadConnection.read { transaction in
|
|
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction)
|
|
friendRequestStatuses = linkedDevices.map { device in
|
|
return storage.getFriendRequestStatus(for: device, transaction: transaction)
|
|
}
|
|
}
|
|
// If the current user is friends with any of the other user's devices, the input bar should be enabled
|
|
if friendRequestStatuses.contains(where: { $0 == .friends }) { return true }
|
|
// If no friend request has been sent, the input bar should be enabled
|
|
if friendRequestStatuses.allSatisfy({ $0 == .none || $0 == .requestExpired }) { return true }
|
|
// There must be a pending friend request
|
|
return false
|
|
}
|
|
|
|
@objc(shouldAttachmentButtonBeEnabledForThread:)
|
|
public static func shouldAttachmentButtonBeEnabled(for thread: TSThread) -> Bool {
|
|
// 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 thread.isNoteToSelf() { return true }
|
|
let contactID = thread.contactIdentifier()
|
|
var friendRequestStatuses: [LKFriendRequestStatus] = []
|
|
storage.dbReadConnection.read { transaction in
|
|
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction)
|
|
friendRequestStatuses = linkedDevices.map { device in
|
|
storage.getFriendRequestStatus(for: device, transaction: transaction)
|
|
}
|
|
}
|
|
// If the current user is friends with any of the other user's devices, the attachment button should be enabled
|
|
if friendRequestStatuses.contains(where: { $0 == .friends }) { return true }
|
|
// Otherwise don't allow attachments at all
|
|
return false
|
|
}
|
|
|
|
@objc(getFriendRequestUIStateForThread:)
|
|
public static func getFriendRequestUIState(for thread: TSThread) -> FriendRequestUIState {
|
|
// 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
|
|
if thread.isNoteToSelf() { return .none }
|
|
var friendRequestStatuses: [LKFriendRequestStatus] = []
|
|
storage.dbReadConnection.read { transaction in
|
|
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: thread.contactIdentifier(), in: transaction)
|
|
friendRequestStatuses = linkedDevices.map { device in
|
|
return storage.getFriendRequestStatus(for: device, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
if friendRequestStatuses.contains(where: { $0 == .friends }) { return .friends }
|
|
if friendRequestStatuses.contains(where: { $0 == .requestReceived }) { return .received }
|
|
if friendRequestStatuses.contains(where: { $0 == .requestSent }) { return .sent }
|
|
|
|
return .none
|
|
}
|
|
|
|
// MARK: - Sending
|
|
@objc(acceptFriendRequestFromHexEncodedPublicKey:using:)
|
|
public static func acceptFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
|
// 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.
|
|
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
|
|
assertionFailure("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)
|
|
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: Need to track these so that we can expire them and resend incase the other user wasn't online after we sent
|
|
MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: device, in: transaction) // NOT hexEncodedPublicKey
|
|
.done(on: OWSDispatch.sendingQueue()) { autoGeneratedFRMessageSend in
|
|
let messageSender = SSKEnvironment.shared.messageSender
|
|
messageSender.sendMessage(autoGeneratedFRMessageSend)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:using:)
|
|
public static func sendFriendRequestAcceptanceMessage(to hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
|
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
|
|
assertionFailure("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(declineFriendRequestFromHexEncodedPublicKey:using:)
|
|
public static func declineFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
|
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
|
|
assertionFailure("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 any 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.
|
|
storage.removePreKeyBundle(forContact: device, transaction: transaction)
|
|
storage.setFriendRequestStatus(.none, for: device, transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc(sendingFriendRequestToHexEncodedPublicKey:transaction:)
|
|
public static func sendingFriendRequest(to hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
|
|
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
|
|
if (friendRequestStatus == .none || friendRequestStatus == .requestExpired) {
|
|
storage.setFriendRequestStatus(.requestSending, for: hexEncodedPublicKey, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
@objc(sentFriendRequestToHexEncodedPublicKey:transaction:)
|
|
public static func sentFriendRequest(to hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
|
|
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
|
|
if (friendRequestStatus == .none || friendRequestStatus == .requestExpired || friendRequestStatus == .requestSending) {
|
|
storage.setFriendRequestStatus(.requestSent, for: hexEncodedPublicKey, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
@objc(failedToSendFriendRequestToHexEncodedPublicKey:transaction:)
|
|
public static func failedToSendFriendRequest(to hexEncodedPublicKey: String, transaction: YapDatabaseReadWriteTransaction) {
|
|
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
|
|
if (friendRequestStatus == .requestSending) {
|
|
storage.setFriendRequestStatus(.none, for: hexEncodedPublicKey, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: - Receiving
|
|
@objc(isFriendRequestFromBeforeRestoration:)
|
|
public static func isFriendRequestFromBeforeRestoration(_ envelope: SSKProtoEnvelope) -> Bool {
|
|
// The envelope type is set during UD decryption
|
|
let restorationTimeInMs = UInt64(storage.getRestorationTime() * 1000)
|
|
return (envelope.type == .friendRequest && envelope.timestamp < restorationTimeInMs)
|
|
}
|
|
|
|
@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
|
|
// friend request accepted message succeeds. Even if it doesn't, the thread's current friend
|
|
// request status will be set to LKThreadFriendRequestStatusFriends for Alice making it possible
|
|
// for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
|
|
// will then be set to LKThreadFriendRequestStatusFriends. If we do check for a successful send
|
|
// before updating Alice's thread's friend request status to LKThreadFriendRequestStatusFriends,
|
|
// we can end up in a deadlock where both users' threads' friend request statuses are
|
|
// LKThreadFriendRequestStatusRequestSent.
|
|
return true
|
|
}
|
|
// Auto-accept any friend requests from the user's own linked devices
|
|
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 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
|
|
}
|
|
|
|
@objc(handleFriendRequestAcceptanceIfNeeded:in:)
|
|
public static func handleFriendRequestAcceptanceIfNeeded(_ envelope: SSKProtoEnvelope, in transaction: YapDatabaseReadWriteTransaction) {
|
|
// The envelope source is set during UD decryption
|
|
let hexEncodedPublicKey = envelope.source!
|
|
// The envelope type is set during UD decryption.
|
|
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 friendRequestStatus != .none else { return }
|
|
// Become friends
|
|
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: 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)
|
|
}
|
|
*/
|
|
}
|
|
|
|
@objc(handleFriendRequestMessageIfNeeded:associatedWith:wrappedIn:in:using:)
|
|
public static func handleFriendRequestMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, associatedWith message: TSIncomingMessage, wrappedIn envelope: SSKProtoEnvelope, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
|
|
guard !envelope.isGroupChatMessage else {
|
|
print("[Loki] Ignoring friend request in group chat.")
|
|
return
|
|
}
|
|
// The envelope source is set during UD decryption
|
|
let hexEncodedPublicKey = envelope.source!
|
|
// The envelope type is set during UD decryption.
|
|
guard envelope.type == .friendRequest else {
|
|
print("[Loki] Ignoring friend request logic for non friend request type envelope.")
|
|
return
|
|
}
|
|
if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, using: transaction) {
|
|
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: 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, 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.
|
|
storage.setFriendRequestStatus(.requestReceived, for: hexEncodedPublicKey, transaction: 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.
|
|
}
|
|
}
|
|
}
|