core: update api (#3221)

This commit is contained in:
Evgeny Poberezkin 2023-10-15 00:18:04 +01:00 committed by GitHub
parent f5e9bd4f8b
commit 41b86e07f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 126 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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<RemoteHostInfo>? {
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<RemoteCtrlInfo>? {
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<Chat> = 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<UserRef?> { 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<UserRef?> { 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<RemoteHostInfo>): 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<RemoteCtrlInfo>): 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<UpMigration>, val agentMigrations: List<UpMigration>): 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 })}"

View File

@ -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_

View File

@ -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}

View File

@ -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

View File

@ -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)

View File

@ -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 =

View File

@ -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]