core: track network statuses, use in commands/events (#3211)

* core: track network statuses, use in commands/events

* ui types, test

* remove comment
This commit is contained in:
Evgeny Poberezkin
2023-10-13 11:51:01 +01:00
committed by GitHub
parent 675fc19745
commit ab290fb068
13 changed files with 275 additions and 102 deletions

View File

@@ -688,41 +688,3 @@ final class Chat: ObservableObject, Identifiable {
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
} }
enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
case error(String)
var statusString: LocalizedStringKey {
get {
switch self {
case .connected: return "connected"
case .error: return "error"
default: return "connecting"
}
}
}
var statusExplanation: LocalizedStringKey {
get {
switch self {
case .connected: return "You are connected to the server used to receive messages from this contact."
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
default: return "Trying to connect to the server used to receive messages from this contact."
}
}
}
var imageName: String {
get {
switch self {
case .unknown: return "circle.dotted"
case .connected: return "circle.fill"
case .disconnected: return "ellipsis.circle.fill"
case .error: return "exclamationmark.circle.fill"
}
}
}
}

View File

@@ -944,6 +944,12 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
} }
} }
func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
let r = chatSendCmdSync(.apiGetNetworkStatuses)
if case let .networkStatuses(_, statuses) = r { return statuses }
throw r
}
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async { func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
do { do {
if chat.chatStats.unreadCount > 0 { if chat.chatStats.unreadCount > 0 {
@@ -1348,13 +1354,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await updateContactsStatus(contactRefs, status: .connected) await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs): case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected) await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(user, contact, chatError):
await MainActor.run {
if active(user) {
m.updateContact(contact)
}
processContactSubError(contact, chatError)
}
case let .contactSubSummary(_, contactSubscriptions): case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run { await MainActor.run {
for sub in contactSubscriptions { for sub in contactSubscriptions {
@@ -1369,6 +1368,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
} }
} }
} }
case let .networkStatus(status, connections):
await MainActor.run {
for cId in connections {
m.networkStatuses[cId] = status
}
}
case let .networkStatuses(statuses): ()
await MainActor.run {
for s in statuses {
m.networkStatuses[s.agentConnId] = s.networkStatus
}
}
case let .newChatItem(user, aChatItem): case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem let cItem = aChatItem.chatItem
@@ -1649,7 +1660,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError) default: err = String(describing: chatError)
} }
m.setContactNetworkStatus(contact, .error(err)) m.setContactNetworkStatus(contact, .error(connectionError: err))
} }
func refreshCallInvitations() throws { func refreshCallInvitations() throws {

View File

@@ -110,6 +110,7 @@ public enum ChatCommand {
case apiEndCall(contact: Contact) case apiEndCall(contact: Contact)
case apiGetCallInvitations case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?) case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
@@ -241,6 +242,7 @@ public enum ChatCommand {
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
case .apiGetCallInvitations: return "/_call get" case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId, encrypted, inline): case let .receiveFile(fileId, encrypted, inline):
@@ -356,6 +358,7 @@ public enum ChatCommand {
case .apiEndCall: return "apiEndCall" case .apiEndCall: return "apiEndCall"
case .apiGetCallInvitations: return "apiGetCallInvitations" case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus" case .apiCallStatus: return "apiCallStatus"
case .apiGetNetworkStatuses: return "apiGetNetworkStatuses"
case .apiChatRead: return "apiChatRead" case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread" case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile" case .receiveFile: return "receiveFile"
@@ -480,11 +483,14 @@ public enum ChatResponse: Decodable, Error {
case acceptingContactRequest(user: UserRef, contact: Contact) case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef) case contactRequestRejected(user: UserRef)
case contactUpdated(user: UserRef, toContact: Contact) case contactUpdated(user: UserRef, toContact: Contact)
// TODO remove events below
case contactsSubscribed(server: String, contactRefs: [ContactRef]) case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef]) case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubError(user: UserRef, contact: Contact, chatError: ChatError)
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus]) case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
case groupSubscribed(user: UserRef, groupInfo: GroupInfo) // TODO remove events above
case networkStatus(networkStatus: NetworkStatus, connections: [String])
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case groupSubscribed(user: UserRef, groupInfo: GroupRef)
case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError]) case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError])
case groupEmpty(user: UserRef, groupInfo: GroupInfo) case groupEmpty(user: UserRef, groupInfo: GroupInfo)
case userContactLinkSubscribed case userContactLinkSubscribed
@@ -620,8 +626,9 @@ public enum ChatResponse: Decodable, Error {
case .contactUpdated: return "contactUpdated" case .contactUpdated: return "contactUpdated"
case .contactsSubscribed: return "contactsSubscribed" case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected" case .contactsDisconnected: return "contactsDisconnected"
case .contactSubError: return "contactSubError"
case .contactSubSummary: return "contactSubSummary" case .contactSubSummary: return "contactSubSummary"
case .networkStatus: return "networkStatus"
case .networkStatuses: return "networkStatuses"
case .groupSubscribed: return "groupSubscribed" case .groupSubscribed: return "groupSubscribed"
case .memberSubErrors: return "memberSubErrors" case .memberSubErrors: return "memberSubErrors"
case .groupEmpty: return "groupEmpty" case .groupEmpty: return "groupEmpty"
@@ -757,8 +764,9 @@ public enum ChatResponse: Decodable, Error {
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))" case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))" case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubError(u, contact, chatError): return withUser(u, "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))")
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions)) case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors)) case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
@@ -1181,6 +1189,49 @@ public struct KeepAliveOpts: Codable, Equatable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
} }
public enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
case error(connectionError: String)
public var statusString: LocalizedStringKey {
get {
switch self {
case .connected: return "connected"
case .error: return "error"
default: return "connecting"
}
}
}
public var statusExplanation: LocalizedStringKey {
get {
switch self {
case .connected: return "You are connected to the server used to receive messages from this contact."
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
default: return "Trying to connect to the server used to receive messages from this contact."
}
}
}
public var imageName: String {
get {
switch self {
case .unknown: return "circle.dotted"
case .connected: return "circle.fill"
case .disconnected: return "ellipsis.circle.fill"
case .error: return "exclamationmark.circle.fill"
}
}
}
}
public struct ConnNetworkStatus: Decodable {
public var agentConnId: String
public var networkStatus: NetworkStatus
}
public struct ChatSettings: Codable { public struct ChatSettings: Codable {
public var enableNtfs: MsgFilter public var enableNtfs: MsgFilter
public var sendRcpts: Bool? public var sendRcpts: Bool?

View File

@@ -1729,6 +1729,11 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
) )
} }
public struct GroupRef: Decodable {
public var groupId: Int64
var localDisplayName: GroupName
}
public struct GroupProfile: Codable, NamedChat { public struct GroupProfile: Codable, NamedChat {
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) { public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
self.displayName = displayName self.displayName = displayName
@@ -1871,6 +1876,11 @@ public struct GroupMemberRef: Decodable {
var profile: Profile var profile: Profile
} }
public struct GroupMemberIds: Decodable {
var groupMemberId: Int64
var groupId: Int64
}
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer" case observer = "observer"
case member = "member" case member = "member"
@@ -1963,7 +1973,7 @@ public enum InvitedBy: Decodable {
} }
public struct MemberSubError: Decodable { public struct MemberSubError: Decodable {
var member: GroupMember var member: GroupMemberIds
var memberError: ChatError var memberError: ChatError
} }

