mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
046269f1df
Fixed a bug with how the open group URL processing was working (one of the example URLs wasn't getting processed correctly)
732 lines
35 KiB
Swift
732 lines
35 KiB
Swift
import PromiseKit
|
|
import Sodium
|
|
import SessionUtilitiesKit
|
|
import SessionSnodeKit
|
|
|
|
// MARK: - OGMCacheType
|
|
|
|
public protocol OGMCacheType {
|
|
var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? { get set }
|
|
var groupImagePromises: [String: Promise<Data>] { get set }
|
|
|
|
var pollers: [String: OpenGroupAPI.Poller] { get set }
|
|
var isPolling: Bool { get set }
|
|
|
|
var moderators: [String: [String: Set<String>]] { get set }
|
|
var admins: [String: [String: Set<String>]] { get set }
|
|
|
|
var hasPerformedInitialPoll: [String: Bool] { get set }
|
|
var timeSinceLastPoll: [String: TimeInterval] { get set }
|
|
|
|
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
|
|
}
|
|
|
|
extension OGMCacheType {
|
|
func getTimeSinceLastOpen() -> TimeInterval {
|
|
return getTimeSinceLastOpen(using: Dependencies())
|
|
}
|
|
}
|
|
|
|
// MARK: - OpenGroupManager
|
|
|
|
@objc(SNOpenGroupManager)
|
|
public final class OpenGroupManager: NSObject {
|
|
// MARK: - Cache
|
|
|
|
public class Cache: OGMCacheType {
|
|
public var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>?
|
|
public var groupImagePromises: [String: Promise<Data>] = [:]
|
|
|
|
public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
|
|
public var isPolling: Bool = false
|
|
|
|
/// Server URL to room ID to set of user IDs
|
|
public var moderators: [String: [String: Set<String>]] = [:]
|
|
public var admins: [String: [String: Set<String>]] = [:]
|
|
|
|
/// Server URL to value
|
|
public var hasPerformedInitialPoll: [String: Bool] = [:]
|
|
public var timeSinceLastPoll: [String: TimeInterval] = [:]
|
|
|
|
fileprivate var _timeSinceLastOpen: TimeInterval?
|
|
public func getTimeSinceLastOpen(using dependencies: Dependencies = Dependencies()) -> TimeInterval {
|
|
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
|
|
return storedTimeSinceLastOpen
|
|
}
|
|
|
|
guard let lastOpen: Date = dependencies.standardUserDefaults[.lastOpen] else {
|
|
_timeSinceLastOpen = .greatestFiniteMagnitude
|
|
return .greatestFiniteMagnitude
|
|
}
|
|
|
|
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen)
|
|
return dependencies.date.timeIntervalSince(lastOpen)
|
|
}
|
|
}
|
|
|
|
// MARK: - Variables
|
|
|
|
@objc public static let shared: OpenGroupManager = OpenGroupManager()
|
|
|
|
/// Note: This should not be accessed directly but rather via the 'OGMDependencies' type
|
|
fileprivate let mutableCache: Atomic<OGMCacheType> = Atomic(Cache())
|
|
|
|
// MARK: - Polling
|
|
|
|
public func startPolling(using dependencies: OGMDependencies = OGMDependencies()) {
|
|
guard !dependencies.cache.isPolling else { return }
|
|
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.isPolling = true
|
|
cache.pollers = Set(dependencies.storage.getAllOpenGroups().values.map { openGroup in openGroup.server })
|
|
.reduce(into: [:]) { prev, server in
|
|
cache.pollers[server]?.stop() // Should never occur
|
|
|
|
let poller = OpenGroupAPI.Poller(for: server)
|
|
poller.startIfNeeded(using: dependencies)
|
|
|
|
prev[server] = poller
|
|
}
|
|
}
|
|
}
|
|
|
|
public func stopPolling(using dependencies: OGMDependencies = OGMDependencies()) {
|
|
dependencies.mutableCache.mutate {
|
|
$0.pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
|
|
$0.pollers.removeAll()
|
|
$0.isPolling = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Adding & Removing
|
|
|
|
public func add(roomToken: String, server: String, publicKey: String, isConfigMessage: Bool, using transaction: YapDatabaseReadWriteTransaction, dependencies: OGMDependencies = OGMDependencies()) -> Promise<Void> {
|
|
// If we are currently polling for this server and already have a TSGroupThread for this room the do nothing
|
|
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)")
|
|
|
|
if dependencies.cache.pollers[server] != nil && TSGroupThread.fetch(groupId: groupId, transaction: transaction) != nil {
|
|
SNLog("Ignoring join open group attempt (already joined), user initiated: \(!isConfigMessage)")
|
|
return Promise.value(())
|
|
}
|
|
|
|
// Clear any existing data if needed
|
|
dependencies.storage.removeOpenGroupSequenceNumber(for: roomToken, on: server, using: transaction)
|
|
|
|
// Store the public key
|
|
dependencies.storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
|
|
|
|
let (promise, seal) = Promise<Void>.pending()
|
|
|
|
transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) {
|
|
OpenGroupAPI.capabilitiesAndRoom(for: roomToken, on: server, using: dependencies)
|
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { response in
|
|
dependencies.storage.write { anyTransaction in
|
|
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
|
|
|
|
// Store the capabilities first
|
|
OpenGroupManager.handleCapabilities(
|
|
response.capabilities.data,
|
|
on: server,
|
|
using: transaction,
|
|
dependencies: dependencies
|
|
)
|
|
|
|
// Then the room
|
|
OpenGroupManager.handlePollInfo(
|
|
OpenGroupAPI.RoomPollInfo(room: response.room.data),
|
|
publicKey: publicKey,
|
|
for: roomToken,
|
|
on: server,
|
|
using: transaction,
|
|
dependencies: dependencies
|
|
) {
|
|
seal.fulfill(())
|
|
}
|
|
}
|
|
}
|
|
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
SNLog("Failed to join open group.")
|
|
seal.reject(error)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction, dependencies: OGMDependencies = OGMDependencies()) {
|
|
// Stop the poller if needed
|
|
let openGroups = dependencies.storage.getAllOpenGroups().values.filter { $0.server == openGroup.server }
|
|
if openGroups.count == 1 && openGroups.last == openGroup {
|
|
let poller = dependencies.cache.pollers[openGroup.server]
|
|
poller?.stop()
|
|
dependencies.mutableCache.mutate { $0.pollers[openGroup.server] = nil }
|
|
}
|
|
|
|
// Remove all data
|
|
var messageIDs: Set<String> = []
|
|
var messageTimestamps: Set<UInt64> = []
|
|
thread.enumerateInteractions(with: transaction) { interaction, _ in
|
|
messageIDs.insert(interaction.uniqueId!)
|
|
messageTimestamps.insert(interaction.timestamp)
|
|
}
|
|
dependencies.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
|
|
dependencies.storage.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
|
|
dependencies.storage.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
|
|
|
|
thread.removeAllThreadInteractions(with: transaction)
|
|
thread.remove(with: transaction)
|
|
dependencies.storage.removeOpenGroup(for: thread.uniqueId!, using: transaction)
|
|
|
|
// Only remove the open group public key and server info if the user isn't in any other rooms
|
|
if openGroups.count <= 1 {
|
|
dependencies.storage.removeOpenGroupServer(name: openGroup.server, using: transaction)
|
|
dependencies.storage.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: - Response Processing
|
|
|
|
internal static func handleCapabilities(
|
|
_ capabilities: OpenGroupAPI.Capabilities,
|
|
on server: String,
|
|
using transaction: YapDatabaseReadWriteTransaction,
|
|
dependencies: OGMDependencies = OGMDependencies()
|
|
) {
|
|
let updatedServer: OpenGroupAPI.Server = OpenGroupAPI.Server(
|
|
name: server,
|
|
capabilities: capabilities
|
|
)
|
|
|
|
dependencies.storage.setOpenGroupServer(updatedServer, using: transaction)
|
|
}
|
|
|
|
internal static func handlePollInfo(
|
|
_ pollInfo: OpenGroupAPI.RoomPollInfo,
|
|
publicKey maybePublicKey: String?,
|
|
for roomToken: String,
|
|
on server: String,
|
|
using transaction: YapDatabaseReadWriteTransaction,
|
|
dependencies: OGMDependencies = OGMDependencies(),
|
|
completion: (() -> ())? = nil
|
|
) {
|
|
// Create the open group model and get or create the thread
|
|
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)")
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
|
|
let initialModel: TSGroupModel = TSGroupModel(
|
|
title: (pollInfo.details?.name ?? ""),
|
|
memberIds: [ userPublicKey ],
|
|
image: nil,
|
|
groupId: groupId,
|
|
groupType: .openGroup,
|
|
adminIds: (pollInfo.details?.admins ?? []),
|
|
moderatorIds: (pollInfo.details?.moderators ?? [])
|
|
)
|
|
var maybeUpdatedModel: TSGroupModel? = nil
|
|
|
|
// Store/Update everything
|
|
let thread = TSGroupThread.getOrCreateThread(with: initialModel, transaction: transaction)
|
|
let existingOpenGroup: OpenGroup? = thread.uniqueId.flatMap { uniqueId -> OpenGroup? in
|
|
dependencies.storage.getOpenGroup(for: uniqueId)
|
|
}
|
|
|
|
guard let threadUniqueId: String = thread.uniqueId else { return }
|
|
guard let publicKey: String = (maybePublicKey ?? existingOpenGroup?.publicKey) else { return }
|
|
|
|
let updatedModel: TSGroupModel = TSGroupModel(
|
|
title: (pollInfo.details?.name ?? thread.groupModel.groupName),
|
|
memberIds: Array(Set(thread.groupModel.groupMemberIds).inserting(userPublicKey)),
|
|
image: thread.groupModel.groupImage,
|
|
groupId: groupId,
|
|
groupType: .openGroup,
|
|
adminIds: (pollInfo.details?.admins ?? thread.groupModel.groupAdminIds),
|
|
moderatorIds: (pollInfo.details?.moderators ?? thread.groupModel.groupModeratorIds)
|
|
)
|
|
maybeUpdatedModel = updatedModel
|
|
let updatedOpenGroup: OpenGroup = OpenGroup(
|
|
server: server,
|
|
room: pollInfo.token,
|
|
publicKey: publicKey,
|
|
name: (pollInfo.details?.name ?? thread.name()),
|
|
groupDescription: (pollInfo.details?.roomDescription ?? existingOpenGroup?.groupDescription),
|
|
imageID: (pollInfo.details?.imageId.map { "\($0)" } ?? existingOpenGroup?.imageID),
|
|
infoUpdates: ((pollInfo.details?.infoUpdates ?? existingOpenGroup?.infoUpdates) ?? 0)
|
|
)
|
|
|
|
// - Thread changes
|
|
thread.shouldBeVisible = true
|
|
thread.groupModel = updatedModel
|
|
thread.save(with: transaction)
|
|
|
|
// - Open Group changes
|
|
dependencies.storage.setOpenGroup(updatedOpenGroup, for: threadUniqueId, using: transaction)
|
|
|
|
// - User Count
|
|
dependencies.storage.setUserCount(
|
|
to: UInt64(pollInfo.activeUsers),
|
|
forOpenGroupWithID: updatedOpenGroup.id,
|
|
using: transaction
|
|
)
|
|
|
|
transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) {
|
|
// Start the poller if needed
|
|
if dependencies.cache.pollers[server] == nil {
|
|
dependencies.mutableCache.mutate {
|
|
$0.pollers[server] = OpenGroupAPI.Poller(for: server)
|
|
$0.pollers[server]?.startIfNeeded(using: dependencies)
|
|
}
|
|
}
|
|
|
|
// - Moderators
|
|
if let moderators: [String] = (pollInfo.details?.moderators ?? maybeUpdatedModel?.groupModeratorIds) {
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.moderators[server] = (cache.moderators[server] ?? [:]).setting(roomToken, Set(moderators))
|
|
}
|
|
}
|
|
|
|
// - Admins
|
|
if let admins: [String] = (pollInfo.details?.admins ?? maybeUpdatedModel?.groupAdminIds) {
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.admins[server] = (cache.admins[server] ?? [:]).setting(roomToken, Set(admins))
|
|
}
|
|
}
|
|
|
|
// - Room image (if there is one and it's different from the existing one, or we don't have the existing one)
|
|
if let imageId: UInt64 = UInt64(updatedOpenGroup.imageID ?? ""), (updatedModel.groupImage == nil || updatedOpenGroup.imageID != existingOpenGroup?.imageID) {
|
|
OpenGroupManager.roomImage(imageId, for: roomToken, on: server, using: dependencies)
|
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
|
dependencies.storage.write { transaction in
|
|
// Update the thread
|
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
let thread = TSGroupThread.getOrCreateThread(with: initialModel, transaction: transaction)
|
|
thread.groupModel.groupImage = UIImage(data: data)
|
|
thread.save(with: transaction)
|
|
}
|
|
}
|
|
.retainUntilComplete()
|
|
}
|
|
|
|
// Finish
|
|
completion?()
|
|
}
|
|
}
|
|
|
|
internal static func handleMessages(
|
|
_ messages: [OpenGroupAPI.Message],
|
|
for roomToken: String,
|
|
on server: String,
|
|
isBackgroundPoll: Bool,
|
|
using transaction: YapDatabaseReadWriteTransaction,
|
|
dependencies: OGMDependencies = OGMDependencies()
|
|
) {
|
|
// Sorting the messages by server ID before importing them fixes an issue where messages
|
|
// that quote older messages can't find those older messages
|
|
let openGroupID = "\(server).\(roomToken)"
|
|
let openGroupIdData: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroupID)
|
|
let sortedMessages: [OpenGroupAPI.Message] = messages
|
|
.sorted { lhs, rhs in lhs.id < rhs.id }
|
|
let seqNo: Int64? = sortedMessages.map { $0.seqNo }.max()
|
|
var messageServerIDsToRemove: [UInt64] = []
|
|
|
|
// Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId')
|
|
if let seqNo: Int64 = seqNo {
|
|
dependencies.storage.setOpenGroupSequenceNumber(for: roomToken, on: server, to: seqNo, using: transaction)
|
|
}
|
|
|
|
// Process the messages
|
|
sortedMessages.forEach { message in
|
|
guard let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString) else {
|
|
// A message with no data has been deleted so add it to the list to remove
|
|
messageServerIDsToRemove.append(UInt64(message.id))
|
|
return
|
|
}
|
|
guard let sender: String = message.sender else { return } // Need a sender in order to process the message
|
|
|
|
// 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(data)
|
|
envelope.setSource(sender)
|
|
|
|
do {
|
|
let data = try envelope.buildSerializedData()
|
|
let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.id), isRetry: false, using: transaction, dependencies: dependencies)
|
|
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction, dependencies: dependencies)
|
|
}
|
|
catch {
|
|
SNLog("Couldn't receive open group message due to error: \(error).")
|
|
}
|
|
}
|
|
|
|
// Handle any deletions that are needed
|
|
guard !messageServerIDsToRemove.isEmpty else { return }
|
|
guard let thread = TSGroupThread.fetch(groupId: openGroupIdData, transaction: transaction) else { return }
|
|
|
|
var messagesToRemove: [TSMessage] = []
|
|
|
|
thread.enumerateInteractions(with: transaction) { interaction, stop in
|
|
guard let message: TSMessage = interaction as? TSMessage, messageServerIDsToRemove.contains(message.openGroupServerMessageID) else {
|
|
return
|
|
}
|
|
messagesToRemove.append(message)
|
|
}
|
|
|
|
messagesToRemove.forEach { $0.remove(with: transaction) }
|
|
}
|
|
|
|
internal static func handleDirectMessages(
|
|
_ messages: [OpenGroupAPI.DirectMessage],
|
|
fromOutbox: Bool,
|
|
on server: String,
|
|
isBackgroundPoll: Bool,
|
|
using transaction: YapDatabaseReadWriteTransaction,
|
|
dependencies: OGMDependencies = OGMDependencies()
|
|
) {
|
|
// Don't need to do anything if we have no messages (it's a valid case)
|
|
guard !messages.isEmpty else { return }
|
|
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: server) else {
|
|
SNLog("Couldn't receive inbox message.")
|
|
return
|
|
}
|
|
|
|
// Sorting the messages by server ID before importing them fixes an issue where messages
|
|
// that quote older messages can't find those older messages
|
|
let sortedMessages: [OpenGroupAPI.DirectMessage] = messages
|
|
.sorted { lhs, rhs in lhs.id < rhs.id }
|
|
let latestMessageId: Int64 = sortedMessages[sortedMessages.count - 1].id
|
|
var mappingCache: [String: BlindedIdMapping] = [:] // Only want this cache to exist for the current loop
|
|
|
|
// Update the 'latestMessageId' value
|
|
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
|
|
guard let messageData = Data(base64Encoded: message.base64EncodedMessage) else {
|
|
SNLog("Couldn't receive inbox message.")
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
|
|
do {
|
|
let data = try envelope.buildSerializedData()
|
|
let (receivedMessage, proto) = try MessageReceiver.parse(
|
|
data,
|
|
openGroupMessageServerID: nil,
|
|
openGroupServerPublicKey: serverPublicKey,
|
|
isOutgoing: fromOutbox,
|
|
otherBlindedPublicKey: (fromOutbox ? message.recipient : message.sender),
|
|
isRetry: false,
|
|
using: transaction,
|
|
dependencies: dependencies
|
|
)
|
|
|
|
// 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 {
|
|
// Attempt to un-blind the 'message.recipient'
|
|
let mapping: BlindedIdMapping
|
|
|
|
// Minor optimisation to avoid processing the same sender multiple times in the same
|
|
// 'handleMessages' call (since the 'mapping' call is done within a transaction we
|
|
// will never have a mapping come through part-way through processing these messages)
|
|
if let result: BlindedIdMapping = mappingCache[message.recipient] {
|
|
mapping = result
|
|
}
|
|
else if let result: BlindedIdMapping = ContactUtilities.mapping(for: message.recipient, serverPublicKey: serverPublicKey, using: transaction, dependencies: dependencies) {
|
|
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: message.recipient,
|
|
serverPublicKey: ""
|
|
)
|
|
}
|
|
|
|
switch receivedMessage {
|
|
case let receivedMessage as VisibleMessage: receivedMessage.syncTarget = mapping.sessionId
|
|
case let receivedMessage as ExpirationTimerUpdate: receivedMessage.syncTarget = mapping.sessionId
|
|
default: break
|
|
}
|
|
|
|
mappingCache[message.recipient] = mapping
|
|
}
|
|
|
|
try MessageReceiver.handle(receivedMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction, dependencies: dependencies)
|
|
|
|
// If this message is from the outbox then we should add the open group details back to the
|
|
// thread just in case this is from a restore (otherwise the user won't be able to send a new
|
|
// message to the target inbox if they are still blinded)
|
|
if fromOutbox, let contactThread: TSContactThread = TSContactThread.fetch(uniqueId: TSContactThread.threadID(fromContactSessionID: message.recipient), transaction: transaction) {
|
|
contactThread.originalOpenGroupServer = server
|
|
contactThread.originalOpenGroupPublicKey = serverPublicKey
|
|
contactThread.save(with: transaction)
|
|
}
|
|
}
|
|
catch let error {
|
|
SNLog("Couldn't receive inbox message due to error: \(error).")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
/// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group
|
|
public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String, using dependencies: OGMDependencies = OGMDependencies()) -> Bool {
|
|
let modAndAdminKeys: Set<String> = (dependencies.cache.moderators[server]?[room] ?? Set())
|
|
.union(dependencies.cache.admins[server]?[room] ?? Set())
|
|
|
|
// If the publicKey is in the set then return immediately, otherwise only continue if it's the
|
|
// current user
|
|
guard !modAndAdminKeys.contains(publicKey) else { return true }
|
|
guard let sessionId: SessionId = SessionId(from: publicKey) else { return false }
|
|
|
|
// Conveniently the logic for these different cases works in order so we can fallthrough each
|
|
// case with only minor efficiency losses
|
|
switch sessionId.prefix {
|
|
case .standard:
|
|
guard publicKey == getUserHexEncodedPublicKey(using: dependencies) else { return false }
|
|
fallthrough
|
|
|
|
case .unblinded:
|
|
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return false }
|
|
guard sessionId.prefix != .unblinded || publicKey == SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString else {
|
|
return false
|
|
}
|
|
fallthrough
|
|
|
|
case .blinded:
|
|
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return false }
|
|
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: server) else {
|
|
return false
|
|
}
|
|
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
|
|
return false
|
|
}
|
|
guard sessionId.prefix != .blinded || publicKey == SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString else {
|
|
return false
|
|
}
|
|
|
|
// If we got to here that means that the 'publicKey' value matches one of the current
|
|
// users 'standard', 'unblinded' or 'blinded' keys and as such we should check if any
|
|
// of them exist in the `modsAndAminKeys` Set
|
|
let possibleKeys: Set<String> = Set([
|
|
getUserHexEncodedPublicKey(using: dependencies),
|
|
SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
|
|
SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString
|
|
])
|
|
|
|
return !modAndAdminKeys.intersection(possibleKeys).isEmpty
|
|
}
|
|
}
|
|
|
|
@discardableResult public static func getDefaultRoomsIfNeeded(using dependencies: OGMDependencies = OGMDependencies()) -> Promise<[OpenGroupAPI.Room]> {
|
|
// Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again
|
|
if let existingPromise: Promise<[OpenGroupAPI.Room]> = dependencies.cache.defaultRoomsPromise {
|
|
return existingPromise
|
|
}
|
|
|
|
let (promise, seal) = Promise<[OpenGroupAPI.Room]>.pending()
|
|
|
|
dependencies.storage.write(
|
|
with: { transaction in
|
|
dependencies.storage.setOpenGroupPublicKey(
|
|
for: OpenGroupAPI.defaultServer,
|
|
to: OpenGroupAPI.defaultServerPublicKey,
|
|
using: transaction
|
|
)
|
|
},
|
|
completion: {
|
|
let internalPromise: Promise<[OpenGroupAPI.Room]> = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
|
|
OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies)
|
|
.map { _, data in data }
|
|
}
|
|
internalPromise
|
|
.done(on: OpenGroupAPI.workQueue) { items in
|
|
items
|
|
.compactMap { room -> (UInt64, String)? in
|
|
guard let imageId: UInt64 = room.imageId else { return nil}
|
|
|
|
return (imageId, room.token)
|
|
}
|
|
.forEach { imageId, roomToken in
|
|
roomImage(imageId, for: roomToken, on: OpenGroupAPI.defaultServer, using: dependencies)
|
|
.retainUntilComplete()
|
|
}
|
|
seal.fulfill(items)
|
|
}
|
|
.retainUntilComplete()
|
|
|
|
internalPromise
|
|
.catch(on: OpenGroupAPI.workQueue) { error in
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.defaultRoomsPromise = nil
|
|
}
|
|
seal.reject(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.defaultRoomsPromise = promise
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func roomImage(
|
|
_ fileId: UInt64,
|
|
for roomToken: String,
|
|
on server: String,
|
|
using dependencies: OGMDependencies = OGMDependencies()
|
|
) -> Promise<Data> {
|
|
// Normally the image for a given group is stored with the group thread, so it's only
|
|
// fetched once. However, on the join open group screen we show images for groups the
|
|
// user * hasn't * joined yet. We don't want to re-fetch these images every time the
|
|
// user opens the app because that could slow the app down or be data-intensive. So
|
|
// instead we assume that these images don't change that often and just fetch them once
|
|
// a week. We also assume that they're all fetched at the same time as well, so that
|
|
// we only need to maintain one date in user defaults. On top of all of this we also
|
|
// don't double up on fetch requests by storing the existing request as a promise if
|
|
// there is one.
|
|
let lastOpenGroupImageUpdate: Date? = dependencies.standardUserDefaults[.lastOpenGroupImageUpdate]
|
|
let now: Date = dependencies.date
|
|
let timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
|
|
let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
|
|
|
|
if let data = dependencies.storage.getOpenGroupImage(for: roomToken, on: server), server == OpenGroupAPI.defaultServer, timeSinceLastUpdate < updateInterval {
|
|
return Promise.value(data)
|
|
}
|
|
|
|
if let promise = dependencies.cache.groupImagePromises["\(server).\(roomToken)"] {
|
|
return promise
|
|
}
|
|
|
|
let promise: Promise<Data> = OpenGroupAPI
|
|
.downloadFile(fileId, from: roomToken, on: server, using: dependencies)
|
|
.map { _, data in data }
|
|
_ = promise.done(on: OpenGroupAPI.workQueue) { imageData in
|
|
if server == OpenGroupAPI.defaultServer {
|
|
dependencies.storage.write { transaction in
|
|
dependencies.storage.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction)
|
|
}
|
|
dependencies.standardUserDefaults[.lastOpenGroupImageUpdate] = now
|
|
}
|
|
}
|
|
dependencies.mutableCache.mutate { cache in
|
|
cache.groupImagePromises["\(server).\(roomToken)"] = promise
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func parseOpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? {
|
|
guard let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }), let query = url.query else { return nil }
|
|
// Inputs that should work:
|
|
// https://sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// https://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// http://sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// http://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c (does NOT go to HTTPS)
|
|
// sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c (does NOT go to HTTPS)
|
|
// https://143.198.213.225:443/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// https://143.198.213.225:443/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// 143.198.213.255:80/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
// 143.198.213.255:80/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
|
|
let useTLS = (url.scheme == "https")
|
|
|
|
// If there is no scheme then the host is included in the path (so handle that case)
|
|
let hostFreePath = (url.host != nil ? url.path : url.path.substring(from: host.count))
|
|
let updatedPath = (hostFreePath.starts(with: "/r/") ? hostFreePath.substring(from: 2) : hostFreePath)
|
|
let room = String(updatedPath.dropFirst()) // Drop the leading slash
|
|
let queryParts = query.split(separator: "=")
|
|
guard !room.isEmpty && !room.contains("/"), queryParts.count == 2, queryParts[0] == "public_key" else { return nil }
|
|
let publicKey = String(queryParts[1])
|
|
guard publicKey.count == 64 && Hex.isValid(publicKey) else { return nil }
|
|
var server = (useTLS ? "https://" : "http://") + host
|
|
if let port = url.port { server += ":\(port)" }
|
|
return (room: room, server: server, publicKey: publicKey)
|
|
}
|
|
}
|
|
|
|
// MARK: - Objective C Methods
|
|
|
|
extension OpenGroupManager {
|
|
@objc(startPolling)
|
|
public func objc_startPolling() {
|
|
startPolling()
|
|
}
|
|
|
|
@objc(stopPolling)
|
|
public func objc_stopPolling() {
|
|
stopPolling()
|
|
}
|
|
|
|
@objc(getDefaultRoomsIfNeeded)
|
|
public static func objc_getDefaultRoomsIfNeeded() {
|
|
getDefaultRoomsIfNeeded()
|
|
}
|
|
|
|
@objc(isUserModeratorOrAdmin:forRoom:onServer:)
|
|
public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String) -> Bool {
|
|
return isUserModeratorOrAdmin(publicKey, for: room, on: server, using: OGMDependencies())
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - OGMDependencies
|
|
|
|
extension OpenGroupManager {
|
|
public class OGMDependencies: Dependencies {
|
|
internal var _mutableCache: Atomic<OGMCacheType>?
|
|
public var mutableCache: Atomic<OGMCacheType> {
|
|
get { Dependencies.getValueSettingIfNull(&_mutableCache) { OpenGroupManager.shared.mutableCache } }
|
|
set { _mutableCache = newValue }
|
|
}
|
|
|
|
public var cache: OGMCacheType { return mutableCache.wrappedValue }
|
|
|
|
public init(
|
|
cache: Atomic<OGMCacheType>? = nil,
|
|
onionApi: OnionRequestAPIType.Type? = nil,
|
|
identityManager: IdentityManagerProtocol? = nil,
|
|
storage: SessionMessagingKitStorageProtocol? = nil,
|
|
sodium: SodiumType? = nil,
|
|
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
|
|
sign: SignType? = nil,
|
|
genericHash: GenericHashType? = nil,
|
|
ed25519: Ed25519Type? = nil,
|
|
nonceGenerator16: NonceGenerator16ByteType? = nil,
|
|
nonceGenerator24: NonceGenerator24ByteType? = nil,
|
|
standardUserDefaults: UserDefaultsType? = nil,
|
|
date: Date? = nil
|
|
) {
|
|
_mutableCache = cache
|
|
|
|
super.init(
|
|
onionApi: onionApi,
|
|
identityManager: identityManager,
|
|
storage: storage,
|
|
sodium: sodium,
|
|
aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf,
|
|
sign: sign,
|
|
genericHash: genericHash,
|
|
ed25519: ed25519,
|
|
nonceGenerator16: nonceGenerator16,
|
|
nonceGenerator24: nonceGenerator24,
|
|
standardUserDefaults: standardUserDefaults,
|
|
date: date
|
|
)
|
|
}
|
|
}
|
|
}
|