Merge branch 'master-ghc8107' into master-android
This commit is contained in:
commit
79e208193a
@ -83,7 +83,7 @@ final class ChatModel: ObservableObject {
|
|||||||
// current WebRTC call
|
// current WebRTC call
|
||||||
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
||||||
@Published var activeCall: Call?
|
@Published var activeCall: Call?
|
||||||
@Published var callCommand: WCallCommand?
|
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
||||||
@Published var showCallView = false
|
@Published var showCallView = false
|
||||||
// remote desktop
|
// remote desktop
|
||||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||||
|
@ -605,27 +605,29 @@ func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
|
func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? {
|
||||||
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
||||||
if let alert = alert {
|
if let alert = alert {
|
||||||
AlertManager.shared.showAlert(alert)
|
AlertManager.shared.showAlert(alert)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return connReqType
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) {
|
func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
|
||||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||||
logger.error("apiConnect: no current user")
|
logger.error("apiConnect: no current user")
|
||||||
return (nil, nil)
|
return (nil, nil)
|
||||||
}
|
}
|
||||||
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
|
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
|
||||||
|
let m = ChatModel.shared
|
||||||
switch r {
|
switch r {
|
||||||
case .sentConfirmation: return (.invitation, nil)
|
case let .sentConfirmation(_, connection):
|
||||||
case .sentInvitation: return (.contact, nil)
|
return ((.invitation, connection), nil)
|
||||||
|
case let .sentInvitation(_, connection):
|
||||||
|
return ((.contact, connection), nil)
|
||||||
case let .contactAlreadyExists(_, contact):
|
case let .contactAlreadyExists(_, contact):
|
||||||
let m = ChatModel.shared
|
|
||||||
if let c = m.getContactChat(contact.contactId) {
|
if let c = m.getContactChat(contact.contactId) {
|
||||||
await MainActor.run { m.chatId = c.id }
|
await MainActor.run { m.chatId = c.id }
|
||||||
}
|
}
|
||||||
@ -1362,18 +1364,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||||
switch res {
|
switch res {
|
||||||
case let .newContactConnection(user, connection):
|
|
||||||
if active(user) {
|
|
||||||
await MainActor.run {
|
|
||||||
m.updateContactConnection(connection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .contactConnectionDeleted(user, connection):
|
|
||||||
if active(user) {
|
|
||||||
await MainActor.run {
|
|
||||||
m.removeChat(connection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .contactDeletedByContact(user, contact):
|
case let .contactDeletedByContact(user, contact):
|
||||||
if active(user) && contact.directOrUsed {
|
if active(user) && contact.directOrUsed {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@ -1666,36 +1656,40 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
activateCall(invitation)
|
activateCall(invitation)
|
||||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
call.callState = .offerReceived
|
await MainActor.run {
|
||||||
call.peerMedia = callType.media
|
call.callState = .offerReceived
|
||||||
call.sharedKey = sharedKey
|
call.peerMedia = callType.media
|
||||||
|
call.sharedKey = sharedKey
|
||||||
|
}
|
||||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||||
let iceServers = getIceServers()
|
let iceServers = getIceServers()
|
||||||
logger.debug(".callOffer useRelay \(useRelay)")
|
logger.debug(".callOffer useRelay \(useRelay)")
|
||||||
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
||||||
m.callCommand = .offer(
|
await m.callCommand.processCommand(.offer(
|
||||||
offer: offer.rtcSession,
|
offer: offer.rtcSession,
|
||||||
iceCandidates: offer.rtcIceCandidates,
|
iceCandidates: offer.rtcIceCandidates,
|
||||||
media: callType.media, aesKey: sharedKey,
|
media: callType.media, aesKey: sharedKey,
|
||||||
iceServers: iceServers,
|
iceServers: iceServers,
|
||||||
relay: useRelay
|
relay: useRelay
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
case let .callAnswer(_, contact, answer):
|
case let .callAnswer(_, contact, answer):
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
call.callState = .answerReceived
|
await MainActor.run {
|
||||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
call.callState = .answerReceived
|
||||||
|
}
|
||||||
|
await m.callCommand.processCommand(.answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates))
|
||||||
}
|
}
|
||||||
case let .callExtraInfo(_, contact, extraInfo):
|
case let .callExtraInfo(_, contact, extraInfo):
|
||||||
await withCall(contact) { _ in
|
await withCall(contact) { _ in
|
||||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
await m.callCommand.processCommand(.ice(iceCandidates: extraInfo.rtcIceCandidates))
|
||||||
}
|
}
|
||||||
case let .callEnded(_, contact):
|
case let .callEnded(_, contact):
|
||||||
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
||||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||||
}
|
}
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
m.callCommand = .end
|
await m.callCommand.processCommand(.end)
|
||||||
CallController.shared.reportCallRemoteEnded(call: call)
|
CallController.shared.reportCallRemoteEnded(call: call)
|
||||||
}
|
}
|
||||||
case .chatSuspended:
|
case .chatSuspended:
|
||||||
@ -1753,9 +1747,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
logger.debug("unsupported event: \(res.responseType)")
|
logger.debug("unsupported event: \(res.responseType)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
|
func withCall(_ contact: Contact, _ perform: (Call) async -> Void) async {
|
||||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||||
await MainActor.run { perform(call) }
|
await perform(call)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,10 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
logger.debug("ActiveCallView: disappear")
|
logger.debug("ActiveCallView: disappear")
|
||||||
|
Task { await m.callCommand.setClient(nil) }
|
||||||
AppDelegate.keepScreenOn(false)
|
AppDelegate.keepScreenOn(false)
|
||||||
client?.endCall()
|
client?.endCall()
|
||||||
}
|
}
|
||||||
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
|
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
@ -60,19 +60,8 @@ struct ActiveCallView: View {
|
|||||||
private func createWebRTCClient() {
|
private func createWebRTCClient() {
|
||||||
if client == nil && canConnectCall {
|
if client == nil && canConnectCall {
|
||||||
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
|
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
|
||||||
sendCommandToClient()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendCommandToClient() {
|
|
||||||
if call == m.activeCall,
|
|
||||||
m.activeCall != nil,
|
|
||||||
let client = client,
|
|
||||||
let cmd = m.callCommand {
|
|
||||||
m.callCommand = nil
|
|
||||||
logger.debug("sendCallCommand: \(cmd.cmdType)")
|
|
||||||
Task {
|
Task {
|
||||||
await client.sendCallCommand(command: cmd)
|
await m.callCommand.setClient(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,8 +157,10 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
case let .error(message):
|
case let .error(message):
|
||||||
logger.debug("ActiveCallView: command error: \(message)")
|
logger.debug("ActiveCallView: command error: \(message)")
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Error"), message: Text(message)))
|
||||||
case let .invalid(type):
|
case let .invalid(type):
|
||||||
logger.debug("ActiveCallView: invalid response: \(type)")
|
logger.debug("ActiveCallView: invalid response: \(type)")
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Invalid response"), message: Text(type)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,7 +246,6 @@ struct ActiveCallOverlay: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text(call.encryptionStatus)
|
Text(call.encryptionStatus)
|
||||||
if let connInfo = call.connectionInfo {
|
if let connInfo = call.connectionInfo {
|
||||||
// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
|
|
||||||
Text("(") + Text(connInfo.text) + Text(")")
|
Text("(") + Text(connInfo.text) + Text(")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ class CallManager {
|
|||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
if let call = m.activeCall, call.callkitUUID == callUUID {
|
if let call = m.activeCall, call.callkitUUID == callUUID {
|
||||||
m.showCallView = true
|
m.showCallView = true
|
||||||
m.callCommand = .capabilities(media: call.localMedia)
|
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -57,19 +57,21 @@ class CallManager {
|
|||||||
m.activeCall = call
|
m.activeCall = call
|
||||||
m.showCallView = true
|
m.showCallView = true
|
||||||
|
|
||||||
m.callCommand = .start(
|
Task {
|
||||||
|
await m.callCommand.processCommand(.start(
|
||||||
media: invitation.callType.media,
|
media: invitation.callType.media,
|
||||||
aesKey: invitation.sharedKey,
|
aesKey: invitation.sharedKey,
|
||||||
iceServers: iceServers,
|
iceServers: iceServers,
|
||||||
relay: useRelay
|
relay: useRelay
|
||||||
)
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
||||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
m.callCommand = .media(media: media, enable: enable)
|
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -94,11 +96,13 @@ class CallManager {
|
|||||||
completed()
|
completed()
|
||||||
} else {
|
} else {
|
||||||
logger.debug("CallManager.endCall: ending call...")
|
logger.debug("CallManager.endCall: ending call...")
|
||||||
m.callCommand = .end
|
|
||||||
m.activeCall = nil
|
|
||||||
m.showCallView = false
|
|
||||||
completed()
|
|
||||||
Task {
|
Task {
|
||||||
|
await m.callCommand.processCommand(.end)
|
||||||
|
await MainActor.run {
|
||||||
|
m.activeCall = nil
|
||||||
|
m.showCallView = false
|
||||||
|
completed()
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
try await apiEndCall(call.contact)
|
try await apiEndCall(call.contact)
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -335,6 +335,50 @@ extension WCallResponse: Encodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor WebRTCCommandProcessor {
|
||||||
|
private var client: WebRTCClient? = nil
|
||||||
|
private var commands: [WCallCommand] = []
|
||||||
|
private var running: Bool = false
|
||||||
|
|
||||||
|
func setClient(_ client: WebRTCClient?) async {
|
||||||
|
logger.debug("WebRTC: setClient, commands count \(self.commands.count)")
|
||||||
|
self.client = client
|
||||||
|
if client != nil {
|
||||||
|
await processAllCommands()
|
||||||
|
} else {
|
||||||
|
commands.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processCommand(_ c: WCallCommand) async {
|
||||||
|
// logger.debug("WebRTC: process command \(c.cmdType)")
|
||||||
|
commands.append(c)
|
||||||
|
if !running && client != nil {
|
||||||
|
await processAllCommands()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAllCommands() async {
|
||||||
|
logger.debug("WebRTC: process all commands, commands count \(self.commands.count), client == nil \(self.client == nil)")
|
||||||
|
if let client = client {
|
||||||
|
running = true
|
||||||
|
while let c = commands.first, shouldRunCommand(client, c) {
|
||||||
|
commands.remove(at: 0)
|
||||||
|
await client.sendCallCommand(command: c)
|
||||||
|
logger.debug("WebRTC: processed cmd \(c.cmdType)")
|
||||||
|
}
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
|
||||||
|
switch c {
|
||||||
|
case .capabilities, .start, .offer, .end: true
|
||||||
|
default: client.activeCall.wrappedValue != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ConnectionState: Codable, Equatable {
|
struct ConnectionState: Codable, Equatable {
|
||||||
var connectionState: String
|
var connectionState: String
|
||||||
var iceConnectionState: String
|
var iceConnectionState: String
|
||||||
@ -358,26 +402,12 @@ struct ConnectionInfo: Codable, Equatable {
|
|||||||
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
|
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocolText: String {
|
|
||||||
let unknown = NSLocalizedString("unknown", comment: "connection info")
|
|
||||||
let local = localCandidate?.protocol?.uppercased() ?? unknown
|
|
||||||
let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown
|
|
||||||
let remote = remoteCandidate?.protocol?.uppercased() ?? unknown
|
|
||||||
let localText = localRelay == local || localCandidate?.relayProtocol == nil
|
|
||||||
? local
|
|
||||||
: "\(local) (\(localRelay))"
|
|
||||||
return local == remote
|
|
||||||
? localText
|
|
||||||
: "\(localText) / \(remote)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||||
struct RTCIceCandidate: Codable, Equatable {
|
struct RTCIceCandidate: Codable, Equatable {
|
||||||
var candidateType: RTCIceCandidateType?
|
var candidateType: RTCIceCandidateType?
|
||||||
var `protocol`: String?
|
var `protocol`: String?
|
||||||
var relayProtocol: String?
|
|
||||||
var sdpMid: String?
|
var sdpMid: String?
|
||||||
var sdpMLineIndex: Int?
|
var sdpMLineIndex: Int?
|
||||||
var candidate: String
|
var candidate: String
|
||||||
|
@ -21,7 +21,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
|
|
||||||
struct Call {
|
struct Call {
|
||||||
var connection: RTCPeerConnection
|
var connection: RTCPeerConnection
|
||||||
var iceCandidates: [RTCIceCandidate]
|
var iceCandidates: IceCandidates
|
||||||
var localMedia: CallMediaType
|
var localMedia: CallMediaType
|
||||||
var localCamera: RTCVideoCapturer?
|
var localCamera: RTCVideoCapturer?
|
||||||
var localVideoSource: RTCVideoSource?
|
var localVideoSource: RTCVideoSource?
|
||||||
@ -33,10 +33,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
var frameDecryptor: RTCFrameDecryptor?
|
var frameDecryptor: RTCFrameDecryptor?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor IceCandidates {
|
||||||
|
private var candidates: [RTCIceCandidate] = []
|
||||||
|
|
||||||
|
func getAndClear() async -> [RTCIceCandidate] {
|
||||||
|
let cs = candidates
|
||||||
|
candidates = []
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ c: RTCIceCandidate) async {
|
||||||
|
candidates.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||||
private let audioQueue = DispatchQueue(label: "audio")
|
private let audioQueue = DispatchQueue(label: "audio")
|
||||||
private var sendCallResponse: (WVAPIMessage) async -> Void
|
private var sendCallResponse: (WVAPIMessage) async -> Void
|
||||||
private var activeCall: Binding<Call?>
|
var activeCall: Binding<Call?>
|
||||||
private var localRendererAspectRatio: Binding<CGFloat?>
|
private var localRendererAspectRatio: Binding<CGFloat?>
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@ -60,7 +74,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||||
]
|
]
|
||||||
|
|
||||||
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ remoteIceCandidates: [RTCIceCandidate], _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
||||||
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
|
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
|
||||||
connection.delegate = self
|
connection.delegate = self
|
||||||
createAudioSender(connection)
|
createAudioSender(connection)
|
||||||
@ -87,7 +101,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
}
|
}
|
||||||
return Call(
|
return Call(
|
||||||
connection: connection,
|
connection: connection,
|
||||||
iceCandidates: remoteIceCandidates,
|
iceCandidates: IceCandidates(),
|
||||||
localMedia: mediaType,
|
localMedia: mediaType,
|
||||||
localCamera: localCamera,
|
localCamera: localCamera,
|
||||||
localVideoSource: localVideoSource,
|
localVideoSource: localVideoSource,
|
||||||
@ -144,26 +158,18 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
logger.debug("starting incoming call - create webrtc session")
|
logger.debug("starting incoming call - create webrtc session")
|
||||||
if activeCall.wrappedValue != nil { endCall() }
|
if activeCall.wrappedValue != nil { endCall() }
|
||||||
let encryption = WebRTCClient.enableEncryption
|
let encryption = WebRTCClient.enableEncryption
|
||||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), [], media, encryption ? aesKey : nil, relay)
|
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
|
||||||
activeCall.wrappedValue = call
|
activeCall.wrappedValue = call
|
||||||
call.connection.offer { answer in
|
let (offer, error) = await call.connection.offer()
|
||||||
Task {
|
if let offer = offer {
|
||||||
let gotCandidates = await self.waitWithTimeout(10_000, stepMs: 1000, until: { self.activeCall.wrappedValue?.iceCandidates.count ?? 0 > 0 })
|
resp = .offer(
|
||||||
if gotCandidates {
|
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
|
||||||
await self.sendCallResponse(.init(
|
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
|
||||||
corrId: nil,
|
capabilities: CallCapabilities(encryption: encryption)
|
||||||
resp: .offer(
|
)
|
||||||
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
self.waitForMoreIceCandidates()
|
||||||
iceCandidates: compressToBase64(input: encodeJSON(self.activeCall.wrappedValue?.iceCandidates ?? [])),
|
} else {
|
||||||
capabilities: CallCapabilities(encryption: encryption)
|
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
|
||||||
),
|
|
||||||
command: command)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.endCall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
||||||
if activeCall.wrappedValue != nil {
|
if activeCall.wrappedValue != nil {
|
||||||
@ -172,26 +178,21 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
resp = .error(message: "accept: encryption is not supported")
|
resp = .error(message: "accept: encryption is not supported")
|
||||||
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
|
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
|
||||||
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
||||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), remoteIceCandidates, media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
|
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
|
||||||
activeCall.wrappedValue = call
|
activeCall.wrappedValue = call
|
||||||
let pc = call.connection
|
let pc = call.connection
|
||||||
if let type = offer.type, let sdp = offer.sdp {
|
if let type = offer.type, let sdp = offer.sdp {
|
||||||
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
||||||
pc.answer { answer in
|
let (answer, error) = await pc.answer()
|
||||||
|
if let answer = answer {
|
||||||
self.addIceCandidates(pc, remoteIceCandidates)
|
self.addIceCandidates(pc, remoteIceCandidates)
|
||||||
// Task {
|
resp = .answer(
|
||||||
// try? await Task.sleep(nanoseconds: 32_000 * 1000000)
|
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
||||||
Task {
|
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates()))
|
||||||
await self.sendCallResponse(.init(
|
)
|
||||||
corrId: nil,
|
self.waitForMoreIceCandidates()
|
||||||
resp: .answer(
|
} else {
|
||||||
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
resp = .error(message: "answer error: \(error?.localizedDescription ?? "unknown error")")
|
||||||
iceCandidates: compressToBase64(input: encodeJSON(call.iceCandidates))
|
|
||||||
),
|
|
||||||
command: command)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resp = .error(message: "accept: remote description is not set")
|
resp = .error(message: "accept: remote description is not set")
|
||||||
@ -234,6 +235,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
resp = .ok
|
resp = .ok
|
||||||
}
|
}
|
||||||
case .end:
|
case .end:
|
||||||
|
// TODO possibly, endCall should be called before returning .ok
|
||||||
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
|
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
|
||||||
endCall()
|
endCall()
|
||||||
}
|
}
|
||||||
@ -242,6 +244,33 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInitialIceCandidates() async -> [RTCIceCandidate] {
|
||||||
|
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
|
||||||
|
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
|
||||||
|
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForMoreIceCandidates() {
|
||||||
|
Task {
|
||||||
|
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
|
||||||
|
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
|
||||||
|
if candidates.count > 0 {
|
||||||
|
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
|
||||||
|
await self.sendIceCandidates(candidates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendIceCandidates(_ candidates: [RTCIceCandidate]) async {
|
||||||
|
await self.sendCallResponse(.init(
|
||||||
|
corrId: nil,
|
||||||
|
resp: .ice(iceCandidates: compressToBase64(input: encodeJSON(candidates))),
|
||||||
|
command: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
|
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
|
||||||
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
|
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
|
||||||
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
|
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
|
||||||
@ -387,12 +416,13 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
audioSessionToDefaults()
|
audioSessionToDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitWithTimeout(_ timeoutMs: UInt64, stepMs: UInt64, until success: () -> Bool) async -> Bool {
|
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
|
||||||
let startedAt = DispatchTime.now()
|
var t: UInt64 = 0
|
||||||
while !success() && startedAt.uptimeNanoseconds + timeoutMs * 1000000 > DispatchTime.now().uptimeNanoseconds {
|
repeat {
|
||||||
guard let _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) else { break }
|
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
|
||||||
}
|
t += stepMs
|
||||||
return success()
|
await action()
|
||||||
|
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,25 +435,33 @@ extension WebRTC.RTCPeerConnection {
|
|||||||
optionalConstraints: nil)
|
optionalConstraints: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func offer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
func offer() async -> (RTCSessionDescription?, Error?) {
|
||||||
offer(for: mediaConstraints()) { (sdp, error) in
|
await withCheckedContinuation { cont in
|
||||||
guard let sdp = sdp else {
|
offer(for: mediaConstraints()) { (sdp, error) in
|
||||||
return
|
self.processSDP(cont, sdp, error)
|
||||||
}
|
}
|
||||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
|
||||||
completion(sdp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func answer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
func answer() async -> (RTCSessionDescription?, Error?) {
|
||||||
answer(for: mediaConstraints()) { (sdp, error) in
|
await withCheckedContinuation { cont in
|
||||||
guard let sdp = sdp else {
|
answer(for: mediaConstraints()) { (sdp, error) in
|
||||||
return
|
self.processSDP(cont, sdp, error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processSDP(_ cont: CheckedContinuation<(RTCSessionDescription?, Error?), Never>, _ sdp: RTCSessionDescription?, _ error: Error?) {
|
||||||
|
if let sdp = sdp {
|
||||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
self.setLocalDescription(sdp, completionHandler: { (error) in
|
||||||
completion(sdp)
|
if let error = error {
|
||||||
|
cont.resume(returning: (nil, error))
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: (sdp, nil))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: (nil, error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -479,6 +517,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
default: enableSpeaker = false
|
default: enableSpeaker = false
|
||||||
}
|
}
|
||||||
setSpeakerEnabledAndConfigureSession(enableSpeaker)
|
setSpeakerEnabledAndConfigureSession(enableSpeaker)
|
||||||
|
case .connected: sendConnectedEvent(connection)
|
||||||
case .disconnected, .failed: endCall()
|
case .disconnected, .failed: endCall()
|
||||||
default: do {}
|
default: do {}
|
||||||
}
|
}
|
||||||
@ -491,7 +530,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
|
|
||||||
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
|
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
|
||||||
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
|
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
|
||||||
activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil, nil))
|
Task {
|
||||||
|
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
|
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
|
||||||
@ -506,10 +547,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
lastReceivedMs lastDataReceivedMs: Int32,
|
lastReceivedMs lastDataReceivedMs: Int32,
|
||||||
changeReason reason: String) {
|
changeReason reason: String) {
|
||||||
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
|
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
|
||||||
sendConnectedEvent(connection, local: local, remote: remote)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection, local: WebRTC.RTCIceCandidate, remote: WebRTC.RTCIceCandidate) {
|
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection) {
|
||||||
connection.statistics { (stats: RTCStatisticsReport) in
|
connection.statistics { (stats: RTCStatisticsReport) in
|
||||||
stats.statistics.values.forEach { stat in
|
stats.statistics.values.forEach { stat in
|
||||||
// logger.debug("Stat \(stat.debugDescription)")
|
// logger.debug("Stat \(stat.debugDescription)")
|
||||||
@ -517,24 +557,25 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
let localId = stat.values["localCandidateId"] as? String,
|
let localId = stat.values["localCandidateId"] as? String,
|
||||||
let remoteId = stat.values["remoteCandidateId"] as? String,
|
let remoteId = stat.values["remoteCandidateId"] as? String,
|
||||||
let localStats = stats.statistics[localId],
|
let localStats = stats.statistics[localId],
|
||||||
let remoteStats = stats.statistics[remoteId],
|
let remoteStats = stats.statistics[remoteId]
|
||||||
local.sdp.contains("\((localStats.values["ip"] as? String ?? "--")) \((localStats.values["port"] as? String ?? "--"))") &&
|
|
||||||
remote.sdp.contains("\((remoteStats.values["ip"] as? String ?? "--")) \((remoteStats.values["port"] as? String ?? "--"))")
|
|
||||||
{
|
{
|
||||||
Task {
|
Task {
|
||||||
await self.sendCallResponse(.init(
|
await self.sendCallResponse(.init(
|
||||||
corrId: nil,
|
corrId: nil,
|
||||||
resp: .connected(connectionInfo: ConnectionInfo(
|
resp: .connected(connectionInfo: ConnectionInfo(
|
||||||
localCandidate: local.toCandidate(
|
localCandidate: RTCIceCandidate(
|
||||||
RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
|
candidateType: RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
|
||||||
localStats.values["protocol"] as? String,
|
protocol: localStats.values["protocol"] as? String,
|
||||||
localStats.values["relayProtocol"] as? String
|
sdpMid: nil,
|
||||||
|
sdpMLineIndex: nil,
|
||||||
|
candidate: ""
|
||||||
),
|
),
|
||||||
remoteCandidate: remote.toCandidate(
|
remoteCandidate: RTCIceCandidate(
|
||||||
RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
|
candidateType: RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
|
||||||
remoteStats.values["protocol"] as? String,
|
protocol: remoteStats.values["protocol"] as? String,
|
||||||
remoteStats.values["relayProtocol"] as? String
|
sdpMid: nil,
|
||||||
))),
|
sdpMLineIndex: nil,
|
||||||
|
candidate: ""))),
|
||||||
command: nil)
|
command: nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -634,11 +675,10 @@ extension RTCIceCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension WebRTC.RTCIceCandidate {
|
extension WebRTC.RTCIceCandidate {
|
||||||
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?, _ relayProtocol: String?) -> RTCIceCandidate {
|
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?) -> RTCIceCandidate {
|
||||||
RTCIceCandidate(
|
RTCIceCandidate(
|
||||||
candidateType: candidateType,
|
candidateType: candidateType,
|
||||||
protocol: `protocol`,
|
protocol: `protocol`,
|
||||||
relayProtocol: relayProtocol,
|
|
||||||
sdpMid: sdpMid,
|
sdpMid: sdpMid,
|
||||||
sdpMLineIndex: Int(sdpMLineIndex),
|
sdpMLineIndex: Int(sdpMLineIndex),
|
||||||
candidate: sdp
|
candidate: sdp
|
||||||
|
@ -73,6 +73,7 @@ struct CreateLinkView: View {
|
|||||||
Task {
|
Task {
|
||||||
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
m.updateContactConnection(pcc)
|
||||||
connReqInvitation = connReq
|
connReqInvitation = connReq
|
||||||
contactConnection = pcc
|
contactConnection = pcc
|
||||||
m.connReqInv = connReq
|
m.connReqInv = connReq
|
||||||
|
@ -52,6 +52,9 @@ struct NewChatButton: View {
|
|||||||
func addContactAction() {
|
func addContactAction() {
|
||||||
Task {
|
Task {
|
||||||
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.updateContactConnection(pcc)
|
||||||
|
}
|
||||||
actionSheet = .createLink(link: connReq, connection: pcc)
|
actionSheet = .createLink(link: connReq, connection: pcc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,7 +349,10 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn
|
|||||||
|
|
||||||
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
||||||
Task {
|
Task {
|
||||||
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.updateContactConnection(pcc)
|
||||||
|
}
|
||||||
let crt: ConnReqType
|
let crt: ConnReqType
|
||||||
if let plan = connectionPlan {
|
if let plan = connectionPlan {
|
||||||
crt = planToConnReqType(plan)
|
crt = planToConnReqType(plan)
|
||||||
|
@ -505,8 +505,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
||||||
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
||||||
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
|
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
|
||||||
case sentConfirmation(user: UserRef)
|
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
|
||||||
case sentInvitation(user: UserRef)
|
case sentInvitation(user: UserRef, connection: PendingContactConnection)
|
||||||
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
|
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
|
||||||
case contactAlreadyExists(user: UserRef, contact: Contact)
|
case contactAlreadyExists(user: UserRef, contact: Contact)
|
||||||
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
||||||
@ -605,7 +605,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case ntfTokenStatus(status: NtfTknStatus)
|
case ntfTokenStatus(status: NtfTknStatus)
|
||||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||||
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||||
case newContactConnection(user: UserRef, connection: PendingContactConnection)
|
|
||||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||||
// remote desktop responses/events
|
// remote desktop responses/events
|
||||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||||
@ -752,7 +751,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||||
case .ntfToken: return "ntfToken"
|
case .ntfToken: return "ntfToken"
|
||||||
case .ntfMessages: return "ntfMessages"
|
case .ntfMessages: return "ntfMessages"
|
||||||
case .newContactConnection: return "newContactConnection"
|
|
||||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||||
case .remoteCtrlList: return "remoteCtrlList"
|
case .remoteCtrlList: return "remoteCtrlList"
|
||||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||||
@ -803,11 +801,11 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||||
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
|
case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)")
|
||||||
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||||
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
|
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
|
||||||
case .sentConfirmation: return noDetails
|
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
|
||||||
case .sentInvitation: return noDetails
|
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
|
||||||
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
||||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||||
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
||||||
@ -900,7 +898,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .ntfTokenStatus(status): return String(describing: status)
|
case let .ntfTokenStatus(status): return String(describing: status)
|
||||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
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 .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
||||||
|
@ -370,7 +370,6 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
|
|||||||
InfoText(call.callState.text)
|
InfoText(call.callState.text)
|
||||||
|
|
||||||
val connInfo = call.connectionInfo
|
val connInfo = call.connectionInfo
|
||||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
|
||||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||||
InfoText(call.encryptionStatus + connInfoText)
|
InfoText(call.encryptionStatus + connInfoText)
|
||||||
}
|
}
|
||||||
@ -585,8 +584,8 @@ fun PreviewActiveCallOverlayVideo() {
|
|||||||
localMedia = CallMediaType.Video,
|
localMedia = CallMediaType.Video,
|
||||||
peerMedia = CallMediaType.Video,
|
peerMedia = CallMediaType.Video,
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
speakerCanBeEnabled = true,
|
speakerCanBeEnabled = true,
|
||||||
@ -611,8 +610,8 @@ fun PreviewActiveCallOverlayAudio() {
|
|||||||
localMedia = CallMediaType.Audio,
|
localMedia = CallMediaType.Audio,
|
||||||
peerMedia = CallMediaType.Audio,
|
peerMedia = CallMediaType.Audio,
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
speakerCanBeEnabled = true,
|
speakerCanBeEnabled = true,
|
||||||
|
@ -173,6 +173,8 @@ class AppPreferences {
|
|||||||
val connectRemoteViaMulticastAuto = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO, true)
|
val connectRemoteViaMulticastAuto = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO, true)
|
||||||
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
||||||
|
|
||||||
|
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||||
|
|
||||||
private fun mkIntPreference(prefName: String, default: Int) =
|
private fun mkIntPreference(prefName: String, default: Int) =
|
||||||
SharedPreference(
|
SharedPreference(
|
||||||
get = fun() = settings.getInt(prefName, default),
|
get = fun() = settings.getInt(prefName, default),
|
||||||
@ -317,6 +319,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
|
||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||||
|
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,20 +894,21 @@ object ChatController {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): Boolean {
|
suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): PendingContactConnection? {
|
||||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||||
Log.e(TAG, "apiConnect: no current user")
|
Log.e(TAG, "apiConnect: no current user")
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq))
|
val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq))
|
||||||
when {
|
when {
|
||||||
r is CR.SentConfirmation || r is CR.SentInvitation -> return true
|
r is CR.SentConfirmation -> return r.connection
|
||||||
|
r is CR.SentInvitation -> return r.connection
|
||||||
r is CR.ContactAlreadyExists -> {
|
r is CR.ContactAlreadyExists -> {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
generalGetString(MR.strings.contact_already_exists),
|
generalGetString(MR.strings.contact_already_exists),
|
||||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName)
|
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
||||||
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
||||||
@ -912,7 +916,7 @@ object ChatController {
|
|||||||
generalGetString(MR.strings.invalid_connection_link),
|
generalGetString(MR.strings.invalid_connection_link),
|
||||||
generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one)
|
generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||||
&& r.chatError.agentError is AgentErrorType.SMP
|
&& r.chatError.agentError is AgentErrorType.SMP
|
||||||
@ -921,13 +925,13 @@ object ChatController {
|
|||||||
generalGetString(MR.strings.connection_error_auth),
|
generalGetString(MR.strings.connection_error_auth),
|
||||||
generalGetString(MR.strings.connection_error_auth_desc)
|
generalGetString(MR.strings.connection_error_auth_desc)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (!(networkErrorAlert(r))) {
|
if (!(networkErrorAlert(r))) {
|
||||||
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
||||||
}
|
}
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1526,16 +1530,6 @@ object ChatController {
|
|||||||
fun active(user: UserLike): Boolean = activeUser(rhId, user)
|
fun active(user: UserLike): Boolean = activeUser(rhId, user)
|
||||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.NewContactConnection -> {
|
|
||||||
if (active(r.user)) {
|
|
||||||
chatModel.updateContactConnection(rhId, r.connection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CR.ContactConnectionDeleted -> {
|
|
||||||
if (active(r.user)) {
|
|
||||||
chatModel.removeChat(rhId, r.connection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CR.ContactDeletedByContact -> {
|
is CR.ContactDeletedByContact -> {
|
||||||
if (active(r.user) && r.contact.directOrUsed) {
|
if (active(r.user) && r.contact.directOrUsed) {
|
||||||
chatModel.updateContact(rhId, r.contact)
|
chatModel.updateContact(rhId, r.contact)
|
||||||
@ -3707,8 +3701,8 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
||||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR()
|
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR()
|
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
|
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
|
||||||
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
||||||
@ -3802,7 +3796,6 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
||||||
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
||||||
@Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): 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()
|
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
// remote events (desktop)
|
// remote events (desktop)
|
||||||
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
||||||
@ -3951,7 +3944,6 @@ sealed class CR {
|
|||||||
is CallAnswer -> "callAnswer"
|
is CallAnswer -> "callAnswer"
|
||||||
is CallExtraInfo -> "callExtraInfo"
|
is CallExtraInfo -> "callExtraInfo"
|
||||||
is CallEnded -> "callEnded"
|
is CallEnded -> "callEnded"
|
||||||
is NewContactConnection -> "newContactConnection"
|
|
||||||
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
||||||
is RemoteHostList -> "remoteHostList"
|
is RemoteHostList -> "remoteHostList"
|
||||||
is CurrentRemoteHost -> "currentRemoteHost"
|
is CurrentRemoteHost -> "currentRemoteHost"
|
||||||
@ -4006,11 +3998,11 @@ sealed class CR {
|
|||||||
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
||||||
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
||||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||||
is Invitation -> withUser(user, connReqInvitation)
|
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
||||||
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||||
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
||||||
is SentConfirmation -> withUser(user, noDetails())
|
is SentConfirmation -> withUser(user, json.encodeToString(connection))
|
||||||
is SentInvitation -> withUser(user, noDetails())
|
is SentInvitation -> withUser(user, json.encodeToString(connection))
|
||||||
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
|
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
||||||
@ -4098,7 +4090,6 @@ sealed class CR {
|
|||||||
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
||||||
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
||||||
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
||||||
is NewContactConnection -> withUser(user, json.encodeToString(connection))
|
|
||||||
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
||||||
// remote events (mobile)
|
// remote events (mobile)
|
||||||
is RemoteHostList -> json.encodeToString(remoteHosts)
|
is RemoteHostList -> json.encodeToString(remoteHosts)
|
||||||
|
@ -127,18 +127,10 @@ sealed class WCallResponse {
|
|||||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val protocolText: String get() {
|
|
||||||
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
|
|
||||||
return if (local == remote) localText else "$localText / $remote"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||||
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
|
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?)
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ private fun createInvitation(
|
|||||||
withApi {
|
withApi {
|
||||||
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
|
m.updateContactConnection(rhId, r.second)
|
||||||
connReqInvitation.value = r.first
|
connReqInvitation.value = r.first
|
||||||
contactConnection.value = r.second
|
contactConnection.value = r.second
|
||||||
} else {
|
} else {
|
||||||
|
@ -283,10 +283,11 @@ suspend fun connectViaUri(
|
|||||||
incognito: Boolean,
|
incognito: Boolean,
|
||||||
connectionPlan: ConnectionPlan?,
|
connectionPlan: ConnectionPlan?,
|
||||||
close: (() -> Unit)?
|
close: (() -> Unit)?
|
||||||
): Boolean {
|
) {
|
||||||
val r = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||||
if (r) {
|
if (pcc != null) {
|
||||||
|
chatModel.updateContactConnection(rhId, pcc)
|
||||||
close?.invoke()
|
close?.invoke()
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.connection_request_sent),
|
title = generalGetString(MR.strings.connection_request_sent),
|
||||||
@ -299,7 +300,6 @@ suspend fun connectViaUri(
|
|||||||
hostDevice = hostDevice(rhId),
|
hostDevice = hostDevice(rhId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
|
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
|
||||||
|
@ -15,7 +15,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.desktopPlatform
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.common.views.TerminalView
|
import chat.simplex.common.views.TerminalView
|
||||||
@ -31,10 +30,30 @@ import java.io.File
|
|||||||
val simplexWindowState = SimplexWindowState()
|
val simplexWindowState = SimplexWindowState()
|
||||||
|
|
||||||
fun showApp() = application {
|
fun showApp() = application {
|
||||||
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
// Creates file if not exists; comes with proper defaults
|
||||||
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
val state = getStoredWindowState()
|
||||||
val width = if (desktopPlatform.isLinux()) 1376.dp else 1366.dp
|
|
||||||
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = width, height = 768.dp)
|
val windowState: WindowState = rememberWindowState(
|
||||||
|
placement = WindowPlacement.Floating,
|
||||||
|
width = state.width.dp,
|
||||||
|
height = state.height.dp,
|
||||||
|
position = WindowPosition(state.x.dp, state.y.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
windowState.position.x.value,
|
||||||
|
windowState.position.y.value,
|
||||||
|
windowState.size.width.value,
|
||||||
|
windowState.size.height.value
|
||||||
|
) {
|
||||||
|
storeWindowState(WindowPositionSize(
|
||||||
|
x = windowState.position.x.value.toInt(),
|
||||||
|
y = windowState.position.y.value.toInt(),
|
||||||
|
width = windowState.size.width.value.toInt(),
|
||||||
|
height = windowState.size.height.value.toInt()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
simplexWindowState.windowState = windowState
|
simplexWindowState.windowState = windowState
|
||||||
// Reload all strings in all @Composable's after language change at runtime
|
// Reload all strings in all @Composable's after language change at runtime
|
||||||
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package chat.simplex.common
|
||||||
|
|
||||||
|
import chat.simplex.common.model.json
|
||||||
|
import chat.simplex.common.platform.appPreferences
|
||||||
|
import chat.simplex.common.platform.desktopPlatform
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WindowPositionSize(
|
||||||
|
val width: Int = 1366,
|
||||||
|
val height: Int = 768,
|
||||||
|
val x: Int = 0,
|
||||||
|
val y: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getStoredWindowState(): WindowPositionSize =
|
||||||
|
try {
|
||||||
|
val str = appPreferences.desktopWindowState.get()
|
||||||
|
var state = if (str == null) {
|
||||||
|
WindowPositionSize()
|
||||||
|
} else {
|
||||||
|
json.decodeFromString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
||||||
|
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
||||||
|
if (desktopPlatform.isLinux() && state.width == 1366) {
|
||||||
|
state = state.copy(width = 1376)
|
||||||
|
}
|
||||||
|
state
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
WindowPositionSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun storeWindowState(state: WindowPositionSize) =
|
||||||
|
appPreferences.desktopWindowState.set(json.encodeToString(state))
|
@ -136,7 +136,6 @@ private fun SendStateUpdates() {
|
|||||||
.collect { call ->
|
.collect { call ->
|
||||||
val state = call.callState.text
|
val state = call.callState.text
|
||||||
val connInfo = call.connectionInfo
|
val connInfo = call.connectionInfo
|
||||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
|
||||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||||
val description = call.encryptionStatus + connInfoText
|
val description = call.encryptionStatus + connInfoText
|
||||||
chatModel.callCommand.add(WCallCommand.Description(state, description))
|
chatModel.callCommand.add(WCallCommand.Description(state, description))
|
||||||
|
@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 281bdebcb82aed4c8c2c08438b9cafc7908183a1
|
tag: 6bffcc8503e5193d57e543ac0100712a8e27d454
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
@ -86,7 +86,6 @@ export type ChatResponse =
|
|||||||
| CRGroupUpdated
|
| CRGroupUpdated
|
||||||
| CRUserContactLinkSubscribed
|
| CRUserContactLinkSubscribed
|
||||||
| CRUserContactLinkSubError
|
| CRUserContactLinkSubError
|
||||||
| CRNewContactConnection
|
|
||||||
| CRContactConnectionDeleted
|
| CRContactConnectionDeleted
|
||||||
| CRMessageError
|
| CRMessageError
|
||||||
| CRChatCmdError
|
| CRChatCmdError
|
||||||
@ -731,12 +730,6 @@ export interface CRUserContactLinkSubError extends CR {
|
|||||||
chatError: ChatError
|
chatError: ChatError
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CRNewContactConnection extends CR {
|
|
||||||
type: "newContactConnection"
|
|
||||||
user: User
|
|
||||||
connection: PendingContactConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CRContactConnectionDeleted extends CR {
|
export interface CRContactConnectionDeleted extends CR {
|
||||||
type: "contactConnectionDeleted"
|
type: "contactConnectionDeleted"
|
||||||
user: User
|
user: User
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."281bdebcb82aed4c8c2c08438b9cafc7908183a1" = "0dly5rnpcnb7mbfxgpxna5xbabk6n0dh5qz53nm4l93gzdy18hpb";
|
"https://github.com/simplex-chat/simplexmq.git"."6bffcc8503e5193d57e543ac0100712a8e27d454" = "131kdcvh01985lnf4azss4rg7swpjjh647c29m95b33hd1f7mf17";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6";
|
"https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||||
|
@ -124,6 +124,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20231107_indexes
|
Simplex.Chat.Migrations.M20231107_indexes
|
||||||
Simplex.Chat.Migrations.M20231113_group_forward
|
Simplex.Chat.Migrations.M20231113_group_forward
|
||||||
Simplex.Chat.Migrations.M20231114_remote_control
|
Simplex.Chat.Migrations.M20231114_remote_control
|
||||||
|
Simplex.Chat.Migrations.M20231126_remote_ctrl_address
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
|
@ -103,6 +103,7 @@ import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
|||||||
import Simplex.Messaging.Util
|
import Simplex.Messaging.Util
|
||||||
import Simplex.Messaging.Version
|
import Simplex.Messaging.Version
|
||||||
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
|
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
|
||||||
|
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||||
import System.Exit (ExitCode, exitFailure, exitSuccess)
|
import System.Exit (ExitCode, exitFailure, exitSuccess)
|
||||||
import System.FilePath (takeFileName, (</>))
|
import System.FilePath (takeFileName, (</>))
|
||||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
|
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
|
||||||
@ -1401,7 +1402,6 @@ processChatCommand = \case
|
|||||||
subMode <- chatReadVar subscriptionMode
|
subMode <- chatReadVar subscriptionMode
|
||||||
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
|
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
|
||||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode
|
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode
|
||||||
toView $ CRNewContactConnection user conn
|
|
||||||
pure $ CRInvitation user cReq conn
|
pure $ CRInvitation user cReq conn
|
||||||
AddContact incognito -> withUser $ \User {userId} ->
|
AddContact incognito -> withUser $ \User {userId} ->
|
||||||
processChatCommand $ APIAddContact userId incognito
|
processChatCommand $ APIAddContact userId incognito
|
||||||
@ -1431,8 +1431,7 @@ processChatCommand = \case
|
|||||||
dm <- directMessage $ XInfo profileToSend
|
dm <- directMessage $ XInfo profileToSend
|
||||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode
|
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode
|
||||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode
|
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode
|
||||||
toView $ CRNewContactConnection user conn
|
pure $ CRSentConfirmation user conn
|
||||||
pure $ CRSentConfirmation user
|
|
||||||
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
|
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
|
||||||
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
||||||
Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do
|
Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do
|
||||||
@ -1964,16 +1963,16 @@ processChatCommand = \case
|
|||||||
let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_
|
let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_
|
||||||
updateGroupProfileByName gName $ \p ->
|
updateGroupProfileByName gName $ \p ->
|
||||||
p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p}
|
p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p}
|
||||||
SetLocalDeviceName name -> withUser_ $ chatWriteVar localDeviceName name >> ok_
|
SetLocalDeviceName name -> chatWriteVar localDeviceName name >> ok_
|
||||||
ListRemoteHosts -> withUser_ $ CRRemoteHostList <$> listRemoteHosts
|
ListRemoteHosts -> CRRemoteHostList <$> listRemoteHosts
|
||||||
SwitchRemoteHost rh_ -> withUser_ $ CRCurrentRemoteHost <$> switchRemoteHost rh_
|
SwitchRemoteHost rh_ -> CRCurrentRemoteHost <$> switchRemoteHost rh_
|
||||||
StartRemoteHost rh_ -> withUser_ $ do
|
StartRemoteHost rh_ ca_ bp_ -> do
|
||||||
(remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_
|
(localAddrs, remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_ ca_ bp_
|
||||||
pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port}
|
pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port, localAddrs}
|
||||||
StopRemoteHost rh_ -> withUser_ $ closeRemoteHost rh_ >> ok_
|
StopRemoteHost rh_ -> closeRemoteHost rh_ >> ok_
|
||||||
DeleteRemoteHost rh -> withUser_ $ deleteRemoteHost rh >> ok_
|
DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_
|
||||||
StoreRemoteFile rh encrypted_ localPath -> withUser_ $ CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath
|
StoreRemoteFile rh encrypted_ localPath -> CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath
|
||||||
GetRemoteFile rh rf -> withUser_ $ getRemoteFile rh rf >> ok_
|
GetRemoteFile rh rf -> getRemoteFile rh rf >> ok_
|
||||||
ConnectRemoteCtrl inv -> withUser_ $ do
|
ConnectRemoteCtrl inv -> withUser_ $ do
|
||||||
(remoteCtrl_, ctrlAppInfo) <- connectRemoteCtrlURI inv
|
(remoteCtrl_, ctrlAppInfo) <- connectRemoteCtrlURI inv
|
||||||
pure CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion = currentAppVersion}
|
pure CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion = currentAppVersion}
|
||||||
@ -2103,8 +2102,7 @@ processChatCommand = \case
|
|||||||
connect' groupLinkId cReqHash xContactId = do
|
connect' groupLinkId cReqHash xContactId = do
|
||||||
(connId, incognitoProfile, subMode) <- requestContact user incognito cReq xContactId
|
(connId, incognitoProfile, subMode) <- requestContact user incognito cReq xContactId
|
||||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode
|
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode
|
||||||
toView $ CRNewContactConnection user conn
|
pure $ CRSentInvitation user conn incognitoProfile
|
||||||
pure $ CRSentInvitation user incognitoProfile
|
|
||||||
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> m ChatResponse
|
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> m ChatResponse
|
||||||
connectContactViaAddress user incognito ct cReq =
|
connectContactViaAddress user incognito ct cReq =
|
||||||
withChatLock "connectViaContact" $ do
|
withChatLock "connectViaContact" $ do
|
||||||
@ -6189,7 +6187,7 @@ chatCommandP =
|
|||||||
"/set device name " *> (SetLocalDeviceName <$> textP),
|
"/set device name " *> (SetLocalDeviceName <$> textP),
|
||||||
"/list remote hosts" $> ListRemoteHosts,
|
"/list remote hosts" $> ListRemoteHosts,
|
||||||
"/switch remote host " *> (SwitchRemoteHost <$> ("local" $> Nothing <|> (Just <$> A.decimal))),
|
"/switch remote host " *> (SwitchRemoteHost <$> ("local" $> Nothing <|> (Just <$> A.decimal))),
|
||||||
"/start remote host " *> (StartRemoteHost <$> ("new" $> Nothing <|> (Just <$> ((,) <$> A.decimal <*> (" multicast=" *> onOffP <|> pure False))))),
|
"/start remote host " *> (StartRemoteHost <$> ("new" $> Nothing <|> (Just <$> ((,) <$> A.decimal <*> (" multicast=" *> onOffP <|> pure False)))) <*> optional (A.space *> rcCtrlAddressP) <*> optional (" port=" *> A.decimal)),
|
||||||
"/stop remote host " *> (StopRemoteHost <$> ("new" $> RHNew <|> RHId <$> A.decimal)),
|
"/stop remote host " *> (StopRemoteHost <$> ("new" $> RHNew <|> RHId <$> A.decimal)),
|
||||||
"/delete remote host " *> (DeleteRemoteHost <$> A.decimal),
|
"/delete remote host " *> (DeleteRemoteHost <$> A.decimal),
|
||||||
"/store remote file " *> (StoreRemoteFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <* A.space <*> filePath),
|
"/store remote file " *> (StoreRemoteFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <* A.space <*> filePath),
|
||||||
@ -6327,6 +6325,8 @@ chatCommandP =
|
|||||||
(pure Nothing)
|
(pure Nothing)
|
||||||
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
|
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
|
||||||
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
|
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
|
||||||
|
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> text1P)
|
||||||
|
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||||
char_ = optional . A.char
|
char_ = optional . A.char
|
||||||
|
|
||||||
adminContactReq :: ConnReqContact
|
adminContactReq :: ConnReqContact
|
||||||
|
@ -41,6 +41,7 @@ import Data.String
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time (NominalDiffTime, UTCTime)
|
import Data.Time (NominalDiffTime, UTCTime)
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
import Data.Word (Word16)
|
||||||
import Language.Haskell.TH (Exp, Q, runIO)
|
import Language.Haskell.TH (Exp, Q, runIO)
|
||||||
import Numeric.Natural
|
import Numeric.Natural
|
||||||
import qualified Paths_simplex_chat as SC
|
import qualified Paths_simplex_chat as SC
|
||||||
@ -426,7 +427,7 @@ data ChatCommand
|
|||||||
| SetGroupTimedMessages GroupName (Maybe Int)
|
| SetGroupTimedMessages GroupName (Maybe Int)
|
||||||
| SetLocalDeviceName Text
|
| SetLocalDeviceName Text
|
||||||
| ListRemoteHosts
|
| ListRemoteHosts
|
||||||
| StartRemoteHost (Maybe (RemoteHostId, Bool)) -- Start new or known remote host with optional multicast for known host
|
| StartRemoteHost (Maybe (RemoteHostId, Bool)) (Maybe RCCtrlAddress) (Maybe Word16) -- Start new or known remote host with optional multicast for known host
|
||||||
| SwitchRemoteHost (Maybe RemoteHostId) -- Switch current remote host
|
| SwitchRemoteHost (Maybe RemoteHostId) -- Switch current remote host
|
||||||
| StopRemoteHost RHKey -- Shut down a running session
|
| StopRemoteHost RHKey -- Shut down a running session
|
||||||
| DeleteRemoteHost RemoteHostId -- Unregister remote host and remove its data
|
| DeleteRemoteHost RemoteHostId -- Unregister remote host and remove its data
|
||||||
@ -469,7 +470,7 @@ allowRemoteCommand = \case
|
|||||||
APIGetNetworkConfig -> False
|
APIGetNetworkConfig -> False
|
||||||
SetLocalDeviceName _ -> False
|
SetLocalDeviceName _ -> False
|
||||||
ListRemoteHosts -> False
|
ListRemoteHosts -> False
|
||||||
StartRemoteHost _ -> False
|
StartRemoteHost {} -> False
|
||||||
SwitchRemoteHost {} -> False
|
SwitchRemoteHost {} -> False
|
||||||
StoreRemoteFile {} -> False
|
StoreRemoteFile {} -> False
|
||||||
GetRemoteFile {} -> False
|
GetRemoteFile {} -> False
|
||||||
@ -556,8 +557,8 @@ data ChatResponse
|
|||||||
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection}
|
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection}
|
||||||
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
|
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
|
||||||
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
||||||
| CRSentConfirmation {user :: User}
|
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
|
||||||
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
|
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
|
||||||
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
||||||
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||||
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
||||||
@ -654,11 +655,10 @@ data ChatResponse
|
|||||||
| CRNtfTokenStatus {status :: NtfTknStatus}
|
| CRNtfTokenStatus {status :: NtfTknStatus}
|
||||||
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
|
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
|
||||||
| CRNtfMessages {user_ :: Maybe User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
| CRNtfMessages {user_ :: Maybe User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
||||||
| CRNewContactConnection {user :: User, connection :: PendingContactConnection}
|
|
||||||
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
||||||
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
|
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
|
||||||
| CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo}
|
| CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo}
|
||||||
| CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String}
|
| CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String, localAddrs :: NonEmpty RCCtrlAddress}
|
||||||
| CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
|
| CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
|
||||||
| CRNewRemoteHost {remoteHost :: RemoteHostInfo}
|
| CRNewRemoteHost {remoteHost :: RemoteHostInfo}
|
||||||
| CRRemoteHostConnected {remoteHost :: RemoteHostInfo}
|
| CRRemoteHostConnected {remoteHost :: RemoteHostInfo}
|
||||||
|
22
src/Simplex/Chat/Migrations/M20231126_remote_ctrl_address.hs
Normal file
22
src/Simplex/Chat/Migrations/M20231126_remote_ctrl_address.hs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20231126_remote_ctrl_address where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20231126_remote_ctrl_address :: Query
|
||||||
|
m20231126_remote_ctrl_address =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_addr TEXT;
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_iface TEXT;
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_port INTEGER;
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20231126_remote_ctrl_address :: Query
|
||||||
|
down_m20231126_remote_ctrl_address =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_addr;
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_iface;
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_port;
|
||||||
|
|]
|
@ -537,6 +537,10 @@ CREATE TABLE remote_hosts(
|
|||||||
id_key BLOB NOT NULL, -- long-term/identity signing key
|
id_key BLOB NOT NULL, -- long-term/identity signing key
|
||||||
host_fingerprint BLOB NOT NULL, -- remote host CA cert fingerprint, set when connected
|
host_fingerprint BLOB NOT NULL, -- remote host CA cert fingerprint, set when connected
|
||||||
host_dh_pub BLOB NOT NULL -- last session DH key
|
host_dh_pub BLOB NOT NULL -- last session DH key
|
||||||
|
,
|
||||||
|
bind_addr TEXT,
|
||||||
|
bind_iface TEXT,
|
||||||
|
bind_port INTEGER
|
||||||
);
|
);
|
||||||
CREATE TABLE remote_controllers(
|
CREATE TABLE remote_controllers(
|
||||||
-- e.g., desktops known to a mobile app
|
-- e.g., desktops known to a mobile app
|
||||||
|
@ -26,13 +26,14 @@ import qualified Data.ByteString.Base64.URL as B64U
|
|||||||
import Data.ByteString.Builder (Builder)
|
import Data.ByteString.Builder (Builder)
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
import Data.List.NonEmpty (nonEmpty)
|
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
||||||
|
import qualified Data.List.NonEmpty as L
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||||
import Data.Word (Word32)
|
import Data.Word (Word16, Word32)
|
||||||
import qualified Network.HTTP.Types as N
|
import qualified Network.HTTP.Types as N
|
||||||
import Network.HTTP2.Server (responseStreaming)
|
import Network.HTTP2.Server (responseStreaming)
|
||||||
import qualified Paths_simplex_chat as SC
|
import qualified Paths_simplex_chat as SC
|
||||||
@ -135,8 +136,8 @@ setNewRemoteHostId sseq rhId = do
|
|||||||
where
|
where
|
||||||
err = pure . Left . ChatErrorRemoteHost RHNew
|
err = pure . Left . ChatErrorRemoteHost RHNew
|
||||||
|
|
||||||
startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> m (Maybe RemoteHostInfo, RCSignedInvitation)
|
startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> m (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation)
|
||||||
startRemoteHost rh_ = do
|
startRemoteHost rh_ rcAddrPrefs_ port_ = do
|
||||||
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
|
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
|
||||||
Just (rhId, multicast) -> do
|
Just (rhId, multicast) -> do
|
||||||
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
|
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
|
||||||
@ -144,19 +145,20 @@ startRemoteHost rh_ = do
|
|||||||
Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
|
Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
|
||||||
sseq <- startRemoteHostSession rhKey
|
sseq <- startRemoteHostSession rhKey
|
||||||
ctrlAppInfo <- mkCtrlAppInfo
|
ctrlAppInfo <- mkCtrlAppInfo
|
||||||
(invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast
|
(localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_
|
||||||
|
let rcAddr_ = L.head localAddrs <$ rcAddrPrefs_
|
||||||
cmdOk <- newEmptyTMVarIO
|
cmdOk <- newEmptyTMVarIO
|
||||||
rhsWaitSession <- async $ do
|
rhsWaitSession <- async $ do
|
||||||
rhKeyVar <- newTVarIO rhKey
|
rhKeyVar <- newTVarIO rhKey
|
||||||
atomically $ takeTMVar cmdOk
|
atomically $ takeTMVar cmdOk
|
||||||
handleHostError sseq rhKeyVar $ waitForHostSession remoteHost_ rhKey sseq rhKeyVar vars
|
handleHostError sseq rhKeyVar $ waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars
|
||||||
let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_}
|
let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_}
|
||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
RHSessionStarting ->
|
RHSessionStarting ->
|
||||||
let inv = decodeLatin1 $ strEncode invitation
|
let inv = decodeLatin1 $ strEncode invitation
|
||||||
in Right ((), RHSessionConnecting inv rhs)
|
in Right ((), RHSessionConnecting inv rhs)
|
||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
(remoteHost_, invitation) <$ atomically (putTMVar cmdOk ())
|
(localAddrs, remoteHost_, invitation) <$ atomically (putTMVar cmdOk ())
|
||||||
where
|
where
|
||||||
mkCtrlAppInfo = do
|
mkCtrlAppInfo = do
|
||||||
deviceName <- chatReadVar localDeviceName
|
deviceName <- chatReadVar localDeviceName
|
||||||
@ -179,8 +181,8 @@ startRemoteHost rh_ = do
|
|||||||
action `catchChatError` \err -> do
|
action `catchChatError` \err -> do
|
||||||
logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err
|
logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err
|
||||||
readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err))
|
readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err))
|
||||||
waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m ()
|
waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m ()
|
||||||
waitForHostSession remoteHost_ rhKey sseq rhKeyVar vars = do
|
waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars = do
|
||||||
(sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars
|
(sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars
|
||||||
let sessionCode = verificationCode sessId
|
let sessionCode = verificationCode sessId
|
||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
@ -194,7 +196,7 @@ startRemoteHost rh_ = do
|
|||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
RHSessionPendingConfirmation _ tls' rhs' -> Right ((), RHSessionConfirmed tls' rhs')
|
RHSessionPendingConfirmation _ tls' rhs' -> Right ((), RHSessionConfirmed tls' rhs')
|
||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' rh_' hostDeviceName sseq RHSConfirmed {sessionCode}
|
rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' rh_' rcAddr_ hostDeviceName sseq RHSConfirmed {sessionCode}
|
||||||
let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew
|
let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew
|
||||||
when (rhKey' /= rhKey) $ do
|
when (rhKey' /= rhKey) $ do
|
||||||
atomically $ writeTVar rhKeyVar rhKey'
|
atomically $ writeTVar rhKeyVar rhKey'
|
||||||
@ -209,17 +211,17 @@ startRemoteHost rh_ = do
|
|||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
|
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
|
||||||
toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
|
toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
|
||||||
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo
|
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo
|
||||||
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ hostDeviceName sseq state = do
|
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do
|
||||||
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
|
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
|
||||||
case rhi_ of
|
case rhi_ of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
storePath <- liftIO randomStorePath
|
storePath <- liftIO randomStorePath
|
||||||
rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath pairing' >>= getRemoteHost db
|
rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath rcAddr_ port_ pairing' >>= getRemoteHost db
|
||||||
setNewRemoteHostId sseq remoteHostId
|
setNewRemoteHostId sseq remoteHostId
|
||||||
pure $ remoteHostInfo rh $ Just state
|
pure $ remoteHostInfo rh $ Just state
|
||||||
Just rhi@RemoteHostInfo {remoteHostId} -> do
|
Just rhi@RemoteHostInfo {remoteHostId} -> do
|
||||||
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey'
|
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey' rcAddr_ port_
|
||||||
pure (rhi :: RemoteHostInfo) {sessionState = Just state}
|
pure (rhi :: RemoteHostInfo) {sessionState = Just state}
|
||||||
onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m ()
|
onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m ()
|
||||||
onDisconnected rhKey sseq = do
|
onDisconnected rhKey sseq = do
|
||||||
@ -317,8 +319,8 @@ switchRemoteHost rhId_ = do
|
|||||||
rhi_ <$ chatWriteVar currentRemoteHost rhId_
|
rhi_ <$ chatWriteVar currentRemoteHost rhId_
|
||||||
|
|
||||||
remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo
|
remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo
|
||||||
remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName} sessionState =
|
remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_} sessionState =
|
||||||
RemoteHostInfo {remoteHostId, storePath, hostDeviceName, sessionState}
|
RemoteHostInfo {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_, sessionState}
|
||||||
|
|
||||||
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
||||||
deleteRemoteHost rhId = do
|
deleteRemoteHost rhId = do
|
||||||
|
@ -18,6 +18,7 @@ import qualified Data.Aeson.TH as J
|
|||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import Data.Word (Word16)
|
||||||
import Simplex.Chat.Remote.AppVersion
|
import Simplex.Chat.Remote.AppVersion
|
||||||
import Simplex.Chat.Types (verificationCode)
|
import Simplex.Chat.Types (verificationCode)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
@ -128,6 +129,8 @@ data RemoteHost = RemoteHost
|
|||||||
{ remoteHostId :: RemoteHostId,
|
{ remoteHostId :: RemoteHostId,
|
||||||
hostDeviceName :: Text,
|
hostDeviceName :: Text,
|
||||||
storePath :: FilePath,
|
storePath :: FilePath,
|
||||||
|
bindAddress_ :: Maybe RCCtrlAddress,
|
||||||
|
bindPort_ :: Maybe Word16,
|
||||||
hostPairing :: RCHostPairing
|
hostPairing :: RCHostPairing
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +139,8 @@ data RemoteHostInfo = RemoteHostInfo
|
|||||||
{ remoteHostId :: RemoteHostId,
|
{ remoteHostId :: RemoteHostId,
|
||||||
hostDeviceName :: Text,
|
hostDeviceName :: Text,
|
||||||
storePath :: FilePath,
|
storePath :: FilePath,
|
||||||
|
bindAddress_ :: Maybe RCCtrlAddress,
|
||||||
|
bindPort_ :: Maybe Word16,
|
||||||
sessionState :: Maybe RemoteHostSessionState
|
sessionState :: Maybe RemoteHostSessionState
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
@ -158,6 +163,7 @@ data PlatformEncoding
|
|||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
localEncoding :: PlatformEncoding
|
localEncoding :: PlatformEncoding
|
||||||
|
|
||||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||||
localEncoding = PESwift
|
localEncoding = PESwift
|
||||||
#else
|
#else
|
||||||
|
@ -90,6 +90,7 @@ import Simplex.Chat.Migrations.M20231030_xgrplinkmem_received
|
|||||||
import Simplex.Chat.Migrations.M20231107_indexes
|
import Simplex.Chat.Migrations.M20231107_indexes
|
||||||
import Simplex.Chat.Migrations.M20231113_group_forward
|
import Simplex.Chat.Migrations.M20231113_group_forward
|
||||||
import Simplex.Chat.Migrations.M20231114_remote_control
|
import Simplex.Chat.Migrations.M20231114_remote_control
|
||||||
|
import Simplex.Chat.Migrations.M20231126_remote_ctrl_address
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@ -179,7 +180,8 @@ schemaMigrations =
|
|||||||
("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received),
|
("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received),
|
||||||
("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes),
|
("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes),
|
||||||
("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward),
|
("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward),
|
||||||
("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control)
|
("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control),
|
||||||
|
("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
@ -8,6 +8,8 @@ module Simplex.Chat.Store.Remote where
|
|||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import Data.Text.Encoding (encodeUtf8, decodeASCII)
|
||||||
|
import Data.Word (Word16)
|
||||||
import Database.SQLite.Simple (Only (..))
|
import Database.SQLite.Simple (Only (..))
|
||||||
import qualified Database.SQLite.Simple as SQL
|
import qualified Database.SQLite.Simple as SQL
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
@ -16,11 +18,12 @@ import Simplex.Chat.Store.Shared
|
|||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
|
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||||
import Simplex.RemoteControl.Types
|
import Simplex.RemoteControl.Types
|
||||||
import UnliftIO
|
import UnliftIO
|
||||||
|
|
||||||
insertRemoteHost :: DB.Connection -> Text -> FilePath -> RCHostPairing -> ExceptT StoreError IO RemoteHostId
|
insertRemoteHost :: DB.Connection -> Text -> FilePath -> Maybe RCCtrlAddress -> Maybe Word16 -> RCHostPairing -> ExceptT StoreError IO RemoteHostId
|
||||||
insertRemoteHost db hostDeviceName storePath RCHostPairing {caKey, caCert, idPrivKey, knownHost = kh_} = do
|
insertRemoteHost db hostDeviceName storePath rcAddr_ bindPort_ RCHostPairing {caKey, caCert, idPrivKey, knownHost = kh_} = do
|
||||||
KnownHostPairing {hostFingerprint, hostDhPubKey} <-
|
KnownHostPairing {hostFingerprint, hostDhPubKey} <-
|
||||||
maybe (throwError SERemoteHostUnknown) pure kh_
|
maybe (throwError SERemoteHostUnknown) pure kh_
|
||||||
checkConstraint SERemoteHostDuplicateCA . liftIO $
|
checkConstraint SERemoteHostDuplicateCA . liftIO $
|
||||||
@ -28,12 +31,14 @@ insertRemoteHost db hostDeviceName storePath RCHostPairing {caKey, caCert, idPri
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO remote_hosts
|
INSERT INTO remote_hosts
|
||||||
(host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub)
|
(host_device_name, store_path, bind_addr, bind_iface, bind_port, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?)
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|]
|
|]
|
||||||
(hostDeviceName, storePath, caKey, C.SignedObject caCert, idPrivKey, hostFingerprint, hostDhPubKey)
|
(hostDeviceName, storePath, bindAddr_, bindIface_, bindPort_, caKey, C.SignedObject caCert, idPrivKey, hostFingerprint, hostDhPubKey)
|
||||||
liftIO $ insertedRowId db
|
liftIO $ insertedRowId db
|
||||||
|
where
|
||||||
|
(bindAddr_, bindIface_) = rcCtrlAddressFields_ rcAddr_
|
||||||
|
|
||||||
getRemoteHosts :: DB.Connection -> IO [RemoteHost]
|
getRemoteHosts :: DB.Connection -> IO [RemoteHost]
|
||||||
getRemoteHosts db =
|
getRemoteHosts db =
|
||||||
@ -52,27 +57,34 @@ getRemoteHostByFingerprint db fingerprint =
|
|||||||
remoteHostQuery :: SQL.Query
|
remoteHostQuery :: SQL.Query
|
||||||
remoteHostQuery =
|
remoteHostQuery =
|
||||||
[sql|
|
[sql|
|
||||||
SELECT remote_host_id, host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub
|
SELECT remote_host_id, host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub, bind_iface, bind_addr, bind_port
|
||||||
FROM remote_hosts
|
FROM remote_hosts
|
||||||
|]
|
|]
|
||||||
|
|
||||||
toRemoteHost :: (Int64, Text, FilePath, C.APrivateSignKey, C.SignedObject C.Certificate, C.PrivateKeyEd25519, C.KeyHash, C.PublicKeyX25519) -> RemoteHost
|
toRemoteHost :: (Int64, Text, FilePath, C.APrivateSignKey, C.SignedObject C.Certificate, C.PrivateKeyEd25519, C.KeyHash, C.PublicKeyX25519, Maybe Text, Maybe Text, Maybe Word16) -> RemoteHost
|
||||||
toRemoteHost (remoteHostId, hostDeviceName, storePath, caKey, C.SignedObject caCert, idPrivKey, hostFingerprint, hostDhPubKey) =
|
toRemoteHost (remoteHostId, hostDeviceName, storePath, caKey, C.SignedObject caCert, idPrivKey, hostFingerprint, hostDhPubKey, ifaceName_, ifaceAddr_, bindPort_) =
|
||||||
RemoteHost {remoteHostId, hostDeviceName, storePath, hostPairing}
|
RemoteHost {remoteHostId, hostDeviceName, storePath, hostPairing, bindAddress_, bindPort_}
|
||||||
where
|
where
|
||||||
hostPairing = RCHostPairing {caKey, caCert, idPrivKey, knownHost = Just knownHost}
|
hostPairing = RCHostPairing {caKey, caCert, idPrivKey, knownHost = Just knownHost}
|
||||||
knownHost = KnownHostPairing {hostFingerprint, hostDhPubKey}
|
knownHost = KnownHostPairing {hostFingerprint, hostDhPubKey}
|
||||||
|
bindAddress_ = RCCtrlAddress <$> (decodeAddr <$> ifaceAddr_) <*> ifaceName_
|
||||||
|
decodeAddr = either (error "Error parsing TransportHost") id . strDecode . encodeUtf8
|
||||||
|
|
||||||
updateHostPairing :: DB.Connection -> RemoteHostId -> Text -> C.PublicKeyX25519 -> IO ()
|
updateHostPairing :: DB.Connection -> RemoteHostId -> Text -> C.PublicKeyX25519 -> Maybe RCCtrlAddress -> Maybe Word16 -> IO ()
|
||||||
updateHostPairing db rhId hostDeviceName hostDhPubKey =
|
updateHostPairing db rhId hostDeviceName hostDhPubKey rcAddr_ bindPort_ =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
UPDATE remote_hosts
|
UPDATE remote_hosts
|
||||||
SET host_device_name = ?, host_dh_pub = ?
|
SET host_device_name = ?, host_dh_pub = ?, bind_addr = ?, bind_iface = ?, bind_port = ?
|
||||||
WHERE remote_host_id = ?
|
WHERE remote_host_id = ?
|
||||||
|]
|
|]
|
||||||
(hostDeviceName, hostDhPubKey, rhId)
|
(hostDeviceName, hostDhPubKey, bindAddr_, bindIface_, bindPort_, rhId)
|
||||||
|
where
|
||||||
|
(bindAddr_, bindIface_) = rcCtrlAddressFields_ rcAddr_
|
||||||
|
|
||||||
|
rcCtrlAddressFields_ :: Maybe RCCtrlAddress -> (Maybe Text, Maybe Text)
|
||||||
|
rcCtrlAddressFields_ = maybe (Nothing, Nothing) $ \RCCtrlAddress {address, interface} -> (Just . decodeASCII $ strEncode address, Just interface)
|
||||||
|
|
||||||
deleteRemoteHostRecord :: DB.Connection -> RemoteHostId -> IO ()
|
deleteRemoteHostRecord :: DB.Connection -> RemoteHostId -> IO ()
|
||||||
deleteRemoteHostRecord db remoteHostId = DB.execute db "DELETE FROM remote_hosts WHERE remote_host_id = ?" (Only remoteHostId)
|
deleteRemoteHostRecord db remoteHostId = DB.execute db "DELETE FROM remote_hosts WHERE remote_host_id = ?" (Only remoteHostId)
|
||||||
|
@ -64,6 +64,7 @@ import qualified Simplex.Messaging.Protocol as SMP
|
|||||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||||
import Simplex.Messaging.Util (bshow, tshow)
|
import Simplex.Messaging.Util (bshow, tshow)
|
||||||
import Simplex.Messaging.Version hiding (version)
|
import Simplex.Messaging.Version hiding (version)
|
||||||
|
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||||
import System.Console.ANSI.Types
|
import System.Console.ANSI.Types
|
||||||
|
|
||||||
type CurrentTime = UTCTime
|
type CurrentTime = UTCTime
|
||||||
@ -162,8 +163,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq
|
CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq
|
||||||
CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c
|
CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c
|
||||||
CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan
|
CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan
|
||||||
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"]
|
||||||
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
||||||
CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
||||||
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
||||||
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
|
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
|
||||||
@ -273,7 +274,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRCallInvitations _ -> []
|
CRCallInvitations _ -> []
|
||||||
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
|
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
|
||||||
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
|
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
|
||||||
CRNewContactConnection u _ -> ttyUser u []
|
|
||||||
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
|
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
|
||||||
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
|
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
|
||||||
CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)]
|
CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)]
|
||||||
@ -285,13 +285,13 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
rhi_
|
rhi_
|
||||||
]
|
]
|
||||||
CRRemoteHostList hs -> viewRemoteHosts hs
|
CRRemoteHostList hs -> viewRemoteHosts hs
|
||||||
CRRemoteHostStarted {remoteHost_, invitation, ctrlPort} ->
|
CRRemoteHostStarted {remoteHost_, invitation, localAddrs = RCCtrlAddress {address} :| _, ctrlPort} ->
|
||||||
[ plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_,
|
[ plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_,
|
||||||
"Remote session invitation:",
|
"Remote session invitation:",
|
||||||
plain invitation
|
plain invitation
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
started = " started on port " <> ctrlPort
|
started = " started on " <> B.unpack (strEncode address) <> ":" <> ctrlPort
|
||||||
CRRemoteHostSessionCode {remoteHost_, sessionCode} ->
|
CRRemoteHostSessionCode {remoteHost_, sessionCode} ->
|
||||||
[ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_,
|
[ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_,
|
||||||
"Compare session code with host:",
|
"Compare session code with host:",
|
||||||
@ -1712,8 +1712,13 @@ viewRemoteHosts = \case
|
|||||||
[] -> ["No remote hosts"]
|
[] -> ["No remote hosts"]
|
||||||
hs -> "Remote hosts: " : map viewRemoteHostInfo hs
|
hs -> "Remote hosts: " : map viewRemoteHostInfo hs
|
||||||
where
|
where
|
||||||
viewRemoteHostInfo RemoteHostInfo {remoteHostId, hostDeviceName, sessionState} =
|
viewRemoteHostInfo RemoteHostInfo {remoteHostId, hostDeviceName, sessionState, bindAddress_, bindPort_} =
|
||||||
plain $ tshow remoteHostId <> ". " <> hostDeviceName <> maybe "" viewSessionState sessionState
|
plain $ tshow remoteHostId <> ". " <> hostDeviceName <> maybe "" viewSessionState sessionState <> ctrlBinds bindAddress_ bindPort_
|
||||||
|
ctrlBinds Nothing Nothing = ""
|
||||||
|
ctrlBinds rca_ port_ = mconcat [" [", maybe "" rca rca_, maybe "" port port_, "]"]
|
||||||
|
where
|
||||||
|
rca RCCtrlAddress {interface, address} = interface <> " " <> decodeLatin1 (strEncode address)
|
||||||
|
port p = ":" <> tshow p
|
||||||
viewSessionState = \case
|
viewSessionState = \case
|
||||||
RHSStarting -> " (starting)"
|
RHSStarting -> " (starting)"
|
||||||
RHSConnecting _ -> " (connecting)"
|
RHSConnecting _ -> " (connecting)"
|
||||||
|
@ -49,7 +49,7 @@ extra-deps:
|
|||||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||||
# - ../simplexmq
|
# - ../simplexmq
|
||||||
- github: simplex-chat/simplexmq
|
- github: simplex-chat/simplexmq
|
||||||
commit: 281bdebcb82aed4c8c2c08438b9cafc7908183a1
|
commit: 6bffcc8503e5193d57e543ac0100712a8e27d454
|
||||||
- github: kazu-yamamoto/http2
|
- github: kazu-yamamoto/http2
|
||||||
commit: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb
|
commit: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb
|
||||||
# - ../direct-sqlcipher
|
# - ../direct-sqlcipher
|
||||||
|
@ -38,6 +38,7 @@ remoteTests = describe "Remote" $ do
|
|||||||
it "connects with stored pairing" remoteHandshakeStoredTest
|
it "connects with stored pairing" remoteHandshakeStoredTest
|
||||||
it "connects with multicast discovery" remoteHandshakeDiscoverTest
|
it "connects with multicast discovery" remoteHandshakeDiscoverTest
|
||||||
it "refuses invalid client cert" remoteHandshakeRejectTest
|
it "refuses invalid client cert" remoteHandshakeRejectTest
|
||||||
|
it "connects with stored server bindings" storedBindingsTest
|
||||||
it "sends messages" remoteMessageTest
|
it "sends messages" remoteMessageTest
|
||||||
describe "remote files" $ do
|
describe "remote files" $ do
|
||||||
it "store/get/send/receive files" remoteStoreFileTest
|
it "store/get/send/receive files" remoteStoreFileTest
|
||||||
@ -117,7 +118,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
|
|||||||
mobileBob ##> "/set device name MobileBob"
|
mobileBob ##> "/set device name MobileBob"
|
||||||
mobileBob <## "ok"
|
mobileBob <## "ok"
|
||||||
desktop ##> "/start remote host 1"
|
desktop ##> "/start remote host 1"
|
||||||
desktop <##. "remote host 1 started on port "
|
desktop <##. "remote host 1 started on "
|
||||||
desktop <## "Remote session invitation:"
|
desktop <## "Remote session invitation:"
|
||||||
inv <- getTermLine desktop
|
inv <- getTermLine desktop
|
||||||
mobileBob ##> ("/connect remote ctrl " <> inv)
|
mobileBob ##> ("/connect remote ctrl " <> inv)
|
||||||
@ -138,6 +139,37 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
|
|||||||
desktop <## "remote host 1 connected"
|
desktop <## "remote host 1 connected"
|
||||||
stopMobile mobile desktop
|
stopMobile mobile desktop
|
||||||
|
|
||||||
|
storedBindingsTest :: HasCallStack => FilePath -> IO ()
|
||||||
|
storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do
|
||||||
|
desktop ##> "/set device name My desktop"
|
||||||
|
desktop <## "ok"
|
||||||
|
mobile ##> "/set device name Mobile"
|
||||||
|
mobile <## "ok"
|
||||||
|
|
||||||
|
desktop ##> "/start remote host new addr=127.0.0.1 iface=lo port=52230"
|
||||||
|
desktop <##. "new remote host started on 127.0.0.1:52230" -- TODO: show ip?
|
||||||
|
desktop <## "Remote session invitation:"
|
||||||
|
inv <- getTermLine desktop
|
||||||
|
|
||||||
|
mobile ##> ("/connect remote ctrl " <> inv)
|
||||||
|
mobile <## ("connecting new remote controller: My desktop, v" <> versionNumber)
|
||||||
|
desktop <## "new remote host connecting"
|
||||||
|
mobile <## "new remote controller connected"
|
||||||
|
verifyRemoteCtrl mobile desktop
|
||||||
|
mobile <## "remote controller 1 session started with My desktop"
|
||||||
|
desktop <## "new remote host 1 added: Mobile"
|
||||||
|
desktop <## "remote host 1 connected"
|
||||||
|
|
||||||
|
desktop ##> "/list remote hosts"
|
||||||
|
desktop <## "Remote hosts:"
|
||||||
|
desktop <## "1. Mobile (connected) [lo 127.0.0.1:52230]"
|
||||||
|
stopDesktop mobile desktop
|
||||||
|
desktop ##> "/list remote hosts"
|
||||||
|
desktop <## "Remote hosts:"
|
||||||
|
desktop <## "1. Mobile [lo 127.0.0.1:52230]"
|
||||||
|
|
||||||
|
-- TODO: more parser tests
|
||||||
|
|
||||||
remoteMessageTest :: HasCallStack => FilePath -> IO ()
|
remoteMessageTest :: HasCallStack => FilePath -> IO ()
|
||||||
remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do
|
remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do
|
||||||
startRemote mobile desktop
|
startRemote mobile desktop
|
||||||
@ -475,7 +507,7 @@ startRemote mobile desktop = do
|
|||||||
mobile ##> "/set device name Mobile"
|
mobile ##> "/set device name Mobile"
|
||||||
mobile <## "ok"
|
mobile <## "ok"
|
||||||
desktop ##> "/start remote host new"
|
desktop ##> "/start remote host new"
|
||||||
desktop <##. "new remote host started on port "
|
desktop <##. "new remote host started on "
|
||||||
desktop <## "Remote session invitation:"
|
desktop <## "Remote session invitation:"
|
||||||
inv <- getTermLine desktop
|
inv <- getTermLine desktop
|
||||||
mobile ##> ("/connect remote ctrl " <> inv)
|
mobile ##> ("/connect remote ctrl " <> inv)
|
||||||
@ -490,7 +522,7 @@ startRemote mobile desktop = do
|
|||||||
startRemoteStored :: TestCC -> TestCC -> IO ()
|
startRemoteStored :: TestCC -> TestCC -> IO ()
|
||||||
startRemoteStored mobile desktop = do
|
startRemoteStored mobile desktop = do
|
||||||
desktop ##> "/start remote host 1"
|
desktop ##> "/start remote host 1"
|
||||||
desktop <##. "remote host 1 started on port "
|
desktop <##. "remote host 1 started on "
|
||||||
desktop <## "Remote session invitation:"
|
desktop <## "Remote session invitation:"
|
||||||
inv <- getTermLine desktop
|
inv <- getTermLine desktop
|
||||||
mobile ##> ("/connect remote ctrl " <> inv)
|
mobile ##> ("/connect remote ctrl " <> inv)
|
||||||
@ -504,7 +536,7 @@ startRemoteStored mobile desktop = do
|
|||||||
startRemoteDiscover :: TestCC -> TestCC -> IO ()
|
startRemoteDiscover :: TestCC -> TestCC -> IO ()
|
||||||
startRemoteDiscover mobile desktop = do
|
startRemoteDiscover mobile desktop = do
|
||||||
desktop ##> "/start remote host 1 multicast=on"
|
desktop ##> "/start remote host 1 multicast=on"
|
||||||
desktop <##. "remote host 1 started on port "
|
desktop <##. "remote host 1 started on "
|
||||||
desktop <## "Remote session invitation:"
|
desktop <## "Remote session invitation:"
|
||||||
_inv <- getTermLine desktop -- will use multicast instead
|
_inv <- getTermLine desktop -- will use multicast instead
|
||||||
mobile ##> "/find remote ctrl"
|
mobile ##> "/find remote ctrl"
|
||||||
|
Loading…
Reference in New Issue
Block a user