NCRoomsManagerExtensions.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. //
  2. // SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. @objc extension NCRoomsManager {
  7. public static let statusCodeFailedToJoinExternal = 997
  8. public static let statusCodeShouldIgnoreAttemptButJoinedSuccessfully = 998
  9. public static let statusCodeIgnoreJoinAttempt = 999
  10. // MARK: - Join/Leave room
  11. public func joinRoom(_ token: String, forCall call: Bool) {
  12. NCUtils.log("Joining room \(token) for call \(call)")
  13. // Clean up joining room flag and attempts
  14. self.joiningRoomToken = nil
  15. self.joiningSessionId = nil
  16. self.joiningAttempts = 0
  17. self.joinRoomTask?.cancel()
  18. // Check if we try to join a room, we're still trying to leave
  19. if self.isLeavingRoom(withToken: token) {
  20. self.leaveRoomTask?.cancel()
  21. self.leaveRoomTask = nil
  22. self.leavingRoomToken = nil
  23. }
  24. self.joinRoomHelper(token, forCall: call)
  25. }
  26. private func joinRoomHelper(_ token: String, forCall call: Bool) {
  27. var userInfo: [AnyHashable: Any] = [:]
  28. userInfo["token"] = token
  29. if let roomController = self.activeRooms[token] as? NCRoomController {
  30. NCUtils.log("JoinRoomHelper: Found active room controller")
  31. if call {
  32. roomController.inCall = true
  33. } else {
  34. roomController.inChat = true
  35. }
  36. userInfo["roomController"] = roomController
  37. NotificationCenter.default.post(name: .NCRoomsManagerDidJoinRoom, object: self, userInfo: userInfo)
  38. return
  39. }
  40. self.joiningRoomToken = token
  41. self.joinRoomHelper(token, forCall: call) { sessionId, room, error, statusCode, statusReason in
  42. if statusCode == NCRoomsManager.statusCodeIgnoreJoinAttempt {
  43. // Not joining the room any more. Ignore response
  44. return
  45. } else if statusCode == NCRoomsManager.statusCodeShouldIgnoreAttemptButJoinedSuccessfully {
  46. // We joined the Nextcloud server successfully, but locally we are not trying to join that room anymore.
  47. // We need to make sure that we leave the room on the server again to not leave an active session.
  48. // Do a direct API call here, as the join method will check for an active NCRoomController, which we don't have
  49. if !self.isLeavingRoom(withToken: token) {
  50. self.leavingRoomToken = token
  51. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  52. self.leaveRoomTask = NCAPIController.sharedInstance().exitRoom(token, for: activeAccount, withCompletionBlock: { _ in
  53. self.leaveRoomTask = nil
  54. self.leavingRoomToken = nil
  55. })
  56. }
  57. return
  58. }
  59. if error == nil {
  60. let controller = NCRoomController()
  61. controller.userSessionId = sessionId
  62. controller.inChat = !call
  63. controller.inCall = call
  64. userInfo["roomController"] = controller
  65. if let room {
  66. userInfo["room"] = room
  67. }
  68. // Set room as active
  69. self.activeRooms[token] = controller
  70. } else {
  71. if self.joiningAttempts < 3 && statusCode != 403 {
  72. NCUtils.log("Error joining room, retrying. \(self.joiningAttempts)")
  73. self.joiningAttempts += 1
  74. self.joinRoomHelper(token, forCall: call)
  75. return
  76. }
  77. // Add error to user info
  78. userInfo["error"] = error
  79. userInfo["statusCode"] = statusCode
  80. userInfo["errorReason"] = self.getJoinRoomErrorReason(statusCode, andReason: statusReason)
  81. if statusCode == 403, statusReason == "ban" {
  82. userInfo["isBanned"] = true
  83. }
  84. NCUtils.log("Could not join room. Status code: \(statusCode). Error: \(error?.localizedDescription ?? "")")
  85. }
  86. self.joiningRoomToken = nil
  87. self.joiningSessionId = nil
  88. NotificationCenter.default.post(name: .NCRoomsManagerDidJoinRoom, object: self, userInfo: userInfo)
  89. }
  90. }
  91. private func isJoiningRoom(withToken token: String) -> Bool {
  92. guard let joiningRoomToken = self.joiningRoomToken else { return false }
  93. return joiningRoomToken == token
  94. }
  95. private func isLeavingRoom(withToken token: String) -> Bool {
  96. guard let leavingRoomToken = self.leavingRoomToken else { return false }
  97. return leavingRoomToken == token
  98. }
  99. private func isJoiningRoom(withSessionId sessionId: String) -> Bool {
  100. guard let joiningSessionId = self.joiningSessionId else { return false }
  101. return joiningSessionId == sessionId
  102. }
  103. private func joinRoomHelper(_ token: String, forCall call: Bool, completionBlock: @escaping (_ sessionId: String?, _ room: NCRoom?, _ error: Error?, _ statusCode: Int, _ statusReason: String?) -> Void) {
  104. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  105. self.joinRoomTask = NCAPIController.sharedInstance().joinRoom(token, for: activeAccount, withCompletionBlock: { sessionId, room, error, statusCode, statusReason in
  106. if !self.isJoiningRoom(withToken: token) {
  107. // Treat a cancelled request as success, as we can't determine if the request was processed on the server or not
  108. if let error = error as? NSError, error.code != NSURLErrorCancelled {
  109. NCUtils.log("Not joining the room any more. Ignore attempt as the join request failed anyway.")
  110. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeIgnoreJoinAttempt, nil)
  111. } else {
  112. NCUtils.log("Not joining the room any more, but our join request was successful.")
  113. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeShouldIgnoreAttemptButJoinedSuccessfully, nil)
  114. }
  115. return
  116. }
  117. // Failed to join room in NC
  118. if let error {
  119. completionBlock(nil, nil, error, statusCode, statusReason)
  120. return
  121. }
  122. NCUtils.log("Joined room \(token) in NC successfully")
  123. // Remember the latest sessionId we're using to join a room, to be able to check when joining the external signaling server
  124. self.joiningSessionId = sessionId
  125. self.getExternalSignalingHelper(for: activeAccount, forRoom: token) { extSignalingController, signalingSettings, error in
  126. guard error == nil else {
  127. // There was an error to ensure we have the correct signaling settings for joining a federated conversation
  128. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeFailedToJoinExternal, nil)
  129. return
  130. }
  131. guard let extSignalingController else {
  132. // Joined room in NC successfully and no external signaling server configured.
  133. completionBlock(sessionId, room, nil, 0, nil)
  134. return
  135. }
  136. NCUtils.log("Trying to join room \(token) in external signaling server...")
  137. let federation = signalingSettings?.getFederationJoinDictionary()
  138. extSignalingController.joinRoom(token, withSessionId: sessionId, withFederation: federation) { error in
  139. // If the sessionId is not the same anymore we tried to join with, we either already left again before
  140. // joining the external signaling server succeeded, or we already have another join in process
  141. if !self.isJoiningRoom(withToken: token) {
  142. NCUtils.log("Not joining the room any more. Ignore external signaling completion block, but we joined the Nextcloud instance before.")
  143. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeShouldIgnoreAttemptButJoinedSuccessfully, nil)
  144. return
  145. }
  146. if !self.isJoiningRoom(withSessionId: sessionId ?? "") {
  147. NCUtils.log("Joining the same room with a different sessionId. Ignore external signaling completion block.")
  148. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeIgnoreJoinAttempt, nil)
  149. return
  150. }
  151. if error == nil {
  152. NCUtils.log("Joined room \(token) in external signaling server successfully.")
  153. completionBlock(sessionId, room, nil, 0, nil)
  154. } else {
  155. NCUtils.log("Failed joining room \(token) in external signaling server.")
  156. completionBlock(nil, nil, error, statusCode, statusReason)
  157. }
  158. }
  159. }
  160. })
  161. }
  162. private func getExternalSignalingHelper(for account: TalkAccount, forRoom token: String, withCompletion completion: @escaping (NCExternalSignalingController?, SignalingSettings?, Error?) -> Void) {
  163. let room = NCDatabaseManager.sharedInstance().room(withToken: token, forAccountId: account.accountId)
  164. guard room?.supportsFederatedCalling ?? false else {
  165. // No federated room -> just ensure that we have a signaling configuration and a potential external signaling controller
  166. NCSettingsController.sharedInstance().ensureSignalingConfiguration(forAccountId: account.accountId, with: nil) { extSignalingController in
  167. completion(extSignalingController, nil, nil)
  168. }
  169. return
  170. }
  171. // This is a federated conversation (with federated calling supported), so we require signaling settings for joining
  172. // the external signaling controller
  173. NCAPIController.sharedInstance().getSignalingSettings(for: account, forRoom: token) { signalingSettings, _ in
  174. guard let signalingSettings else {
  175. // We need to fail if we are unable to get signaling settings for a federation conversation
  176. completion(nil, nil, NSError(domain: NSCocoaErrorDomain, code: 0))
  177. return
  178. }
  179. NCSettingsController.sharedInstance().ensureSignalingConfiguration(forAccountId: account.accountId, with: signalingSettings) { extSignalingController in
  180. completion(extSignalingController, signalingSettings, nil)
  181. }
  182. }
  183. }
  184. public func rejoinRoomForCall(_ token: String, completionBlock: @escaping (_ sessionId: String?, _ room: NCRoom?, _ error: Error?, _ statusCode: Int, _ statusReason: String?) -> Void) {
  185. NCUtils.log("Rejoining room \(token)")
  186. guard let roomController = self.activeRooms[token] as? NCRoomController else { return }
  187. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  188. self.joiningRoomToken = token
  189. self.joinRoomTask = NCAPIController.sharedInstance().joinRoom(token, for: activeAccount, withCompletionBlock: { sessionId, room, error, statusCode, statusReason in
  190. if error == nil {
  191. roomController.userSessionId = sessionId
  192. roomController.inCall = true
  193. self.getExternalSignalingHelper(for: activeAccount, forRoom: token) { extSignalingController, signalingSettings, error in
  194. guard error == nil else {
  195. // There was an error to ensure we have the correct signaling settings for joining a federated conversation
  196. completionBlock(nil, nil, nil, NCRoomsManager.statusCodeFailedToJoinExternal, nil)
  197. return
  198. }
  199. guard let extSignalingController else {
  200. // Joined room in NC successfully and no external signaling server configured.
  201. completionBlock(sessionId, room, nil, 0, nil)
  202. return
  203. }
  204. let federation = signalingSettings?.getFederationJoinDictionary()
  205. extSignalingController.joinRoom(token, withSessionId: sessionId, withFederation: federation) { error in
  206. if error == nil {
  207. NCUtils.log("Re-Joined room \(token) in external signaling server successfully.")
  208. completionBlock(sessionId, room, nil, 0, nil)
  209. } else {
  210. NCUtils.log("Failed re-joining room \(token) in external signaling server.")
  211. completionBlock(nil, nil, error, statusCode, statusReason)
  212. }
  213. }
  214. }
  215. } else {
  216. NCUtils.log("Could not re-join room \(token). Status code: \(statusCode). Error: \(error?.localizedDescription ?? "Unknown")")
  217. completionBlock(nil, nil, error, statusCode, statusReason)
  218. }
  219. self.joiningRoomToken = nil
  220. self.joiningSessionId = nil
  221. })
  222. }
  223. public func leaveRoom(_ token: String) {
  224. // Check if leaving the room we are joining
  225. if self.isJoiningRoom(withToken: token) {
  226. NCUtils.log("Leaving room \(token), but still joining -> cancel")
  227. self.joiningRoomToken = nil
  228. self.joiningSessionId = nil
  229. self.joinRoomTask?.cancel()
  230. }
  231. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  232. // Remove room controller and exit room
  233. if let roomController = self.activeRooms[token] as? NCRoomController,
  234. !roomController.inCall, !roomController.inChat {
  235. self.activeRooms.removeObject(forKey: token)
  236. self.leavingRoomToken = token
  237. self.leaveRoomTask = NCAPIController.sharedInstance().exitRoom(token, for: activeAccount, withCompletionBlock: { error in
  238. var userInfo = [:]
  239. userInfo["token"] = token
  240. self.leaveRoomTask = nil
  241. self.leavingRoomToken = nil
  242. if let error {
  243. userInfo["error"] = error
  244. print("Could not exit room. Error: \(error.localizedDescription)")
  245. } else {
  246. if let extSignalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: activeAccount.accountId) {
  247. extSignalingController.leaveRoom(token)
  248. }
  249. self.checkForPendingToStartCalls()
  250. }
  251. NotificationCenter.default.post(name: .NCRoomsManagerDidLeaveRoom, object: self, userInfo: userInfo)
  252. })
  253. } else {
  254. self.checkForPendingToStartCalls()
  255. }
  256. }
  257. private func getJoinRoomErrorReason(_ statusCode: Int, andReason statusReason: String?) -> String {
  258. var errorReason = ""
  259. switch statusCode {
  260. case 0:
  261. errorReason = NSLocalizedString("No response from server", comment: "")
  262. case 403:
  263. if statusReason == "ban" {
  264. errorReason = NSLocalizedString("No permission to join this conversation", comment: "")
  265. } else {
  266. errorReason = NSLocalizedString("The password is wrong", comment: "")
  267. }
  268. case 404:
  269. errorReason = NSLocalizedString("Conversation not found", comment: "")
  270. case 409:
  271. // Currently not triggered, needs to be enabled in API with sending force=false
  272. errorReason = NSLocalizedString("Duplicate session", comment: "")
  273. case 422:
  274. errorReason = NSLocalizedString("Remote server is unreachable", comment: "")
  275. case 503:
  276. errorReason = NSLocalizedString("Server is currently in maintenance mode", comment: "")
  277. default:
  278. errorReason = NSLocalizedString("Unknown error occurred", comment: "")
  279. }
  280. return errorReason
  281. }
  282. // MARK: - Room
  283. public func roomsForAccountId(_ accountId: String, withRealm realm: RLMRealm?) -> [NCRoom] {
  284. let query = NSPredicate(format: "accountId = %@", accountId)
  285. var managedRooms: RLMResults<AnyObject>
  286. if let realm {
  287. managedRooms = NCRoom.objects(in: realm, with: query)
  288. } else {
  289. managedRooms = NCRoom.objects(with: query)
  290. }
  291. // Create an unmanaged copy of the rooms
  292. var unmanagedRooms: [NCRoom] = []
  293. for case let managedRoom as NCRoom in managedRooms {
  294. if managedRoom.isBreakoutRoom, managedRoom.lobbyState == .moderatorsOnly {
  295. continue
  296. }
  297. unmanagedRooms.append(NCRoom(value: managedRoom))
  298. }
  299. // Sort by favorites first, then by lastActivity
  300. unmanagedRooms.sort { first, second in
  301. (first.isFavorite ? 1 : 0, first.lastActivity) > (second.isFavorite ? 1 : 0, second.lastActivity)
  302. }
  303. return unmanagedRooms
  304. }
  305. public func resendOfflineMessagesWithCompletionBlock(_ block: SendOfflineMessagesCompletionBlock?) {
  306. // Try to send offline messages for all rooms
  307. self.resendOfflineMessages(forToken: nil, withCompletionBlock: block)
  308. }
  309. public func resendOfflineMessages(forToken token: String?, withCompletionBlock completionBlock: SendOfflineMessagesCompletionBlock?) {
  310. var query: NSPredicate
  311. if let token {
  312. query = NSPredicate(format: "isOfflineMessage = true AND token = %@", token)
  313. } else {
  314. query = NSPredicate(format: "isOfflineMessage = true")
  315. }
  316. let realm = RLMRealm.default()
  317. let managedTemporaryMessages = NCChatMessage.objects(with: query)
  318. let twelveHoursAgoTimestamp = Int(Date().timeIntervalSince1970 - (60 * 60 * 12))
  319. for case let offlineMessage as NCChatMessage in managedTemporaryMessages {
  320. // If we were unable to send a message after 12 hours, mark as failed
  321. if offlineMessage.timestamp < twelveHoursAgoTimestamp {
  322. try? realm.transaction {
  323. offlineMessage.isOfflineMessage = false
  324. offlineMessage.sendingFailed = true
  325. }
  326. var userInfo: [AnyHashable: Any] = [:]
  327. userInfo["message"] = offlineMessage
  328. userInfo["isOfflineMessage"] = false
  329. if offlineMessage.referenceId != nil {
  330. userInfo["referenceId"] = offlineMessage.referenceId
  331. }
  332. // Inform the chatViewController about this change
  333. NotificationCenter.default.post(name: .NCChatControllerDidSendChatMessage, object: self, userInfo: userInfo)
  334. } else {
  335. if let room = NCDatabaseManager.sharedInstance().room(withToken: offlineMessage.token, forAccountId: offlineMessage.accountId),
  336. let chatController = NCChatController(for: room) {
  337. chatController.send(offlineMessage)
  338. }
  339. }
  340. }
  341. completionBlock?()
  342. }
  343. // MARK: - Federation invitations
  344. public func checkUpdateNeededForPendingFederationInvitations() {
  345. guard NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityFederationV1) else { return }
  346. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  347. let tenMinutesAgo = Int(Date().timeIntervalSince1970 - (10 * 60))
  348. if activeAccount.lastPendingFederationInvitationFetch == 0 || activeAccount.lastPendingFederationInvitationFetch < tenMinutesAgo {
  349. self.updatePendingFederationInvitations()
  350. }
  351. }
  352. public func updatePendingFederationInvitations() {
  353. guard NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityFederationV1) else { return }
  354. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  355. NCAPIController.sharedInstance().getFederationInvitations(for: activeAccount.accountId) { invitations in
  356. guard let invitations else { return }
  357. let pendingInvitations = invitations.filter { $0.invitationState != .accepted }
  358. if activeAccount.pendingFederationInvitations != pendingInvitations.count {
  359. NCDatabaseManager.sharedInstance().setPendingFederationInvitationForAccountId(activeAccount.accountId, with: pendingInvitations.count)
  360. }
  361. }
  362. }
  363. }