Started adding logic for the outbox endpoint

Moved the BlindedIdMapping retrieval logic to ContactUtilities so it's reusable
Added the 'outbox' endpoints (need testing as they aren't deployed to test yet)
pull/592/head
Morgan Pretty 3 years ago
parent 6936f35f2a
commit 8a7db1d48f

@ -298,7 +298,6 @@
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C59247F214E001123EF /* UIView+Glow.swift */; };
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; };
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; };
C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; };
@ -784,6 +783,7 @@
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
FD83B9AA27CF149D005E1583 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9A927CF149D005E1583 /* ContactUtilities.swift */; };
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; };
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; };
@ -1410,7 +1410,6 @@
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = "<group>"; };
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
@ -1921,6 +1920,7 @@
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FD83B9A927CF149D005E1583 /* ContactUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
@ -2229,7 +2229,6 @@
B8544E3223D50E4900299F14 /* SNAppearance.swift */,
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */,
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */,
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */,
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */,
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
@ -3395,6 +3394,7 @@
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FD83B9A927CF149D005E1583 /* ContactUtilities.swift */,
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
C37F53E8255BA9BB002AEA92 /* Environment.h */,
@ -5218,6 +5218,7 @@
FDC4382C27B380E300C60D73 /* LegacyMemberCountResponse.swift in Sources */,
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
FD83B9AA27CF149D005E1583 /* ContactUtilities.swift in Sources */,
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
C32C5AAB256DBE8F003C73A2 /* TSIncomingMessage+Conversion.swift in Sources */,
@ -5446,7 +5447,6 @@
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */,
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,

@ -1,4 +1,5 @@
import PromiseKit
import SessionMessagingKit
@objc(SNEditClosedGroupVC)
final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {

@ -1,4 +1,5 @@
import PromiseKit
import SessionMessagingKit
private protocol TableViewTouchDelegate {

@ -4,6 +4,7 @@ import Photos
import PhotosUI
import Sodium
import PromiseKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
@ -866,68 +867,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) {
// If the sessionId is blinded then check if there is an existing un-blinded thread with the contact
if SessionId.Prefix(from: sessionId) == .blinded {
// TODO: Ensure the above case isn't going to be an issue due to legacy messages?
// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard
// sessionId, as a result in order to see if there is an unblinded contact for this blindedId we
// can only really generate blinded ids for each contact and check if any match
//
// Due to this we have made a few optimisations to try and early-out as often as possible, first
// we try to retrieve a direct cached mapping
if let mapping: BlindedIdMapping = Storage.shared.getBlindedIdMapping(with: sessionId) {
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
let conversationVC: ConversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
return
}
var didFindContact: Bool = false
// Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match
ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in
guard Sodium().sessionId(contact.sessionID, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
return
}
// Cache the mapping
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: contact.sessionID, serverPublicKey: openGroupPublicKey)
Storage.shared.cacheBlindedIdMapping(mapping)
// Open the existing thread
let conversationVC: ConversationVC = ConversationVC(thread: contactThread)
self.navigationController?.pushViewController(conversationVC, animated: true)
didFindContact = true
stop.pointee = true
}
// Don't continue if we found the contact
guard !didFindContact else { return }
if SessionId.Prefix(from: sessionId) == .blinded, let mapping: BlindedIdMapping = ContactUtilities.mapping(for: sessionId, serverPublicKey: openGroupPublicKey) {
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
let conversationVC: ConversationVC = ConversationVC(thread: thread)
// Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had
// a thread with this contact in a different SOGS and had cached the mapping)
Storage.shared.enumerateBlindedIdMapping { mapping, stop in
guard mapping.serverPublicKey != openGroupPublicKey else { return }
guard Sodium().sessionId(mapping.sessionId, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
return
}
// Cache the new mapping
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: mapping.sessionId, serverPublicKey: openGroupPublicKey)
Storage.shared.cacheBlindedIdMapping(newMapping)
// Open the existing thread
let conversationVC: ConversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
didFindContact = true
stop.pointee = true
}
// Don't continue if we found the contact
guard !didFindContact else { return }
self.navigationController?.pushViewController(conversationVC, animated: true)
return
}
// Just create a new thread with the provided sessionId

