mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-09 16:41:47 +00:00
core, ui: better error on failed channel creation (#6825)
This commit is contained in:
@@ -942,6 +942,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
// group responses
|
||||
case groupCreated(user: UserRef, groupInfo: GroupInfo)
|
||||
case publicGroupCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink, groupRelays: [GroupRelay])
|
||||
case publicGroupCreationFailed(user: UserRef, addRelayResults: [AddRelayResult])
|
||||
case groupRelays(user: UserRef, groupInfo: GroupInfo, groupRelays: [GroupRelay])
|
||||
case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?)
|
||||
@@ -994,6 +995,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
switch self {
|
||||
case .groupCreated: "groupCreated"
|
||||
case .publicGroupCreated: "publicGroupCreated"
|
||||
case .publicGroupCreationFailed: "publicGroupCreationFailed"
|
||||
case .groupRelays: "groupRelays"
|
||||
case .sentGroupInvitation: "sentGroupInvitation"
|
||||
case .userAcceptedGroupSent: "userAcceptedGroupSent"
|
||||
@@ -1042,6 +1044,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
switch self {
|
||||
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .publicGroupCreated(u, groupInfo, groupLink, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)\ngroupRelays: \(groupRelays)")
|
||||
case let .publicGroupCreationFailed(u, addRelayResults): return withUser(u, "addRelayResults: \(addRelayResults)")
|
||||
case let .groupRelays(u, groupInfo, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupRelays: \(groupRelays)")
|
||||
case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)")
|
||||
case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))")
|
||||
@@ -1981,6 +1984,11 @@ struct RelayConnectionResult: Decodable {
|
||||
var relayError: ChatError?
|
||||
}
|
||||
|
||||
struct AddRelayResult: Decodable {
|
||||
var relay: UserChatRelay
|
||||
var relayError: ChatError?
|
||||
}
|
||||
|
||||
enum ProtocolTestStep: String, Decodable, Equatable {
|
||||
case connect
|
||||
case disconnect
|
||||
|
||||
@@ -1117,6 +1117,27 @@ private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
|
||||
}
|
||||
}
|
||||
|
||||
func connErrorText(_ e: ChatError) -> String {
|
||||
switch e {
|
||||
case .error(.invalidConnReq):
|
||||
NSLocalizedString("Invalid connection link", comment: "conn error description")
|
||||
case .error(.unsupportedConnReq):
|
||||
NSLocalizedString("Unsupported connection link", comment: "conn error description")
|
||||
case .errorAgent(.SMP(_, .AUTH)):
|
||||
NSLocalizedString("Connection error (AUTH)", comment: "conn error description")
|
||||
case let .errorAgent(.SMP(_, .BLOCKED(info))):
|
||||
NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description")
|
||||
case .errorAgent(.SMP(_, .QUOTA)):
|
||||
NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description")
|
||||
default:
|
||||
if getNetworkErrorAlert(e) != nil {
|
||||
NSLocalizedString("Network error", comment: "conn error description")
|
||||
} else {
|
||||
"\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
|
||||
mkAlert(
|
||||
title: "Contact already exists",
|
||||
@@ -1847,12 +1868,19 @@ func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInf
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> (GroupInfo, GroupLink, [GroupRelay])? {
|
||||
enum PublicGroupCreationResult {
|
||||
case created(GroupInfo, GroupLink, [GroupRelay])
|
||||
case creationFailed([AddRelayResult])
|
||||
}
|
||||
|
||||
func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> PublicGroupCreationResult? {
|
||||
let userId = try currentUserId("apiNewPublicGroup")
|
||||
let r: APIResult<ChatResponse2>? = await chatApiSendCmdWithRetry(.apiNewPublicGroup(userId: userId, incognito: incognito, relayIds: relayIds, groupProfile: groupProfile))
|
||||
switch r {
|
||||
case let .result(.publicGroupCreated(_, groupInfo, groupLink, groupRelays)):
|
||||
return (groupInfo, groupLink, groupRelays)
|
||||
return .created(groupInfo, groupLink, groupRelays)
|
||||
case let .result(.publicGroupCreationFailed(_, addRelayResults)):
|
||||
return .creationFailed(addRelayResults)
|
||||
default: if let r { throw r.unexpected } else { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,20 +174,32 @@ struct AddChannelView: View {
|
||||
}
|
||||
return
|
||||
}
|
||||
guard let (gInfo, gLink, gRelays) = try await apiNewPublicGroup(
|
||||
guard let result = try await apiNewPublicGroup(
|
||||
incognito: false, relayIds: relayIds, groupProfile: profile
|
||||
) else {
|
||||
await MainActor.run { creationInProgress = false }
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
m.updateGroup(gInfo)
|
||||
m.creatingChannelId = gInfo.id
|
||||
groupInfo = gInfo
|
||||
groupLink = gLink
|
||||
groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) }
|
||||
channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays)
|
||||
creationInProgress = false
|
||||
switch result {
|
||||
case let .created(gInfo, gLink, gRelays):
|
||||
await MainActor.run {
|
||||
m.updateGroup(gInfo)
|
||||
m.creatingChannelId = gInfo.id
|
||||
groupInfo = gInfo
|
||||
groupLink = gLink
|
||||
groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) }
|
||||
channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays)
|
||||
creationInProgress = false
|
||||
}
|
||||
case let .creationFailed(relayResults):
|
||||
await MainActor.run {
|
||||
creationInProgress = false
|
||||
showAlert(
|
||||
NSLocalizedString("Error creating channel", comment: "alert title"),
|
||||
message: NSLocalizedString("Relay results:", comment: "alert message") + "\n" +
|
||||
relayResults.map { "\(chatRelayDisplayName($0.relay)): \($0.relayError.map { connErrorText($0) } ?? "ok")" }.joined(separator: "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
@@ -429,6 +441,11 @@ func relayDisplayName(_ relay: GroupRelay) -> String {
|
||||
return "relay \(relay.groupRelayId)"
|
||||
}
|
||||
|
||||
private func chatRelayDisplayName(_ relay: UserChatRelay) -> String {
|
||||
if !relay.displayName.isEmpty { return relay.displayName }
|
||||
return relay.address
|
||||
}
|
||||
|
||||
func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false, memberStatus: GroupMemberStatus? = nil) -> some View {
|
||||
let removed = memberStatus.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false
|
||||
let color: Color = connFailed || removed ? .red : (status == .rsActive ? .green : .yellow)
|
||||
|
||||
+34
-2
@@ -1564,6 +1564,23 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
fun connErrorText(e: ChatError): String = when {
|
||||
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq ->
|
||||
generalGetString(MR.strings.invalid_connection_link)
|
||||
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq ->
|
||||
generalGetString(MR.strings.unsupported_connection_link)
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH ->
|
||||
generalGetString(MR.strings.connection_error_auth)
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED ->
|
||||
"${generalGetString(MR.strings.connection_error_blocked)}: ${e.agentError.smpErr.blockInfo.reason.text}"
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.QUOTA ->
|
||||
generalGetString(MR.strings.connection_reached_limit_of_undelivered_messages)
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.BROKER ->
|
||||
generalGetString(MR.strings.network_error)
|
||||
else ->
|
||||
"${generalGetString(MR.strings.error_prefix)}: ${e.string}"
|
||||
}
|
||||
|
||||
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? {
|
||||
val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null }
|
||||
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData))
|
||||
@@ -2118,10 +2135,16 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List<Long>, groupProfile: GroupProfile): Triple<GroupInfo, GroupLink, List<GroupRelay>>? {
|
||||
sealed class PublicGroupCreationResult {
|
||||
data class Created(val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>): PublicGroupCreationResult()
|
||||
data class CreationFailed(val addRelayResults: List<AddRelayResult>): PublicGroupCreationResult()
|
||||
}
|
||||
|
||||
suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List<Long>, groupProfile: GroupProfile): PublicGroupCreationResult? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiNewPublicGroup") }.getOrElse { return null }
|
||||
val r = sendCmdWithRetry(rh, CC.ApiNewPublicGroup(userId, incognito, relayIds, groupProfile))
|
||||
if (r is API.Result && r.res is CR.PublicGroupCreated) return Triple(r.res.groupInfo, r.res.groupLink, r.res.groupRelays)
|
||||
if (r is API.Result && r.res is CR.PublicGroupCreated) return PublicGroupCreationResult.Created(r.res.groupInfo, r.res.groupLink, r.res.groupRelays)
|
||||
if (r is API.Result && r.res is CR.PublicGroupCreationFailed) return PublicGroupCreationResult.CreationFailed(r.res.addRelayResults)
|
||||
if (r != null) throw Exception("${r.responseType}: ${r.details}")
|
||||
return null
|
||||
}
|
||||
@@ -4569,6 +4592,12 @@ data class RelayConnectionResult(
|
||||
val relayError: ChatError? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddRelayResult(
|
||||
val relay: UserChatRelay,
|
||||
val relayError: ChatError? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupShortLinkInfo(
|
||||
val direct: Boolean,
|
||||
@@ -6345,6 +6374,7 @@ sealed class CR {
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("publicGroupCreated") class PublicGroupCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>): CR()
|
||||
@Serializable @SerialName("publicGroupCreationFailed") class PublicGroupCreationFailed(val user: UserRef, val addRelayResults: List<AddRelayResult>): CR()
|
||||
@Serializable @SerialName("groupRelays") class GroupRelays(val user: UserRef, val groupInfo: GroupInfo, val groupRelays: List<GroupRelay>): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
|
||||
@@ -6533,6 +6563,7 @@ sealed class CR {
|
||||
is ForwardPlan -> "forwardPlan"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is PublicGroupCreated -> "publicGroupCreated"
|
||||
is PublicGroupCreationFailed -> "publicGroupCreationFailed"
|
||||
is GroupRelays -> "groupRelays"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
|
||||
@@ -6714,6 +6745,7 @@ sealed class CR {
|
||||
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is PublicGroupCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays")
|
||||
is PublicGroupCreationFailed -> withUser(user, "addRelayResults: $addRelayResults")
|
||||
is GroupRelays -> withUser(user, "groupInfo: $groupInfo\ngroupRelays: $groupRelays")
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
|
||||
|
||||
+28
-12
@@ -130,19 +130,31 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
relayIds = relayIds,
|
||||
groupProfile = profile
|
||||
)
|
||||
if (result != null) {
|
||||
val (gI, gL, gR) = result
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId = null, gI)
|
||||
chatModel.creatingChannelId.value = gI.id
|
||||
groupInfo.value = gI
|
||||
groupLink.value = gL
|
||||
groupRelays.value = gR.sortedBy { relayDisplayName(it) }
|
||||
ChannelRelaysModel.set(gI.groupId, gR)
|
||||
creationInProgress.value = false
|
||||
when (result) {
|
||||
is ChatController.PublicGroupCreationResult.Created -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo)
|
||||
chatModel.creatingChannelId.value = result.groupInfo.id
|
||||
groupInfo.value = result.groupInfo
|
||||
groupLink.value = result.groupLink
|
||||
groupRelays.value = result.groupRelays.sortedBy { relayDisplayName(it) }
|
||||
ChannelRelaysModel.set(result.groupInfo.groupId, result.groupRelays)
|
||||
creationInProgress.value = false
|
||||
}
|
||||
}
|
||||
is ChatController.PublicGroupCreationResult.CreationFailed -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
creationInProgress.value = false
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_creating_channel),
|
||||
text = generalGetString(MR.strings.relay_results) + "\n" +
|
||||
result.addRelayResults.joinToString("\n") { "${chatRelayDisplayName(it.relay)}: ${it.relayError?.let { e -> ChatController.connErrorText(e) } ?: "ok"}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
null -> {
|
||||
withContext(Dispatchers.Main) { creationInProgress.value = false }
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) { creationInProgress.value = false }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -545,6 +557,10 @@ fun relayDisplayName(relay: GroupRelay): String {
|
||||
return "relay ${relay.groupRelayId}"
|
||||
}
|
||||
|
||||
private fun chatRelayDisplayName(relay: UserChatRelay): String {
|
||||
if (relay.displayName.isNotEmpty()) return relay.displayName
|
||||
return relay.address
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false, memberStatus: GroupMemberStatus? = null) {
|
||||
|
||||
@@ -2943,6 +2943,10 @@
|
||||
<string name="channel_display_name_field">Channel name</string>
|
||||
<string name="creating_channel">Creating channel</string>
|
||||
<string name="error_creating_channel">Error creating channel</string>
|
||||
<string name="relay_results">Relay results:</string>
|
||||
<string name="connection_reached_limit_of_undelivered_messages">The connection reached the limit of undelivered messages</string>
|
||||
<string name="network_error">Network error</string>
|
||||
<string name="error_prefix">Error</string>
|
||||
<string name="cancel_creating_channel_question">Cancel creating channel?</string>
|
||||
<string name="cancel_creating_channel_confirm">Cancel</string>
|
||||
<string name="enable_at_least_one_chat_relay">Enable at least one chat relay to create a channel.</string>
|
||||
|
||||
@@ -983,6 +983,11 @@ PublicGroupCreated: Public group created.
|
||||
- groupLink: [GroupLink](./TYPES.md#grouplink)
|
||||
- groupRelays: [[GroupRelay](./TYPES.md#grouprelay)]
|
||||
|
||||
PublicGroupCreationFailed: Public group creation failed.
|
||||
- type: "publicGroupCreationFailed"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- addRelayResults: [[AddRelayResult](./TYPES.md#addrelayresult)]
|
||||
|
||||
ChatCmdError: Command error (only used in WebSockets API).
|
||||
- type: "chatCmdError"
|
||||
- chatError: [ChatError](./TYPES.md#chaterror)
|
||||
|
||||
@@ -5,6 +5,7 @@ This file is generated automatically.
|
||||
- [ACIReaction](#acireaction)
|
||||
- [AChat](#achat)
|
||||
- [AChatItem](#achatitem)
|
||||
- [AddRelayResult](#addrelayresult)
|
||||
- [AddressSettings](#addresssettings)
|
||||
- [AgentCryptoError](#agentcryptoerror)
|
||||
- [AgentErrorType](#agenterrortype)
|
||||
@@ -220,6 +221,15 @@ This file is generated automatically.
|
||||
- chatItem: [ChatItem](#chatitem)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## AddRelayResult
|
||||
|
||||
**Record type**:
|
||||
- relay: [UserChatRelay](#userchatrelay)
|
||||
- relayError: [ChatError](#chaterror)?
|
||||
|
||||
|
||||
---
|
||||
|
||||
## AddressSettings
|
||||
|
||||
@@ -117,7 +117,7 @@ chatCommandsDocsData =
|
||||
("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser", "CRChatCmdError"], [], Just UNBackground, "/_leave #" <> Param "groupId"),
|
||||
("APIListMembers", [], "Get group members.", ["CRGroupMembers", "CRChatCmdError"], [], Nothing, "/_members #" <> Param "groupId"),
|
||||
("APINewGroup", [], "Create group.", ["CRGroupCreated", "CRChatCmdError"], [], Nothing, "/_group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Json "groupProfile"),
|
||||
("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"),
|
||||
("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRPublicGroupCreationFailed", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"),
|
||||
("APIGetGroupRelays", [], "Get group relays.", ["CRGroupRelays", "CRChatCmdError"], [], Nothing, "/_get relays #" <> Param "groupId"),
|
||||
("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile")
|
||||
]
|
||||
|
||||
@@ -69,6 +69,7 @@ chatResponsesDocsData =
|
||||
("CRGroupLinkDeleted", ""),
|
||||
("CRGroupCreated", ""),
|
||||
("CRPublicGroupCreated", ""),
|
||||
("CRPublicGroupCreationFailed", ""),
|
||||
("CRGroupRelays", ""),
|
||||
("CRGroupMembers", ""),
|
||||
("CRGroupUpdated", ""),
|
||||
|
||||
@@ -202,6 +202,7 @@ chatTypesDocsData =
|
||||
(sti @(ContactUserPref SimplePreference), STUnion, "CUP", [], "", ""),
|
||||
(sti @(ContactUserPreference SimplePreference), STRecord, "", [], "", ""),
|
||||
(sti @(CreatedConnLink 'CMContact), STRecord, "", [], Param "connFullLink" <> Optional "" (" " <> Param "$0") "connShortLink", ""),
|
||||
(sti @AddRelayResult, STRecord, "", [], "", ""),
|
||||
(sti @AddressSettings, STRecord, "", [], "", ""),
|
||||
(sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types
|
||||
(sti @AgentErrorType, STUnion, "", [], "", ""),
|
||||
@@ -405,6 +406,7 @@ deriving instance Generic (CIReaction c d)
|
||||
deriving instance Generic (ContactUserPref p)
|
||||
deriving instance Generic (ContactUserPreference p)
|
||||
deriving instance Generic (CreatedConnLink m)
|
||||
deriving instance Generic AddRelayResult
|
||||
deriving instance Generic AddressSettings
|
||||
deriving instance Generic AgentCryptoError
|
||||
deriving instance Generic AgentErrorType
|
||||
|
||||
@@ -351,7 +351,7 @@ export interface APINewPublicGroup {
|
||||
}
|
||||
|
||||
export namespace APINewPublicGroup {
|
||||
export type Response = CR.PublicGroupCreated | CR.ChatCmdError
|
||||
export type Response = CR.PublicGroupCreated | CR.PublicGroupCreationFailed | CR.ChatCmdError
|
||||
|
||||
export function cmdString(self: APINewPublicGroup): string {
|
||||
return '/_public group ' + self.userId + (self.incognito ? ' incognito=on' : '') + ' ' + self.relayIds.join(',') + ' ' + JSON.stringify(self.groupProfile)
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ChatResponse =
|
||||
| CR.GroupLinkDeleted
|
||||
| CR.GroupCreated
|
||||
| CR.PublicGroupCreated
|
||||
| CR.PublicGroupCreationFailed
|
||||
| CR.GroupRelays
|
||||
| CR.GroupMembers
|
||||
| CR.GroupUpdated
|
||||
@@ -81,6 +82,7 @@ export namespace CR {
|
||||
| "groupLinkDeleted"
|
||||
| "groupCreated"
|
||||
| "publicGroupCreated"
|
||||
| "publicGroupCreationFailed"
|
||||
| "groupRelays"
|
||||
| "groupMembers"
|
||||
| "groupUpdated"
|
||||
@@ -258,6 +260,12 @@ export namespace CR {
|
||||
groupRelays: T.GroupRelay[]
|
||||
}
|
||||
|
||||
export interface PublicGroupCreationFailed extends Interface {
|
||||
type: "publicGroupCreationFailed"
|
||||
user: T.User
|
||||
addRelayResults: T.AddRelayResult[]
|
||||
}
|
||||
|
||||
export interface GroupRelays extends Interface {
|
||||
type: "groupRelays"
|
||||
user: T.User
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface AChatItem {
|
||||
chatItem: ChatItem
|
||||
}
|
||||
|
||||
export interface AddRelayResult {
|
||||
relay: UserChatRelay
|
||||
relayError?: ChatError
|
||||
}
|
||||
|
||||
export interface AddressSettings {
|
||||
businessAddress: boolean
|
||||
autoAccept?: AutoAccept
|
||||
|
||||
@@ -651,6 +651,12 @@ data RelayConnectionResult = RelayConnectionResult
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data AddRelayResult = AddRelayResult
|
||||
{ relay :: UserChatRelay,
|
||||
relayError :: Maybe ChatError
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data RelayTestStep
|
||||
= RTSGetLink
|
||||
| RTSDecodeLink
|
||||
@@ -721,6 +727,7 @@ data ChatResponse
|
||||
| CRWelcome {user :: User}
|
||||
| CRGroupCreated {user :: User, groupInfo :: GroupInfo}
|
||||
| CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]}
|
||||
| CRPublicGroupCreationFailed {user :: User, addRelayResults :: [AddRelayResult]}
|
||||
| CRGroupRelays {user :: User, groupInfo :: GroupInfo, groupRelays :: [GroupRelay]}
|
||||
| CRGroupMembers {user :: User, group :: Group}
|
||||
| CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]}
|
||||
@@ -1713,6 +1720,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RelayConnectionResult)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''AddRelayResult)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "RTS") ''RelayTestStep)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RelayTestFailure)
|
||||
|
||||
@@ -2479,13 +2479,29 @@ processChatCommand vr nm = \case
|
||||
APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do
|
||||
(gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user
|
||||
gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1)
|
||||
(gLink, groupRelays) <- setupLink gInfo `catchAllErrors` \e -> do
|
||||
(gLink, results) <- setupLink gInfo `catchAllErrors` \e -> do
|
||||
deleteInProgressGroup user gInfo
|
||||
throwError e
|
||||
createNewGroupItems user gInfo
|
||||
pure $ CRPublicGroupCreated user gInfo gLink groupRelays
|
||||
case partitionEithers (map snd results) of
|
||||
([], groupRelays) -> do
|
||||
createNewGroupItems user gInfo
|
||||
pure $ CRPublicGroupCreated user gInfo gLink groupRelays
|
||||
(errors@(e : _), _) -> do
|
||||
deleteInProgressGroup user gInfo
|
||||
-- If all errors are temporary (network, timeout, host), throw to allow retry
|
||||
if all isTempErr errors
|
||||
then throwError e
|
||||
else do
|
||||
let relayResults = map toRelayResult results
|
||||
toRelayResult (r, Left e) = AddRelayResult r (Just e)
|
||||
toRelayResult (r, Right _) = AddRelayResult r Nothing
|
||||
pure $ CRPublicGroupCreationFailed user relayResults
|
||||
where
|
||||
prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [GroupRelay]))
|
||||
isTempErr :: ChatError -> Bool
|
||||
isTempErr = \case
|
||||
ChatErrorAgent {agentError = e} -> temporaryOrHostError e
|
||||
_ -> False
|
||||
prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [(UserChatRelay, Either ChatError GroupRelay)]))
|
||||
prepareGroupLink user = do
|
||||
gVar <- asks random
|
||||
groupLinkId <- GroupLinkId <$> drgRandomBytes 16
|
||||
@@ -2514,8 +2530,8 @@ processChatCommand vr nm = \case
|
||||
subRole <- asks $ channelSubscriberRole . config
|
||||
gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId subRole subMode
|
||||
relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds)
|
||||
groupRelays <- addRelays user gInfo sLnk relays
|
||||
pure (gLink, groupRelays)
|
||||
results <- addRelays user gInfo sLnk relays
|
||||
pure (gLink, results)
|
||||
pure (groupProfile', memberId, groupKeys, setupLink)
|
||||
NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} ->
|
||||
processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile
|
||||
@@ -3862,44 +3878,43 @@ processChatCommand vr nm = \case
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci)
|
||||
addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [GroupRelay]
|
||||
addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [(UserChatRelay, Either ChatError GroupRelay)]
|
||||
addRelays user gInfo@GroupInfo {membership} groupSLink relays =
|
||||
mapConcurrently addRelay relays
|
||||
where
|
||||
addRelay :: UserChatRelay -> CM GroupRelay
|
||||
addRelay :: UserChatRelay -> CM (UserChatRelay, Either ChatError GroupRelay)
|
||||
addRelay relay@UserChatRelay {address} = do
|
||||
-- TODO [relays] owner: track and reuse relay profiles
|
||||
-- TODO - single profile linked to relay configuration record (chat_relays)
|
||||
-- TODO - update when fetching link data from relay address
|
||||
(FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address
|
||||
lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case
|
||||
Nothing -> throwChatError CEInvalidConnReq
|
||||
Just (agentV, _) -> do
|
||||
let chatV = agentToChatVersion agentV
|
||||
gVar <- asks random
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff
|
||||
(relayMember, conn, groupRelay) <- withFastStore $ \db -> do
|
||||
relayMember <- createRelayForOwner db vr gVar user gInfo relay
|
||||
groupRelay <- createGroupRelayRecord db gInfo relayMember relay
|
||||
conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode
|
||||
pure (relayMember, conn, groupRelay)
|
||||
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
|
||||
GroupMember {memberId = relayMemberId} = relayMember
|
||||
relayInv = GroupRelayInvitation {
|
||||
fromMember = MemberIdRole userMemberId userRole,
|
||||
fromMemberProfile = membershipProfile,
|
||||
relayMemberId,
|
||||
groupLink = groupSLink
|
||||
}
|
||||
dm <- encodeConnInfo $ XGrpRelayInv relayInv
|
||||
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode
|
||||
let newConnStatus = if sqSecured then ConnSndReady else ConnJoined
|
||||
withFastStore' $ \db -> do
|
||||
void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus
|
||||
updateRelayStatusFromTo db groupRelay RSNew RSInvited
|
||||
r <- tryAllErrors $ do
|
||||
(FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address
|
||||
lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case
|
||||
Nothing -> throwChatError CEInvalidConnReq
|
||||
Just (agentV, _) -> do
|
||||
let chatV = agentToChatVersion agentV
|
||||
gVar <- asks random
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff
|
||||
(relayMember, conn, groupRelay) <- withFastStore $ \db -> do
|
||||
relayMember <- createRelayForOwner db vr gVar user gInfo relay
|
||||
groupRelay <- createGroupRelayRecord db gInfo relayMember relay
|
||||
conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode
|
||||
pure (relayMember, conn, groupRelay)
|
||||
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
|
||||
GroupMember {memberId = relayMemberId} = relayMember
|
||||
relayInv = GroupRelayInvitation {
|
||||
fromMember = MemberIdRole userMemberId userRole,
|
||||
fromMemberProfile = membershipProfile,
|
||||
relayMemberId,
|
||||
groupLink = groupSLink
|
||||
}
|
||||
dm <- encodeConnInfo $ XGrpRelayInv relayInv
|
||||
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode
|
||||
let newConnStatus = if sqSecured then ConnSndReady else ConnJoined
|
||||
withFastStore' $ \db -> do
|
||||
void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus
|
||||
updateRelayStatusFromTo db groupRelay RSNew RSInvited
|
||||
pure (relay, r)
|
||||
privateGetUser :: UserId -> CM User
|
||||
privateGetUser userId =
|
||||
tryAllErrors (withStore (`getUser` userId)) >>= \case
|
||||
|
||||
@@ -7131,6 +7131,10 @@ Query: UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ?
|
||||
Plan:
|
||||
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE groups SET relay_request_err_reason = ?, updated_at = ? WHERE group_id = ?
|
||||
Plan:
|
||||
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ?
|
||||
Plan:
|
||||
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
@@ -180,6 +180,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"]
|
||||
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results
|
||||
CRGroupRelays u g relays -> ttyUser u $ viewGroupRelays g relays
|
||||
CRGroupMembers u g -> ttyUser u $ viewGroupMembers g
|
||||
CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms
|
||||
@@ -1238,6 +1239,14 @@ viewGroupCreated g testView =
|
||||
where
|
||||
relaysInstruction = "wait for selected relay(s) to join, then you can invite members via group link"
|
||||
|
||||
viewPublicGroupCreationFailed :: [AddRelayResult] -> [StyledString]
|
||||
viewPublicGroupCreationFailed results =
|
||||
["channel not created, results:"]
|
||||
<> map showRelayResult results
|
||||
where
|
||||
showRelayResult (AddRelayResult UserChatRelay {chatRelayId = DBEntityId i} err_) =
|
||||
" relay " <> sShow i <> ": " <> maybe "ok" (plain . tshow) err_
|
||||
|
||||
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
||||
viewCannotResendInvitation g c =
|
||||
[ ttyContact c <> " is already invited to group " <> ttyGroup' g,
|
||||
|
||||
@@ -265,6 +265,7 @@ chatGroupTests = do
|
||||
it "relay should leave channel" testChannelRelayLeave
|
||||
it "owner should update profile in channel (signed)" testChannelOwnerProfileUpdate
|
||||
it "subscriber should update profile in channel (signed)" testChannelSubscriberProfileUpdate
|
||||
it "should report relay results when one relay deleted its address" testChannelCreateDeletedRelay
|
||||
describe "channel message operations" $ do
|
||||
it "should update channel message" testChannelMessageUpdate
|
||||
it "should delete channel message" testChannelMessageDelete
|
||||
@@ -9539,6 +9540,33 @@ testChannelSubscriberProfileUpdate ps =
|
||||
dan `hasContactProfiles` ["alice", "bob", "kate", "dave"]
|
||||
eve `hasContactProfiles` ["alice", "bob", "kate", "dave", "eve"]
|
||||
|
||||
testChannelCreateDeletedRelay :: HasCallStack => TestParams -> IO ()
|
||||
testChannelCreateDeletedRelay ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do
|
||||
bob ##> "/ad"
|
||||
(bobSLink, _) <- getContactLinks bob True
|
||||
cath ##> "/ad"
|
||||
(cathSLink, _) <- getContactLinks cath True
|
||||
|
||||
alice ##> ("/relays name=bob " <> bobSLink <> " name=cath " <> cathSLink)
|
||||
alice <## "ok"
|
||||
|
||||
-- cath deletes her address - simulates relay becoming unavailable
|
||||
cath ##> "/da"
|
||||
cath <## "Your chat address is deleted - accepted contacts will remain connected."
|
||||
cath <## "To create a new chat address use /ad"
|
||||
|
||||
-- channel creation fails because one relay's address was deleted
|
||||
alice ##> "/public group relays=1,2 #team"
|
||||
alice <## "channel not created, results:"
|
||||
alice <## " relay 1: ok"
|
||||
alice <##. " relay 2: ChatErrorAgent"
|
||||
-- deleteInProgressGroup deletes relay connection alice joined on bob;
|
||||
-- bob's agent reports AUTH error when the queue is gone — drain it.
|
||||
void $ getTermLine bob
|
||||
|
||||
testChannelMessageUpdate :: HasCallStack => TestParams -> IO ()
|
||||
testChannelMessageUpdate ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
|
||||
Reference in New Issue
Block a user