View File

@@ -789,16 +789,19 @@ sealed class NetworkStatus {
val statusExplanation: String get() = val statusExplanation: String get() =
when (this) { when (this) {
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact) is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), error) is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError)
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages) else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
} }
@Serializable @SerialName("unknown") class Unknown: NetworkStatus() @Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus() @Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus() @Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus()
} }
@Serializable
data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus)
@Serializable @Serializable
data class Contact( data class Contact(
val contactId: Long, val contactId: Long,
@@ -1051,6 +1054,9 @@ data class GroupInfo (
} }
} }
@Serializable
data class GroupRef(val groupId: Long, val localDisplayName: String)
@Serializable @Serializable
data class GroupProfile ( data class GroupProfile (
override val displayName: String, override val displayName: String,
@@ -1159,11 +1165,17 @@ data class GroupMember (
data class GroupMemberSettings(val showMessages: Boolean) {} data class GroupMemberSettings(val showMessages: Boolean) {}
@Serializable @Serializable
class GroupMemberRef( data class GroupMemberRef(
val groupMemberId: Long, val groupMemberId: Long,
val profile: Profile val profile: Profile
) )
@Serializable
data class GroupMemberIds(
val groupMemberId: Long,
val groupId: Long
)
@Serializable @Serializable
enum class GroupMemberRole(val memberRole: String) { enum class GroupMemberRole(val memberRole: String) {
@SerialName("observer") Observer("observer"), // order matters in comparisons @SerialName("observer") Observer("observer"), // order matters in comparisons
@@ -1257,7 +1269,7 @@ class LinkPreview (
@Serializable @Serializable
class MemberSubError ( class MemberSubError (
val member: GroupMember, val member: GroupMemberIds,
val memberError: ChatError val memberError: ChatError
) )

View File

@@ -1082,6 +1082,13 @@ object ChatController {
return r is CR.CmdOk return r is CR.CmdOk
} }
suspend fun apiGetNetworkStatuses(): List<ConnNetworkStatus>? {
val r = sendCmd(CC.ApiGetNetworkStatuses())
if (r is CR.NetworkStatuses) return r.networkStatuses
Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean { suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
val r = sendCmd(CC.ApiChatRead(type, id, range)) val r = sendCmd(CC.ApiChatRead(type, id, range))
if (r is CR.CmdOk) return true if (r is CR.CmdOk) return true
@@ -1425,12 +1432,6 @@ object ChatController {
} }
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
is CR.ContactSubError -> {
if (active(r.user)) {
chatModel.updateContact(r.contact)
}
processContactSubError(r.contact, r.chatError)
}
is CR.ContactSubSummary -> { is CR.ContactSubSummary -> {
for (sub in r.contactSubscriptions) { for (sub in r.contactSubscriptions) {
if (active(r.user)) { if (active(r.user)) {
@@ -1444,6 +1445,16 @@ object ChatController {
} }
} }
} }
is CR.NetworkStatusResp -> {
for (cId in r.connections) {
chatModel.networkStatuses[cId] = r.networkStatus
}
}
is CR.NetworkStatuses -> {
for (s in r.networkStatuses) {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
}
}
is CR.NewChatItem -> { is CR.NewChatItem -> {
val cInfo = r.chatItem.chatInfo val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem val cItem = r.chatItem.chatItem
@@ -1915,6 +1926,7 @@ sealed class CC {
class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC() class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC()
class ApiEndCall(val contact: Contact): CC() class ApiEndCall(val contact: Contact): CC()
class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC() class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC()
class ApiGetNetworkStatuses(): CC()
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
class ApiRejectContact(val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
@@ -2024,6 +2036,7 @@ sealed class CC {
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}" is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
is ApiEndCall -> "/_call end @${contact.apiId}" is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiGetNetworkStatuses -> "/_network_statuses"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}") is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
@@ -2120,6 +2133,7 @@ sealed class CC {
is ApiSendCallExtraInfo -> "apiSendCallExtraInfo" is ApiSendCallExtraInfo -> "apiSendCallExtraInfo"
is ApiEndCall -> "apiEndCall" is ApiEndCall -> "apiEndCall"
is ApiCallStatus -> "apiCallStatus" is ApiCallStatus -> "apiCallStatus"
is ApiGetNetworkStatuses -> "apiGetNetworkStatuses"
is ApiChatRead -> "apiChatRead" is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread" is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile" is ReceiveFile -> "receiveFile"
@@ -3333,11 +3347,14 @@ sealed class CR {
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR()
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR() @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
// TODO remove below
@Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR() @Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR()
@Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR() @Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR()
@Serializable @SerialName("contactSubError") class ContactSubError(val user: UserRef, val contact: Contact, val chatError: ChatError): CR()
@Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List<ContactSubStatus>): CR() @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List<ContactSubStatus>): CR()
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupInfo): CR() // TODO remove above
@Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List<String>): CR()
@Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List<ConnNetworkStatus>): CR()
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR()
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR() @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR()
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR() @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
@@ -3467,8 +3484,9 @@ sealed class CR {
is ContactUpdated -> "contactUpdated" is ContactUpdated -> "contactUpdated"
is ContactsSubscribed -> "contactsSubscribed" is ContactsSubscribed -> "contactsSubscribed"
is ContactsDisconnected -> "contactsDisconnected" is ContactsDisconnected -> "contactsDisconnected"
is ContactSubError -> "contactSubError"
is ContactSubSummary -> "contactSubSummary" is ContactSubSummary -> "contactSubSummary"
is NetworkStatusResp -> "networkStatus"
is NetworkStatuses -> "networkStatuses"
is GroupSubscribed -> "groupSubscribed" is GroupSubscribed -> "groupSubscribed"
is MemberSubErrors -> "memberSubErrors" is MemberSubErrors -> "memberSubErrors"
is GroupEmpty -> "groupEmpty" is GroupEmpty -> "groupEmpty"
@@ -3596,8 +3614,9 @@ sealed class CR {
is ContactUpdated -> withUser(user, json.encodeToString(toContact)) is ContactUpdated -> withUser(user, json.encodeToString(toContact))
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
is ContactSubError -> withUser(user, "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}")
is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions)) is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions))
is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections"
is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses))
is GroupSubscribed -> withUser(user, json.encodeToString(group)) is GroupSubscribed -> withUser(user, json.encodeToString(group))
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors)) is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
is GroupEmpty -> withUser(user, json.encodeToString(group)) is GroupEmpty -> withUser(user, json.encodeToString(group))

View File

@@ -143,7 +143,8 @@ defaultChatConfig =
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
cleanupManagerInterval = 30 * 60, -- 30 minutes cleanupManagerInterval = 30 * 60, -- 30 minutes
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
coreApi = False
} }
_defaultSMPServers :: NonEmpty SMPServerWithAuth _defaultSMPServers :: NonEmpty SMPServerWithAuth
@@ -195,6 +196,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
idsDrg <- newTVarIO =<< liftIO drgNew idsDrg <- newTVarIO =<< liftIO drgNew
inputQ <- newTBQueueIO tbqSize inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize
connNetworkStatuses <- atomically TM.empty
subscriptionMode <- newTVarIO SMSubscribe subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO chatLock <- newEmptyTMVarIO
sndFiles <- newTVarIO M.empty sndFiles <- newTVarIO M.empty
@@ -221,6 +223,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
idsDrg, idsDrg,
inputQ, inputQ,
outputQ, outputQ,
connNetworkStatuses,
subscriptionMode, subscriptionMode,
chatLock, chatLock,
sndFiles, sndFiles,
@@ -1086,6 +1089,8 @@ processChatCommand = \case
user <- getUserByContactId db contactId user <- getUserByContactId db contactId
contact <- getContact db user contactId contact <- getContact db user contactId
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs} pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs}
APIGetNetworkStatuses -> withUser $ \_ ->
CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses
APICallStatus contactId receivedStatus -> APICallStatus contactId receivedStatus ->
withCurrentCall contactId $ \user ct call -> withCurrentCall contactId $ \user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call updateCallItemStatus user ct call receivedStatus Nothing $> Just call
@@ -1688,6 +1693,8 @@ processChatCommand = \case
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
-- [incognito] reuse membership incognito profile -- [incognito] reuse membership incognito profile
ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
-- TODO not sure it is correct to set connections status here?
setContactNetworkStatus ct NSConnected
pure $ CRNewMemberContact user ct g m pure $ CRNewMemberContact user ct g m
_ -> throwChatError CEGroupMemberNotActive _ -> throwChatError CEGroupMemberNotActive
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
@@ -2627,6 +2634,7 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
rs <- withAgent $ \a -> agentBatchSubscribe a conns rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view -- send connection events to view
contactSubsToView rs cts ce contactSubsToView rs cts ce
-- TODO possibly, we could either disable these events or replace with less noisy for API
contactLinkSubsToView rs ucs contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts sndFileSubsToView rs sfts
@@ -2687,12 +2695,30 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
let connIds = map aConnId' pcs let connIds = map aConnId' pcs
pure (connIds, M.fromList $ zip connIds pcs) pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m () contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m ()
contactSubsToView rs cts ce = do contactSubsToView rs cts ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
where where
cRs = resultsFor rs cts notifyCLI = do
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs let cRs = resultsFor rs cts
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
notifyAPI = do
let statuses = M.foldrWithKey' addStatus [] cts
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
toView $ CRNetworkStatuses (Just user) $ map (uncurry ConnNetworkStatus) statuses
where
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
addStatus connId ct =
let ns = (contactAgentConnId ct, netStatus $ resultErr connId rs)
in (ns :)
netStatus :: Maybe ChatError -> NetworkStatus
netStatus = maybe NSConnected $ NSError . errorNetworkStatus
errorNetworkStatus :: ChatError -> String
errorNetworkStatus = \case
ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted"
e -> show e
-- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m () contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m ()
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m () groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m ()
@@ -2742,12 +2768,12 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
resultsFor rs = M.foldrWithKey' addResult [] resultsFor rs = M.foldrWithKey' addResult []
where where
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)] addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
addResult connId = (:) . (,err) addResult connId = (:) . (,resultErr connId rs)
where resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
err = case M.lookup connId rs of resultErr connId rs = case M.lookup connId rs of
Just (Left e) -> Just $ ChatErrorAgent e Nothing Just (Left e) -> Just $ ChatErrorAgent e Nothing
Just _ -> Nothing Just _ -> Nothing
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId _ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
cleanupManager :: forall m. ChatMonad m => m () cleanupManager :: forall m. ChatMonad m => m ()
cleanupManager = do cleanupManager = do
@@ -2892,16 +2918,22 @@ processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone ->
processAgentMessageNoConn = \case processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
DOWN srv conns -> serverEvent srv conns CRContactsDisconnected DOWN srv conns -> serverEvent srv conns NSDisconnected CRContactsDisconnected
UP srv conns -> serverEvent srv conns CRContactsSubscribed UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
SUSPENDED -> toView CRChatSuspended SUSPENDED -> toView CRChatSuspended
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
where where
hostEvent :: ChatResponse -> m () hostEvent :: ChatResponse -> m ()
hostEvent = whenM (asks $ hostEvents . config) . toView hostEvent = whenM (asks $ hostEvents . config) . toView
serverEvent srv conns event = do serverEvent srv conns nsStatus event = ifM (asks $ coreApi . config) notifyAPI notifyCLI
cs <- withStore' (`getConnectionsContacts` conns) where
toView $ event srv cs notifyAPI = do
let connIds = map AgentConnId conns
chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds
toView $ CRNetworkStatus nsStatus connIds
notifyCLI = do
cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs
processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m () processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
processAgentMsgSndFile _corrId aFileId msg = processAgentMsgSndFile _corrId aFileId msg =
@@ -3188,6 +3220,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Nothing -> do Nothing -> do
-- [incognito] print incognito profile used for this contact -- [incognito] print incognito profile used for this contact
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
setContactNetworkStatus ct NSConnected
toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile) toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile)
when (directOrUsed ct) $ createFeatureEnabledItems ct when (directOrUsed ct) $ createFeatureEnabledItems ct
when (contactConnInitiated conn) $ do when (contactConnInitiated conn) $ do
@@ -3762,6 +3795,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m () notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
notifyMemberConnected gInfo m ct_ = do notifyMemberConnected gInfo m ct_ = do
memberConnectedChatItem gInfo m memberConnectedChatItem gInfo m
mapM_ (`setContactNetworkStatus` NSConnected) ct_
toView $ CRConnectedToGroupMember user gInfo m ct_ toView $ CRConnectedToGroupMember user gInfo m ct_
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m () probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
@@ -5569,6 +5603,7 @@ chatCommandP =
"/_call end @" *> (APIEndCall <$> A.decimal), "/_call end @" *> (APIEndCall <$> A.decimal),
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
"/_call get" $> APIGetCallInvitations, "/_call get" $> APIGetCallInvitations,
"/_network_statuses" $> APIGetNetworkStatuses,
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),

