core, ui: better error on failed channel creation (#6825)

This commit is contained in:
spaced4ndy
2026-04-20 08:17:42 +00:00
committed by GitHub
parent a7c6f0af95
commit 2eb25d124f
19 changed files with 268 additions and 67 deletions
+8
View File
@@ -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
+30 -2
View File
@@ -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)
@@ -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)
@@ -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>
+5
View File
@@ -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)
+10
View File
@@ -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
+1 -1
View File
@@ -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")
]
+1
View File
@@ -69,6 +69,7 @@ chatResponsesDocsData =
("CRGroupLinkDeleted", ""),
("CRGroupCreated", ""),
("CRPublicGroupCreated", ""),
("CRPublicGroupCreationFailed", ""),
("CRGroupRelays", ""),
("CRGroupMembers", ""),
("CRGroupUpdated", ""),
+2
View File
@@ -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
+9
View File
@@ -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)
+55 -40
View File
@@ -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=?)
+9
View File
@@ -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,
+28
View File
@@ -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 ->