@ -1,3 +1,4 @@
import SessionMessagingKit
@objc(SNUserSelectionVC)
final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate {

@ -1,51 +0,0 @@
enum ContactUtilities {
private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? {
guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil }
guard thread.shouldBeVisible else { return nil }
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else {
return nil
}
guard contact.didApproveMe else { return nil }
return contact
}
static func getAllContacts() -> [String] {
// Collect all contacts
var result: [Contact] = []
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
result.append(contact)
}
}
func getDisplayName(for publicKey: String) -> String {
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
}
// Remove the current user
if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) {
result.remove(at: index)
}
// Sort alphabetically
return result
.sorted(by: { lhs, rhs in
(lhs.displayName(for: .regular) ?? lhs.sessionID) < (rhs.displayName(for: .regular) ?? rhs.sessionID)
})
.map { $0.sessionID }
}
static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer<ObjCBool>) -> ()) {
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in
guard let contactThread: TSContactThread = object as? TSContactThread else { return }
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
block(contactThread, contact, stop)
}
}
}
}

@ -131,6 +131,29 @@ extension Storage {
let collection = Storage.openGroupInboxLatestMessageIdCollection
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
}
// MARK: - -- Open Group Outbox Latest Message Id
public static let openGroupOutboxLatestMessageIdCollection = "SNOpenGroupOutboxLatestMessageIdCollection"
public func getOpenGroupOutboxLatestMessageId(for server: String) -> Int64? {
let collection = Storage.openGroupOutboxLatestMessageIdCollection
var result: Int64? = nil
Storage.read { transaction in
result = transaction.object(forKey: server, inCollection: collection) as? Int64
}
return result
}
public func setOpenGroupOutboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) {
let collection = Storage.openGroupOutboxLatestMessageIdCollection
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
}
public func removeOpenGroupOutboxLatestMessageId(for server: String, using transaction: Any) {
let collection = Storage.openGroupOutboxLatestMessageIdCollection
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
}
// MARK: - Metadata