View File

@@ -32,6 +32,7 @@ import Data.Char (ord)
import Data.Int (Int64) import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.String import Data.String
import Data.Text (Text) import Data.Text (Text)
import Data.Time (NominalDiffTime, UTCTime) import Data.Time (NominalDiffTime, UTCTime)
@@ -124,7 +125,8 @@ data ChatConfig = ChatConfig
initialCleanupManagerDelay :: Int64, initialCleanupManagerDelay :: Int64,
cleanupManagerInterval :: NominalDiffTime, cleanupManagerInterval :: NominalDiffTime,
cleanupManagerStepDelay :: Int64, cleanupManagerStepDelay :: Int64,
ciExpirationInterval :: Int64 -- microseconds ciExpirationInterval :: Int64, -- microseconds
coreApi :: Bool
} }
data DefaultAgentServers = DefaultAgentServers data DefaultAgentServers = DefaultAgentServers
@@ -164,6 +166,7 @@ data ChatController = ChatController
idsDrg :: TVar ChaChaDRG, idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue String, inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, ChatResponse), outputQ :: TBQueue (Maybe CorrId, ChatResponse),
connNetworkStatuses :: TMap AgentConnId NetworkStatus,
subscriptionMode :: TVar SubscriptionMode, subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock, chatLock :: Lock,
sndFiles :: TVar (Map Int64 Handle), sndFiles :: TVar (Map Int64 Handle),
@@ -251,6 +254,7 @@ data ChatCommand
| APIEndCall ContactId | APIEndCall ContactId
| APIGetCallInvitations | APIGetCallInvitations
| APICallStatus ContactId WebRTCCallStatus | APICallStatus ContactId WebRTCCallStatus
| APIGetNetworkStatuses
| APIUpdateProfile UserId Profile | APIUpdateProfile UserId Profile
| APISetContactPrefs ContactId Preferences | APISetContactPrefs ContactId Preferences
| APISetContactAlias ContactId LocalAlias | APISetContactAlias ContactId LocalAlias
@@ -528,6 +532,8 @@ data ChatResponse
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError} | CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} | CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} | CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
| CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo} | CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
@@ -1044,6 +1050,13 @@ chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m ()
chatWriteVar f value = asks f >>= atomically . (`writeTVar` value) chatWriteVar f value = asks f >>= atomically . (`writeTVar` value)
{-# INLINE chatWriteVar #-} {-# INLINE chatWriteVar #-}
chatModifyVar :: ChatMonad' m => (ChatController -> TVar a) -> (a -> a) -> m ()
chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
{-# INLINE chatModifyVar #-}
setContactNetworkStatus :: ChatMonad' m => Contact -> NetworkStatus -> m ()
setContactNetworkStatus ct = chatModifyVar connNetworkStatuses . M.insert (contactAgentConnId ct)
tryChatError :: ChatMonad m => m a -> m (Either ChatError a) tryChatError :: ChatMonad m => m a -> m (Either ChatError a)
tryChatError = tryAllErrors mkChatError tryChatError = tryAllErrors mkChatError
{-# INLINE tryChatError #-} {-# INLINE tryChatError #-}

View File

@@ -169,7 +169,8 @@ defaultMobileConfig :: ChatConfig
defaultMobileConfig = defaultMobileConfig =
defaultChatConfig defaultChatConfig
{ confirmMigrations = MCYesUp, { confirmMigrations = MCYesUp,
logLevel = CLLError logLevel = CLLError,
coreApi = True
} }
getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ :: SQLiteStore -> IO (Maybe User)

View File

@@ -192,6 +192,9 @@ instance ToJSON Contact where
contactConn :: Contact -> Connection contactConn :: Contact -> Connection
contactConn Contact {activeConn} = activeConn contactConn Contact {activeConn} = activeConn
contactAgentConnId :: Contact -> AgentConnId
contactAgentConnId Contact {activeConn = Connection {agentConnId}} = agentConnId
contactConnId :: Contact -> ConnId contactConnId :: Contact -> ConnId
contactConnId = aConnId . contactConn contactConnId = aConnId . contactConn
@@ -1140,13 +1143,16 @@ liveRcvFileTransferPath ft = fp <$> liveRcvFileTransferInfo ft
fp RcvFileInfo {filePath} = filePath fp RcvFileInfo {filePath} = filePath
newtype AgentConnId = AgentConnId ConnId newtype AgentConnId = AgentConnId ConnId
deriving (Eq, Show) deriving (Eq, Ord, Show)
instance StrEncoding AgentConnId where instance StrEncoding AgentConnId where
strEncode (AgentConnId connId) = strEncode connId strEncode (AgentConnId connId) = strEncode connId
strDecode s = AgentConnId <$> strDecode s strDecode s = AgentConnId <$> strDecode s
strP = AgentConnId <$> strP strP = AgentConnId <$> strP
instance FromJSON AgentConnId where
parseJSON = strParseJSON "AgentConnId"
instance ToJSON AgentConnId where instance ToJSON AgentConnId where
toJSON = strToJSON toJSON = strToJSON
toEncoding = strToJEncoding toEncoding = strToJEncoding
@@ -1477,6 +1483,35 @@ serializeIntroStatus = \case
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
data NetworkStatus
= NSUnknown
| NSConnected
| NSDisconnected
| NSError {connectionError :: String}
deriving (Eq, Ord, Show, Generic)
netStatusStr :: NetworkStatus -> String
netStatusStr = \case
NSUnknown -> "unknown"
NSConnected -> "connected"
NSDisconnected -> "disconnected"
NSError e -> "error: " <> e
instance FromJSON NetworkStatus where
parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "NS"
instance ToJSON NetworkStatus where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "NS"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "NS"
data ConnNetworkStatus = ConnNetworkStatus
{ agentConnId :: AgentConnId,
networkStatus :: NetworkStatus
}
deriving (Show, Generic, FromJSON)
instance ToJSON ConnNetworkStatus where toEncoding = J.genericToEncoding J.defaultOptions
type CommandId = Int64 type CommandId = Int64
aCorrId :: CommandId -> ACorrId aCorrId :: CommandId -> ACorrId

View File

@@ -19,7 +19,7 @@ import Data.Char (isSpace, toUpper)
import Data.Function (on) import Data.Function (on)
import Data.Int (Int64) import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn) import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
@@ -210,6 +210,8 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary (addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g] CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
@@ -800,6 +802,12 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"] viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"] viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
viewNetworkStatuses :: [ConnNetworkStatus] -> [StyledString]
viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortOn netStatus
where
netStatus ConnNetworkStatus {networkStatus} = networkStatus
viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s)
viewUserJoinedGroup :: GroupInfo -> [StyledString] viewUserJoinedGroup :: GroupInfo -> [StyledString]
viewUserJoinedGroup g = viewUserJoinedGroup g =
case incognitoMembershipProfile g of case incognitoMembershipProfile g of

