// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import Combine import GRDB import SessionUtilitiesKit import SessionSnodeKit public enum GroupInviteMemberJob: JobExecutor { public static var maxFailureCount: Int = 1 public static var requiresThreadId: Bool = true public static var requiresInteractionId: Bool = false public static func run( _ job: Job, queue: DispatchQueue, success: @escaping (Job, Bool, Dependencies) -> (), failure: @escaping (Job, Error?, Bool, Dependencies) -> (), deferred: @escaping (Job, Dependencies) -> (), using dependencies: Dependencies ) { guard let threadId: String = job.threadId, let detailsData: Data = job.details, let currentInfo: (groupName: String, adminProfile: Profile) = dependencies[singleton: .storage].read({ db in let maybeGroupName: String? = try ClosedGroup .filter(id: threadId) .select(.name) .asRequest(of: String.self) .fetchOne(db) guard let groupName: String = maybeGroupName else { throw StorageError.objectNotFound } return (groupName, Profile.fetchOrCreateCurrentUser(db, using: dependencies)) }), let details: Details = try? JSONDecoder(using: dependencies).decode(Details.self, from: detailsData) else { SNLog("[GroupInviteMemberJob] Failing due to missing details") failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs(using: dependencies) /// Perform the actual message sending dependencies[singleton: .storage] .readPublisher { db -> HTTP.PreparedRequest in try MessageSender.preparedSend( db, message: try GroupUpdateInviteMessage( inviteeSessionIdHexString: details.memberSessionIdHexString, groupSessionId: SessionId(.group, hex: threadId), groupName: currentInfo.groupName, memberAuthData: details.memberAuthData, profile: VisibleMessage.VMProfile.init( profile: currentInfo.adminProfile, blocksCommunityMessageRequests: nil ), sentTimestamp: UInt64(sentTimestamp), authMethod: try Authentication.with( db, sessionIdHexString: threadId, using: dependencies ), using: dependencies ), to: .contact(publicKey: details.memberSessionIdHexString), namespace: .default, interactionId: nil, fileIds: [], isSyncMessage: false, using: dependencies ) } .flatMap { $0.send(using: dependencies) } .subscribe(on: queue, using: dependencies) .receive(on: queue, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in switch result { case .finished: success(job, false, dependencies) case .failure(let error): SNLog("[GroupInviteMemberJob] Couldn't send message due to error: \(error).") // Update the invite status of the group member (only if the role is 'standard' and // the role status isn't already 'accepted') dependencies[singleton: .storage].write(using: dependencies) { db in try GroupMember .filter( GroupMember.Columns.groupId == threadId && GroupMember.Columns.profileId == details.memberSessionIdHexString && GroupMember.Columns.role == GroupMember.Role.standard && GroupMember.Columns.roleStatus != GroupMember.RoleStatus.accepted ) .updateAllAndConfig( db, GroupMember.Columns.roleStatus.set(to: GroupMember.RoleStatus.failed), using: dependencies ) } // Register the failure switch error { case let senderError as MessageSenderError where !senderError.isRetryable: failure(job, error, true, dependencies) case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 429: // Rate limited failure(job, error, true, dependencies) case SnodeAPIError.clockOutOfSync: SNLog("[GroupInviteMemberJob] Permanently Failing to send due to clock out of sync issue.") failure(job, error, true, dependencies) default: failure(job, error, false, dependencies) } } } ) // TODO: Need to batch errors together and send a toast indicating invitation failures } } // MARK: - GroupInviteMemberJob.Details extension GroupInviteMemberJob { public struct Details: Codable { public let memberSessionIdHexString: String public let memberAuthData: Data public init( memberSessionIdHexString: String, authInfo: Authentication.Info ) throws { self.memberSessionIdHexString = memberSessionIdHexString switch authInfo { case .groupMember(_, let authData): self.memberAuthData = authData default: throw MessageSenderError.invalidMessage } } } }