@ -7,6 +7,7 @@ extension OpenGroupAPI {
enum CodingKeys: String, CodingKey {
case id
case sender
case recipient
case posted = "posted_at"
case expires = "expires_at"
case base64EncodedMessage = "message"
@ -15,8 +16,11 @@ extension OpenGroupAPI {
/// The unique integer message id
public let id: Int64
/// The (blinded) Session ID of the sender of the message
public let sender: String
/// The (blinded) Session ID of the sender of the message (null on outgoing messages)
public let sender: String?
/// The (blinded) Session ID of the recipient of the message (null on incoming message)
public let recipient: String?
/// Unix timestamp when the message was received by SOGS
public let posted: TimeInterval

@ -41,12 +41,15 @@ public final class OpenGroupAPI: NSObject {
/// - Poll Info
/// - Messages (includes additions and deletions)
/// - Inbox for the server
/// - Outbox for the server
public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
// Store a local copy of the cached state for this server
let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll.wrappedValue[server] == true)
let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll.wrappedValue[server] ?? min(lastPollTime.wrappedValue, timeSinceLastOpen.wrappedValue))
let maybeLastInboxMessageId: Int64? = dependencies.storage.getOpenGroupInboxLatestMessageId(for: server)
let maybeLastOutboxMessageId: Int64? = dependencies.storage.getOpenGroupOutboxLatestMessageId(for: server)
let lastInboxMessageId: Int64 = (maybeLastInboxMessageId ?? 0)
let lastOutboxMessageId: Int64 = (maybeLastOutboxMessageId ?? 0)
// Update the cached state for this server
hasPerformedInitialPoll.mutate { $0[server] = true }
@ -95,15 +98,13 @@ public final class OpenGroupAPI: NSObject {
.roomMessagesRecent(openGroup.room) :
.roomMessagesSince(openGroup.room, seqNo: targetSeqNo)
)
// TODO: Limit?
// queryParameters: [ .limit: 256 ]
),
responseType: [Message].self
)
]
}
)
.appending(
.appending([
// Inbox
BatchRequestInfo(
request: Request<NoBody>(
@ -112,12 +113,22 @@ public final class OpenGroupAPI: NSObject {
.inbox :
.inboxSince(id: lastInboxMessageId)
)
// TODO: Limit?
// queryParameters: [ .limit: 256 ]
),
responseType: [DirectMessage]?.self // 'inboxSince' will return a `304` with an empty response if no messages
),
// Outbox
BatchRequestInfo(
request: Request<NoBody>(
server: server,
endpoint: (maybeLastOutboxMessageId == nil ?
.outbox :
.outboxSince(id: lastOutboxMessageId)
)
),
responseType: [DirectMessage]?.self // 'outboxSince' will return a `304` with an empty response if no messages
)
)
])
return batch(server, requests: requestResponseType, using: dependencies)
}
@ -499,13 +510,13 @@ public final class OpenGroupAPI: NSObject {
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Inbox (Message Requests)
// MARK: - Inbox/Outbox (Message Requests)
/// Retrieves all of the user's current DMs (up to limit)
///
/// **Note:** This is the direct request to retrieve DMs for a specific Open Group so should be retrieved automatically from the `poll()`
/// method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the
/// `OpenGroupManager.handleInbox` method to ensure things are processed correctly
/// `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
@ -521,7 +532,7 @@ public final class OpenGroupAPI: NSObject {
///
/// **Note:** This is the direct request to retrieve messages requests for a specific Open Group since a given messages so should be retrieved
/// automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response
/// of this method to the `OpenGroupManager.handleInbox` method to ensure things are processed correctly
/// of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
@ -551,6 +562,38 @@ public final class OpenGroupAPI: NSObject {
return send(request, using: dependencies)
}
/// Retrieves all of the user's sent DMs (up to limit)
///
/// **Note:** This is the direct request to retrieve DMs sent by the user for a specific Open Group so should be retrieved automatically
/// from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response of
/// this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
public static func outbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
server: server,
endpoint: .outbox
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
/// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages
///
/// **Note:** This is the direct request to retrieve messages requests sent by the user for a specific Open Group since a given messages so
/// should be retrieved automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure
/// to route the response of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
public static func outboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
server: server,
endpoint: .outboxSince(id: id)
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Users
/// Applies a ban of a user from specific rooms, or from the server globally

@ -248,7 +248,7 @@ public final class OpenGroupManager: NSObject {
maybeUpdatedModel = updatedModel
let updatedOpenGroup: OpenGroup = OpenGroup(
server: server,
room: (pollInfo.token ?? roomToken),
room: pollInfo.token,
publicKey: publicKey,
name: (pollInfo.details?.name ?? thread.name()),
groupDescription: (pollInfo.details?.description ?? existingOpenGroup?.description),
@ -311,8 +311,11 @@ public final class OpenGroupManager: NSObject {
)
}
internal static func handleInbox(
internal static func handleDirectMessages(
_ messages: [OpenGroupAPI.DirectMessage],
// We could infer where the messages come from based on their sender/recipient values but being since they
// are different endpoints being explicit here reduces the chance a future change will break things
fromOutbox: Bool,
on server: String,
isBackgroundPoll: Bool,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()
@ -329,10 +332,17 @@ public final class OpenGroupManager: NSObject {
let sortedMessages: [OpenGroupAPI.DirectMessage] = messages
.sorted { lhs, rhs in lhs.id < rhs.id }
let latestMessageId: Int64 = (sortedMessages.last?.id ?? 0)
let userSessionId: String = getUserHexEncodedPublicKey()
var mappingCache: [String: BlindedIdMapping] = [:]
dependencies.storage.write { transaction in
// Update the 'latestMessageId' value
dependencies.storage.setOpenGroupInboxLatestMessageId(for: server, to: latestMessageId, using: transaction)
if fromOutbox {
dependencies.storage.setOpenGroupOutboxLatestMessageId(for: server, to: latestMessageId, using: transaction)
}
else {
dependencies.storage.setOpenGroupInboxLatestMessageId(for: server, to: latestMessageId, using: transaction)
}
// Process the messages
sortedMessages.forEach { message in
@ -344,12 +354,47 @@ public final class OpenGroupManager: NSObject {
// Note: The `posted` value is in seconds but all messages in the database use milliseconds for timestamps
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted * 1000)))
envelope.setContent(messageData)
envelope.setSource(message.sender)
envelope.setSource(message.sender ?? userSessionId) // Outbox messages have no 'sender' so default to current user
do {
let data = try envelope.buildSerializedData()
let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: nil, openGroupServerPublicKey: serverPublicKey, isRetry: false, using: transaction)
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction)
let (receivedMessage, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: nil, openGroupServerPublicKey: serverPublicKey, isRetry: false, using: transaction)
// TODO: Need to test and validate this unblinding logic
// If the message was an outgoing message then attempt to unblind the recipient (this will help put
// messages in the correct thread in case of message request approval race conditions as well as
// during device sync'ing and restoration)
if fromOutbox, let recipientBlindedId: String = message.recipient {
// Attempt to un-blind the 'message.recipient'
let mapping: BlindedIdMapping
// Minor optimisation to avoid processing the same sender multiple times
if let result: BlindedIdMapping = mappingCache[recipientBlindedId] {
mapping = result
}
else if let result: BlindedIdMapping = ContactUtilities.mapping(for: recipientBlindedId, serverPublicKey: serverPublicKey) {
mapping = result
}
else {
// Cache an "invalid" mapping that has the 'sessionId' set to the recipient so we don't
// re-process this recipient if there is another message from them
mapping = BlindedIdMapping(
blindedId: "",
sessionId: recipientBlindedId,
serverPublicKey: ""
)
}
switch receivedMessage {
case let receivedMessage as VisibleMessage: receivedMessage.syncTarget = mapping.sessionId
case let receivedMessage as ExpirationTimerUpdate: receivedMessage.syncTarget = mapping.sessionId
default: break
}
mappingCache[recipientBlindedId] = mapping
}
try MessageReceiver.handle(receivedMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction)
}
catch let error {
SNLog("Couldn't receive inbox message due to error: \(error).")

@ -9,61 +9,61 @@ import SessionSnodeKit
extension OpenGroupAPI {
public class Dependencies {
private var _api: OnionRequestAPIType.Type?
var api: OnionRequestAPIType.Type {
public var api: OnionRequestAPIType.Type {
get { getValueSettingIfNull(&_api) { OnionRequestAPI.self } }
set { _api = newValue }
}
private var _storage: SessionMessagingKitStorageProtocol?
var storage: SessionMessagingKitStorageProtocol {
public var storage: SessionMessagingKitStorageProtocol {
get { getValueSettingIfNull(&_storage) { SNMessagingKitConfiguration.shared.storage } }
set { _storage = newValue }
}
private var _sodium: SodiumType?
var sodium: SodiumType {
public var sodium: SodiumType {
get { getValueSettingIfNull(&_sodium) { Sodium() } }
set { _sodium = newValue }
}
private var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType?
var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
get { getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } }
set { _aeadXChaCha20Poly1305Ietf = newValue }
}
private var _sign: SignType?
var sign: SignType {
public var sign: SignType {
get { getValueSettingIfNull(&_sign) { sodium.getSign() } }
set { _sign = newValue }
}
private var _genericHash: GenericHashType?
var genericHash: GenericHashType {
public var genericHash: GenericHashType {
get { getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } }
set { _genericHash = newValue }
}
private var _ed25519: Ed25519Type.Type?
var ed25519: Ed25519Type.Type {
public var ed25519: Ed25519Type.Type {
get { getValueSettingIfNull(&_ed25519) { Ed25519.self } }
set { _ed25519 = newValue }
}
private var _nonceGenerator16: NonceGenerator16ByteType?
var nonceGenerator16: NonceGenerator16ByteType {
public var nonceGenerator16: NonceGenerator16ByteType {
get { getValueSettingIfNull(&_nonceGenerator16) { NonceGenerator16Byte() } }
set { _nonceGenerator16 = newValue }
}
private var _nonceGenerator24: NonceGenerator24ByteType?
var nonceGenerator24: NonceGenerator24ByteType {
public var nonceGenerator24: NonceGenerator24ByteType {
get { getValueSettingIfNull(&_nonceGenerator24) { NonceGenerator24Byte() } }
set { _nonceGenerator24 = newValue }
}
private var _date: Date?
var date: Date {
public var date: Date {
get { getValueSettingIfNull(&_date) { Date() } }
set { _date = newValue }
}

@ -38,12 +38,15 @@ extension OpenGroupAPI {
case roomFileIndividual(String, Int64)
case roomFileIndividualJson(String, Int64)
// Inbox (Message Requests)
// Inbox/Outbox (Message Requests)
case inbox
case inboxSince(id: Int64)
case inboxFor(sessionId: String)
case outbox
case outboxSince(id: Int64)
// Users
case userBan(String)
@ -133,11 +136,14 @@ extension OpenGroupAPI {
case .roomFileIndividualJson(let roomToken, let fileId):
return "room/\(roomToken)/file/\(fileId)"
// Inbox (Message Requests)
// Inbox/Outbox (Message Requests)
case .inbox: return "inbox"
case .inboxSince(let id): return "inbox/since/\(id)"
case .inboxFor(let sessionId): return "inbox/\(sessionId)"
case .outbox: return "outbox"
case .outboxSince(let id): return "outbox/since/\(id)"
// Users

@ -123,14 +123,22 @@ extension OpenGroupAPI {
on: server
)
case .inbox, .inboxSince:
case .inbox, .inboxSince, .outbox, .outboxSince:
guard let responseData: BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? BatchSubResponse<[DirectMessage]?>, let responseBody: [DirectMessage]? = responseData.body else {
SNLog("Open group polling failed due to invalid data.")
return
}
OpenGroupManager.handleInbox(
let fromOutbox: Bool = {
switch endpoint {
case .outbox, .outboxSince: return true
default: return false
}
}()
OpenGroupManager.handleDirectMessages(
(responseBody ?? []),
fromOutbox: fromOutbox,
on: server,
isBackgroundPoll: isBackgroundPoll
)

@ -1,5 +1,6 @@
import PromiseKit
import Sodium
import YapDatabase
public protocol SessionMessagingKitStorageProtocol {
@ -19,6 +20,15 @@ public protocol SessionMessagingKitStorageProtocol {
func getUser() -> Contact?
func getAllContacts() -> Set<Contact>
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
// MARK: - Blinded Id cache
func getBlindedIdMapping(with blindedId: String) -> BlindedIdMapping?
func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping?
func cacheBlindedIdMapping(_ mapping: BlindedIdMapping)
func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction)
func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> ())
func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> (), transaction: YapDatabaseReadTransaction)
// MARK: - Closed Groups
@ -72,6 +82,12 @@ public protocol SessionMessagingKitStorageProtocol {
func getOpenGroupInboxLatestMessageId(for server: String) -> Int64?
func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any)
func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any)
// MARK: - -- Open Group Outbox Latest Message Id
func getOpenGroupOutboxLatestMessageId(for server: String) -> Int64?
func setOpenGroupOutboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any)
func removeOpenGroupOutboxLatestMessageId(for server: String, using transaction: Any)
// MARK: - Message Handling

@ -0,0 +1,99 @@
import SessionUtilitiesKit
enum ContactUtilities {
private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? {
guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil }
guard thread.shouldBeVisible else { return nil }
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else {
return nil
}
guard contact.didApproveMe else { return nil }
return contact
}
static func getAllContacts() -> [String] {
// Collect all contacts
var result: [Contact] = []
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
result.append(contact)
}
}
func getDisplayName(for publicKey: String) -> String {
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
}
// Remove the current user
if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) {
result.remove(at: index)
}
// Sort alphabetically
return result
.sorted(by: { lhs, rhs in
(lhs.displayName(for: .regular) ?? lhs.sessionID) < (rhs.displayName(for: .regular) ?? rhs.sessionID)
})
.map { $0.sessionID }
}
static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer<ObjCBool>) -> ()) {
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in
guard let contactThread: TSContactThread = object as? TSContactThread else { return }
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
block(contactThread, contact, stop)
}
}
}
static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? {
// TODO: Ensure the above case isn't going to be an issue due to legacy messages?.
// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard
// sessionId, as a result in order to see if there is an unblinded contact for this blindedId we
// can only really generate blinded ids for each contact and check if any match
//
// Due to this we have made a few optimisations to try and early-out as often as possible, first
// we try to retrieve a direct cached mapping
var mappingResult: BlindedIdMapping? = dependencies.storage.getBlindedIdMapping(with: blindedId)
// No need to continue if we already have a result
if let mapping: BlindedIdMapping = mappingResult { return mapping }
// Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match
ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in
guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else {
return
}
// Cache the mapping
let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: contact.sessionID, serverPublicKey: serverPublicKey)
dependencies.storage.cacheBlindedIdMapping(newMapping)
mappingResult = newMapping
stop.pointee = true
}
// Finish if we have a result
if let mapping: BlindedIdMapping = mappingResult { return mapping }
// Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had
// a thread with this contact in a different SOGS and had cached the mapping)
dependencies.storage.enumerateBlindedIdMapping { mapping, stop in
guard mapping.serverPublicKey != serverPublicKey else { return }
guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else {
return
}
// Cache the new mapping
let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: mapping.sessionId, serverPublicKey: serverPublicKey)
dependencies.storage.cacheBlindedIdMapping(newMapping)
mappingResult = newMapping
stop.pointee = true
}
return mappingResult
}
}
Loading…
Cancel
Save