View File

@@ -119,6 +119,8 @@ chatDirectTests = do
testReqVRange vr11 supportedChatVRange testReqVRange vr11 supportedChatVRange
testReqVRange vr11 vr11 testReqVRange vr11 vr11
it "update peer version range on received messages" testUpdatePeerChatVRange it "update peer version range on received messages" testUpdatePeerChatVRange
describe "network statuses" $ do
it "should get network statuses" testGetNetworkStatuses
where where
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
@@ -2623,6 +2625,20 @@ testUpdatePeerChatVRange tmp =
where where
cfg11 = testCfg {chatVRange = vr11} :: ChatConfig cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
testGetNetworkStatuses :: HasCallStack => FilePath -> IO ()
testGetNetworkStatuses tmp = do
withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
connectUsers alice bob
alice ##> "/_network_statuses"
alice <## "1 connections connected"
withTestChatCfg tmp cfg "alice" $ \alice ->
withTestChatCfg tmp cfg "bob" $ \bob -> do
alice <## "1 connections connected"
bob <## "1 connections connected"
where
cfg = testCfg {coreApi = True}
vr11 :: VersionRange vr11 :: VersionRange
vr11 = mkVersionRange 1 1 vr11 = mkVersionRange 1 1

View File

@@ -120,19 +120,19 @@ chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}"
chatStartedTagged :: LB.ByteString chatStartedTagged :: LB.ByteString
chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}" chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}"
contactSubSummary :: LB.ByteString networkStatuses :: LB.ByteString
contactSubSummary = networkStatuses =
#if defined(darwin_HOST_OS) && defined(swiftJSON) #if defined(darwin_HOST_OS) && defined(swiftJSON)
contactSubSummarySwift networkStatusesSwift
#else #else
contactSubSummaryTagged networkStatusesTagged
#endif #endif
contactSubSummarySwift :: LB.ByteString networkStatusesSwift :: LB.ByteString
contactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}" networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
contactSubSummaryTagged :: LB.ByteString networkStatusesTagged :: LB.ByteString
contactSubSummaryTagged = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}" networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
memberSubSummary :: LB.ByteString memberSubSummary :: LB.ByteString
memberSubSummary = memberSubSummary =
@@ -143,10 +143,10 @@ memberSubSummary =
#endif #endif
memberSubSummarySwift :: LB.ByteString memberSubSummarySwift :: LB.ByteString
memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}" memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
memberSubSummaryTagged :: LB.ByteString memberSubSummaryTagged :: LB.ByteString
memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}" memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}"
userContactSubSummary :: LB.ByteString userContactSubSummary :: LB.ByteString
userContactSubSummary = userContactSubSummary =
@@ -157,10 +157,10 @@ userContactSubSummary =
#endif #endif
userContactSubSummarySwift :: LB.ByteString userContactSubSummarySwift :: LB.ByteString
userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
userContactSubSummaryTagged :: LB.ByteString userContactSubSummaryTagged :: LB.ByteString
userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}" userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}"
pendingSubSummary :: LB.ByteString pendingSubSummary :: LB.ByteString
pendingSubSummary = pendingSubSummary =
@@ -171,13 +171,13 @@ pendingSubSummary =
#endif #endif
pendingSubSummarySwift :: LB.ByteString pendingSubSummarySwift :: LB.ByteString
pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
pendingSubSummaryTagged :: LB.ByteString pendingSubSummaryTagged :: LB.ByteString
pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}" pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}"
userJSON :: LB.ByteString userJSON :: LB.ByteString
userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
parsedMarkdown :: LB.ByteString parsedMarkdown :: LB.ByteString
parsedMarkdown = parsedMarkdown =
@@ -215,7 +215,7 @@ testChatApi tmp = do
chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u" `shouldReturn` activeUser
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatRecvMsg cc `shouldReturn` contactSubSummary chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsg cc `shouldReturn` memberSubSummary chatRecvMsg cc `shouldReturn` memberSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary