diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a1c8cee77..ad76364e9 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -882,6 +882,38 @@ func apiCancelFile(fileId: Int64) async -> AChatItem? { } } +func startRemoteCtrl() async throws { + try await sendCommandOkResp(.startRemoteCtrl) +} + +func registerRemoteCtrl(_ remoteCtrlOOB: RemoteCtrlOOB) async throws -> Int64 { + let r = await chatSendCmd(.registerRemoteCtrl(remoteCtrlOOB: remoteCtrlOOB)) + if case let .remoteCtrlRegistered(rcId) = r { return rcId } + throw r +} + +func listRemoteCtrls() async throws -> [RemoteCtrlInfo] { + let r = await chatSendCmd(.listRemoteCtrls) + if case let .remoteCtrlList(rcInfo) = r { return rcInfo } + throw r +} + +func acceptRemoteCtrl(_ rcId: Int64) async throws { + try await sendCommandOkResp(.acceptRemoteCtrl(remoteCtrlId: rcId)) +} + +func rejectRemoteCtrl(_ rcId: Int64) async throws { + try await sendCommandOkResp(.rejectRemoteCtrl(remoteCtrlId: rcId)) +} + +func stopRemoteCtrl() async throws { + try await sendCommandOkResp(.stopRemoteCtrl) +} + +func deleteRemoteCtrl(_ rcId: Int64) async throws { + try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) +} + func networkErrorAlert(_ r: ChatResponse) -> Alert? { switch r { case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 9eb9b9084..4b79800e1 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -117,6 +117,13 @@ public enum ChatCommand { case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?) case setFileToReceive(fileId: Int64, encrypted: Bool) case cancelFile(fileId: Int64) + case startRemoteCtrl + case registerRemoteCtrl(remoteCtrlOOB: RemoteCtrlOOB) + case listRemoteCtrls + case acceptRemoteCtrl(remoteCtrlId: Int64) + case rejectRemoteCtrl(remoteCtrlId: Int64) + case stopRemoteCtrl + case deleteRemoteCtrl(remoteCtrlId: Int64) case showVersion case string(String) @@ -255,6 +262,13 @@ public enum ChatCommand { return s case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))" case let .cancelFile(fileId): return "/fcancel \(fileId)" + case .startRemoteCtrl: return "/start remote ctrl" + case let .registerRemoteCtrl(oob): return "/register remote ctrl \(oob.caFingerprint)" + case let .acceptRemoteCtrl(rcId): return "/accept remote ctrl \(rcId)" + case let .rejectRemoteCtrl(rcId): return "/reject remote ctrl \(rcId)" + case .listRemoteCtrls: return "/list remote ctrls" + case .stopRemoteCtrl: return "/stop remote ctrl" + case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)" case .showVersion: return "/version" case let .string(str): return str } @@ -367,6 +381,13 @@ public enum ChatCommand { case .receiveFile: return "receiveFile" case .setFileToReceive: return "setFileToReceive" case .cancelFile: return "cancelFile" + case .startRemoteCtrl: return "startRemoteCtrl" + case .registerRemoteCtrl: return "registerRemoteCtrl" + case .listRemoteCtrls: return "listRemoteCtrls" + case .acceptRemoteCtrl: return "acceptRemoteCtrl" + case .rejectRemoteCtrl: return "rejectRemoteCtrl" + case .stopRemoteCtrl: return "stopRemoteCtrl" + case .deleteRemoteCtrl: return "deleteRemoteCtrl" case .showVersion: return "showVersion" case .string: return "console command" } @@ -563,6 +584,13 @@ public enum ChatResponse: Decodable, Error { case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case newContactConnection(user: UserRef, connection: PendingContactConnection) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) + case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) + case remoteCtrlRegistered(remoteCtrlId: Int64) + case remoteCtrlAnnounce(fingerprint: String) + case remoteCtrlFound(remoteCtrl: RemoteCtrl) + case remoteCtrlConnecting(remoteCtrlId: Int64, displayName: String) + case remoteCtrlConnected(remoteCtrlId: Int64, displayName: String) + case remoteCtrlStopped case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) case chatCmdError(user_: UserRef?, chatError: ChatError) @@ -699,6 +727,13 @@ public enum ChatResponse: Decodable, Error { case .ntfMessages: return "ntfMessages" case .newContactConnection: return "newContactConnection" case .contactConnectionDeleted: return "contactConnectionDeleted" + case .remoteCtrlList: return "remoteCtrlList" + case .remoteCtrlRegistered: return "remoteCtrlRegistered" + case .remoteCtrlAnnounce: return "remoteCtrlAnnounce" + case .remoteCtrlFound: return "remoteCtrlFound" + case .remoteCtrlConnecting: return "remoteCtrlConnecting" + case .remoteCtrlConnected: return "remoteCtrlConnected" + case .remoteCtrlStopped: return "remoteCtrlStopped" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" @@ -838,6 +873,13 @@ public enum ChatResponse: Decodable, Error { case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") case let .newContactConnection(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) + case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) + case let .remoteCtrlRegistered(rcId): return "remote ctrl ID: \(rcId)" + case let .remoteCtrlAnnounce(fingerprint): return "fingerprint: \(fingerprint)" + case let .remoteCtrlFound(remoteCtrl): return "remote ctrl: \(String(describing: remoteCtrl))" + case let .remoteCtrlConnecting(rcId, displayName): return "remote ctrl ID: \(rcId)\nhost displayName: \(displayName)" + case let .remoteCtrlConnected(rcId, displayName): return "remote ctrl ID: \(rcId)\nhost displayName: \(displayName)" + case .remoteCtrlStopped: return noDetails case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) @@ -1461,6 +1503,23 @@ public enum NotificationPreviewMode: String, SelectableItem { public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden] } +public struct RemoteCtrlOOB { + public var caFingerprint: String +} + +public struct RemoteCtrlInfo: Decodable { + public var remoteCtrlId: Int64 + public var displayName: String + public var sessionActive: Bool +} + +public struct RemoteCtrl: Decodable { + var remoteCtrlId: Int64 + var displayName: String + var fingerprint: String + var accepted: Bool? +} + public struct CoreVersionInfo: Decodable { public var version: String public var simplexmqVersion: String @@ -1488,6 +1547,7 @@ public enum ChatError: Decodable { case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) case errorDatabase(databaseError: DatabaseError) + case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError) case invalidJSON(json: String) } @@ -1739,3 +1799,15 @@ public enum ArchiveError: Decodable { case `import`(chatError: ChatError) case importFile(file: String, chatError: ChatError) } + +public enum RemoteCtrlError: Decodable { + case missing(remoteCtrlId: Int64) + case inactive + case busy + case timeout + case disconnected(remoteCtrlId: Int64, reason: String) + case connectionLost(remoteCtrlId: Int64, reason: String) + case certificateExpired(remoteCtrlId: Int64) + case certificateUntrusted(remoteCtrlId: Int64) + case badFingerprint +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9d726c620..9ab6060fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1938,8 +1938,8 @@ sealed class CC { class StartRemoteHost(val remoteHostId: Long): CC() class StopRemoteHost(val remoteHostId: Long): CC() class DeleteRemoteHost(val remoteHostId: Long): CC() - class RegisterRemoteCtrl(val remoteCtrlOOB: RemoteCtrlOOB): CC() class StartRemoteCtrl(): CC() + class RegisterRemoteCtrl(val remoteCtrlOOB: RemoteCtrlOOB): CC() class ListRemoteCtrls(): CC() class AcceptRemoteCtrl(val remoteCtrlId: Long): CC() class RejectRemoteCtrl(val remoteCtrlId: Long): CC() @@ -2167,8 +2167,8 @@ sealed class CC { is StartRemoteHost -> "startRemoteHost" is StopRemoteHost -> "stopRemoteHost" is DeleteRemoteHost -> "deleteRemoteHost" - is RegisterRemoteCtrl -> "registerRemoteCtrl" is StartRemoteCtrl -> "startRemoteCtrl" + is RegisterRemoteCtrl -> "registerRemoteCtrl" is ListRemoteCtrls -> "listRemoteCtrls" is AcceptRemoteCtrl -> "acceptRemoteCtrl" is RejectRemoteCtrl -> "rejectRemoteCtrl" @@ -3483,30 +3483,24 @@ sealed class CR { @Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("newContactConnection") class NewContactConnection(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR() + // remote events (desktop) + @Serializable @SerialName("remoteHostCreated") class RemoteHostCreated(val remoteHostId: Long, val oobData: RemoteCtrlOOB): CR() + @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() + @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHostId: Long): CR() + @Serializable @SerialName("remoteHostStopped") class RemoteHostStopped(val remoteHostId: Long): CR() + // remote events (mobile) + @Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List): CR() + @Serializable @SerialName("remoteCtrlRegistered") class RemoteCtrlRegistered(val remoteCtrlId: Long): CR() + @Serializable @SerialName("remoteCtrlAnnounce") class RemoteCtrlAnnounce(val fingerprint: String): CR() + @Serializable @SerialName("remoteCtrlFound") class RemoteCtrlFound(val remoteCtrl: RemoteCtrl): CR() + @Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrlId: Long, val displayName: String): CR() + @Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrlId: Long, val displayName: String): CR() + @Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(): CR() @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List, val agentMigrations: List): CR() @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() - // remote events (desktop) - @Serializable @SerialName("remoteHostCreated") class RemoteHostCreated(val remoteHostId: Long, val oobData: RemoteCtrlOOB): CR() - @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() - @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHostId: Long): CR() - @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHostId: Long): CR() - @Serializable @SerialName("remoteHostStopped") class RemoteHostStopped(val remoteHostId: Long): CR() - @Serializable @SerialName("remoteHostDeleted") class RemoteHostDeleted(val remoteHostId: Long): CR() - // remote events (mobile) - @Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List): CR() - @Serializable @SerialName("remoteCtrlRegistered") class RemoteCtrlRegistered(val remoteCtrlId: Long): CR() - @Serializable @SerialName("remoteCtrlStarted") class RemoteCtrlStarted(): CR() - @Serializable @SerialName("remoteCtrlAnnounce") class RemoteCtrlAnnounce(val fingerprint: String): CR() - @Serializable @SerialName("remoteCtrlFound") class RemoteCtrlFound(val remoteCtrl: RemoteCtrl): CR() - @Serializable @SerialName("remoteCtrlAccepted") class RemoteCtrlAccepted(val remoteCtrlId: Long): CR() - @Serializable @SerialName("remoteCtrlRejected") class RemoteCtrlRejected(val remoteCtrlId: Long): CR() - @Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrlId: Long, val displayName: String): CR() - @Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrlId: Long, val displayName: String): CR() - @Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(): CR() - @Serializable @SerialName("remoteCtrlDeleted") class RemoteCtrlDeleted(val remoteCtrlId: Long): CR() // general @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -3632,28 +3626,22 @@ sealed class CR { is CallEnded -> "callEnded" is NewContactConnection -> "newContactConnection" is ContactConnectionDeleted -> "contactConnectionDeleted" + is RemoteHostCreated -> "remoteHostCreated" + is RemoteHostList -> "remoteHostList" + is RemoteHostConnected -> "remoteHostConnected" + is RemoteHostStopped -> "remoteHostStopped" + is RemoteCtrlList -> "remoteCtrlList" + is RemoteCtrlRegistered -> "remoteCtrlRegistered" + is RemoteCtrlAnnounce -> "remoteCtrlAnnounce" + is RemoteCtrlFound -> "remoteCtrlFound" + is RemoteCtrlConnecting -> "remoteCtrlConnecting" + is RemoteCtrlConnected -> "remoteCtrlConnected" + is RemoteCtrlStopped -> "remoteCtrlStopped" is VersionInfo -> "versionInfo" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" is ArchiveImported -> "archiveImported" - is RemoteHostCreated -> "remoteHostCreated" - is RemoteHostList -> "remoteHostList" - is RemoteHostStarted -> "remoteHostStarted" - is RemoteHostConnected -> "remoteHostConnected" - is RemoteHostStopped -> "remoteHostStopped" - is RemoteHostDeleted -> "remoteHostDeleted" - is RemoteCtrlList -> "remoteCtrlList" - is RemoteCtrlRegistered -> "remoteCtrlRegistered" - is RemoteCtrlStarted -> "remoteCtrlStarted" - is RemoteCtrlAnnounce -> "remoteCtrlAnnounce" - is RemoteCtrlFound -> "remoteCtrlFound" - is RemoteCtrlAccepted -> "remoteCtrlAccepted" - is RemoteCtrlRejected -> "remoteCtrlRejected" - is RemoteCtrlConnecting -> "remoteCtrlConnecting" - is RemoteCtrlConnected -> "remoteCtrlConnected" - is RemoteCtrlStopped -> "remoteCtrlStopped" - is RemoteCtrlDeleted -> "remoteCtrlDeleted" is Response -> "* $type" is Invalid -> "* invalid json" } @@ -3779,6 +3767,17 @@ sealed class CR { is CallEnded -> withUser(user, "contact: ${contact.id}") is NewContactConnection -> withUser(user, json.encodeToString(connection)) is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) + is RemoteHostCreated -> "remote host ID: $remoteHostId\noobData ${json.encodeToString(oobData)}" + is RemoteHostList -> "remote hosts: ${json.encodeToString(remoteHosts)}" + is RemoteHostConnected -> "remote host ID: $remoteHostId" + is RemoteHostStopped -> "remote host ID: $remoteHostId" + is RemoteCtrlList -> json.encodeToString(remoteCtrls) + is RemoteCtrlRegistered -> "remote ctrl ID: $remoteCtrlId" + is RemoteCtrlAnnounce -> "fingerprint: $fingerprint" + is RemoteCtrlFound -> "remote ctrl: ${json.encodeToString(remoteCtrl)}" + is RemoteCtrlConnecting -> "remote ctrl ID: $remoteCtrlId\nhost displayName: $displayName" + is RemoteCtrlConnected -> "remote ctrl ID: $remoteCtrlId\nhost displayName: $displayName" + is RemoteCtrlStopped -> "" is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + "agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}" @@ -3786,23 +3785,6 @@ sealed class CR { is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) is ArchiveImported -> "${archiveErrors.map { it.string } }" - is RemoteHostCreated -> "remote host ID: $remoteHostId\noobData ${json.encodeToString(oobData)}" - is RemoteHostList -> "remote hosts: ${json.encodeToString(remoteHosts)}" - is RemoteHostStarted -> "remote host $remoteHostId" - is RemoteHostConnected -> "remote host ID: $remoteHostId" - is RemoteHostStopped -> "remote host ID: $remoteHostId" - is RemoteHostDeleted -> "remote host ID: $remoteHostId" - is RemoteCtrlList -> json.encodeToString(remoteCtrls) - is RemoteCtrlRegistered -> "remote ctrl ID: $remoteCtrlId" - is RemoteCtrlStarted -> "" - is RemoteCtrlAnnounce -> "fingerprint: $fingerprint" - is RemoteCtrlFound -> "remote ctrl: ${json.encodeToString(remoteCtrl)}" - is RemoteCtrlAccepted -> "remote ctrl ID: $remoteCtrlId" - is RemoteCtrlRejected -> "remote ctrl ID: $remoteCtrlId" - is RemoteCtrlConnecting -> "remote ctrl ID: $remoteCtrlId\nhost displayName: $displayName" - is RemoteCtrlConnected -> "remote ctrl ID: $remoteCtrlId\nhost displayName: $displayName" - is RemoteCtrlStopped -> "" - is RemoteCtrlDeleted -> "remote ctrl ID: $remoteCtrlId" is Response -> json is Invalid -> str } @@ -3948,16 +3930,16 @@ sealed class ChatError { is ChatErrorAgent -> "agent ${agentError.string}" is ChatErrorStore -> "store ${storeError.string}" is ChatErrorDatabase -> "database ${databaseError.string}" - is ChatErrorRemoteCtrl -> "remoteCtrl ${remoteCtrlError.string}" is ChatErrorRemoteHost -> "remoteHost ${remoteHostError.string}" + is ChatErrorRemoteCtrl -> "remoteCtrl ${remoteCtrlError.string}" is ChatErrorInvalidJSON -> "invalid json ${json}" } @Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError() @Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError() @Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError() @Serializable @SerialName("errorDatabase") class ChatErrorDatabase(val databaseError: DatabaseError): ChatError() - @Serializable @SerialName("errorRemoteCtrl") class ChatErrorRemoteCtrl(val remoteCtrlError: RemoteCtrlError): ChatError() @Serializable @SerialName("errorRemoteHost") class ChatErrorRemoteHost(val remoteHostError: RemoteHostError): ChatError() + @Serializable @SerialName("errorRemoteCtrl") class ChatErrorRemoteCtrl(val remoteCtrlError: RemoteCtrlError): ChatError() @Serializable @SerialName("invalidJSON") class ChatErrorInvalidJSON(val json: String): ChatError() } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3e91bd621..454e87ef5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1891,18 +1891,18 @@ processChatCommand = \case let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_ updateGroupProfileByName gName $ \p -> p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p} - CreateRemoteHost -> createRemoteHost - ListRemoteHosts -> listRemoteHosts - StartRemoteHost rh -> startRemoteHost rh - StopRemoteHost rh -> closeRemoteHostSession rh - DeleteRemoteHost rh -> deleteRemoteHost rh - StartRemoteCtrl -> startRemoteCtrl (execChatCommand Nothing) - AcceptRemoteCtrl rc -> acceptRemoteCtrl rc - RejectRemoteCtrl rc -> rejectRemoteCtrl rc - StopRemoteCtrl -> stopRemoteCtrl - RegisterRemoteCtrl oob -> registerRemoteCtrl oob - ListRemoteCtrls -> listRemoteCtrls - DeleteRemoteCtrl rc -> deleteRemoteCtrl rc + CreateRemoteHost -> uncurry CRRemoteHostCreated <$> createRemoteHost + ListRemoteHosts -> CRRemoteHostList <$> listRemoteHosts + StartRemoteHost rh -> startRemoteHost rh >> ok_ + StopRemoteHost rh -> closeRemoteHostSession rh >> ok_ + DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_ + StartRemoteCtrl -> startRemoteCtrl (execChatCommand Nothing) >> ok_ + AcceptRemoteCtrl rc -> acceptRemoteCtrl rc >> ok_ + RejectRemoteCtrl rc -> rejectRemoteCtrl rc >> ok_ + StopRemoteCtrl -> stopRemoteCtrl >> ok_ + RegisterRemoteCtrl oob -> CRRemoteCtrlRegistered <$> registerRemoteCtrl oob + ListRemoteCtrls -> CRRemoteCtrlList <$> listRemoteCtrls + DeleteRemoteCtrl rc -> deleteRemoteCtrl rc >> ok_ QuitChat -> liftIO exitSuccess ShowVersion -> do let versionInfo = coreVersionInfo $(simplexmqCommitQ) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e4085ca79..5448f4960 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -425,8 +425,8 @@ data ChatCommand -- | SwitchRemoteHost (Maybe RemoteHostId) -- ^ Switch current remote host | StopRemoteHost RemoteHostId -- ^ Shut down a running session | DeleteRemoteHost RemoteHostId -- ^ Unregister remote host and remove its data - | RegisterRemoteCtrl RemoteCtrlOOB -- ^ Register OOB data for satellite discovery and handshake | StartRemoteCtrl -- ^ Start listening for announcements from all registered controllers + | RegisterRemoteCtrl RemoteCtrlOOB -- ^ Register OOB data for satellite discovery and handshake | ListRemoteCtrls | AcceptRemoteCtrl RemoteCtrlId -- ^ Accept discovered data and store confirmation | RejectRemoteCtrl RemoteCtrlId -- ^ Reject and blacklist discovered data @@ -631,21 +631,15 @@ data ChatResponse | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostCreated {remoteHostId :: RemoteHostId, oobData :: RemoteCtrlOOB} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} -- XXX: RemoteHostInfo is mostly concerned with session setup - | CRRemoteHostStarted {remoteHostId :: RemoteHostId} | CRRemoteHostConnected {remoteHostId :: RemoteHostId} | CRRemoteHostStopped {remoteHostId :: RemoteHostId} - | CRRemoteHostDeleted {remoteHostId :: RemoteHostId} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} | CRRemoteCtrlRegistered {remoteCtrlId :: RemoteCtrlId} - | CRRemoteCtrlStarted | CRRemoteCtrlAnnounce {fingerprint :: C.KeyHash} -- unregistered fingerprint, needs confirmation | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrl} -- registered fingerprint, may connect - | CRRemoteCtrlAccepted {remoteCtrlId :: RemoteCtrlId} - | CRRemoteCtrlRejected {remoteCtrlId :: RemoteCtrlId} | CRRemoteCtrlConnecting {remoteCtrlId :: RemoteCtrlId, displayName :: Text} | CRRemoteCtrlConnected {remoteCtrlId :: RemoteCtrlId, displayName :: Text} | CRRemoteCtrlStopped - | CRRemoteCtrlDeleted {remoteCtrlId :: RemoteCtrlId} | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} @@ -667,21 +661,15 @@ allowRemoteEvent :: ChatResponse -> Bool allowRemoteEvent = \case CRRemoteHostCreated {} -> False CRRemoteHostList {} -> False - CRRemoteHostStarted {} -> False CRRemoteHostConnected {} -> False CRRemoteHostStopped {} -> False - CRRemoteHostDeleted {} -> False CRRemoteCtrlList {} -> False CRRemoteCtrlRegistered {} -> False - CRRemoteCtrlStarted {} -> False CRRemoteCtrlAnnounce {} -> False CRRemoteCtrlFound {} -> False - CRRemoteCtrlAccepted {} -> False - CRRemoteCtrlRejected {} -> False CRRemoteCtrlConnecting {} -> False CRRemoteCtrlConnected {} -> False CRRemoteCtrlStopped {} -> False - CRRemoteCtrlDeleted {} -> False _ -> True logResponseToFile :: ChatResponse -> Bool diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 26d4f4bfd..37283511f 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -79,21 +79,20 @@ withRemoteHost remoteHostId action = Nothing -> throwError $ ChatErrorRemoteHost remoteHostId RHMissing Just rh -> action rh -startRemoteHost :: (ChatMonad m) => RemoteHostId -> m ChatResponse +startRemoteHost :: (ChatMonad m) => RemoteHostId -> m () startRemoteHost remoteHostId = do asks remoteHostSessions >>= atomically . TM.lookup remoteHostId >>= \case Just _ -> throwError $ ChatErrorRemoteHost remoteHostId RHBusy Nothing -> withRemoteHost remoteHostId $ \rh -> do announcer <- async $ run rh chatModifyVar remoteHostSessions $ M.insert remoteHostId RemoteHostSessionStarting {announcer} - pure CRRemoteHostStarted {remoteHostId} where cleanup finished = do logInfo "Remote host http2 client fininshed" atomically $ writeTVar finished True M.lookup remoteHostId <$> chatReadVar remoteHostSessions >>= \case Nothing -> logInfo $ "Session already closed for remote host " <> tshow remoteHostId - Just _ -> closeRemoteHostSession remoteHostId >>= toView + Just _ -> closeRemoteHostSession remoteHostId >> toView (CRRemoteHostStopped remoteHostId) run RemoteHost {storePath, caKey, caCert} = do finished <- newTVarIO False let parent = (C.signatureKeyPair caKey, caCert) @@ -142,42 +141,41 @@ pollRemote finished http path action = loop readTVarIO finished >>= (`unless` loop) req = HTTP2Client.requestNoBody "GET" path mempty -closeRemoteHostSession :: (ChatMonad m) => RemoteHostId -> m ChatResponse +closeRemoteHostSession :: (ChatMonad m) => RemoteHostId -> m () closeRemoteHostSession remoteHostId = withRemoteHostSession remoteHostId $ \session -> do logInfo $ "Closing remote host session for " <> tshow remoteHostId liftIO $ cancelRemoteHostSession session chatWriteVar currentRemoteHost Nothing chatModifyVar remoteHostSessions $ M.delete remoteHostId - pure CRRemoteHostStopped {remoteHostId} cancelRemoteHostSession :: (MonadUnliftIO m) => RemoteHostSession -> m () cancelRemoteHostSession = \case RemoteHostSessionStarting {announcer} -> cancel announcer RemoteHostSessionStarted {ctrlClient} -> liftIO $ HTTP2.closeHTTP2Client ctrlClient -createRemoteHost :: (ChatMonad m) => m ChatResponse +createRemoteHost :: (ChatMonad m) => m (RemoteHostId, RemoteCtrlOOB) createRemoteHost = do let displayName = "TODO" -- you don't have remote host name here, it will be passed from remote host ((_, caKey), caCert) <- liftIO $ genCredentials Nothing (-25, 24 * 365) displayName storePath <- liftIO randomStorePath remoteHostId <- withStore' $ \db -> insertRemoteHost db storePath displayName caKey caCert let oobData = RemoteCtrlOOB {caFingerprint = C.certificateFingerprint caCert} - pure CRRemoteHostCreated {remoteHostId, oobData} + pure (remoteHostId, oobData) -- | Generate a random 16-char filepath without / in it by using base64url encoding. randomStorePath :: IO FilePath randomStorePath = B.unpack . B64U.encode <$> getRandomBytes 12 -listRemoteHosts :: (ChatMonad m) => m ChatResponse +listRemoteHosts :: (ChatMonad m) => m [RemoteHostInfo] listRemoteHosts = do stored <- withStore' getRemoteHosts active <- chatReadVar remoteHostSessions - pure $ CRRemoteHostList $ do + pure $ do RemoteHost {remoteHostId, storePath, displayName} <- stored let sessionActive = M.member remoteHostId active pure RemoteHostInfo {remoteHostId, storePath, displayName, sessionActive} -deleteRemoteHost :: (ChatMonad m) => RemoteHostId -> m ChatResponse +deleteRemoteHost :: (ChatMonad m) => RemoteHostId -> m () deleteRemoteHost remoteHostId = withRemoteHost remoteHostId $ \RemoteHost {storePath} -> do chatReadVar filesFolder >>= \case Just baseDir -> do @@ -185,7 +183,6 @@ deleteRemoteHost remoteHostId = withRemoteHost remoteHostId $ \RemoteHost {store logError $ "TODO: remove " <> tshow hostStore Nothing -> logWarn "Local file store not available while deleting remote host" withStore' $ \db -> deleteRemoteHostRecord db remoteHostId - pure CRRemoteHostDeleted {remoteHostId} processRemoteCommand :: (ChatMonad m) => RemoteHostSession -> (ByteString, ChatCommand) -> m ChatResponse processRemoteCommand RemoteHostSessionStarting {} _ = pure . CRChatError Nothing . ChatError $ CEInternalError "sending remote commands before session started" @@ -393,7 +390,7 @@ processControllerRequest execChatCommand HTTP2.HTTP2Request {request, reqBody, s -- * ChatRequest handlers -startRemoteCtrl :: (ChatMonad m) => (ByteString -> m ChatResponse) -> m ChatResponse +startRemoteCtrl :: (ChatMonad m) => (ByteString -> m ChatResponse) -> m () startRemoteCtrl execChatCommand = chatReadVar remoteCtrlSession >>= \case Just _busy -> throwError $ ChatErrorRemoteCtrl RCEBusy @@ -416,7 +413,6 @@ startRemoteCtrl execChatCommand = chatWriteVar remoteCtrlSession Nothing toView CRRemoteCtrlStopped chatWriteVar remoteCtrlSession $ Just RemoteCtrlSession {discoverer, supervisor, hostServer = Nothing, discovered, accepted, remoteOutputQ} - pure CRRemoteCtrlStarted discoverRemoteCtrls :: (ChatMonad m) => TM.TMap C.KeyHash TransportHost -> m () discoverRemoteCtrls discovered = Discovery.withListener go @@ -445,33 +441,32 @@ discoverRemoteCtrls discovered = Discovery.withListener go Just RemoteCtrlSession {accepted} -> atomically $ void $ tryPutTMVar accepted remoteCtrlId -- previously accepted controller, connect automatically _nonV4 -> go sock -registerRemoteCtrl :: (ChatMonad m) => RemoteCtrlOOB -> m ChatResponse +registerRemoteCtrl :: (ChatMonad m) => RemoteCtrlOOB -> m RemoteCtrlId registerRemoteCtrl RemoteCtrlOOB {caFingerprint} = do let displayName = "TODO" -- maybe include into OOB data remoteCtrlId <- withStore' $ \db -> insertRemoteCtrl db displayName caFingerprint - pure $ CRRemoteCtrlRegistered {remoteCtrlId} + pure remoteCtrlId -listRemoteCtrls :: (ChatMonad m) => m ChatResponse +listRemoteCtrls :: (ChatMonad m) => m [RemoteCtrlInfo] listRemoteCtrls = do stored <- withStore' getRemoteCtrls active <- chatReadVar remoteCtrlSession >>= \case Nothing -> pure Nothing Just RemoteCtrlSession {accepted} -> atomically (tryReadTMVar accepted) - pure $ CRRemoteCtrlList $ do + pure $ do RemoteCtrl {remoteCtrlId, displayName} <- stored let sessionActive = active == Just remoteCtrlId pure RemoteCtrlInfo {remoteCtrlId, displayName, sessionActive} -acceptRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m ChatResponse +acceptRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m () acceptRemoteCtrl remoteCtrlId = do withStore' $ \db -> markRemoteCtrlResolution db remoteCtrlId True chatReadVar remoteCtrlSession >>= \case Nothing -> throwError $ ChatErrorRemoteCtrl RCEInactive Just RemoteCtrlSession {accepted} -> atomically . void $ tryPutTMVar accepted remoteCtrlId -- the remote host can now proceed with connection - pure $ CRRemoteCtrlAccepted {remoteCtrlId} -rejectRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m ChatResponse +rejectRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m () rejectRemoteCtrl remoteCtrlId = do withStore' $ \db -> markRemoteCtrlResolution db remoteCtrlId False chatReadVar remoteCtrlSession >>= \case @@ -479,9 +474,8 @@ rejectRemoteCtrl remoteCtrlId = do Just RemoteCtrlSession {discoverer, supervisor} -> do cancel discoverer cancel supervisor - pure $ CRRemoteCtrlRejected {remoteCtrlId} -stopRemoteCtrl :: (ChatMonad m) => m ChatResponse +stopRemoteCtrl :: (ChatMonad m) => m () stopRemoteCtrl = chatReadVar remoteCtrlSession >>= \case Nothing -> throwError $ ChatErrorRemoteCtrl RCEInactive @@ -489,7 +483,6 @@ stopRemoteCtrl = cancelRemoteCtrlSession rcs $ do chatWriteVar remoteCtrlSession Nothing toView CRRemoteCtrlStopped - pure $ CRCmdOk Nothing cancelRemoteCtrlSession_ :: (MonadUnliftIO m) => RemoteCtrlSession -> m () cancelRemoteCtrlSession_ rcs = cancelRemoteCtrlSession rcs $ pure () @@ -503,12 +496,10 @@ cancelRemoteCtrlSession RemoteCtrlSession {discoverer, supervisor, hostServer} c cancel supervisor -- supervisor is blocked until session progresses cleanup -deleteRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m ChatResponse +deleteRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m () deleteRemoteCtrl remoteCtrlId = chatReadVar remoteCtrlSession >>= \case - Nothing -> do - withStore' $ \db -> deleteRemoteCtrlRecord db remoteCtrlId - pure $ CRRemoteCtrlDeleted {remoteCtrlId} + Nothing -> withStore' $ \db -> deleteRemoteCtrlRecord db remoteCtrlId Just _ -> throwError $ ChatErrorRemoteCtrl RCEBusy withRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> (RemoteCtrl -> m a) -> m a diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 1d474792a..b5dce1ba8 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -264,21 +264,15 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei CRNtfMessages {} -> [] CRRemoteHostCreated rhId oobData -> ("remote host " <> sShow rhId <> " created") : viewRemoteCtrlOOBData oobData CRRemoteHostList hs -> viewRemoteHosts hs - CRRemoteHostStarted rhId -> ["remote host " <> sShow rhId <> " started"] CRRemoteHostConnected rhId -> ["remote host " <> sShow rhId <> " connected"] CRRemoteHostStopped rhId -> ["remote host " <> sShow rhId <> " stopped"] - CRRemoteHostDeleted rhId -> ["remote host " <> sShow rhId <> " deleted"] CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlRegistered rcId -> ["remote controller " <> sShow rcId <> " registered"] - CRRemoteCtrlStarted -> ["remote controller started"] CRRemoteCtrlAnnounce fingerprint -> ["remote controller announced", "connection code:", plain $ strEncode fingerprint] CRRemoteCtrlFound rc -> ["remote controller found:", viewRemoteCtrl rc] - CRRemoteCtrlAccepted rcId -> ["remote controller " <> sShow rcId <> " accepted"] - CRRemoteCtrlRejected rcId -> ["remote controller " <> sShow rcId <> " rejected"] CRRemoteCtrlConnecting rcId rcName -> ["remote controller " <> sShow rcId <> " connecting to " <> plain rcName] CRRemoteCtrlConnected rcId rcName -> ["remote controller " <> sShow rcId <> " connected, " <> plain rcName] CRRemoteCtrlStopped -> ["remote controller stopped"] - CRRemoteCtrlDeleted rcId -> ["remote controller " <> sShow rcId <> " deleted"] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} = diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index 479febbca..68ef6788e 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -110,10 +110,10 @@ remoteHandshakeTest = testChat2 aliceProfile bobProfile $ \desktop mobile -> do desktop <## "Remote hosts:" desktop <## "1. TODO" -- TODO host name probably should be Maybe, as when host is created there is no name yet desktop ##> "/start remote host 1" - desktop <## "remote host 1 started" + desktop <## "ok" mobile ##> "/start remote ctrl" - mobile <## "remote controller started" + mobile <## "ok" mobile <## "remote controller announced" mobile <## "connection code:" fingerprint' <- getTermLine mobile @@ -126,7 +126,7 @@ remoteHandshakeTest = testChat2 aliceProfile bobProfile $ \desktop mobile -> do mobile <## "Remote controllers:" mobile <## "1. TODO" mobile ##> "/accept remote ctrl 1" - mobile <## "remote controller 1 accepted" -- alternative scenario: accepted before controller start + mobile <## "ok" -- alternative scenario: accepted before controller start mobile <## "remote controller 1 connecting to TODO" mobile <## "remote controller 1 connected, TODO" @@ -140,9 +140,9 @@ remoteHandshakeTest = testChat2 aliceProfile bobProfile $ \desktop mobile -> do traceM " - Shutting desktop" desktop ##> "/stop remote host 1" - desktop <## "remote host 1 stopped" + desktop <## "ok" desktop ##> "/delete remote host 1" - desktop <## "remote host 1 deleted" + desktop <## "ok" desktop ##> "/list remote hosts" desktop <## "No remote hosts" @@ -151,7 +151,7 @@ remoteHandshakeTest = testChat2 aliceProfile bobProfile $ \desktop mobile -> do mobile <## "ok" mobile <## "remote controller stopped" mobile ##> "/delete remote ctrl 1" - mobile <## "remote controller 1 deleted" + mobile <## "ok" mobile ##> "/list remote ctrls" mobile <## "No remote controllers" @@ -173,10 +173,10 @@ remoteCommandTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob fingerprint <- getTermLine desktop desktop ##> "/start remote host 1" - desktop <## "remote host 1 started" + desktop <## "ok" mobile ##> "/start remote ctrl" - mobile <## "remote controller started" + mobile <## "ok" mobile <## "remote controller announced" mobile <## "connection code:" fingerprint' <- getTermLine mobile @@ -184,7 +184,7 @@ remoteCommandTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob mobile ##> ("/register remote ctrl " <> fingerprint') mobile <## "remote controller 1 registered" mobile ##> "/accept remote ctrl 1" - mobile <## "remote controller 1 accepted" -- alternative scenario: accepted before controller start + mobile <## "ok" -- alternative scenario: accepted before controller start mobile <## "remote controller 1 connecting to TODO" mobile <## "remote controller 1 connected, TODO" desktop <## "remote host 1 connected" diff --git a/tests/Test.hs b/tests/Test.hs index 071ff3791..6af51a072 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -33,7 +33,7 @@ main = do describe "SimpleX chat client" chatTests xdescribe'' "SimpleX Broadcast bot" broadcastBotTests xdescribe'' "SimpleX Directory service bot" directoryServiceTests - describe "Remote session" remoteTests + fdescribe "Remote session" remoteTests where testBracket test = do t <- getSystemTime