diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ad76364e9..0089fd087 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -886,9 +886,9 @@ func startRemoteCtrl() async throws { try await sendCommandOkResp(.startRemoteCtrl) } -func registerRemoteCtrl(_ remoteCtrlOOB: RemoteCtrlOOB) async throws -> Int64 { +func registerRemoteCtrl(_ remoteCtrlOOB: RemoteCtrlOOB) async throws -> RemoteCtrlInfo { let r = await chatSendCmd(.registerRemoteCtrl(remoteCtrlOOB: remoteCtrlOOB)) - if case let .remoteCtrlRegistered(rcId) = r { return rcId } + if case let .remoteCtrlRegistered(rcInfo) = r { return rcInfo } throw r } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4b79800e1..756ab3034 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -117,6 +117,7 @@ public enum ChatCommand { case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?) case setFileToReceive(fileId: Int64, encrypted: Bool) case cancelFile(fileId: Int64) + case setLocalDeviceName(displayName: String) case startRemoteCtrl case registerRemoteCtrl(remoteCtrlOOB: RemoteCtrlOOB) case listRemoteCtrls @@ -262,6 +263,7 @@ 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 let .setLocalDeviceName(displayName): return "/set device name \(displayName)" 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)" @@ -381,6 +383,7 @@ public enum ChatCommand { case .receiveFile: return "receiveFile" case .setFileToReceive: return "setFileToReceive" case .cancelFile: return "cancelFile" + case .setLocalDeviceName: return "setLocalDeviceName" case .startRemoteCtrl: return "startRemoteCtrl" case .registerRemoteCtrl: return "registerRemoteCtrl" case .listRemoteCtrls: return "listRemoteCtrls" @@ -585,11 +588,11 @@ public enum ChatResponse: Decodable, Error { case newContactConnection(user: UserRef, connection: PendingContactConnection) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) - case remoteCtrlRegistered(remoteCtrlId: Int64) + case remoteCtrlRegistered(remoteCtrl: RemoteCtrlInfo) case remoteCtrlAnnounce(fingerprint: String) - case remoteCtrlFound(remoteCtrl: RemoteCtrl) - case remoteCtrlConnecting(remoteCtrlId: Int64, displayName: String) - case remoteCtrlConnected(remoteCtrlId: Int64, displayName: String) + case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo) + case remoteCtrlConnecting(remoteCtrl: RemoteCtrlInfo) + case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) case remoteCtrlStopped case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) @@ -874,11 +877,11 @@ public enum ChatResponse: Decodable, Error { 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 .remoteCtrlRegistered(remoteCtrl): return String(describing: remoteCtrl) 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 let .remoteCtrlFound(remoteCtrl): return String(describing: remoteCtrl) + case let .remoteCtrlConnecting(remoteCtrl): return String(describing: remoteCtrl) + case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) 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 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 9ab6060fe..f128fcb75 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 @@ -166,6 +166,7 @@ class AppPreferences { val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) val customDisappearingMessageTime = mkIntPreference(SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME, 300) + val deviceNameForRemoteAccess = mkStrPreference(SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS, "Desktop") private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( @@ -306,6 +307,7 @@ class AppPreferences { private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" + private const val SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS = "DeviceNameForRemoteAccess" } } @@ -342,6 +344,11 @@ object ChatController { val users = listUsers() chatModel.users.clear() chatModel.users.addAll(users) + val remoteHosts = listRemoteHosts() + if (remoteHosts != null) { + chatModel.remoteHosts.clear() + chatModel.remoteHosts.addAll(remoteHosts) + } if (justStarted) { chatModel.currentUser.value = user chatModel.userCreated.value = true @@ -432,15 +439,16 @@ object ChatController { } } - private fun recvMsg(ctrl: ChatCtrl): CR? { + private fun recvMsg(ctrl: ChatCtrl): APIResponse? { val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) return if (json == "") { null } else { - val r = APIResponse.decodeStr(json).resp + val apiResp = APIResponse.decodeStr(json) + val r = apiResp.resp Log.d(TAG, "chatRecvMsg: ${r.responseType}") if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") - r + apiResp } } @@ -1327,6 +1335,59 @@ object ChatController { } } + suspend fun setLocalDeviceName(displayName: String): Boolean = sendCommandOkResp(CC.SetLocalDeviceName(displayName)) + + suspend fun createRemoteHost(): RemoteHostInfo? { + val r = sendCmd(CC.CreateRemoteHost()) + if (r is CR.RemoteHostCreated) return r.remoteHost + apiErrorAlert("createRemoteHost", generalGetString(MR.strings.error), r) + return null + } + + suspend fun listRemoteHosts(): List? { + val r = sendCmd(CC.ListRemoteHosts()) + if (r is CR.RemoteHostList) return r.remoteHosts + apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error), r) + return null + } + + suspend fun startRemoteHost(rhId: Long): Boolean = sendCommandOkResp(CC.StartRemoteHost(rhId)) + + suspend fun registerRemoteCtrl(oob: RemoteCtrlOOB): RemoteCtrlInfo? { + val r = sendCmd(CC.RegisterRemoteCtrl(oob)) + if (r is CR.RemoteCtrlRegistered) return r.remoteCtrl + apiErrorAlert("registerRemoteCtrl", generalGetString(MR.strings.error), r) + return null + } + + suspend fun listRemoteCtrls(): List? { + val r = sendCmd(CC.ListRemoteCtrls()) + if (r is CR.RemoteCtrlList) return r.remoteCtrls + apiErrorAlert("listRemoteCtrls", generalGetString(MR.strings.error), r) + return null + } + + suspend fun stopRemoteHost(rhId: Long): Boolean = sendCommandOkResp(CC.StopRemoteHost(rhId)) + + suspend fun deleteRemoteHost(rhId: Long): Boolean = sendCommandOkResp(CC.DeleteRemoteHost(rhId)) + + suspend fun startRemoteCtrl(): Boolean = sendCommandOkResp(CC.StartRemoteCtrl()) + + suspend fun acceptRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(CC.AcceptRemoteCtrl(rcId)) + + suspend fun rejectRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(CC.RejectRemoteCtrl(rcId)) + + suspend fun stopRemoteCtrl(): Boolean = sendCommandOkResp(CC.StopRemoteCtrl()) + + suspend fun deleteRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(CC.DeleteRemoteCtrl(rcId)) + + private suspend fun sendCommandOkResp(cmd: CC): Boolean { + val r = sendCmd(cmd) + val ok = r is CR.CmdOk + if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error), r) + return ok + } + suspend fun apiGetVersion(): CoreVersionInfo? { val r = sendCmd(CC.ShowVersion()) return if (r is CR.VersionInfo) { @@ -1361,14 +1422,15 @@ object ChatController { } } - fun apiErrorAlert(method: String, title: String, r: CR) { + private fun apiErrorAlert(method: String, title: String, r: CR) { val errMsg = "${r.responseType}: ${r.details}" Log.e(TAG, "$method bad response: $errMsg") AlertManager.shared.showAlertMsg(title, errMsg) } - suspend fun processReceivedMsg(r: CR) { + private suspend fun processReceivedMsg(apiResp: APIResponse) { lastMsgReceivedTimestamp = System.currentTimeMillis() + val r = apiResp.resp chatModel.addTerminalItem(TerminalItem.resp(r)) when (r) { is CR.NewContactConnection -> { @@ -1674,6 +1736,13 @@ object ChatController { chatModel.updateContactConnectionStats(r.contact, r.ratchetSyncProgress.connectionStats) is CR.GroupMemberRatchetSync -> chatModel.updateGroupMemberConnectionStats(r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + is CR.RemoteHostConnected -> { + // update + chatModel.connectingRemoteHost.value = r.remoteHost + } + is CR.RemoteHostStopped -> { + // + } else -> Log.d(TAG , "unsupported event: ${r.responseType}") } @@ -1933,6 +2002,7 @@ sealed class CC { class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() + class SetLocalDeviceName(val displayName: String): CC() class CreateRemoteHost(): CC() class ListRemoteHosts(): CC() class StartRemoteHost(val remoteHostId: Long): CC() @@ -2053,13 +2123,14 @@ sealed class CC { is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" + is SetLocalDeviceName -> "/set device name $displayName" is CreateRemoteHost -> "/create remote host" is ListRemoteHosts -> "/list remote hosts" is StartRemoteHost -> "/start remote host $remoteHostId" is StopRemoteHost -> "/stop remote host $remoteHostId" is DeleteRemoteHost -> "/delete remote host $remoteHostId" is StartRemoteCtrl -> "/start remote ctrl" - is RegisterRemoteCtrl -> "/register remote ctrl ${remoteCtrlOOB.caFingerprint}" + is RegisterRemoteCtrl -> "/register remote ctrl ${remoteCtrlOOB.fingerprint}" is AcceptRemoteCtrl -> "/accept remote ctrl $remoteCtrlId" is RejectRemoteCtrl -> "/reject remote ctrl $remoteCtrlId" is ListRemoteCtrls -> "/list remote ctrls" @@ -2162,6 +2233,7 @@ sealed class CC { is ApiChatUnread -> "apiChatUnread" is ReceiveFile -> "receiveFile" is CancelFile -> "cancelFile" + is SetLocalDeviceName -> "setLocalDeviceName" is CreateRemoteHost -> "createRemoteHost" is ListRemoteHosts -> "listRemoteHosts" is StartRemoteHost -> "startRemoteHost" @@ -3246,7 +3318,8 @@ data class RemoteCtrl ( @Serializable data class RemoteCtrlOOB ( - val caFingerprint: String + val fingerprint: String, + val displayName: String ) @Serializable @@ -3261,6 +3334,7 @@ data class RemoteHostInfo ( val remoteHostId: Long, val storePath: String, val displayName: String, + val remoteCtrlOOB: RemoteCtrlOOB, val sessionActive: Boolean ) @@ -3277,7 +3351,7 @@ val yaml = Yaml(configuration = YamlConfiguration( )) @Serializable -class APIResponse(val resp: CR, val corr: String? = null) { +class APIResponse(val resp: CR, val remoteHostId: Long?, val corr: String? = null) { companion object { fun decodeStr(str: String): APIResponse { return try { @@ -3287,48 +3361,35 @@ class APIResponse(val resp: CR, val corr: String? = null) { Log.d(TAG, e.localizedMessage ?: "") val data = json.parseToJsonElement(str).jsonObject val resp = data["resp"]!!.jsonObject - val type = resp["type"]?.jsonPrimitive?.content ?: "invalid" + val type = resp["type"]?.jsonPrimitive?.contentOrNull ?: "invalid" + val corr = data["corr"]?.toString() + val remoteHostId = data["remoteHostId"]?.jsonPrimitive?.longOrNull try { if (type == "apiChats") { val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chats: List = resp["chats"]!!.jsonArray.map { parseChatData(it) } - return APIResponse( - resp = CR.ApiChats(user, chats), - corr = data["corr"]?.toString() - ) + return APIResponse(CR.ApiChats(user, chats), remoteHostId, corr) } else if (type == "apiChat") { val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chat = parseChatData(resp["chat"]!!) - return APIResponse( - resp = CR.ApiChat(user, chat), - corr = data["corr"]?.toString() - ) + return APIResponse(CR.ApiChat(user, chat), remoteHostId, corr) } else if (type == "chatCmdError") { val userObject = resp["user_"]?.jsonObject val user = runCatching { json.decodeFromJsonElement(userObject!!) }.getOrNull() - return APIResponse( - resp = CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), - corr = data["corr"]?.toString() - ) + return APIResponse(CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), remoteHostId, corr) } else if (type == "chatError") { val userObject = resp["user_"]?.jsonObject val user = runCatching { json.decodeFromJsonElement(userObject!!) }.getOrNull() - return APIResponse( - resp = CR.ChatRespError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), - corr = data["corr"]?.toString() - ) + return APIResponse(CR.ChatRespError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), remoteHostId, corr) } } catch (e: Exception) { Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString()) } - APIResponse( - resp = CR.Response(type, json.encodeToString(data)), - corr = data["corr"]?.toString() - ) + APIResponse(CR.Response(type, json.encodeToString(data)), remoteHostId, corr) } catch(e: Exception) { - APIResponse(CR.Invalid(str)) + APIResponse(CR.Invalid(str), remoteHostId = null) } } } @@ -3484,17 +3545,17 @@ sealed class 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("remoteHostCreated") class RemoteHostCreated(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() - @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHostId: Long): CR() + @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): 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("remoteCtrlRegistered") class RemoteCtrlRegistered(val remoteCtrl: RemoteCtrlInfo): 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("remoteCtrlFound") class RemoteCtrlFound(val remoteCtrl: RemoteCtrlInfo): CR() + @Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrl: RemoteCtrlInfo): CR() + @Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): 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() @@ -3767,17 +3828,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 RemoteHostCreated -> json.encodeToString(remoteHost) + is RemoteHostList -> json.encodeToString(remoteHosts) + is RemoteHostConnected -> json.encodeToString(remoteHost) is RemoteHostStopped -> "remote host ID: $remoteHostId" is RemoteCtrlList -> json.encodeToString(remoteCtrls) - is RemoteCtrlRegistered -> "remote ctrl ID: $remoteCtrlId" + is RemoteCtrlRegistered -> json.encodeToString(remoteCtrl) 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 RemoteCtrlFound -> json.encodeToString(remoteCtrl) + is RemoteCtrlConnecting -> json.encodeToString(remoteCtrl) + is RemoteCtrlConnected -> json.encodeToString(remoteCtrl) + is RemoteCtrlStopped -> noDetails() 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 })}" diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 726fbdce8..9c25afbd5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -70,6 +70,7 @@ import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles +import Simplex.Chat.Store.Remote import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -1900,7 +1901,7 @@ processChatCommand = \case StopRemoteHost rh -> closeRemoteHostSession rh >> ok_ DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_ StartRemoteCtrl -> startRemoteCtrl (execChatCommand Nothing) >> ok_ - RegisterRemoteCtrl oob -> CRRemoteCtrlRegistered <$> registerRemoteCtrl oob + RegisterRemoteCtrl oob -> CRRemoteCtrlRegistered <$> withStore' (`insertRemoteCtrl` oob) AcceptRemoteCtrl rc -> acceptRemoteCtrl rc >> ok_ RejectRemoteCtrl rc -> rejectRemoteCtrl rc >> ok_ StopRemoteCtrl -> stopRemoteCtrl >> ok_ diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2a2b7cff9..22c2649f5 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -633,14 +633,14 @@ data ChatResponse | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostCreated {remoteHost :: RemoteHostInfo} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} - | CRRemoteHostConnected {remoteHostId :: RemoteHostId} -- TODO add displayName + | CRRemoteHostConnected {remoteHost :: RemoteHostInfo} | CRRemoteHostStopped {remoteHostId :: RemoteHostId} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} - | CRRemoteCtrlRegistered {remoteCtrlId :: RemoteCtrlId} + | CRRemoteCtrlRegistered {remoteCtrl :: RemoteCtrlInfo} | CRRemoteCtrlAnnounce {fingerprint :: C.KeyHash} -- unregistered fingerprint, needs confirmation - | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrl} -- registered fingerprint, may connect - | CRRemoteCtrlConnecting {remoteCtrlId :: RemoteCtrlId, displayName :: Text} - | CRRemoteCtrlConnected {remoteCtrlId :: RemoteCtrlId, displayName :: Text} + | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo} -- registered fingerprint, may connect + | CRRemoteCtrlConnecting {remoteCtrl :: RemoteCtrlInfo} + | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} | CRRemoteCtrlStopped | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} @@ -693,34 +693,6 @@ logResponseToFile = \case CRMessageError {} -> True _ -> False -data RemoteCtrlOOB = RemoteCtrlOOB - { caFingerprint :: C.KeyHash, - displayName :: Text - } - deriving (Show, Generic, FromJSON) - -instance ToJSON RemoteCtrlOOB where toEncoding = J.genericToEncoding J.defaultOptions - -data RemoteHostInfo = RemoteHostInfo - { remoteHostId :: RemoteHostId, - storePath :: FilePath, - displayName :: Text, - remoteCtrlOOB :: RemoteCtrlOOB, - sessionActive :: Bool - } - deriving (Show, Generic, FromJSON) - -instance ToJSON RemoteHostInfo where toEncoding = J.genericToEncoding J.defaultOptions - -data RemoteCtrlInfo = RemoteCtrlInfo - { remoteCtrlId :: RemoteCtrlId, - displayName :: Text, - sessionActive :: Bool - } - deriving (Eq, Show, Generic, FromJSON) - -instance ToJSON RemoteCtrlInfo where toEncoding = J.genericToEncoding J.defaultOptions - data ConnectionPlan = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan} diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 4d031634f..256e00d6d 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -33,6 +33,7 @@ import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import qualified Network.HTTP.Types as HTTP @@ -93,7 +94,7 @@ startRemoteHost remoteHostId = do M.lookup remoteHostId <$> chatReadVar remoteHostSessions >>= \case Nothing -> logInfo $ "Session already closed for remote host " <> tshow remoteHostId Just _ -> closeRemoteHostSession remoteHostId >> toView (CRRemoteHostStopped remoteHostId) - run RemoteHost {storePath, caKey, caCert} = do + run rh@RemoteHost {storePath, caKey, caCert} = do finished <- newTVarIO False let parent = (C.signatureKeyPair caKey, caCert) sessionCreds <- liftIO $ genCredentials (Just parent) (0, 24) "Session" @@ -120,7 +121,9 @@ startRemoteHost remoteHostId = do Nothing -> toViewRemote chatResponse Just localFile -> toViewRemote CRRcvFileComplete {user = ru, chatItem = AChatItem c d i ci {file = Just localFile}} _ -> toViewRemote chatResponse - toView CRRemoteHostConnected {remoteHostId} + rcName <- chatReadVar localDeviceName + -- TODO what sets session active? + toView CRRemoteHostConnected {remoteHost = remoteHostInfo rh True rcName} sendHello :: (ChatMonad m) => HTTP2Client -> m (Either HTTP2.HTTP2ClientError HTTP2.HTTP2Response) sendHello http = liftIO (HTTP2.sendRequestDirect http req Nothing) @@ -155,13 +158,13 @@ cancelRemoteHostSession = \case createRemoteHost :: (ChatMonad m) => m RemoteHostInfo createRemoteHost = do - let hostDisplayName = "TODO" -- you don't have remote host name here, it will be passed from remote host - ((_, caKey), caCert) <- liftIO $ genCredentials Nothing (-25, 24 * 365) hostDisplayName + let rhName = "TODO" -- you don't have remote host name here, it will be passed from remote host + ((_, caKey), caCert) <- liftIO $ genCredentials Nothing (-25, 24 * 365) rhName storePath <- liftIO randomStorePath - remoteHostId <- withStore' $ \db -> insertRemoteHost db storePath hostDisplayName caKey caCert - displayName <- chatReadVar localDeviceName - let remoteCtrlOOB = RemoteCtrlOOB {caFingerprint = C.certificateFingerprint caCert, displayName} - pure RemoteHostInfo {remoteHostId, storePath, displayName, remoteCtrlOOB, sessionActive = False} + remoteHostId <- withStore' $ \db -> insertRemoteHost db storePath rhName caKey caCert + rcName <- chatReadVar localDeviceName + let remoteCtrlOOB = RemoteCtrlOOB {fingerprint = C.certificateFingerprint caCert, displayName = rcName} + pure RemoteHostInfo {remoteHostId, storePath, displayName = rhName, remoteCtrlOOB, sessionActive = False} -- | Generate a random 16-char filepath without / in it by using base64url encoding. randomStorePath :: IO FilePath @@ -173,10 +176,13 @@ listRemoteHosts = do rcName <- chatReadVar localDeviceName map (rhInfo active rcName) <$> withStore' getRemoteHosts where - rhInfo active rcName RemoteHost {remoteHostId, storePath, displayName, caCert} = - let sessionActive = M.member remoteHostId active - remoteCtrlOOB = RemoteCtrlOOB {caFingerprint = C.certificateFingerprint caCert, displayName = rcName} - in RemoteHostInfo {remoteHostId, storePath, displayName, remoteCtrlOOB, sessionActive} + rhInfo active rcName rh@RemoteHost {remoteHostId} = + remoteHostInfo rh (M.member remoteHostId active) rcName + +remoteHostInfo :: RemoteHost -> Bool -> Text -> RemoteHostInfo +remoteHostInfo RemoteHost {remoteHostId, storePath, displayName, caCert} sessionActive rcName = + let remoteCtrlOOB = RemoteCtrlOOB {fingerprint = C.certificateFingerprint caCert, displayName = rcName} + in RemoteHostInfo {remoteHostId, storePath, displayName, remoteCtrlOOB, sessionActive} deleteRemoteHost :: (ChatMonad m) => RemoteHostId -> m () deleteRemoteHost remoteHostId = withRemoteHost remoteHostId $ \RemoteHost {storePath} -> do @@ -405,13 +411,13 @@ startRemoteCtrl execChatCommand = accepted <- newEmptyTMVarIO supervisor <- async $ do remoteCtrlId <- atomically (readTMVar accepted) - withRemoteCtrl remoteCtrlId $ \RemoteCtrl {displayName, fingerprint} -> do + withRemoteCtrl remoteCtrlId $ \rc@RemoteCtrl {fingerprint} -> do source <- atomically $ TM.lookup fingerprint discovered >>= maybe retry pure - toView $ CRRemoteCtrlConnecting {remoteCtrlId, displayName} + toView $ CRRemoteCtrlConnecting $ remoteCtrlInfo rc False atomically $ writeTVar discovered mempty -- flush unused sources server <- async $ Discovery.connectRevHTTP2 source fingerprint (processControllerRequest execChatCommand) chatModifyVar remoteCtrlSession $ fmap $ \s -> s {hostServer = Just server} - toView $ CRRemoteCtrlConnected {remoteCtrlId, displayName} + toView $ CRRemoteCtrlConnected $ remoteCtrlInfo rc True _ <- waitCatch server chatWriteVar remoteCtrlSession Nothing toView CRRemoteCtrlStopped @@ -436,7 +442,7 @@ discoverRemoteCtrls discovered = Discovery.withListener go withStore' (`getRemoteCtrlByFingerprint` fingerprint) >>= \case Nothing -> toView $ CRRemoteCtrlAnnounce fingerprint -- unknown controller, ui "register" action required Just found@RemoteCtrl {remoteCtrlId, accepted = storedChoice} -> case storedChoice of - Nothing -> toView $ CRRemoteCtrlFound found -- first-time controller, ui "accept" action required + Nothing -> toView $ CRRemoteCtrlFound $ remoteCtrlInfo found False -- first-time controller, ui "accept" action required Just False -> pure () -- skipping a rejected item Just True -> chatReadVar remoteCtrlSession >>= \case @@ -444,11 +450,6 @@ 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 RemoteCtrlId -registerRemoteCtrl RemoteCtrlOOB {caFingerprint, displayName} = do - remoteCtrlId <- withStore' $ \db -> insertRemoteCtrl db displayName caFingerprint - pure remoteCtrlId - listRemoteCtrls :: (ChatMonad m) => m [RemoteCtrlInfo] listRemoteCtrls = do active <- @@ -456,9 +457,12 @@ listRemoteCtrls = do $>>= \RemoteCtrlSession {accepted} -> atomically $ tryReadTMVar accepted map (rcInfo active) <$> withStore' getRemoteCtrls where - rcInfo active RemoteCtrl {remoteCtrlId, displayName} = - let sessionActive = active == Just remoteCtrlId - in RemoteCtrlInfo {remoteCtrlId, displayName, sessionActive} + rcInfo active rc@RemoteCtrl {remoteCtrlId} = + remoteCtrlInfo rc $ active == Just remoteCtrlId + +remoteCtrlInfo :: RemoteCtrl -> Bool -> RemoteCtrlInfo +remoteCtrlInfo RemoteCtrl {remoteCtrlId, displayName, fingerprint, accepted} sessionActive = + RemoteCtrlInfo {remoteCtrlId, displayName, fingerprint, accepted, sessionActive} acceptRemoteCtrl :: (ChatMonad m) => RemoteCtrlId -> m () acceptRemoteCtrl remoteCtrlId = do diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index cdff2b7ac..67fe7c6ff 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -24,6 +24,25 @@ data RemoteHost = RemoteHost } deriving (Show) +data RemoteCtrlOOB = RemoteCtrlOOB + { fingerprint :: C.KeyHash, + displayName :: Text + } + deriving (Show) + +$(J.deriveJSON J.defaultOptions ''RemoteCtrlOOB) + +data RemoteHostInfo = RemoteHostInfo + { remoteHostId :: RemoteHostId, + storePath :: FilePath, + displayName :: Text, + remoteCtrlOOB :: RemoteCtrlOOB, + sessionActive :: Bool + } + deriving (Show) + +$(J.deriveJSON J.defaultOptions ''RemoteHostInfo) + type RemoteCtrlId = Int64 data RemoteCtrl = RemoteCtrl @@ -34,4 +53,15 @@ data RemoteCtrl = RemoteCtrl } deriving (Show) -$(J.deriveJSON J.defaultOptions ''RemoteCtrl) +$(J.deriveJSON J.defaultOptions {J.omitNothingFields = True} ''RemoteCtrl) + +data RemoteCtrlInfo = RemoteCtrlInfo + { remoteCtrlId :: RemoteCtrlId, + displayName :: Text, + fingerprint :: C.KeyHash, + accepted :: Maybe Bool, + sessionActive :: Bool + } + deriving (Show) + +$(J.deriveJSON J.defaultOptions {J.omitNothingFields = True} ''RemoteCtrlInfo) diff --git a/src/Simplex/Chat/Store/Remote.hs b/src/Simplex/Chat/Store/Remote.hs index c231a535b..9189a2776 100644 --- a/src/Simplex/Chat/Store/Remote.hs +++ b/src/Simplex/Chat/Store/Remote.hs @@ -9,14 +9,15 @@ import Data.Text (Text) import Database.SQLite.Simple (Only (..)) import qualified Database.SQLite.Simple as SQL import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Chat.Remote.Types (RemoteCtrl (..), RemoteCtrlId, RemoteHost (..), RemoteHostId) +import Simplex.Chat.Store.Shared (insertedRowId) +import Simplex.Chat.Remote.Types import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow) import qualified Simplex.Messaging.Crypto as C insertRemoteHost :: DB.Connection -> FilePath -> Text -> C.APrivateSignKey -> C.SignedCertificate -> IO RemoteHostId insertRemoteHost db storePath displayName caKey caCert = do DB.execute db "INSERT INTO remote_hosts (store_path, display_name, ca_key, ca_cert) VALUES (?,?,?,?)" (storePath, displayName, caKey, C.SignedObject caCert) - fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" + insertedRowId db getRemoteHosts :: DB.Connection -> IO [RemoteHost] getRemoteHosts db = @@ -37,10 +38,11 @@ toRemoteHost (remoteHostId, storePath, displayName, caKey, C.SignedObject caCert deleteRemoteHostRecord :: DB.Connection -> RemoteHostId -> IO () deleteRemoteHostRecord db remoteHostId = DB.execute db "DELETE FROM remote_hosts WHERE remote_host_id = ?" (Only remoteHostId) -insertRemoteCtrl :: DB.Connection -> Text -> C.KeyHash -> IO RemoteCtrlId -insertRemoteCtrl db displayName fingerprint = do +insertRemoteCtrl :: DB.Connection -> RemoteCtrlOOB -> IO RemoteCtrlInfo +insertRemoteCtrl db RemoteCtrlOOB {fingerprint, displayName} = do DB.execute db "INSERT INTO remote_controllers (display_name, fingerprint) VALUES (?,?)" (displayName, fingerprint) - fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" + remoteCtrlId <- insertedRowId db + pure RemoteCtrlInfo {remoteCtrlId, displayName, fingerprint, accepted = Nothing, sessionActive = False} getRemoteCtrls :: DB.Connection -> IO [RemoteCtrl] getRemoteCtrls db = diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d6826c877..51dcd0c6b 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -264,14 +264,14 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei CRNtfMessages {} -> [] CRRemoteHostCreated RemoteHostInfo {remoteHostId, remoteCtrlOOB} -> ("remote host " <> sShow remoteHostId <> " created") : viewRemoteCtrlOOBData remoteCtrlOOB CRRemoteHostList hs -> viewRemoteHosts hs - CRRemoteHostConnected rhId -> ["remote host " <> sShow rhId <> " connected"] + CRRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] CRRemoteHostStopped rhId -> ["remote host " <> sShow rhId <> " stopped"] CRRemoteCtrlList cs -> viewRemoteCtrls cs - CRRemoteCtrlRegistered rcId -> ["remote controller " <> sShow rcId <> " registered"] + CRRemoteCtrlRegistered RemoteCtrlInfo {remoteCtrlId = rcId} -> ["remote controller " <> sShow rcId <> " registered"] CRRemoteCtrlAnnounce fingerprint -> ["remote controller announced", "connection code:", plain $ strEncode fingerprint] CRRemoteCtrlFound rc -> ["remote controller found:", viewRemoteCtrl rc] - CRRemoteCtrlConnecting rcId rcName -> ["remote controller " <> sShow rcId <> " connecting to " <> plain rcName] - CRRemoteCtrlConnected rcId rcName -> ["remote controller " <> sShow rcId <> " connected, " <> plain rcName] + CRRemoteCtrlConnecting RemoteCtrlInfo {remoteCtrlId = rcId, displayName = rcName} -> ["remote controller " <> sShow rcId <> " connecting to " <> plain rcName] + CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, displayName = rcName} -> ["remote controller " <> sShow rcId <> " connected, " <> plain rcName] CRRemoteCtrlStopped -> ["remote controller stopped"] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> @@ -1633,8 +1633,8 @@ viewVersionInfo logLevel CoreVersionInfo {version, simplexmqVersion, simplexmqCo parens s = " (" <> s <> ")" viewRemoteCtrlOOBData :: RemoteCtrlOOB -> [StyledString] -viewRemoteCtrlOOBData RemoteCtrlOOB {caFingerprint} = - ["connection code:", plain $ strEncode caFingerprint] +viewRemoteCtrlOOBData RemoteCtrlOOB {fingerprint} = + ["connection code:", plain $ strEncode fingerprint] viewRemoteHosts :: [RemoteHostInfo] -> [StyledString] viewRemoteHosts = \case @@ -1653,8 +1653,8 @@ viewRemoteCtrls = \case plain $ tshow remoteCtrlId <> ". " <> displayName <> if sessionActive then " (active)" else "" -- TODO fingerprint, accepted? -viewRemoteCtrl :: RemoteCtrl -> StyledString -viewRemoteCtrl RemoteCtrl {remoteCtrlId, displayName} = +viewRemoteCtrl :: RemoteCtrlInfo -> StyledString +viewRemoteCtrl RemoteCtrlInfo {remoteCtrlId, displayName} = plain $ tshow remoteCtrlId <> ". " <> displayName viewChatError :: ChatLogLevel -> ChatError -> [StyledString]