diff --git a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs index e22f4ed470..aa101d7bf7 100644 --- a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs +++ b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs @@ -22,8 +22,9 @@ import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..)) import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB import Simplex.Chat.Protocol (supportedChatVRange) -import Simplex.Chat.Store.Groups (getGroupInfo, getHostMember) +import Simplex.Chat.Store.Groups (getHostMember) import Simplex.Chat.Store.Profiles (getUsers) +import Simplex.Chat.Store.Shared (getGroupInfo) import Simplex.Chat.Types import Simplex.Messaging.Agent.Store.Common import qualified Simplex.Messaging.Agent.Store.DB as DB diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index b408d8eb30..54ffc977d5 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -30,6 +30,7 @@ This file is generated automatically. - [APILeaveGroup](#apileavegroup) - [APIListMembers](#apilistmembers) - [APINewGroup](#apinewgroup) +- [APINewPublicGroup](#apinewpublicgroup) - [APIUpdateGroupProfile](#apiupdategroupprofile) [Group link commands](#group-link-commands) @@ -593,7 +594,7 @@ Add contact to group. Requires bot to have Admin role. **Syntax**: ``` -/_add # observer|author|member|moderator|admin|owner +/_add # relay|observer|author|member|moderator|admin|owner ``` ```javascript @@ -672,7 +673,7 @@ Accept group member. Requires Admin role. **Syntax**: ``` -/_accept member # observer|author|member|moderator|admin|owner +/_accept member # relay|observer|author|member|moderator|admin|owner ``` ```javascript @@ -715,7 +716,7 @@ Set members role. Requires Admin role. **Syntax**: ``` -/_member role # [,...] observer|author|member|moderator|admin|owner +/_member role # [,...] relay|observer|author|member|moderator|admin|owner ``` ```javascript @@ -940,6 +941,48 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APINewPublicGroup + +Create public group. + +*Network usage*: interactive. + +**Parameters**: +- userId: int64 +- incognito: bool +- relayIds: [int64] +- groupProfile: [GroupProfile](./TYPES.md#groupprofile) + +**Syntax**: + +``` +/_public group [ incognito=on] [,...] +``` + +```javascript +'/_public group ' + userId + (incognito ? ' incognito=on' : '') + ' ' + relayIds.join(',') + ' ' + JSON.stringify(groupProfile) // JavaScript +``` + +```python +'/_public group ' + str(userId) + (' incognito=on' if incognito else '') + ' ' + ','.join(map(str, relayIds)) + ' ' + json.dumps(groupProfile) # Python +``` + +**Responses**: + +PublicGroupCreated: Public group created. +- type: "publicGroupCreated" +- user: [User](./TYPES.md#user) +- groupInfo: [GroupInfo](./TYPES.md#groupinfo) +- groupLink: [GroupLink](./TYPES.md#grouplink) +- groupRelays: [[GroupRelay](./TYPES.md#grouprelay)] + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ### APIUpdateGroupProfile Update group profile. @@ -998,7 +1041,7 @@ Create group link. **Syntax**: ``` -/_create link # observer|author|member|moderator|admin|owner +/_create link # relay|observer|author|member|moderator|admin|owner ``` ```javascript @@ -1037,7 +1080,7 @@ Set member role for group link. **Syntax**: ``` -/_set link role # observer|author|member|moderator|admin|owner +/_set link role # relay|observer|author|member|moderator|admin|owner ``` ```javascript diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index d7405ef846..a71a4540f5 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -38,6 +38,7 @@ This file is generated automatically. - [MemberAcceptedByOther](#memberacceptedbyother) - [MemberBlockedForAll](#memberblockedforall) - [GroupMemberUpdated](#groupmemberupdated) + - [GroupLinkRelaysUpdated](#grouplinkrelaysupdated) [File events](#file-events) - Main events @@ -445,6 +446,20 @@ Another group member profile updated. --- +### GroupLinkRelaysUpdated + +Group link relays updated. + +**Record type**: +- type: "groupLinkRelaysUpdated" +- user: [User](./TYPES.md#user) +- groupInfo: [GroupInfo](./TYPES.md#groupinfo) +- groupLink: [GroupLink](./TYPES.md#grouplink) +- groupRelays: [[GroupRelay](./TYPES.md#grouprelay)] + +--- + + ## File events Bots that send or receive files may process these events to track delivery status and to process completion. diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 2797d8d6b1..398a4afbff 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -102,6 +102,7 @@ This file is generated automatically. - [GroupPreference](#grouppreference) - [GroupPreferences](#grouppreferences) - [GroupProfile](#groupprofile) +- [GroupRelay](#grouprelay) - [GroupShortLinkData](#groupshortlinkdata) - [GroupSummary](#groupsummary) - [GroupSupportChat](#groupsupportchat) @@ -140,6 +141,7 @@ This file is generated automatically. - [RcvFileStatus](#rcvfilestatus) - [RcvFileTransfer](#rcvfiletransfer) - [RcvGroupEvent](#rcvgroupevent) +- [RelayStatus](#relaystatus) - [ReportReason](#reportreason) - [RoleGroupPreference](#rolegrouppreference) - [SMPAgentError](#smpagenterror) @@ -2135,6 +2137,7 @@ MemberSupport: **Record type**: - groupId: int64 - useRelays: bool +- relayOwnStatus: [RelayStatus](#relaystatus)? - localDisplayName: string - groupProfile: [GroupProfile](#groupprofile) - localAlias: string @@ -2177,6 +2180,7 @@ MemberSupport: Ok: - type: "ok" +- direct: bool - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? OwnLink: @@ -2220,7 +2224,6 @@ Known: - createdAt: UTCTime - updatedAt: UTCTime - supportChat: [GroupSupportChat](#groupsupportchat)? -- isChatRelay: bool --- @@ -2257,6 +2260,7 @@ Known: ## GroupMemberRole **Enum type**: +- "relay" - "observer" - "author" - "member" @@ -2331,10 +2335,23 @@ Known: - shortDescr: string? - description: string? - image: string? +- groupLink: string? - groupPreferences: [GroupPreferences](#grouppreferences)? - memberAdmission: [GroupMemberAdmission](#groupmemberadmission)? +--- + +## GroupRelay + +**Record type**: +- groupRelayId: int64 +- groupMemberId: int64 +- userChatRelayId: int64 +- relayStatus: [RelayStatus](#relaystatus) +- relayLink: string? + + --- ## GroupShortLinkData @@ -3043,6 +3060,17 @@ NewMemberPendingReview: - type: "newMemberPendingReview" +--- + +## RelayStatus + +**Enum type**: +- "new" +- "invited" +- "accepted" +- "active" + + --- ## ReportReason @@ -3281,6 +3309,9 @@ UserNotFound: - type: "userNotFound" - userId: int64 +RelayUserNotFound: +- type: "relayUserNotFound" + UserNotFoundByName: - type: "userNotFoundByName" - contactName: string @@ -3384,6 +3415,9 @@ GroupWithoutUser: DuplicateGroupMember: - type: "duplicateGroupMember" +DuplicateMemberId: +- type: "duplicateMemberId" + GroupAlreadyJoined: - type: "groupAlreadyJoined" @@ -3572,6 +3606,18 @@ OperatorNotFound: UsageConditionsNotFound: - type: "usageConditionsNotFound" +UserChatRelayNotFound: +- type: "userChatRelayNotFound" +- chatRelayId: int64 + +GroupRelayNotFound: +- type: "groupRelayNotFound" +- groupRelayId: int64 + +GroupRelayNotFoundByMemberId: +- type: "groupRelayNotFoundByMemberId" +- groupMemberId: int64 + InvalidQuote: - type: "invalidQuote" diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 0094b7d348..fa5dc49c9c 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -117,6 +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"), ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") ] ), @@ -240,6 +241,7 @@ cliCommands = "MemberRole", "MuteUser", "NewGroup", + "NewPublicGroup", "QuitChat", "ReactToMessage", "RejectContact", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index 130ea89846..ec58985ceb 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -97,7 +97,8 @@ chatEventsDocsData = [ ("CEvtConnectedToGroupMember", "Connected to another group member."), ("CEvtMemberAcceptedByOther", "Another group owner, admin or moderator accepted member to the group after review (\"knocking\")."), ("CEvtMemberBlockedForAll", "Another member blocked for all members."), - ("CEvtGroupMemberUpdated", "Another group member profile updated.") + ("CEvtGroupMemberUpdated", "Another group member profile updated."), + ("CEvtGroupLinkRelaysUpdated", "Group link relays updated.") ] ), ( "File events", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 60fe129cdb..321fac1d9c 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -68,6 +68,7 @@ chatResponsesDocsData = ("CRGroupLinkCreated", ""), ("CRGroupLinkDeleted", ""), ("CRGroupCreated", ""), + ("CRPublicGroupCreated", ""), ("CRGroupMembers", ""), ("CRGroupUpdated", ""), ("CRGroupsList", "Groups"), diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 73ad90e91b..77201172f5 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -275,12 +275,13 @@ chatTypesDocsData = (sti @GroupMemberAdmission, STRecord, "", [], "", ""), (sti @GroupMemberCategory, (STEnum' $ dropPfxSfx "GC" "Member"), "", [], "", ""), (sti @GroupMemberRef, STRecord, "", [], "", ""), - (sti @GroupMemberRole, STEnum, "GR", [], "", ""), + (sti @GroupMemberRole, (STEnum' $ dropPfxSfx "GR" ""), "", ["GRUnknown"], "", ""), (sti @GroupMemberSettings, STRecord, "", [], "", ""), (sti @GroupMemberStatus, (STEnum' $ (\case "group_deleted" -> "deleted"; "intro_invited" -> "intro-inv"; s -> s) . consSep "GSMem" '_'), "", [], "", ""), (sti @GroupPreference, STRecord, "", [], "", ""), (sti @GroupPreferences, STRecord, "", [], "", ""), (sti @GroupProfile, STRecord, "", [], "", ""), + (sti @GroupRelay, STRecord, "", [], "", ""), (sti @GroupShortLinkData, STRecord, "", [], "", ""), (sti @GroupSummary, STRecord, "", [], "", ""), (sti @GroupSupportChat, STRecord, "", [], "", ""), @@ -320,6 +321,7 @@ chatTypesDocsData = (sti @RcvFileStatus, STUnion, "RFS", [], "", ""), (sti @RcvFileTransfer, STRecord, "", [], "", ""), (sti @RcvGroupEvent, STUnion, "RGE", [], "", ""), + (sti @RelayStatus, STEnum, "RS", [], "", ""), (sti @ReportReason, (STEnum' $ dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""), (sti @RoleGroupPreference, STRecord, "", [], "", ""), (sti @SecurityCode, STRecord, "", [], "", ""), @@ -468,6 +470,7 @@ deriving instance Generic GroupMemberStatus deriving instance Generic GroupPreference deriving instance Generic GroupPreferences deriving instance Generic GroupProfile +deriving instance Generic GroupRelay deriving instance Generic GroupShortLinkData deriving instance Generic GroupSummary deriving instance Generic GroupSupportChat @@ -513,6 +516,7 @@ deriving instance Generic RcvFileDescr deriving instance Generic RcvFileStatus deriving instance Generic RcvFileTransfer deriving instance Generic RcvGroupEvent +deriving instance Generic RelayStatus deriving instance Generic ReportReason deriving instance Generic SecurityCode deriving instance Generic SimplexLinkType diff --git a/cabal.project b/cabal.project index 390890d258..a03d492bd6 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ca26c69937083deee43b8b2200ec9ef4c004ceac + tag: 89b81d151fa0378196d923c5d7fa0aea08462136 source-repository-package type: git diff --git a/docs/rfcs/2025-10-20-chat-relays.md b/docs/rfcs/2025-10-20-chat-relays.md new file mode 100644 index 0000000000..d1a3180b70 --- /dev/null +++ b/docs/rfcs/2025-10-20-chat-relays.md @@ -0,0 +1,304 @@ +# Chat relays + +## Security objectives + +Group relay protocol should achieve following objectives: +1. Stable message delivery between group members. +2. No possibility for relay to substitute group. +3. No possibility for relay to impersonate owner(s). +4. Prevent relay from altering member roster (member removal, role change, etc.). +5. Prevent relay from terminally destabilizing group by stopping to serve it. At the same time, allow owner to remove (last) relay with possibility to restore group functionality. +6. Allow owner(s) to send messages as "message from channel", hiding specific sender out of multiple owners from members. +7. Prevent relays from altering/dropping messages. + +## Protocol for adding chat relays to group + +Activations (execution bars) with looped arrows indicate internal calls/steps. + +```mermaid +sequenceDiagram + participant O as Owner + participant OSMP as Owner's
SMP server + participant R as Chat relay(s) + participant RSMP as Chat relays'
SMP server(s) + +note over O, RSMP: Owner creates new group, adds chat relays + +activate O +O ->> O: 1. Create new group
(user action) +O ->> O: 2. Prepare group link,
owner key,
group ID (agent) +O ->> O: 3. Add link, owner key
to group profile, sign +O ->> OSMP: 4. Create group link,
signed profile as data +deactivate O +OSMP -->> O: Group link created +activate O +O ->> O: 5. Choose chat relays
(automatic/user choice) +note left of O: Relay status: New +par With each relay + O ->> R: 6. Contact request
(x.grp.relay.inv
incl. group link) + deactivate O + activate R + note left of O: Relay status: Invited + note right of R: Relay status: Invited + R ->> OSMP: 7. Retrieve group link data + deactivate R + OSMP -->> R: Group link data + activate R + R ->> R: 8. Validate group profile,
verify profile signature + opt Bad profile or signature + R -x R: Abort (reject) + end + R ->> RSMP: 9. Create relay link,
set group ID
in immutable data + deactivate R + RSMP -->> R: Relay link created + activate R + R ->> O: 10. Accept request
(x.grp.relay.acpt
incl. relay link) + deactivate R + activate O + note right of R: Relay status: Accepted + note left of O: Relay status: Accepted + note over O, R: RPC connection
with relay is ready + opt Protocol extension - 2 connections + O ->> R: * Connect via relay link
(share same owner key) + deactivate O + R -->> O: Accept messaging connection + activate O + note right of R: Relay status: Accepted,
"Connected" implied from
messaging connection + note left of O: Relay status: Accepted,
"Connected" implied from
messaging connection + note over O, R: Owner: Messaging connection with relay is ready,
relay link is tested + end + create participant M as Member + R --> M: + note over R, M: At this point relay can accept
connection requests from members + O ->> RSMP: 11. Retrieve relay link data + deactivate O + RSMP -->> O: Relay link data + activate O + O ->> O: 12. Validate group ID
in relay link data + opt Bad group ID + O -x O: Abort for relay (don't add) + end + O ->> OSMP: 13. Update group link
(add relay link) + deactivate O + OSMP -->> O: Group link updated + note left of O: Relay status: Active +end + +note over O, M: Chat relay checks link - monitoring + +loop Periodically + R ->> OSMP: Retrieve group link data for served gorup + OSMP -->> R: Group link data + activate R + R ->> R: Check relay link present + deactivate R + note right of R: Relay status: Active +end + +note over O, M: New member connects + +O -->> M: 14. Share group link
(social, out-of-band) +M ->> OSMP: 15. Retrieve short link data +par RPC connection + M ->> R: 16a. Connect via relay link +and + opt Protocol extension - Messaging connection + M ->> R: 16b*. Connect via relay link
(share same member key/
identifier to correlate) + end +end + +note over O, M: Message forwarding + +O ->> R: 17. Send message +R ->> M: 18. Forward message +activate M +M ->> M: 19. Deduplicate message +deactivate M +``` + +Notes: + +- Group ID - unique group identifier (not globally unique) baked in immutable part of group link data, and repeated by chat relays in immutable parts of respective relay links. + + Owner can validate they're adding relay link to the group link specifically for their group. + + Members can validate they join relay links corresponding to group link they connected to. + +- Protocol extension: Create connections pairs between relay and members with different priority for passing regular messages and for relay responding to member requests. + + Invitation sent in step 12 should contain same key as in group link, for relay to match connection to the same owner and "active" relay link (add to `XContact` message). + + Add new connection entity, special for groups with relay, referencing member record - parallel to first member connection. + +- Client can "know" link that will be created before creating it on server - so we can add it to profile before adding profile to group short link data. + + Agent to return link that will be created upon preparing connection record. + +- On adding group short link to group profile. + + Strengthens association between link and profile. Link already contains profile in attached data, but from perspective of group profile link itself is detached. All members "see" the same link they joined via in group profile. Chat relays "see" the same link they created relay links for, and can check it for presence of their relay link at any point. + + Link is recoverable from profile, e.g. for purpose of restoring connection with group via new chat relays. + + Overall it just seems a natural and convenient way to store group link for all members, rather than having it separately. + +- On updating group link data with one relay link at a time vs waiting for all links. + + Overhead is minimal - one request to owner's SMP server per relay. + + Waiting for a relay to send relay link can take indefinitely long. + + In proposed protocol owner doesn't have to wait for links from all relays for simplicity and to minimize wait time - it allows owner to conclude group creation potentially earlier, in case some relays are stuck or offline (owner can add their links later, once they successfully send it). + +- Lock owner group link from accepting connection on SMP server, possibly has some implementation gaps. + + Reject in owner code for foolproofing. + +- What should be in relay link user data: + + - Relay key for group. + - Relay identity if provided. + Operator relays want to provide identity for trust. + User relays may not want to provide identity. + Relay identity: profile, certificate, relay identity key (global across groups). + +## Protocol for removing chat relay from group, restoring connection to group + +```mermaid +sequenceDiagram + participant O as Owner + participant OSMP as Owner's
SMP server + participant R as Chat relay + participant RSMP as Chat relay
SMP server + participant M as Member + +note over O, M: Owner deletes chat relay, notifies relay + +O ->> OSMP: Remove relay link
(update group link data) +O ->> R: Delete chat relay
(x.grp.mem.del)
over RPC connection +par Chat relay to SMP + R ->> RSMP: Delete relay link +and Chat relay to members + R ->> M: Forward relay is deleted
over RPC connection +end + +note over O, M: Scenario 2. Owner deletes chat relay, fails to notify relay + +O ->> OSMP: Remove relay link
(update group link data) +O --x R: Fail to notify relay +opt Chat relay identifies
connection with owner is deleted + par Chat relay to SMP + destroy RSMP + R ->> RSMP: Delete relay link + and Chat relay to members + destroy R + R ->> M: Notify relay is deleted
over RPC connection + end +end + +note over O, M: Last relay is deleted + +O --x M: Owner can't send messages to members +activate M +M ->> M: Attempt to restore
connection to group (manual) +M ->> OSMP: Retrieve group link data +deactivate M +OSMP -->> M: Group link data +activate M +M -x M: Members can't restore connection to group +deactivate M + +note over O, M: Restore connection to group + +create participant NR as New chat relay +O <<->> NR: Add new relay, relay creates and sends link +O <<->> OSMP: Update group link
(add relay link) +activate M +M ->> M: Attempt to restore
connection to group (manual) +M ->> OSMP: Retrieve group link data +deactivate M +OSMP -->> M: Group link data +par RPC connection + M ->> NR: Connect via relay link +and Messaging connection + M ->> NR: Connect via relay link
(share same member key/
identifier to correlate) +end +O ->> NR: Send message +NR ->> M: Forward message +activate M +M ->> M: Deduplicate message +deactivate M +``` + +Notes: + +- New relay doesn't have group history. + + - We can prohibit to remove last relay without adding new one. + - Relays can synchronize history. + - Can be considered after MVP. + +## Correlation of design objectives with design elements + +1. Redundant delivery by multiple relays. High availability of relay clients. +2. Same group ID baked in immutable data of group link and relay links. +3. Owner public key in group link. +4. Actions altering member roster can be signed by owner key, verified by members. +5. Protocol for restoring connection to group by checking group link for new relays. +6. XMsgNew protocol extension - "message from channel" flag - see [channels forwarding rfc](./2025-08-11-channels-forwarding.md). +7. Redundant delivery by multiple relays, highlighting deduplicated messages differences - see [channels forwarding rfc](./2025-08-11-channels-forwarding.md). + +## Threat model + +**Single compromised chat relay / Colluding chat relays** + +can: +- effectively substitute group bar group ID and signed profile, by sending unsigned content from other group (or any arbitrary content), that doesn't require signature verification, such as regular messages. + - one way this could be further mitigated is requiring owner to sign all messages. + - owner could periodically sign message history as merkle dag. +- selectively drop any content or service messages from owner, including actions altering member roster. +- selectively drop messages for some of members. + +cannot: +- technically, redirect newly joining member to a different group. +- substitute group profile. +- impersonate owner, send any member message that requires signature. + +**Compromised chat relay (in situation where not all relays are compromised/colluding)** + +can: +- in case number of compromised relays is same as number of uncompromised ones, compromised relay(s) can drop messages or send arbitrary unsigned messages, misleading members from identifying which relays are compromised. +- ignore "message from channel" directive from owner, revealing which owner sent message. + - this can be revealed to owner by members out-of-band. +- fabricate new members, possibly inflating counts/costs for owner (depends on implementation). + - it can be identified that these imaginary members don't connect to other relays. + +**Member** + +can: +- infer which owner sent message as "message from channel", if group has a single owner. + - owner client should prohibit this option if group has a single owner. + +**Any client** + +can: +- connect to group unlimited number of times, inflating real counts/costs. + +## TODO list + +- Chat commands for creating group with relays. +- Protocol events processing. +- Recovery for both owner and relay when adding relay to group. +- On each subscription retrieve group link data for all groups, actualize connections for present relay links. +- Agent `prepareConnectionToJoin` api to return link that will be created. +- Asynchronous version of agent `setConnShortLink` api, correlation in chat. +- Agent to support adding relays to link (it has stub `relays :: [ConnShortLink 'CMContact]`). +- New connection entity for secondary member-in-relayed-group connection - priority/messages connections. +- Differentiate connection usage by priority in chat logic (receiving messages vs sending requests to relay). +- Finalize model - statuses, schema. +- UI for relay management (user level, similar to list of servers). +- UI for creating group with relays. +- UI for managing relays in group. +- Relay status updates events on adding relays for UI integration. +- Relay removal. +- Relay periodic checks for monitoring relay link presence. diff --git a/docs/rfcs/2026-01-08-relays-new-member-connection.md b/docs/rfcs/2026-01-08-relays-new-member-connection.md new file mode 100644 index 0000000000..471f4ed53f --- /dev/null +++ b/docs/rfcs/2026-01-08-relays-new-member-connection.md @@ -0,0 +1,99 @@ +# Connection of new member to chat relays + +## Problem + +Naive implementation of new member connection to chat relays can lead to partial failures (some relays fail to connect), or requires recovery or clean up. + +After group record is prepared from short link, naive flow is as follows (APIConnectPreparedGroup): + +``` +User clicks "Connect" + -> Fetch relay links from group link (sync getConnShortLink) + -> For each relay: + -> Fetch ConnectionRequestUri from relay link (sync getConnShortLink) + -> Join connection (sync joinConnection) +``` + +Orthogonal smaller problem: + +If new member chooses to connect to group incognito, same incognito profile should be sent to all group relays. + +## Solution + +### Join Connection step + +"Join connection" is the main step, let's consider it first. + +#### Option 1: Synchronous approach with catches + recovery + +Keep all relay connections synchronous, catch on failure to continue for remaining relays, recovery for failed relays. All relays failing would mean full command failure, offer user retry. + +For partial failures it would require to track which relays succeeded/failed, then trigger recovery, basically recreating what asynchronous command processing already does. + +#### Option 2: First relay sync, then async + +Connect to first relay synchronously, connect to remaining asynchronously (using joinConnectionAsync). + +Choice of "first" relay is arbitrary and we may be choosing the one with worse network. + +Mixed (double) implementation - for "first" and remaining relays. + +#### Option 3: All relays async + +In this case agent already handles connection reliability, downside is no immediate failure visible to user on temporary network errors for all relays (for example, client is offline). + +UI already handles "connecting..." state, so async path doesn't hurt UX much other than in mentioned case. UI stays in "connecting..." until at least one relay connection succeeds. + +If all relay connections permanently fail, update state for UI - requires permanent error handling for connection creation on continuation (agent responses in Subscriber). Track relay connection states to detect "all failed", possibly on connection status, TBC at implementation. + +Pros: +- Simple flow: loop through relays, start async connections. +- Async agent commands provide recovery. + +### Link fetches + +We considered handling retries for Join step, but no retry mechanism for link fetch. If it's synchronous and fails for a given relay, it would result in permanent failure to connect to relay, without additional recovery logic. + +#### Option 1: Asynchronous command with continuation + +New agent asynchronous command + complexity in chat Subscriber logic. Seems overkill. + +#### Option 2: Per-relay "relay connection" worker + +An additional state machine, possibly based on relay member records as work items. Also overkill. + +#### Option 3: Make all link fetches synchronously before proceeding + +To avoid adding background recovery mechanisms for link fetching per relay, we could fetch all links data synchronously, and only then connect to relays asynchronously. + +In case any relay link fetch fails, user would be given option to retry. (Whole operation fails and is retried) + +Group link fetch is also synchronous (retrieve list of relay links), and also leads to immediate user retry. + +### On the incognito profile issue + +This should be addressed regardless of which approach to connection we choose. The incognito profile should be: + +1. Created once before starting any relay connections; +2. Passed to all relays on connection attempts. + +In case of synchronous approach and re-use of existing logic, it means `connectViaContact` should accept an optional profile (not just flag). + +### Overall proposed connection flow + +``` +User clicks "Connect" + -> Fetch relay links from group link (sync getConnShortLink) + -> For each relay: Fetch ConnectionRequestUri from relay link (sync getConnShortLink) + -> Once all links are resolved, proceed - create incognito profile ONCE for all relays, if needed + -> For each relay: Start async connection attempt (joinConnectionAsync) + -> Agent handles connection retries internally + -> Subscriber handles JOINED events and errors for each relay + - At least one relay JOINED -> group becomes functional + - All relays permanently fail -> show failure to user +``` + +Link fetches being synchronous in conjunction with asynchronous relay connections allows for similar UI reactivity to current single-connection flows: +- Network failures during link fetches require user retry; +- Connection attempts are retried by agent on network failures; +- Link fetches passing ensures client is not offline when starting async connection attempts (unless user goes offline in-between, but window is very small, and connections would be retried anyway). diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 66f4f6ec5f..742f5b8dd2 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -341,6 +341,23 @@ export namespace APINewGroup { } } +// Create public group. +// Network usage: interactive. +export interface APINewPublicGroup { + userId: number // int64 + incognito: boolean + relayIds: number[] // int64, non-empty + groupProfile: T.GroupProfile +} + +export namespace APINewPublicGroup { + export type Response = CR.PublicGroupCreated | CR.ChatCmdError + + export function cmdString(self: APINewPublicGroup): string { + return '/_public group ' + self.userId + (self.incognito ? ' incognito=on' : '') + ' ' + self.relayIds.join(',') + ' ' + JSON.stringify(self.groupProfile) + } +} + // Update group profile. // Network usage: background. export interface APIUpdateGroupProfile { diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts index cb6ba85c8b..f7b1725843 100644 --- a/packages/simplex-chat-client/types/typescript/src/events.ts +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -29,6 +29,7 @@ export type ChatEvent = | CEvt.MemberAcceptedByOther | CEvt.MemberBlockedForAll | CEvt.GroupMemberUpdated + | CEvt.GroupLinkRelaysUpdated | CEvt.RcvFileDescrReady | CEvt.RcvFileComplete | CEvt.SndFileCompleteXFTP @@ -80,6 +81,7 @@ export namespace CEvt { | "memberAcceptedByOther" | "memberBlockedForAll" | "groupMemberUpdated" + | "groupLinkRelaysUpdated" | "rcvFileDescrReady" | "rcvFileComplete" | "sndFileCompleteXFTP" @@ -296,6 +298,14 @@ export namespace CEvt { toMember: T.GroupMember } + export interface GroupLinkRelaysUpdated extends Interface { + type: "groupLinkRelaysUpdated" + user: T.User + groupInfo: T.GroupInfo + groupLink: T.GroupLink + groupRelays: T.GroupRelay[] + } + export interface RcvFileDescrReady extends Interface { type: "rcvFileDescrReady" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 684aeec7af..de80b8666d 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -27,6 +27,7 @@ export type ChatResponse = | CR.GroupLinkCreated | CR.GroupLinkDeleted | CR.GroupCreated + | CR.PublicGroupCreated | CR.GroupMembers | CR.GroupUpdated | CR.GroupsList @@ -78,6 +79,7 @@ export namespace CR { | "groupLinkCreated" | "groupLinkDeleted" | "groupCreated" + | "publicGroupCreated" | "groupMembers" | "groupUpdated" | "groupsList" @@ -245,6 +247,14 @@ export namespace CR { groupInfo: T.GroupInfo } + export interface PublicGroupCreated extends Interface { + type: "publicGroupCreated" + user: T.User + groupInfo: T.GroupInfo + groupLink: T.GroupLink + groupRelays: T.GroupRelay[] + } + export interface GroupMembers extends Interface { type: "groupMembers" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 5b0a4ff6b5..a05d549d17 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2421,6 +2421,7 @@ export enum GroupFeatureEnabled { export interface GroupInfo { groupId: number // int64 useRelays: boolean + relayOwnStatus?: RelayStatus localDisplayName: string groupProfile: GroupProfile localAlias: string @@ -2467,6 +2468,7 @@ export namespace GroupLinkPlan { export interface Ok extends Interface { type: "ok" + direct: boolean groupSLinkData_?: GroupShortLinkData } @@ -2511,7 +2513,6 @@ export interface GroupMember { createdAt: string // ISO-8601 timestamp updatedAt: string // ISO-8601 timestamp supportChat?: GroupSupportChat - isChatRelay: boolean } export interface GroupMemberAdmission { @@ -2532,6 +2533,7 @@ export interface GroupMemberRef { } export enum GroupMemberRole { + Relay = "relay", Observer = "observer", Author = "author", Member = "member", @@ -2586,10 +2588,19 @@ export interface GroupProfile { shortDescr?: string description?: string image?: string + groupLink?: string groupPreferences?: GroupPreferences memberAdmission?: GroupMemberAdmission } +export interface GroupRelay { + groupRelayId: number // int64 + groupMemberId: number // int64 + userChatRelayId: number // int64 + relayStatus: RelayStatus + relayLink?: string +} + export interface GroupShortLinkData { groupProfile: GroupProfile } @@ -3444,6 +3455,13 @@ export namespace RcvGroupEvent { } } +export enum RelayStatus { + New = "new", + Invited = "invited", + Accepted = "accepted", + Active = "active", +} + export enum ReportReason { Spam = "spam", Content = "content", @@ -3724,6 +3742,7 @@ export namespace SrvError { export type StoreError = | StoreError.DuplicateName | StoreError.UserNotFound + | StoreError.RelayUserNotFound | StoreError.UserNotFoundByName | StoreError.UserNotFoundByContactId | StoreError.UserNotFoundByGroupId @@ -3751,6 +3770,7 @@ export type StoreError = | StoreError.InvalidMemberRelationUpdate | StoreError.GroupWithoutUser | StoreError.DuplicateGroupMember + | StoreError.DuplicateMemberId | StoreError.GroupAlreadyJoined | StoreError.GroupInvitationNotFound | StoreError.NoteFolderAlreadyExists @@ -3799,6 +3819,9 @@ export type StoreError = | StoreError.ProhibitedDeleteUser | StoreError.OperatorNotFound | StoreError.UsageConditionsNotFound + | StoreError.UserChatRelayNotFound + | StoreError.GroupRelayNotFound + | StoreError.GroupRelayNotFoundByMemberId | StoreError.InvalidQuote | StoreError.InvalidMention | StoreError.InvalidDeliveryTask @@ -3811,6 +3834,7 @@ export namespace StoreError { export type Tag = | "duplicateName" | "userNotFound" + | "relayUserNotFound" | "userNotFoundByName" | "userNotFoundByContactId" | "userNotFoundByGroupId" @@ -3838,6 +3862,7 @@ export namespace StoreError { | "invalidMemberRelationUpdate" | "groupWithoutUser" | "duplicateGroupMember" + | "duplicateMemberId" | "groupAlreadyJoined" | "groupInvitationNotFound" | "noteFolderAlreadyExists" @@ -3886,6 +3911,9 @@ export namespace StoreError { | "prohibitedDeleteUser" | "operatorNotFound" | "usageConditionsNotFound" + | "userChatRelayNotFound" + | "groupRelayNotFound" + | "groupRelayNotFoundByMemberId" | "invalidQuote" | "invalidMention" | "invalidDeliveryTask" @@ -3907,6 +3935,10 @@ export namespace StoreError { userId: number // int64 } + export interface RelayUserNotFound extends Interface { + type: "relayUserNotFound" + } + export interface UserNotFoundByName extends Interface { type: "userNotFoundByName" contactName: string @@ -4037,6 +4069,10 @@ export namespace StoreError { type: "duplicateGroupMember" } + export interface DuplicateMemberId extends Interface { + type: "duplicateMemberId" + } + export interface GroupAlreadyJoined extends Interface { type: "groupAlreadyJoined" } @@ -4273,6 +4309,21 @@ export namespace StoreError { type: "usageConditionsNotFound" } + export interface UserChatRelayNotFound extends Interface { + type: "userChatRelayNotFound" + chatRelayId: number // int64 + } + + export interface GroupRelayNotFound extends Interface { + type: "groupRelayNotFound" + groupRelayId: number // int64 + } + + export interface GroupRelayNotFoundByMemberId extends Interface { + type: "groupRelayNotFoundByMemberId" + groupMemberId: number // int64 + } + export interface InvalidQuote extends Interface { type: "invalidQuote" } diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 31166762bc..8e3f039472 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ca26c69937083deee43b8b2200ec9ef4c004ceac" = "1p7jhxcbn95kddfwa5rjpzfx78fzic03wmy9dmh1mj3j14vyfn02"; + "https://github.com/simplex-chat/simplexmq.git"."89b81d151fa0378196d923c5d7fa0aea08462136" = "033vzd7f62plb9ncf8lbdn894682phxp53ysvry9ch79mlf68yqf"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 2063a9f9fc..f9871c87fb 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -80,6 +80,7 @@ library Simplex.Chat.Store.Messages Simplex.Chat.Store.NoteFolders Simplex.Chat.Store.Profiles + Simplex.Chat.Store.RelayRequests Simplex.Chat.Store.Remote Simplex.Chat.Store.Shared Simplex.Chat.Styled diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7074364a61..0125a75fc1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -170,9 +170,11 @@ newChatController chatStoreChanged <- newTVarIO False deliveryTaskWorkers <- TM.emptyIO deliveryJobWorkers <- TM.emptyIO + relayRequestWorkers <- TM.emptyIO expireCIThreads <- TM.emptyIO expireCIFlags <- TM.emptyIO cleanupManagerAsync <- newTVarIO Nothing + relayGroupLinkChecksAsync <- newTVarIO Nothing timedItemThreads <- TM.emptyIO chatActivated <- newTVarIO True showLiveItems <- newTVarIO False @@ -211,9 +213,11 @@ newChatController filesFolder, deliveryTaskWorkers, deliveryJobWorkers, + relayRequestWorkers, expireCIThreads, expireCIFlags, cleanupManagerAsync, + relayGroupLinkChecksAsync, timedItemThreads, chatActivated, showLiveItems, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 96c3c2c80e..de35e4d730 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -248,9 +248,11 @@ data ChatController = ChatController filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, deliveryTaskWorkers :: TMap DeliveryWorkerKey Worker, deliveryJobWorkers :: TMap DeliveryWorkerKey Worker, + relayRequestWorkers :: TMap Int Worker, -- single global worker with key 1 is used to fit into existing worker management framework expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), + relayGroupLinkChecksAsync :: TVar (Maybe (Async ())), chatActivated :: TVar Bool, timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, @@ -393,7 +395,7 @@ data ChatCommand | TestProtoServer AProtoServerWithAuth | GetUserChatRelays | SetUserChatRelays [CLINewRelay] - -- TODO [chat relays] commands to test chat relay + -- TODO [relays] commands to test chat relay -- | APITestChatRelay UserId ConnLinkContact -- | TestChatRelay ConnLinkContact | APIGetServerOperators @@ -465,7 +467,7 @@ data ChatCommand | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink} -- Maybe is used to report link parsing failure as special error | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData - | APIPrepareGroup UserId CreatedLinkContact GroupShortLinkData + | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData | APIChangePreparedContactUser ContactId UserId | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} @@ -507,6 +509,9 @@ data ChatCommand | ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text} | APINewGroup {userId :: UserId, incognito :: IncognitoEnabled, groupProfile :: GroupProfile} | NewGroup IncognitoEnabled GroupProfile + -- TODO [relays] owner: TBC group link's default member role for APINewPublicGroup + | APINewPublicGroup {userId :: UserId, incognito :: IncognitoEnabled, relayIds :: NonEmpty Int64, groupProfile :: GroupProfile} + | NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | AcceptMember GroupName ContactName GroupMemberRole @@ -681,6 +686,7 @@ data ChatResponse | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} | CRGroupCreated {user :: User, groupInfo :: GroupInfo} + | CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CRGroupMembers {user :: User, group :: Group} | CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} -- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]} @@ -839,6 +845,7 @@ data ChatEvent | CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CEvtGroupLinkRelaysUpdated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CEvtJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- there is the same command response | CEvtJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} | CEvtMemberAcceptedByOther {user :: User, groupInfo :: GroupInfo, acceptingMember :: GroupMember, member :: GroupMember} @@ -987,13 +994,15 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {groupSLinkData_ :: Maybe GroupShortLinkData} + = GLPOk {direct :: DirectLink, groupSLinkData_ :: Maybe GroupShortLinkData} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} | GLPKnown {groupInfo :: GroupInfo} deriving (Show) +type DirectLink = Bool + connectionPlanProceed :: ConnectionPlan -> Bool connectionPlanProceed = \case CPInvitationLink ilp -> case ilp of @@ -1007,7 +1016,7 @@ connectionPlanProceed = \case CAPContactViaAddress _ -> True _ -> False CPGroupLink glp -> case glp of - GLPOk _ -> True + GLPOk _direct _ -> True GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True _ -> False diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 6746239752..b7a6d8d2d3 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -122,13 +122,13 @@ import UnliftIO.IO (hClose) import UnliftIO.STM #if defined(dbPostgres) import Data.Bifunctor (bimap, second) -import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary) +import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError) #else import Data.Bifunctor (bimap, first, second) import qualified Data.ByteArray as BA import qualified Database.SQLite.Simple as SQL import Simplex.Chat.Archive -import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary) +import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError) import Simplex.Messaging.Agent.Store.Common (withConnection) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) #endif @@ -190,8 +190,10 @@ startChatController mainApp enableSndFiles = do startXFTP xftpStartWorkers void $ forkIO $ startFilesToReceive users startDeliveryWorkers + startRelayRequestWorker_ startCleanupManager void $ forkIO $ mapM_ startExpireCIs users + startRelayChecks users else when enableSndFiles $ startXFTP xftpStartSndWorkers pure a1 startXFTP startWorkers = do @@ -203,6 +205,10 @@ startChatController mainApp enableSndFiles = do runExceptT (startDeliveryTaskWorkers >> startDeliveryJobWorkers) >>= \case Left e -> liftIO $ putStrLn $ "Error starting delivery workers: " <> show e Right _ -> pure () + startRelayRequestWorker_ = + runExceptT startRelayRequestWorker >>= \case + Left e -> liftIO $ putStrLn $ "Error starting relay request worker: " <> show e + Right _ -> pure () startCleanupManager = do cleanupAsync <- asks cleanupManagerAsync readTVarIO cleanupAsync >>= \case @@ -210,6 +216,15 @@ startChatController mainApp enableSndFiles = do a <- Just <$> async (void $ runExceptT cleanupManager) atomically $ writeTVar cleanupAsync a _ -> pure () + startRelayChecks users = do + let relayUser_ = find (\User {userChatRelay} -> isTrue userChatRelay) users + forM_ relayUser_ $ \relayUser -> do + relayAsync <- asks relayGroupLinkChecksAsync + readTVarIO relayAsync >>= \case + Nothing -> do + a <- Just <$> async (void $ runExceptT $ runRelayGroupLinkChecks relayUser) + atomically $ writeTVar relayAsync a + _ -> pure () startExpireCIs user = whenM shouldExpireChats $ do startExpireCIThread user setExpireCIFlag user True @@ -1176,8 +1191,7 @@ processChatCommand vr nm = \case pure $ CRContactConnectionDeleted user conn CTGroup | isNothing scope -> do gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId - let GroupMember {memberRole = membershipMemRole} = membership - let isOwner = membershipMemRole == GROwner + let isOwner = memberRole' membership == GROwner canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo @@ -1195,9 +1209,9 @@ processChatCommand vr nm = \case withFastStore' $ \db -> deleteGroup db user gInfo pure $ CRGroupDeletedUser user gInfo where - getRecipients gInfo@GroupInfo {useRelays} - | isTrue useRelays = do - relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo + getRecipients gInfo + | useRelays' gInfo = do + relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo pure (relays, relays) | otherwise = do ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo @@ -1630,8 +1644,8 @@ processChatCommand vr nm = \case withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchAllErrors` eToView ok user where - getMembers db gInfo@GroupInfo {useRelays} - | isTrue useRelays = getGroupRelays db vr user gInfo + getMembers db gInfo + | useRelays' gInfo = getGroupRelayMembers db vr user gInfo | otherwise = getGroupMembers db vr user gInfo _ -> throwCmdError "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do @@ -1864,7 +1878,8 @@ processChatCommand vr nm = \case let Profile {preferences} = profile groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences groupProfile = businessGroupProfile profile groupPreferences - (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user groupProfile True ccLink welcomeSharedMsgId + gVar <- asks random + (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db gVar vr user groupProfile True ccLink welcomeSharedMsgId False void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) let cd = CDGroupRcv gInfo Nothing hostMember createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing @@ -1888,11 +1903,17 @@ processChatCommand vr nm = \case Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTDirect chat - APIPrepareGroup userId ccLink groupSLinkData -> withUserId userId $ \user -> do + APIPrepareGroup userId ccLink direct groupSLinkData -> withUserId userId $ \user -> do let GroupShortLinkData {groupProfile = gp@GroupProfile {description}} = groupSLinkData welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId - (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user gp False ccLink welcomeSharedMsgId + let useRelays = not direct + gVar <- asks random + (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db gVar vr user gp False ccLink welcomeSharedMsgId useRelays void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + -- TODO [relays] member: TBC save items as message from channel + -- TODO - hostMember to later be associated with owner profile when relays send it + -- TODO - pick any owner at random from initial introductions, find unknown member in group? + -- TODO - alternatively support not having a member in CDGroupRcv direction? let cd = CDGroupRcv gInfo Nothing hostMember cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo @@ -1964,9 +1985,65 @@ processChatCommand vr nm = \case CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' APIConnectPreparedGroup groupId incognito msgContent_ -> withUser $ \user -> do (gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId - case preparedGroup gInfo of - Nothing -> throwCmdError "group doesn't have link to connect" - Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId} -> do + case gInfo of + GroupInfo {preparedGroup = Nothing} -> throwCmdError "group doesn't have link to connect" + GroupInfo {useRelays = BoolDef True, preparedGroup = Just PreparedGroup {connLinkToConnect}} -> do + sLnk <- case toShortLinkContact connLinkToConnect of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" + (mainCReq@(CRContactUri crData), ContactLinkData _ UserContactData {relays}) <- getShortLinkConnReq nm user sLnk + -- Set group link info and incognito profile once before connecting to relays + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} + gInfo' <- withFastStore $ \db -> setPreparedGroupLinkInfo db vr user gInfo mainCReq cReqHash incognitoProfile + rs <- mapConcurrently (connectToRelay gInfo') relays + let relayFailed = \case (_, _, Left _) -> True; _ -> False + (failed, succeeded) = partition relayFailed rs + if null succeeded + then do + -- Updated group info (connLinkPreparedConnection) - in UI it would lock ability to change + -- user or incognito profile for group, in case server received request while client got network error + toView $ CEvtChatInfoUpdated user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing) + -- Prefer throwing temporary network connection error to enable retry + case find isTempErr failed <|> listToMaybe failed of + Just (_, _, Left e) -> throwError e + _ -> throwChatError $ CEException "no relay connection results" -- shouldn't happen + else do + withFastStore' $ \db -> setPreparedGroupStartedConnection db groupId + -- Async retry failed relays with temporary errors + let retryable = [(l, m) | r@(l, m, _) <- failed, isTempErr r] + void $ mapConcurrently (uncurry $ retryRelayConnectionAsync gInfo') retryable + -- TODO [relays] member: TBC response type for UI to display state of relays connection + -- TODO - differentiate success, temporary failure, permanent failure + -- TODO - possibly, additional status on relay member record + pure $ CRStartedConnectionToGroup user gInfo' incognitoProfile + where + isTempErr = \case + (_, _, Left ChatErrorAgent {agentError = e}) -> temporaryOrHostError e + _ -> False + connectToRelay gInfo' relayLink = do + gVar <- asks random + -- TODO [relays] member: set relay profile before/during connection + -- TODO - on fetching relay link data? (-> relay should add profile to relay link) + -- TODO - or update upon connection, as in regular prepared groups + -- TODO (current logic mimics insertHost_ from createPreparedGroup) + -- Save relayLink to re-use relay member record on retry (check by relayLink) + relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo' relayLink + r <- tryAllErrors $ do + (cReq, _cData) <- getShortLinkConnReq nm user relayLink + let relayLinkToConnect = CCLink cReq (Just relayLink) + void $ connectViaContact user (Just $ PCEGroup gInfo' relayMember) incognito relayLinkToConnect Nothing Nothing + -- Re-read member to get updated activeConn + relayMember' <- withFastStore $ \db -> getGroupMember db vr user groupId (groupMemberId' relayMember) + pure (relayLink, relayMember', r) + retryRelayConnectionAsync gInfo' relayLink relayMember@GroupMember {activeConn} = do + forM_ activeConn $ \conn -> do + deleteAgentConnectionAsync $ aConnId conn + withStore' $ \db -> deleteConnectionRecord db user (dbConnId conn) + subMode <- chatReadVar subscriptionMode + newConnIds <- getAgentConnShortLinkAsync user relayLink + withStore' $ \db -> createRelayMemberConnectionAsync db user gInfo' relayMember relayLink newConnIds subMode + GroupInfo {preparedGroup = Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId}} -> do msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of Just smId -> pure (smId, mc) Nothing -> do @@ -2001,6 +2078,8 @@ processChatCommand vr nm = \case CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do + -- TODO [relays] member: /c api to support groups with relays + -- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup (ccLink, plan) <- connectPlan user cLink `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq @@ -2009,7 +2088,7 @@ processChatCommand vr nm = \case ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do - (cReq, _cData) <- getShortLinkConnReq user sLnk + (cReq, _cData) <- getShortLinkConnReq nm user sLnk pure $ CCLink cReq $ Just sLnk Nothing -> throwCmdError "no address in contact profile" connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do @@ -2033,9 +2112,9 @@ processChatCommand vr nm = \case Left e -> throwError $ ChatErrorStore e Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink subMode <- chatReadVar subscriptionMode - -- TODO [chat relays] relay address creation: - -- TODO - add relay key, identity to link data - -- TODO - validate short link is created (returned by agent) + -- TODO [relays] relay: address creation + -- TODO - add relay key, identity to link data + -- TODO - validate short link is created (returned by agent) let userData = contactShortLinkData (userProfileDirect user Nothing Nothing True) Nothing userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} -- TODO [certs rcv] @@ -2230,19 +2309,53 @@ processChatCommand vr nm = \case chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction - APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do - checkValidName displayName - gVar <- asks random - -- [incognito] generate incognito profile for group membership - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile - let cd = CDGroupSnd gInfo Nothing - createInternalChatItem user cd CIChatBanner (Just epochStart) - createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing - createGroupFeatureItems user cd CISndGroupFeature gInfo + APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do + gInfo <- newGroup user incognito gProfile False pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand vr nm $ APINewGroup userId incognito gProfile + APINewPublicGroup userId incognito relayIds gProfile -> withUserId userId $ \user -> do + gInfo <- newGroup user incognito gProfile True + (gInfo', gLink, groupRelays) <- setupLink user gInfo `catchAllErrors` \e -> do + deleteInProgressGroup user gInfo + throwError e + pure $ CRPublicGroupCreated user gInfo' gLink groupRelays + where + setupLink :: User -> GroupInfo -> CM (GroupInfo, GroupLink, [GroupRelay]) + setupLink user gInfo = do + (gInfo', gLink, sLnk) <- newGroupLink user gInfo + relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) + groupRelays <- addRelays user gInfo' sLnk relays + pure (gInfo', gLink, groupRelays) + newGroupLink :: User -> GroupInfo -> CM (GroupInfo, GroupLink, ShortLinkContact) + newGroupLink user gInfo@GroupInfo {groupProfile} = do + groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode + -- TODO [relays] owner: prepare group link without initially creating on server + -- TODO - add link and owner key to group profile, sign profile + -- TODO - create group link on server with signed profile as data + -- / link creation + let userData = encodeShortLinkData $ GroupShortLinkData groupProfile + userLinkData = UserContactLinkData UserContactData {direct = False, owners = [], relays = [], userData} + crClientData = encodeJSON $ CRDataGroup groupLinkId + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode + ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + sLnk <- case toShortLinkContact ccLink' of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" + let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk} + userData' = encodeShortLinkData $ GroupShortLinkData groupProfile' + userLinkData' = UserContactLinkData UserContactData {direct = False, owners = [], relays = [], userData = userData'} + void $ withAgent (\a -> setConnShortLink a nm connId SCMContact userLinkData' (Just crClientData)) + -- link creation / + gVar <- asks random + (gInfo', gLink) <- withFastStore $ \db -> do + gLink <- createGroupLink db gVar user gInfo connId ccLink' groupLinkId GRMember subMode + gInfo' <- updateGroupProfile db user gInfo groupProfile' + pure (gInfo', gLink) + pure (gInfo', gLink, sLnk) + NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> + processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId @@ -2603,9 +2716,9 @@ processChatCommand vr nm = \case withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} where - getRecipients user gInfo@GroupInfo {useRelays} - | isTrue useRelays = do - relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo + getRecipients user gInfo + | useRelays' gInfo = do + relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo pure (relays, relays) | otherwise = do ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo @@ -3167,15 +3280,15 @@ processChatCommand vr nm = \case when (isJust msg_ && isJust groupLinkId) $ throwChatError CEConnReqMessageProhibited case preparedEntity_ of Just (PCEContact ct@Contact {activeConn}) -> case activeConn of - Nothing -> connect' Nothing Nothing + Nothing -> connect' Nothing Nothing Nothing Just conn@Connection {connStatus, xContactId} -> case connStatus of ConnPrepared -> joinPreparedConn' xContactId conn Nothing _ -> pure $ CVRConnectedContact ct Just (PCEGroup gInfo GroupMember {activeConn}) -> case activeConn of - Nothing -> connect' groupLinkId Nothing + Nothing -> connect' groupLinkId Nothing (Just $ Just gInfo) Just conn@Connection {connStatus, xContactId} -> case connStatus of - ConnPrepared -> joinPreparedConn' xContactId conn $ Just (Just gInfo) - _ -> connect' groupLinkId xContactId -- why not "already connected" for host member? + ConnPrepared -> joinPreparedConn' xContactId conn (Just $ Just gInfo) + _ -> connect' groupLinkId xContactId (Just $ Just gInfo) -- why not "already connected" for host member? Nothing -> withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case Right ct@Contact {activeConn} -> case groupLinkId of @@ -3185,35 +3298,38 @@ processChatCommand vr nm = \case Just gLinkId -> -- allow repeat contact request -- TODO [short links] is this branch needed? it probably remained from the time we created host contact - connect' (Just gLinkId) Nothing + connect' (Just gLinkId) Nothing (Just Nothing) Left conn_ -> case conn_ of - Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn $ groupLinkId $> Nothing + Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn (groupLinkId $> Nothing) -- TODO [short links] this is executed on repeat request after success -- it probably should send the second message without creating the second connection? - Just Connection {xContactId} -> connect' groupLinkId xContactId - Nothing -> connect' groupLinkId Nothing + Just Connection {xContactId} -> connect' groupLinkId xContactId (groupLinkId $> Nothing) + Nothing -> connect' groupLinkId Nothing (groupLinkId $> Nothing) where - cReqHash = ConnReqUriHash . C.sha256Hash . strEncode - cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex} - cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat} + cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} + cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat} joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" + -- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used xContactId <- mkXContactId xContactId_ localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile - connect' groupLinkId xContactId_ = do + connect' groupLinkId xContactId_ gInfo_ = do let inGroup = isJust groupLinkId pqSup = if inGroup then PQSupportOff else PQSupportOn (connId, chatV) <- prepareContact user cReq pqSup xContactId <- mkXContactId xContactId_ - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + -- [incognito] generate profile to send, or use membership profile for relay groups + incognitoProfile_ <- case gInfo_ of + Just (Just gInfo) | useRelays' gInfo -> pure $ ExistingIncognito <$> incognitoMembershipProfile gInfo + _ -> if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + let incognitoProfile = fromIncognitoProfile <$> incognitoProfile_ subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk - conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup - conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ (groupLinkId $> Nothing) pqSup + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile_ groupLinkId subMode chatV pqSup + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) = @@ -3227,7 +3343,7 @@ processChatCommand vr nm = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup ct' <- withStore $ \db -> getContact db vr user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile @@ -3262,7 +3378,12 @@ processChatCommand vr nm = \case let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_' in userProfileInGroup' user allowSimplexLinks incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True - dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_) + chatEvent = case gInfo_ of + Just (Just gInfo) | useRelays' gInfo -> + let GroupInfo {membership = GroupMember {memberId}} = gInfo + in XMember profileToSend memberId + _ -> XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ + dm <- encodeConnInfoPQ pqSup chatV chatEvent subMode <- chatReadVar subscriptionMode void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined @@ -3382,12 +3503,12 @@ processChatCommand vr nm = \case recipients = filter memberCurrentOrPending newMs sendGroupMessage user gInfo' Nothing recipients $ XGrpPrefs ps' Nothing -> do - setGroupLinkData' nm user gInfo' + void $ setGroupLinkData' nm user gInfo' recipients <- getRecipients sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p') where getRecipients - | isTrue (useRelays gInfo') = withFastStore' $ \db -> getGroupRelays db vr user gInfo' + | useRelays' gInfo' = withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo' | otherwise = do ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo' pure $ filter memberCurrentOrPending ms @@ -3404,8 +3525,7 @@ processChatCommand vr nm = \case when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName} assertUserGroupRole :: GroupInfo -> GroupMemberRole -> CM () assertUserGroupRole g@GroupInfo {membership} requiredRole = do - let GroupMember {memberRole = membershipMemRole} = membership - when (membershipMemRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole + when (memberRole' membership < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved unless (memberActive membership) $ throwChatError CEGroupMemberNotActive @@ -3419,13 +3539,13 @@ processChatCommand vr nm = \case delGroupChatItems user gInfo chatScopeInfo items True where assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () - assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items' = + assertDeletable GroupInfo {membership} items' = unless (all itemDeletable items') $ throwChatError CEInvalidChatItemDelete where itemDeletable :: CChatItem 'CTGroup -> Bool itemDeletable (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = case chatDir of - CIGroupRcv GroupMember {memberRole} -> membershipMemRole >= memberRole && isJust itemSharedMsgId + CIGroupRcv GroupMember {memberRole} -> memberRole' membership >= memberRole && isJust itemSharedMsgId CIGroupSnd -> isJust itemSharedMsgId itemsMsgMemIds :: GroupInfo -> [CChatItem 'CTGroup] -> [(SharedMsgId, MemberId)] itemsMsgMemIds GroupInfo {membership = GroupMember {memberId = membershipMemId}} = mapMaybe itemMsgMemIds @@ -3504,6 +3624,18 @@ processChatCommand vr nm = \case groupId <- getGroupIdByName db user gName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) + newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> CM GroupInfo + newGroup user incognito gProfile@GroupProfile {displayName} useRelays = do + checkValidName displayName + gVar <- asks random + -- [incognito] generate incognito profile for group membership + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile useRelays + let cd = CDGroupSnd gInfo Nothing + createInternalChatItem user cd CIChatBanner (Just epochStart) + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo + pure gInfo sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo @@ -3525,8 +3657,44 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) - drgRandomBytes :: Int -> CM ByteString - drgRandomBytes n = asks random >>= atomically . C.randomBytes n + addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [GroupRelay] + addRelays user gInfo@GroupInfo {membership} groupSLink relays = + mapConcurrently addRelay relays + where + addRelay :: UserChatRelay -> CM 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 + (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 privateGetUser :: UserId -> CM User privateGetUser userId = tryAllErrors (withStore (`getUser` userId)) >>= \case @@ -3598,8 +3766,8 @@ processChatCommand vr nm = \case knownLinkPlans l' >>= \case Just r -> pure r Nothing -> do - (cReq, cData) <- getShortLinkConnReq user l' - contactSLinkData_ <- liftIO $ decodeShortLinkData cData + (cReq, cData) <- getShortLinkConnReq nm user l' + contactSLinkData_ <- liftIO $ decodeLinkUserData cData invitationReqAndPlan cReq (Just l') contactSLinkData_ where knownLinkPlans l' = withFastStore $ \db -> do @@ -3624,11 +3792,11 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (cReq, cData) <- getShortLinkConnReq user l' + (cReq, cData) <- getShortLinkConnReq nm user l' withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) _ -> do - contactSLinkData_ <- liftIO $ decodeShortLinkData cData + contactSLinkData_ <- liftIO $ decodeLinkUserData cData plan <- contactRequestPlan user cReq contactSLinkData_ pure (con cReq, plan) where @@ -3643,9 +3811,9 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (cReq, cData) <- getShortLinkConnReq user l' - groupSLinkData_ <- liftIO $ decodeShortLinkData cData - plan <- groupJoinRequestPlan user cReq groupSLinkData_ + (cReq, cData@(ContactLinkData _ UserContactData {direct})) <- getShortLinkConnReq nm user l' + groupSLinkData_ <- liftIO $ decodeLinkUserData cData + plan <- groupJoinRequestPlan user cReq direct groupSLinkData_ pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> @@ -3690,7 +3858,7 @@ processChatCommand vr nm = \case groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of Nothing -> contactRequestPlan user cReq Nothing - Just _ -> groupJoinRequestPlan user cReq Nothing + Just _ -> groupJoinRequestPlan user cReq True Nothing contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do let cReqSchemas = contactCReqSchemas crData @@ -3711,10 +3879,10 @@ processChatCommand vr nm = \case | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo True Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" - groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) groupSLinkData_ = do + groupJoinRequestPlan :: User -> ConnReqContact -> Bool -> Maybe GroupShortLinkData -> CM ConnectionPlan + groupJoinRequestPlan user (CRContactUri crData) direct groupSLinkData_ = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -3723,43 +3891,33 @@ processChatCommand vr nm = \case connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkData_) + (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk direct groupSLinkData_) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk groupSLinkData_) + | otherwise -> pure $ CPGroupLink (GLPOk direct groupSLinkData_) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo groupSLinkData_ - groupPlan :: GroupInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} groupSLinkData_ + (Just gInfo, _) -> groupPlan gInfo direct groupSLinkData_ + groupPlan :: GroupInfo -> Bool -> Maybe GroupShortLinkData -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} direct groupSLinkData_ | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) - | otherwise = pure $ CPGroupLink (GLPOk groupSLinkData_) + | otherwise = pure $ CPGroupLink (GLPOk direct groupSLinkData_) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, CRContactUri crData {crScheme = simplexChat} ) - contactCReqHash :: ConnReqContact -> ConnReqUriHash - contactCReqHash = ConnReqUriHash . C.sha256Hash . strEncode - getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m) - getShortLinkConnReq user l = do - l' <- restoreShortLink' l - (cReq, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' - case cData of - ContactLinkData _ UserContactData {direct} | not direct -> throwChatError CEUnsupportedConnReq - _ -> pure () - pure (cReq, cData) + -- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser, -- and short links are stored with server hostname schema, so they wouldn't match without it. serverShortLink :: ConnShortLink m -> ConnShortLink m serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey - restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) contactShortLinkData :: Profile -> Maybe AddressSettings -> UserLinkData contactShortLinkData p settings = let msg = autoReply =<< settings @@ -4246,6 +4404,8 @@ cleanupManager = do -- TODO remove in future versions: legacy step - contacts are no longer marked as deleted cleanupDeletedContacts user `catchAllErrors` eToView liftIO $ threadDelay' stepDelay + cleanupInProgressGroups user `catchAllErrors` eToView + liftIO $ threadDelay' stepDelay cleanupTimedItems cleanupInterval user = do ts <- liftIO getCurrentTime let startTimedThreadCutoff = addUTCTime cleanupInterval ts @@ -4257,6 +4417,14 @@ cleanupManager = do forM_ contacts $ \ct -> withStore (\db -> deleteContactWithoutGroups db user ct) `catchAllErrors` eToView + cleanupInProgressGroups user = do + vr <- chatVersionRange + ts <- liftIO getCurrentTime + -- older than 30 minutes to avoid deleting a newly created group + let cutoffTs = addUTCTime (- 1800) ts + inProgressGroups <- withStore' $ \db -> getInProgressGroups db vr user cutoffTs + forM_ inProgressGroups $ \gInfo -> + deleteInProgressGroup user gInfo `catchAllErrors` eToView cleanupMessages = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(30 * nominalDay)) ts @@ -4274,6 +4442,20 @@ cleanupManager = do let cutoffTs = addUTCTime (-(14 * nominalDay)) ts withStore' (`deleteOldProbes` cutoffTs) +deleteInProgressGroup :: User -> GroupInfo -> CM () +deleteInProgressGroup user gInfo = do + deleteGroupLinkIfExists user gInfo + deleteGroupConnections user gInfo False + withFastStore' $ \db -> deleteGroup db user gInfo + +runRelayGroupLinkChecks :: User -> CM () +runRelayGroupLinkChecks _user = do + -- TODO [relays] relay: periodically check presence of relay link in group links of served groups + -- TODO - retrieve group link data + -- TODO - if relay link is present, update relay status to RSActive + -- TODO - if relay link is absent and status was RSActive -> update to new "Removed" status? + pure () + expireChatItems :: User -> Int64 -> Bool -> CM () expireChatItems user@User {userId} globalTTL sync = do currentTs <- liftIO getCurrentTime @@ -4557,6 +4739,8 @@ chatCommandP = ("/help" <|> "/h") $> ChatHelp HSMain, ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), + ("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> groupProfile), + "/_public group " *> (APINewPublicGroup <$> A.decimal <*> incognitoOnOffP <*> _strP <* A.space <*> jsonP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)), "/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), @@ -4599,7 +4783,7 @@ chatCommandP = "/contacts" $> ListContacts, "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)), "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), - "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <* A.space <*> jsonP), + "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal), "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), @@ -4818,7 +5002,7 @@ chatCommandP = { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } - pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupPreferences, memberAdmission = Nothing} + pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing} memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) shortDescrP = do descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure "" diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 14d5c711dc..04f13b3a53 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -90,6 +90,7 @@ import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..)) import Simplex.Messaging.Compression (compressionLevel) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -550,7 +551,6 @@ markGroupCIsDeleted user gInfo chatScopeInfo items byGroupMember_ deletedTs = do (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) unless (null errs) $ toView $ CEvtChatErrors errs pure deletions - -- pure $ CRChatItemsDeleted user deletions byUser False where markDeleted db (CChatItem md ci) = do ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs @@ -910,7 +910,7 @@ acceptContactRequestAsync liftIO $ setCommandConnId db user cmdId connId getContact db vr user contactId -acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync user uclId @@ -919,6 +919,7 @@ acceptGroupJoinRequestAsync cReqChatVRange cReqProfile cReqXContactId_ + cReqMemberId_ welcomeMsgId_ gAccepted gLinkMemRole @@ -928,7 +929,7 @@ acceptGroupJoinRequestAsync ((groupMemberId, memberId), currentMemCount) <- withStore $ \db -> liftM2 (,) - (createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus) + (createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus) (liftIO $ getGroupCurrentMembersCount db user gInfo) let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile) GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -963,7 +964,7 @@ acceptGroupJoinSendRejectAsync rejectionReason = do gVar <- asks random (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ Nothing GRObserver GSMemRejected + createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ Nothing Nothing GRObserver GSMemRejected let GroupMember {memberRole = userRole, memberId = userMemberId} = membership msg = XGrpLinkReject $ @@ -1019,9 +1020,29 @@ acceptBusinessJoinRequestAsync -- TODO [short links] get updated business chat group and member? (currently not used) pure (gInfo, clientMember) +acceptRelayJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> InvitationId -> VersionRangeChat -> ShortLinkContact -> CM (GroupInfo, GroupMember) +acceptRelayJoinRequestAsync + user + uclId + gInfo + _ownerMember@GroupMember {groupMemberId} + cReqInvId + cReqChatVRange + relayLink = do + let msg = XGrpRelayAcpt relayLink + subMode <- chatReadVar subscriptionMode + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV + withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted + ownerMember' <- getGroupMemberById db vr user groupMemberId + pure (gInfo', ownerMember') + businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupPreferences = Just groupPreferences, memberAdmission = Nothing} + GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1071,11 +1092,11 @@ introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn shuffledReMembers <- liftIO $ shuffleMembers reMembers if toMember `supportsVersion` batchSendVersion then do - let events = map memberIntro shuffledReMembers + let events = map (memberIntroEvt gInfo) shuffledReMembers forM_ (L.nonEmpty events) $ \events' -> sendGroupMemberMessages user conn events' groupId else forM_ shuffledReMembers $ \reMember -> - void $ sendDirectMemberMessage conn (memberIntro reMember) groupId + void $ sendDirectMemberMessage conn (memberIntroEvt gInfo reMember) groupId updateToMemberVector :: [GroupMember] -> CM () updateToMemberVector reMembers = do let relations = map (\GroupMember {indexInGroup} -> (indexInGroup, (IDReferencedIntroduced, MRIntroduced))) reMembers @@ -1084,11 +1105,6 @@ introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn updateReMembersVectors reMembers = do let GroupMember {indexInGroup} = toMember withStore' $ \db -> setMembersVectorsNewRelation db reMembers indexInGroup IDSubjectIntroduced MRIntroduced - memberIntro :: GroupMember -> ChatMsgEvent 'Json - memberIntro reMember = - let mInfo = memberInfo gInfo reMember - mRestrictions = memberRestrictions reMember - in XGrpMemIntro mInfo mRestrictions shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers reMembers = do let (admins, others) = partition isAdmin reMembers @@ -1099,6 +1115,26 @@ introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn isAdmin GroupMember {memberRole} = memberRole >= GRAdmin hasPicture GroupMember {memberProfile = LocalProfile {image}} = isJust image +memberIntroEvt :: GroupInfo -> GroupMember -> ChatMsgEvent 'Json +memberIntroEvt gInfo reMember = + let mInfo = memberInfo gInfo reMember + mRestrictions = memberRestrictions reMember + in XGrpMemIntro mInfo mRestrictions + +-- Used in groups with relays to introduce moderators and above to a new member. +-- Member is not introduced to anybody: +-- - in channels member will be prohibited to send, so it doesn't matter; +-- - if member does send, recipients will create unknown member record; +-- - later - to do member profile request protocol. +-- This doesn't create introduction records in db, compared to above methods. +introduceModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceModerators _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" +introduceModerators vr user gInfo@GroupInfo {groupId} GroupMember {activeConn = Just conn} = do + modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + let events = map (memberIntroEvt gInfo) modMs + forM_ (L.nonEmpty events) $ \events' -> + sendGroupMemberMessages user conn events' groupId + userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks {-# INLINE userProfileInGroup #-} @@ -1235,23 +1271,55 @@ splitFileDescr partSize rfdText = splitParts 1 rfdText then fileDescr :| [] else fileDescr <| splitParts (partNo + 1) rest -setGroupLinkData' :: NetworkRequestMode -> User -> GroupInfo -> CM () +setGroupLinkData' :: NetworkRequestMode -> User -> GroupInfo -> CM (Maybe GroupLink) setGroupLinkData' nm user gInfo = withFastStore' (\db -> runExceptT $ getGroupLink db user gInfo) >>= \case Right gLink@GroupLink {shortLinkDataSet} - | shortLinkDataSet -> void $ setGroupLinkData nm user gInfo gLink - _ -> pure () + | shortLinkDataSet -> Just <$> setGroupLinkData nm user gInfo gLink + _ -> pure Nothing setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM GroupLink -setGroupLinkData nm user gInfo@GroupInfo {groupProfile} gLink@GroupLink {groupLinkId} = do +setGroupLinkData nm user gInfo gLink = do vr <- chatVersionRange - conn <- withFastStore $ \db -> getGroupLinkConnection db vr user gInfo - let userData = encodeShortLinkData $ GroupShortLinkData groupProfile - userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} - crClientData = encodeJSON $ CRDataGroup groupLinkId + (conn, groupRelays) <- withFastStore $ \db -> + (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getGroupRelays db gInfo) + let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk +setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () +setGroupLinkDataAsync user gInfo gLink = do + vr <- chatVersionRange + (conn, groupRelays) <- withStore $ \db -> + (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getGroupRelays db gInfo) + let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays + setAgentConnShortLinkAsync user conn userLinkData (Just crClientData) + +-- TODO [relays] owner: set owners on updating link data +groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData) +groupLinkData gInfo@GroupInfo {groupProfile} GroupLink {groupLinkId} groupRelays = + let direct = not $ useRelays' gInfo + relays = mapMaybe relayLink groupRelays + userData = encodeShortLinkData $ GroupShortLinkData groupProfile + userLinkData = UserContactLinkData UserContactData {direct, owners = [], relays, userData} + crClientData = encodeJSON $ CRDataGroup groupLinkId + in (userLinkData, crClientData) + +restoreShortLink' :: ConnShortLink m -> CM (ConnShortLink m) +restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) + +getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m) +getShortLinkConnReq nm user@User {userChatRelay} l = do + l' <- restoreShortLink' l + (fd, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + case cData of + ContactLinkData _ UserContactData {direct, relays} + | not supported -> throwChatError CEUnsupportedConnReq + where + supported = direct || not (null relays) || isTrue userChatRelay + _ -> pure () + pure (linkConnReq fd, cData) + encodeShortLinkData :: J.ToJSON a => a -> UserLinkData encodeShortLinkData d = let s = LB.toStrict $ J.encode d @@ -1260,10 +1328,10 @@ encodeShortLinkData d = s' | B.length s > 10240 = B.cons 'X' $ Z1.compress compressionLevel s | otherwise = s - in UserLinkData s' + in UserLinkData s' -decodeShortLinkData :: J.FromJSON a => ConnLinkData c -> IO (Maybe a) -decodeShortLinkData cData +decodeLinkUserData :: J.FromJSON a => ConnLinkData c -> IO (Maybe a) +decodeLinkUserData cData | B.null s = pure Nothing | B.head s == 'X' = case Z1.decompress $ B.drop 1 s of Z1.Error e -> Nothing <$ logError ("Error decompressing link data: " <> tshow e) @@ -1294,6 +1362,9 @@ createdRelayLink (CCLink cReq shortLink) = CCLink cReq (toShortRelayLink <$> sho toShortRelayLink :: ShortLinkContact -> ShortLinkContact toShortRelayLink (CSLContact sch _ srv k) = CSLContact sch CCTRelay srv k +toShortLinkContact :: CreatedLinkContact -> Maybe ShortLinkContact +toShortLinkContact (CCLink _cReq sLink) = sLink + deleteGroupLink' :: User -> GroupInfo -> CM () deleteGroupLink' user gInfo = do vr <- chatVersionRange @@ -1472,10 +1543,10 @@ getChatScopeInfo vr user = \case pure $ GCSIMemberSupport (Just supportMem) getGroupRecipients :: VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> VersionChat -> CM [GroupMember] -getGroupRecipients vr user gInfo@GroupInfo {useRelays, membership} scopeInfo modsCompatVersion - | isTrue useRelays && not (isMemberRelay membership) = do +getGroupRecipients vr user gInfo@GroupInfo {membership} scopeInfo modsCompatVersion + | useRelays' gInfo && not (isRelay membership) = do unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" - withFastStore' $ \db -> getGroupRelays db vr user gInfo + withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo | otherwise = case scopeInfo of Nothing -> do unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" @@ -2026,14 +2097,14 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction GroupInfo {useRelays, membership} events members m@GroupMember {memberRole, memberStatus} +memberSendAction gInfo@GroupInfo {membership} events members m@GroupMember {memberRole, memberStatus} -- groups with relays require newer version - we don't need to check member version for batching and forwarding support - | isTrue useRelays = + | useRelays' gInfo = if -- if user is chat relay, send to all non chat relay members - | isMemberRelay membership && not (isMemberRelay m) -> MSASendBatched . snd <$> readyMemberConn m + | isRelay membership && not (isRelay m) -> MSASendBatched . snd <$> readyMemberConn m -- if user is not chat relay, send only to chat relays - | not (isMemberRelay membership) && isMemberRelay m -> MSASendBatched . snd <$> readyMemberConn m + | not (isRelay membership) && isRelay m -> MSASendBatched . snd <$> readyMemberConn m | otherwise -> Nothing -- TODO [channels fwd] MSAForwarded to create GSSForwarded snd statuses? | otherwise = case memberConn m of Nothing -> pendingOrForwarded @@ -2077,9 +2148,9 @@ memberSendAction GroupInfo {useRelays, membership} events members m@GroupMember readyMemberConn :: GroupMember -> Maybe (GroupMemberId, Connection) readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}, memberStatus} | (connStatus == ConnReady || connStatus == ConnSndReady) - && not (connDisabled conn) - && not (connInactive conn) - && memberStatus /= GSMemRejected = + && not (connDisabled conn) + && not (connInactive conn) + && memberStatus /= GSMemRejected = Just (groupMemberId, conn) | otherwise = Nothing readyMemberConn GroupMember {activeConn = Nothing} = Nothing @@ -2134,7 +2205,7 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta pure (am', conn', msg) saveGroupFwdRcvMsg :: MsgEncodingI e => User -> GroupInfo -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> UTCTime -> CM (Maybe RcvMessage) -saveGroupFwdRcvMsg user GroupInfo {groupId, useRelays} forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} brokerTs = do +saveGroupFwdRcvMsg user gInfo@GroupInfo {groupId} forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} brokerTs = do let newMsg = NewRcvMessage {chatMsgEvent, msgBody, brokerTs} fwdMemberId = Just $ groupMemberId' forwardingMember refAuthorId = Just $ groupMemberId' refAuthorMember @@ -2142,7 +2213,7 @@ saveGroupFwdRcvMsg user GroupInfo {groupId, useRelays} forwardingMember refAutho withStore' (\db -> runExceptT $ createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) >>= \case Right msg -> pure $ Just msg Left e@SEDuplicateGroupMessage {authorGroupMemberId, forwardedByGroupMemberId} - | isTrue useRelays -> pure Nothing -- with chat relays, duplicates are expected + | useRelays' gInfo -> pure Nothing -- with chat relays, duplicates are expected | otherwise -> case (authorGroupMemberId, forwardedByGroupMemberId) of (Just authorGMId, Nothing) -> do vr <- chatVersionRange @@ -2189,7 +2260,7 @@ saveSndChatItems user cd itemsData itemTimed live = do createdAt <- liftIO getCurrentTime vr <- chatVersionRange when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ - void $ withStore' (\db -> updateChatTsStats db vr user cd createdAt Nothing) + void (withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing) lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) where createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) @@ -2225,9 +2296,10 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' in pure (mentions', userMention') CDDirectRcv _ -> pure (M.empty, False) - cInfo' <- if (ciRequiresAttention content || contactChatDeleted cd) - then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) - else pure $ toChatInfo cd + cInfo' <- + if (ciRequiresAttention content || contactChatDeleted cd) + then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) + else pure $ toChatInfo cd (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt let ci = mkChatItem_ cd False ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt @@ -2261,11 +2333,11 @@ createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode pure (cmdId, connId) -joinAgentConnectionAsync :: User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) -joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do - cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo PQSupportOff subMode - pure (cmdId, connId) +joinAgentConnectionAsync :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) +joinAgentConnectionAsync user conn_ enableNtfs cReqUri cInfo subMode = do + cmdId <- withStore' $ \db -> createCommand db user (dbConnId <$> conn_) CFJoinConn + connId' <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) (aConnId <$> conn_) enableNtfs cReqUri cInfo PQSupportOff subMode + pure (cmdId, connId') allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do @@ -2298,6 +2370,18 @@ deleteAgentConnectionsAsync' [] _ = pure () deleteAgentConnectionsAsync' acIds waitDelivery = do withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchAllErrors` eToView +setAgentConnShortLinkAsync :: User -> Connection -> UserConnLinkData 'CMContact -> Maybe CRClientData -> CM () +setAgentConnShortLinkAsync user conn@Connection {connId} userLinkData crClientData_ = do + cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFSetShortLink + withAgent $ \a -> setConnShortLinkAsync a (aCorrId cmdId) (aConnId conn) userLinkData crClientData_ + +getAgentConnShortLinkAsync :: User -> ShortLinkContact -> CM (CommandId, ConnId) +getAgentConnShortLinkAsync user shortLink = do + shortLink' <- restoreShortLink' shortLink + cmdId <- withStore' $ \db -> createCommand db user Nothing CFGetShortLink + connId <- withAgent $ \a -> getConnShortLinkAsync a (aUserId user) (aCorrId cmdId) shortLink' + pure (cmdId, connId) + agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () agentXFTPDeleteRcvFile aFileId fileId = do lift $ withAgent' (`xftpDeleteRcvFile` aFileId) @@ -2590,6 +2674,9 @@ adminContactReq :: ConnReqContact adminContactReq = either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" +contactCReqHash :: ConnReqContact -> ConnReqUriHash +contactCReqHash = ConnReqUriHash . C.sha256Hash . strEncode + simplexTeamContactProfile :: Profile simplexTeamContactProfile = Profile @@ -2625,3 +2712,6 @@ timeItToView s action = do epochStart :: UTCTime epochStart = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0) + +drgRandomBytes :: Int -> CM ByteString +drgRandomBytes n = asks random >>= atomically . C.randomBytes n diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index e10bf2a081..a5e5c0fcd7 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -54,12 +54,13 @@ import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections import Simplex.Chat.Store.ContactRequest -import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Delivery +import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles +import Simplex.Chat.Store.RelayRequests import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.MemberRelations @@ -71,12 +72,13 @@ import Simplex.FileTransfer.Protocol (FilePartyI) import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent -import Simplex.Messaging.Agent.Client (getAgentWorker, waitForWork, withWork_, withWorkItems) -import Simplex.Messaging.Agent.Env.SQLite (Worker (..)) +import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, waitForUserNetwork, waitForWork, waitWhileSuspended, withWorkItems, withWork_) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) +import Simplex.Messaging.Agent.RetryInterval (withRetryInterval) import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Client (ProxyClientError (..), NetworkRequestMode (..)) +import Simplex.Messaging.Client (NetworkRequestMode (..), ProxyClientError (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -84,6 +86,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..), MsgFlags (..)) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (TransportError (..)) import Simplex.Messaging.Util @@ -221,7 +224,7 @@ processAgentMsgSndFile _corrId aFileId msg = do toView $ CEvtSndFileCompleteXFTP user ci' ft where getRecipients - | isTrue (useRelays g) = withStore' $ \db -> getGroupRelays db vr user g + | useRelays' g = withStore' $ \db -> getGroupRelayMembers db vr user g | otherwise = withStore' $ \db -> getGroupMembers db vr user g memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') @@ -363,7 +366,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = RcvGroupMsgConnection conn gInfo m -> processGroupMessage agentMessage entity conn gInfo m UserContactConnection conn uc -> - processUserContactRequest agentMessage entity conn uc + processContactConnMessage agentMessage entity conn uc where updateConnStatus :: ConnectionEntity -> CM ConnectionEntity updateConnStatus acEntity = case agentMsgConnStatus agentMessage of @@ -586,7 +589,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode gVar <- asks random withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode - -- TODO REMOVE LEGACY ^^^ + -- TODO REMOVE LEGACY ^^^ SENT msgId proxy -> do void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId @@ -730,6 +733,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" + XGrpRelayAcpt relayLink + | memberRole' membership == GROwner && isRelay m -> do + withStore $ \db -> do + relay <- getGroupRelayByGMId db (groupMemberId' m) + liftIO $ updateGroupMemberStatus db userId m GSMemAccepted + void $ liftIO $ setRelayLinkAccepted db relay relayLink + allowAgentConnectionAsync user conn' confId XOk + | otherwise -> messageError "x.grp.relay.acpt: only owner can add relay" _ -> messageError "CONF from invited member must have x.grp.acpt" GCHostMember -> case chatMsgEvent of @@ -794,41 +805,63 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure (m {memberStatus = GSMemConnected}, gInfo') toView $ CEvtUserJoinedGroup user gInfo' m' (gInfo'', m'', scopeInfo) <- mkGroupChatScope gInfo' m' - let cd = CDGroupRcv gInfo'' scopeInfo m'' - createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing - let prepared = preparedGroup gInfo'' - unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' - memberConnectedChatItem gInfo'' scopeInfo m'' - let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> prepared - unless (memberPending membership || isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo'' m'' - GCInviteeMember -> do - (gInfo', mStatus) <- - if not (memberPending m) - then do - mStatus <- withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected $> GSMemConnected - pure (gInfo, mStatus) - else do - gInfo' <- withStore' $ \db -> increaseGroupMembersRequireAttention db user gInfo - pure (gInfo', memberStatus m) - (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m - memberConnectedChatItem gInfo'' scopeInfo m' - case scopeInfo of - Just (GCSIMemberSupport _) -> do - createInternalChatItem user (CDGroupRcv gInfo'' scopeInfo m') (CIRcvGroupEvent RGENewMemberPendingReview) Nothing - _ -> pure () - toView $ CEvtJoinedGroupMember user gInfo'' m' {memberStatus = mStatus} - let Connection {viaUserContactLink} = conn - when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' - when (connChatVersion < batchSend2Version) $ getAutoReplyMsg >>= mapM_ (\mc -> sendGroupAutoReply mc Nothing) - case mStatus of - GSMemPendingApproval -> pure () - GSMemPendingReview -> introduceToModerators vr user gInfo'' m' - _ -> do - introduceToAll vr user gInfo'' m' - let memberIsCustomer = case businessChat gInfo'' of - Just BusinessChatInfo {chatType = BCCustomer, customerId} -> memberId' m' == customerId - _ -> False - when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m' + -- Create e2ee, feature and group description chat items only on first connected relay + ifM + firstConnectedHost + ( do + let cd = CDGroupRcv gInfo'' scopeInfo m'' + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + let prepared = preparedGroup gInfo'' + unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' + memberConnectedChatItem gInfo'' scopeInfo m'' + let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> prepared + unless (memberPending membership || isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo'' m'' + ) + (memberConnectedChatItem gInfo'' scopeInfo m'') + where + firstConnectedHost + | useRelays' gInfo = do + relayMems <- withStore' $ \db -> getGroupRelayMembers db vr user gInfo + let numConnected = length $ filter (\GroupMember {memberStatus = ms} -> ms == GSMemConnected) relayMems + pure $ numConnected == 1 + | otherwise = pure True + GCInviteeMember + | isRelay m -> do + withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected + gLink <- withStore $ \db -> getGroupLink db user gInfo + setGroupLinkDataAsync user gInfo gLink + | otherwise -> do + (gInfo', mStatus) <- + if not (memberPending m) + then do + mStatus <- withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected $> GSMemConnected + pure (gInfo, mStatus) + else do + gInfo' <- withStore' $ \db -> increaseGroupMembersRequireAttention db user gInfo + pure (gInfo', memberStatus m) + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + memberConnectedChatItem gInfo'' scopeInfo m' + case scopeInfo of + Just (GCSIMemberSupport _) -> do + createInternalChatItem user (CDGroupRcv gInfo'' scopeInfo m') (CIRcvGroupEvent RGENewMemberPendingReview) Nothing + _ -> pure () + toView $ CEvtJoinedGroupMember user gInfo'' m' {memberStatus = mStatus} + let Connection {viaUserContactLink} = conn + when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' + when (connChatVersion < batchSend2Version) $ getAutoReplyMsg >>= mapM_ (\mc -> sendGroupAutoReply mc Nothing) + if useRelays' gInfo'' + then do + introduceModerators vr user gInfo'' m' + when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m' + else case mStatus of + GSMemPendingApproval -> pure () + GSMemPendingReview -> introduceToModerators vr user gInfo'' m' + _ -> do + introduceToAll vr user gInfo'' m' + let memberIsCustomer = case businessChat gInfo'' of + Just BusinessChatInfo {chatType = BCCustomer, customerId} -> memberId' m' == customerId + _ -> False + when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m' where sendXGrpLinkMem gInfo'' = do let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo'' @@ -873,13 +906,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = aChatMsgs = parseChatMessages msgBody brokerTs = metaBrokerTs msgMeta processAChatMsg :: - GroupInfo - -> GroupMember - -> TVar [Text] - -> Text - -> [NewMessageDeliveryTask] - -> Either String AChatMessage - -> CM [NewMessageDeliveryTask] + GroupInfo -> + GroupMember -> + TVar [Text] -> + Text -> + [NewMessageDeliveryTask] -> + Either String AChatMessage -> + CM [NewMessageDeliveryTask] processAChatMsg gInfo' m' tags eInfo newDeliveryTasks = \case Right (ACMsg SJson chatMsg) -> do newTask_ <- processEvent gInfo' m' tags eInfo chatMsg `catchAllErrors` \e -> eToView e $> Nothing @@ -902,7 +935,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- ! see isForwardedGroupMsg: processing functions should return DeliveryJobScope for same events deliveryJobScope_ <- case event of XMsgNew mc -> memberCanSend m'' scope $ newGroupContentMessage gInfo' m'' mc msg brokerTs False - where ExtMsgContent {scope} = mcExtMsgContent mc + where + ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live @@ -1034,6 +1068,28 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ mc_ $ \mc -> do connReq_ <- withStore' $ \db -> getBusinessContactRequest db user groupId sendGroupAutoReply mc connReq_ + LDATA FixedLinkData {linkConnReq = cReq} _cData -> + -- [async agent commands] CFGetConnShortLink continuation - join relay connection with resolved link + withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> + case cmdFunction of + CFGetShortLink -> case cReq of + CRContactUri crData@ConnReqUriData {crClientData} -> do + let pqSup = PQSupportOff + lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + Just (agentV, _) -> do + let chatV = agentToChatVersion agentV + groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli + cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} + -- Update connection with data derived from cReq, now available after getConnShortLinkAsync + withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup + let GroupMember {memberId = membershipMemId} = membership + incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo + profileToSend = userProfileInGroup user gInfo incognitoProfile + dm <- encodeConnInfo $ XMember profileToSend membershipMemId + subMode <- chatReadVar subscriptionMode + void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode + _ -> throwChatError $ CECommandError "unexpected cmdFunction" QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -1135,15 +1191,44 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo - processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () - processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId = uclId} = case agentMsg of + processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () + processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of REQ invId pqSupport _ connInfo -> do ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo case chatMsgEvent of XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport + XMember p joiningMemberId -> memberJoinRequestViaRelay invId chatVRange p joiningMemberId XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport + XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv -- TODO show/log error, other events in contact request _ -> pure () + LINK _link auData -> + withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> + case cmdFunction of + CFSetShortLink -> + case (ucGroupId_, auData) of + (Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do + (gInfo, gLink, relays) <- withStore $ \db -> do + gInfo <- getGroupInfo db vr user groupId + gLink <- getGroupLink db user gInfo + relays <- liftIO $ getGroupRelays db gInfo + relays' <- liftIO $ mapM (updateRelay db) relays + liftIO $ setGroupInProgressDone db gInfo + pure (gInfo, gLink, relays') + -- TODO [relays] owner: "relays updated" chat item? + toView $ CEvtGroupLinkRelaysUpdated user gInfo gLink relays + where + -- TODO [relays] owner: on relay deletion (link absent from relayLinks) + -- TODO move status RSActive to new "Removed" status / remove relay record + updateRelay :: DB.Connection -> GroupRelay -> IO GroupRelay + updateRelay db relay@GroupRelay {relayLink, relayStatus} = + case relayLink of + Just rLink + | rLink `elem` relayLinks && relayStatus == RSAccepted -> + updateRelayStatus db relay RSActive + _ -> pure relay + _ -> throwChatError $ CECommandError "LINK event expected for a group link only" + _ -> throwChatError $ CECommandError "unexpected cmdFunction" MERR _ err -> do eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) processConnMERR connEntity conn err @@ -1243,9 +1328,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' (\db -> runExceptT $ getDirectChatItemBySharedMsgId db user contactId sharedMsgId) >>= \case Right (cci@(CChatItem SMDRcv _)) -> do currentTs <- liftIO getCurrentTime - deletions <- if featureAllowed SCFFullDelete forContact ct - then deleteDirectCIs user ct [cci] - else markDirectCIsDeleted user ct [cci] currentTs + deletions <- + if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] + else markDirectCIsDeleted user ct [cci] currentTs toView $ CEvtChatItemsDeleted user deletions False False _ -> pure () upsertBusinessRequestItem :: ChatDirection 'CTGroup 'MDRcv -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) @@ -1272,9 +1358,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Right cci@(CChatItem SMDRcv ChatItem {chatDir = CIGroupRcv m'}) | sameMemberId (memberId' clientMember) m' -> do currentTs <- liftIO getCurrentTime - deletions <- if groupFeatureMemberAllowed SGFFullDelete clientMember gInfo - then deleteGroupCIs user gInfo Nothing [cci] Nothing currentTs - else markGroupCIsDeleted user gInfo Nothing [cci] Nothing currentTs + deletions <- + if groupFeatureMemberAllowed SGFFullDelete clientMember gInfo + then deleteGroupCIs user gInfo Nothing [cci] Nothing currentTs + else markGroupCIsDeleted user gInfo Nothing [cci] Nothing currentTs toView $ CEvtChatItemsDeleted user deletions False False _ -> pure () createRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> (SharedMsgId, MsgContent) -> CM AChatItem @@ -1285,8 +1372,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = upsertRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> ((SharedMsgId, MsgContent) -> CM (Maybe AChatItem)) -> (SharedMsgId -> CM ()) -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) upsertRequestItem cd update delete = \case (Just msg, Nothing) -> Just <$> createRequestItem cd msg - (Just msg@(sharedMsgId, _), Just prevSharedMsgId) | sharedMsgId == prevSharedMsgId -> - update msg `catchCINotFound` \_ -> Just <$> createRequestItem cd msg + (Just msg@(sharedMsgId, _), Just prevSharedMsgId) + | sharedMsgId == prevSharedMsgId -> + update msg `catchCINotFound` \_ -> Just <$> createRequestItem cd msg (Nothing, Just prevSharedMsgId) -> Nothing <$ delete prevSharedMsgId _ -> pure Nothing -- ##### Group link join requests (don't create contact requests) ##### @@ -1297,19 +1385,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case Right (acceptance, useRole) | v < groupFastLinkJoinVersion -> - messageError "processUserContactRequest: chat version range incompatible for accepting group join request" + messageError "processContactConnMessage: chat version range incompatible for accepting group join request" | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ welcomeMsgId_ acceptance useRole profileMode + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' Left rjctReason | v < groupJoinRejectVersion -> - messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" + messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" | otherwise -> do mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p xContactId_ rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason + xGrpRelayInv :: InvitationId -> VersionRangeChat -> GroupRelayInvitation -> CM () + xGrpRelayInv invId chatVRange groupRelayInv = do + (_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange + lift $ void $ getRelayRequestWorker True + -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays + -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) + memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> CM () + memberJoinRequestViaRelay invId chatVRange p joiningMemberId = do + (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId + case gLinkInfo_ of + Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do + gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted gLinkMemRole Nothing + (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' + Nothing -> + messageError "memberJoinRequestViaRelay: no group link info for relay link" memberCanSend :: GroupMember -> @@ -1344,7 +1450,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (connInactive conn) $ do quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn when (quotaErrCounter' >= quotaErrInactiveCount) $ - toView $ CEvtConnectionInactive connEntity True + toView (CEvtConnectionInactive connEntity True) _ -> pure () continueSending :: ConnectionEntity -> Connection -> CM Bool @@ -1565,8 +1671,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- we catch error, so that even if processFDMessage fails, message can still be forwarded. processFDMessage fileId aci fileDescr `catchAllErrors` \_ -> pure () pure $ Just $ infoToDeliveryScope g scopeInfo - else - messageError "x.msg.file.descr: file of another member" $> Nothing + else messageError "x.msg.file.descr: file of another member" $> Nothing _ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing processFDMessage :: FileTransferId -> AChatItem -> FileDescr -> CM () @@ -1652,9 +1757,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case msgDir of SMDRcv | rcvItemDeletable ci brokerTs -> do - deletions <- if featureAllowed SCFFullDelete forContact ct - then deleteDirectCIs user ct [cci] - else markDirectCIsDeleted user ct [cci] brokerTs + deletions <- + if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] + else markDirectCIsDeleted user ct [cci] brokerTs toView $ CEvtChatItemsDeleted user deletions False False | otherwise -> messageError "x.msg.del: contact attempted invalid message delete" SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" @@ -1734,18 +1840,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m content msgScope_ if blockedByAdmin m' then createBlockedByAdmin gInfo' m' scopeInfo $> Nothing - else - case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of - Just f -> rejected gInfo' m' scopeInfo f $> Nothing - Nothing -> - withStore' (\db -> getCIModeration db vr user gInfo' memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration gInfo' m' scopeInfo ciModeration - withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ - pure Nothing - Nothing -> do - createContentItem gInfo' m' scopeInfo - pure $ Just $ infoToDeliveryScope gInfo scopeInfo + else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of + Just f -> rejected gInfo' m' scopeInfo f $> Nothing + Nothing -> + withStore' (\db -> getCIModeration db vr user gInfo' memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration gInfo' m' scopeInfo ciModeration + withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ + pure Nothing + Nothing -> do + createContentItem gInfo' m' scopeInfo + pure $ Just $ infoToDeliveryScope gInfo scopeInfo where rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False timed' gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL @@ -1892,9 +1997,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM DeliveryJobScope delete cci byGroupMember = do scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) - deletions <- if groupFeatureMemberAllowed SGFFullDelete m gInfo - then deleteGroupCIs user gInfo scopeInfo [cci] byGroupMember brokerTs - else markGroupCIsDeleted user gInfo scopeInfo [cci] byGroupMember brokerTs + deletions <- + if groupFeatureMemberAllowed SGFFullDelete m gInfo + then deleteGroupCIs user gInfo scopeInfo [cci] byGroupMember brokerTs + else markGroupCIsDeleted user gInfo scopeInfo [cci] byGroupMember brokerTs toView $ CEvtChatItemsDeleted user deletions False False pure $ infoToDeliveryScope gInfo scopeInfo archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () @@ -2047,8 +2153,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = cancelRcvFileTransfer user ft toView $ CEvtRcvFileSndCancelled user aci ft pure $ Just $ infoToDeliveryScope g scopeInfo - else - -- shouldn't happen now that query includes group member id + else -- shouldn't happen now that query includes group member id messageError "x.file.cancel: group member attempted to cancel file of another member" $> Nothing _ -> messageError "x.file.cancel: group member attempted invalid file cancel" $> Nothing @@ -2096,7 +2201,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do subMode <- chatReadVar subscriptionMode dm <- encodeConnInfo $ XGrpAcpt membershipMemId - connIds <- joinAgentConnectionAsync user True connRequest dm subMode + connIds <- joinAgentConnectionAsync user Nothing True connRequest dm subMode withStore' $ \db -> do setViaGroupLinkUri db groupId connId createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode @@ -2545,9 +2650,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do (updatedMember, gInfo') <- withStore $ \db -> do updatedMember <- updateUnknownMemberAnnounced db vr user m unknownMember memInfo initialStatus - gInfo' <- if memberPending updatedMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo + gInfo' <- + if memberPending updatedMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo pure (updatedMember, gInfo') toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember memberAnnouncedToView updatedMember gInfo' @@ -2556,9 +2662,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left _ -> do (newMember, gInfo') <- withStore $ \db -> do newMember <- createNewGroupMember db user gInfo m memInfo GCPostMember initialStatus - gInfo' <- if memberPending newMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo + gInfo' <- + if memberPending newMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo pure (newMember, gInfo') memberAnnouncedToView newMember gInfo' pure $ deliveryJobScope newMember @@ -2591,19 +2698,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case memberCategory m of GCHostMember -> withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Right _ -> messageError "x.grp.mem.intro ignored: member already exists" - Left _ -> do - when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) - case memChatVRange of - Nothing -> messageError "x.grp.mem.intro: member chat version range incompatible" - Just (ChatVersionRange mcvr) - | maxVersion mcvr >= groupDirectInvVersion -> do - subMode <- chatReadVar subscriptionMode - -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createConn subMode - let chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange - void $ withStore $ \db -> createIntroReMember db user gInfo m chatV memInfo memRestrictions groupConnIds subMode - | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" + Right _ -> + unless (useRelays' gInfo) $ + messageError "x.grp.mem.intro ignored: member already exists" + Left _ + | useRelays' gInfo -> + void $ withStore $ \db -> createIntroReMember db user gInfo memInfo memRestrictions + | otherwise -> do + when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) + case memChatVRange of + Nothing -> messageError "x.grp.mem.intro: member chat version range incompatible" + Just (ChatVersionRange mcvr) + | maxVersion mcvr >= groupDirectInvVersion -> do + subMode <- chatReadVar subscriptionMode + -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second + groupConnIds <- createConn subMode + let chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange + void $ withStore $ \db -> do + reMember <- createIntroReMember db user gInfo memInfo memRestrictions + createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode + | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" _ -> messageError "x.grp.mem.intro can be only sent by host member" where createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode @@ -2629,14 +2743,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupMember {memberId = membershipMemId} = membership checkHostRole m memRole toMember <- withStore $ \db -> do - toMember <- getGroupMemberByMemberId db vr user gInfo memId - -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent - -- the situation when member does not exist is an error - -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. - -- For now, this branch compensates for the lack of delayed message delivery. - `catchError` \case - SEGroupMemberNotFoundByMemberId _ -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced - e -> throwError e + toMember <- + getGroupMemberByMemberId db vr user gInfo memId + -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent + -- the situation when member does not exist is an error + -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. + -- For now, this branch compensates for the lack of delayed message delivery. + `catchError` \case + SEGroupMemberNotFoundByMemberId _ -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced + e -> throwError e -- TODO [knocking] separate pending statuses from GroupMemberStatus? -- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.? -- TODO keep as is? (GSMemIntroInvited has no purpose) @@ -2649,8 +2764,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user (chatHasNtfs chatSettings) groupConnReq dm subMode - directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode + groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode + directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user Nothing True dcr dm subMode let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo mcvr = maybe chatInitialVRange fromChatVRange memChatVRange chatV = vr `peerConnChatVersion` mcvr @@ -2691,27 +2806,23 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = msg brokerTs | membershipMemId == memId = pure Nothing -- ignore - XGrpMemRestrict can be sent to restricted member for efficiency - | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Right bm@GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} - | blockedByAdmin == mrsBlocked restriction -> pure Nothing - | senderRole < GRModerator || senderRole < memberRole -> - messageError "x.grp.mem.restrict with insufficient member permissions" $> Nothing - | otherwise -> do - bm' <- setMemberBlocked bm - toggleNtf bm' (not blocked) - let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked - (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m - (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent - groupMsgToView cInfo ci - toView CEvtMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm', blocked} - pure $ memberEventDeliveryScope bm - Left (SEGroupMemberNotFoundByMemberId _) -> do - bm <- createUnknownMember gInfo memId Nothing - bm' <- setMemberBlocked bm - toView $ CEvtUnknownMemberBlocked user gInfo m bm' - pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} - Left e -> throwError $ ChatErrorStore e + | otherwise = do + (bm, unknown) <- withStore $ \db -> getCreateUnknownGMByMemberId db vr user gInfo memId Nothing + let GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} = bm + if + | blockedByAdmin == mrsBlocked restriction -> pure Nothing + | senderRole < GRModerator || senderRole < memberRole -> + messageError "x.grp.mem.restrict with insufficient member permissions" $> Nothing + | otherwise -> do + bm' <- setMemberBlocked bm + toggleNtf bm' (not blocked) + let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent + when unknown $ toView $ CEvtUnknownMemberBlocked user gInfo m bm' + groupMsgToView cInfo ci + toView CEvtMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm', blocked} + pure $ memberEventDeliveryScope bm where setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm blocked = mrsBlocked restriction @@ -2784,11 +2895,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = event = XGrpMsgForward memberId memberName chatMsg brokerTs sendGroupMemberMessage gInfo member event - -- TODO [channels fwd] base on differentiation between groups and channels isUserGrpFwdRelay :: GroupInfo -> Bool - isUserGrpFwdRelay GroupInfo {useRelays, membership = membership@GroupMember {memberRole}} - | isTrue useRelays = isMemberRelay membership - | otherwise = memberRole >= GRAdmin + isUserGrpFwdRelay gInfo@GroupInfo {membership} + | useRelays' gInfo = isRelay membership + | otherwise = memberRole' membership >= GRAdmin + + isMemberGrpFwdRelay :: GroupInfo -> GroupMember -> Bool + isMemberGrpFwdRelay gInfo m + | useRelays' gInfo = isRelay m + | otherwise = memberRole' m >= GRAdmin xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpLeave gInfo m msg brokerTs = do @@ -2826,7 +2941,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') groupMsgToView cInfo ci createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' - void $ forkIO $ setGroupLinkData' NRMBackground user g'' + void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' Just _ -> updateGroupPrefs_ g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} @@ -2868,13 +2983,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else joinExistingContact subMode mCt where groupDirectInv = - GroupDirectInvitation { - groupDirectInvLink = connReq, - fromGroupId_ = Just groupId, - fromGroupMemberId_ = Just (groupMemberId' m), - fromGroupMemberConnId_ = Just mConnId, - groupDirectInvStartedConnection = isTrue $ autoAcceptMemberContacts user - } + GroupDirectInvitation + { groupDirectInvLink = connReq, + fromGroupId_ = Just groupId, + fromGroupMemberId_ = Just (groupMemberId' m), + fromGroupMemberConnId_ = Just mConnId, + groupDirectInvStartedConnection = isTrue $ autoAcceptMemberContacts user + } joinExistingContact subMode mCt@Contact {contactId = mContactId} | isTrue (autoAcceptMemberContacts user) = do (cmdId, acId) <- joinConn subMode @@ -2919,7 +3034,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let p = userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing True -- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ) dm <- encodeConnInfo $ XInfo p - joinAgentConnectionAsync user True connReq dm subMode + joinAgentConnectionAsync user Nothing True connReq dm subMode createItems mCt' m' = do (g', m'', scopeInfo) <- mkGroupChatScope g m' createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing @@ -2934,15 +3049,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> Maybe ContactName -> ChatMessage 'Json -> UTCTime -> UTCTime -> CM () - xGrpMsgForward gInfo m@GroupMember {memberRole, localDisplayName} memberId memberName chatMsg msgTs brokerTs = do - when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName) - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case - Right author -> processForwardedMsg author - Left (SEGroupMemberNotFoundByMemberId _) -> do - unknownAuthor <- createUnknownMember gInfo memberId memberName - toView $ CEvtUnknownMemberCreated user gInfo m unknownAuthor - processForwardedMsg unknownAuthor - Left e -> throwError $ ChatErrorStore e + xGrpMsgForward gInfo m@GroupMember {localDisplayName} memberId memberName chatMsg msgTs brokerTs = do + unless (isMemberGrpFwdRelay gInfo m) $ throwChatError (CEGroupContactRole localDisplayName) + (author, unknown) <- withStore $ \db -> getCreateUnknownGMByMemberId db vr user gInfo memberId memberName + when unknown $ toView $ CEvtUnknownMemberCreated user gInfo m author + processForwardedMsg author where -- ! see isForwardedGroupMsg: forwarded group events should include msgId to be deduplicated processForwardedMsg :: GroupMember -> CM () @@ -2951,7 +3062,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = rcvMsg_ <- saveGroupFwdRcvMsg user gInfo m author body chatMsg brokerTs forM_ rcvMsg_ $ \rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} -> case event of XMsgNew mc -> void $ memberCanSend author scope $ (const Nothing) <$> newGroupContentMessage gInfo author mc rcvMsg msgTs True - where ExtMsgContent {scope} = mcExtMsgContent mc + where + ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> void $ memberCanSend author msgScope $ (const Nothing) <$> groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live @@ -2968,11 +3080,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XGrpPrefs ps' -> void $ xGrpPrefs gInfo author ps' _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) - createUnknownMember :: GroupInfo -> MemberId -> Maybe ContactName -> CM GroupMember - createUnknownMember gInfo memberId memberName = do - let name = fromMaybe (nameFromMemberId memberId) memberName - withStore $ \db -> createNewUnknownGroupMember db vr user gInfo memberId name - directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta `catchAllErrors` \_ -> pure () @@ -3065,7 +3172,7 @@ deleteGroupConnections user gInfo waitDelivery = do deleteMembersConnections' user members waitDelivery where getMembers vr - | isTrue (useRelays gInfo) = withStore' $ \db -> getGroupRelays db vr user gInfo + | useRelays' gInfo = withStore' $ \db -> getGroupRelayMembers db vr user gInfo | otherwise = withStore' $ \db -> getGroupMembers db vr user gInfo startDeliveryTaskWorkers :: CM () @@ -3189,65 +3296,65 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, cursorGMId_ = startingCursor} = job sendBodyToMembers :: CM () sendBodyToMembers - | isTrue (useRelays gInfo) = -- channel - case jobScope of - -- there's no member review in channels, so job spec includePending is ignored - DJSGroup {} -> do - bucketSize <- asks $ deliveryBucketSize . config - sendLoop bucketSize startingCursor - where - sendLoop :: Int -> Maybe GroupMemberId -> CM () - sendLoop bucketSize cursorGMId_ = do - mems <- withStore' $ \db -> getGroupMembersByCursor db vr user gInfo cursorGMId_ singleSenderGMId_ bucketSize - unless (null mems) $ do - deliver body mems - let cursorGMId' = groupMemberId' $ last mems - withStore' $ \db -> updateDeliveryJobCursor db jobId cursorGMId' - unless (length mems < bucketSize) $ sendLoop bucketSize (Just cursorGMId') - DJSMemberSupport scopeGMId -> do - -- for member support scope we just load all recipients in one go, without cursor - modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo - let moderatorFilter m = - memberCurrent m - && maxVersion (memberChatVRange m) >= groupKnockingVersion - && Just (groupMemberId' m) /= singleSenderGMId_ - modMs' = filter moderatorFilter modMs - mems <- - if Just scopeGMId == singleSenderGMId_ - then pure modMs' - else do - scopeMem <- withStore $ \db -> getGroupMemberById db vr user scopeGMId - pure $ scopeMem : modMs' - unless (null mems) $ deliver body mems - | otherwise = -- fully connected group - case singleSenderGMId_ of - Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays" - Just singleSenderGMId -> do - sender <- withStore $ \db -> getGroupMemberById db vr user singleSenderGMId - ms <- buildMemberList sender - unless (null ms) $ deliver body ms - where - buildMemberList sender = do - vec <- withStore (`getMemberRelationsVector` sender) - -- this excludes the sender - let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec - case jobScope of - DJSGroup {jobSpec} -> do - ms <- withStore' $ \db -> getGroupMembersByIndexes db vr user gInfo introducedMemsIdxs - pure $ filter shouldForwardTo ms - where - shouldForwardTo m - | jobSpecImpliedPending jobSpec = memberCurrentOrPending m - | otherwise = memberCurrent m - DJSMemberSupport scopeGMId -> do - ms <- withStore' $ \db -> getSupportScopeMembersByIndexes db vr user gInfo scopeGMId introducedMemsIdxs - pure $ filter shouldForwardTo ms - where - shouldForwardTo m = groupMemberId' m == scopeGMId || currentModerator m - currentModerator m@GroupMember {memberRole} = - memberRole >= GRModerator - && memberCurrent m - && maxVersion (memberChatVRange m) >= groupKnockingVersion + -- channel + | useRelays' gInfo = case jobScope of + -- there's no member review in channels, so job spec includePending is ignored + DJSGroup {} -> do + bucketSize <- asks $ deliveryBucketSize . config + sendLoop bucketSize startingCursor + where + sendLoop :: Int -> Maybe GroupMemberId -> CM () + sendLoop bucketSize cursorGMId_ = do + mems <- withStore' $ \db -> getGroupMembersByCursor db vr user gInfo cursorGMId_ singleSenderGMId_ bucketSize + unless (null mems) $ do + deliver body mems + let cursorGMId' = groupMemberId' $ last mems + withStore' $ \db -> updateDeliveryJobCursor db jobId cursorGMId' + unless (length mems < bucketSize) $ sendLoop bucketSize (Just cursorGMId') + DJSMemberSupport scopeGMId -> do + -- for member support scope we just load all recipients in one go, without cursor + modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + let moderatorFilter m = + memberCurrent m + && maxVersion (memberChatVRange m) >= groupKnockingVersion + && Just (groupMemberId' m) /= singleSenderGMId_ + modMs' = filter moderatorFilter modMs + mems <- + if Just scopeGMId == singleSenderGMId_ + then pure modMs' + else do + scopeMem <- withStore $ \db -> getGroupMemberById db vr user scopeGMId + pure $ scopeMem : modMs' + unless (null mems) $ deliver body mems + -- fully connected group + | otherwise = case singleSenderGMId_ of + Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays" + Just singleSenderGMId -> do + sender <- withStore $ \db -> getGroupMemberById db vr user singleSenderGMId + ms <- buildMemberList sender + unless (null ms) $ deliver body ms + where + buildMemberList sender = do + vec <- withStore (`getMemberRelationsVector` sender) + -- this excludes the sender + let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec + case jobScope of + DJSGroup {jobSpec} -> do + ms <- withStore' $ \db -> getGroupMembersByIndexes db vr user gInfo introducedMemsIdxs + pure $ filter shouldForwardTo ms + where + shouldForwardTo m + | jobSpecImpliedPending jobSpec = memberCurrentOrPending m + | otherwise = memberCurrent m + DJSMemberSupport scopeGMId -> do + ms <- withStore' $ \db -> getSupportScopeMembersByIndexes db vr user gInfo scopeGMId introducedMemsIdxs + pure $ filter shouldForwardTo ms + where + shouldForwardTo m = groupMemberId' m == scopeGMId || currentModerator m + currentModerator m@GroupMember {memberRole} = + memberRole >= GRModerator + && memberCurrent m + && maxVersion (memberChatVRange m) >= groupKnockingVersion where deliver :: ByteString -> [GroupMember] -> CM () deliver msgBody mems = @@ -3268,3 +3375,103 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do Nothing -> VRValue Nothing msgBody -- sending to one member, do not reference body Just 1 -> VRValue (Just 1) msgBody Just _ -> VRRef 1 + +-- Single worker processes all relay requests (XGrpRelayInv). +-- We use map with a single key 1 to fit into existing worker management framework. +relayRequestWorkerKey :: Int +relayRequestWorkerKey = 1 + +startRelayRequestWorker :: CM () +startRelayRequestWorker = do + hasPending <- withStore' hasPendingRelayRequests + when hasPending $ lift resumeRelayRequestWork + +resumeRelayRequestWork :: CM' () +resumeRelayRequestWork = void $ getRelayRequestWorker False + +getRelayRequestWorker :: Bool -> CM' Worker +getRelayRequestWorker hasWork = do + ws <- asks relayRequestWorkers + a <- asks smpAgent + getAgentWorker "relay_request" hasWork a relayRequestWorkerKey ws $ + runRelayRequestWorker a + +runRelayRequestWorker :: AgentClient -> Worker -> CM () +runRelayRequestWorker a Worker {doWork} = do + vr <- chatVersionRange + (user, uclId) <- withStore $ \db -> do + user <- getRelayUser db + UserContactLink {userContactLinkId} <- getUserAddress db user + pure (user, userContactLinkId) + forever $ do + lift $ waitForWork doWork + runRelayRequestOperation vr user uclId + where + runRelayRequestOperation :: VersionRangeChat -> User -> Int64 -> CM () + runRelayRequestOperation vr user uclId = + withWork_ a doWork (withStore' getNextPendingRelayRequest) $ + \(groupId, rrd) -> do + ri <- asks $ reconnectInterval . agentConfig . config + withRetryInterval ri $ \_ loop -> do + liftIO $ waitWhileSuspended a + liftIO $ waitForUserNetwork a + processRelayRequest groupId rrd `catchAllErrors` retryTmpError loop groupId + where + retryTmpError :: CM () -> GroupId -> ChatError -> CM () + retryTmpError loop groupId = \case + ChatErrorAgent {agentError} | temporaryOrHostError agentError -> loop + e -> do + withStore' $ \db -> setRelayRequestErr db groupId (tshow e) + eToView e + processRelayRequest :: GroupId -> RelayRequestData -> CM () + processRelayRequest groupId rrd = do + gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + -- Check if relay link already exists (recovery case) + withStore' (\db -> runExceptT $ getGroupLink db user gInfo) >>= \case + Right GroupLink {connLinkContact = CCLink _ sLnk_} -> + case sLnk_ of + Just sLnk -> acceptOwnerConnection rrd gInfo sLnk + Nothing -> throwChatError $ CEException "processRelayRequest: relay link doesn't have short link" + Left _ -> do + (gInfo', sLnk) <- getLinkDataCreateRelayLink rrd gInfo + acceptOwnerConnection rrd gInfo' sLnk + where + getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) + getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do + (_cReq, cData) <- getShortLinkConnReq NRMBackground user reqGroupLink + liftIO (decodeLinkUserData cData) >>= \case + Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" + Just (GroupShortLinkData gp) -> do + validateGroupProfile gp + gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo gp + sLnk <- createRelayLink gInfo' + pure (gInfo', sLnk) + where + validateGroupProfile :: GroupProfile -> CM () + validateGroupProfile _groupProfile = do + -- TODO [relays] relay: validate group profile, verify owner's signature + pure () + createRelayLink :: GroupInfo -> CM ShortLinkContact + createRelayLink gi@GroupInfo {groupProfile} = do + -- TODO [relays] relay: set relay link data + -- TODO - link data: relay key for group, relay identity (profile, certificate, relay identity key) + -- TODO - TBC link's member role - owner to communicate in invitation? + groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode + let userData = encodeShortLinkData $ GroupShortLinkData groupProfile + userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} + crClientData = encodeJSON $ CRDataGroup groupLinkId + (connId, (ccLink, _serviceId)) <- withAgent $ \a' -> createConnection a' NRMBackground (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) CR.IKPQOff subMode + ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + sLnk <- case toShortLinkContact ccLink' of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "failed to create relay link: no short link" + gVar <- asks random + void $ withFastStore $ \db -> createGroupLink db gVar user gi connId ccLink' groupLinkId GRMember subMode + pure sLnk + acceptOwnerConnection :: RelayRequestData -> GroupInfo -> ShortLinkContact -> CM () + acceptOwnerConnection RelayRequestData {relayInvId, reqChatVRange} gi relayLink = do + ownerMember <- withStore $ \db -> getHostMember db vr user groupId + void $ acceptRelayJoinRequestAsync user uclId gi ownerMember relayInvId reqChatVRange relayLink + -- TODO [relays] relay: group invite accepted event, chat item (?) + pure () diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index af6229e7c8..682e0cff55 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -88,7 +88,7 @@ disabledSimplexChatSMPServers = "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] --- TODO [chat relays] real chat relays +-- TODO [relays] real chat relays simplexChatRelays :: [NewUserChatRelay] simplexChatRelays = [ presetChatRelay True "chat_relay_1" ["simplex.im"] (either error id $ strDecode "https://smp111.simplex.im/r#Pz9qz7ZVljMofoRxiDDpL_w2DZSazK8IgafxqnWKv6Y"), diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 40b667365e..5d501fffb7 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -227,8 +227,7 @@ instance StrEncoding AppMessageBinary where let msgId = if B.null msgId' then Nothing else Just (SharedMsgId msgId') pure AppMessageBinary {tag, msgId, body} -data MsgScope - = MSMember {memberId :: MemberId} -- Admins can use any member id; members can use only their own id +data MsgScope = MSMember {memberId :: MemberId} -- Admins can use any member id; members can use only their own id deriving (Eq, Show) $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MS") ''MsgScope) @@ -321,6 +320,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json XInfo :: Profile -> ChatMsgEvent 'Json XContact :: {profile :: Profile, contactReqId :: Maybe XContactId, welcomeMsgId :: Maybe SharedMsgId, requestMsg :: Maybe (SharedMsgId, MsgContent)} -> ChatMsgEvent 'Json + XMember :: {profile :: Profile, newMemberId :: MemberId} -> ChatMsgEvent 'Json XDirectDel :: ChatMsgEvent 'Json XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json @@ -328,6 +328,8 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json + XGrpRelayInv :: GroupRelayInvitation -> ChatMsgEvent 'Json + XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json @@ -811,6 +813,7 @@ data CMEventTag (e :: MsgEncoding) where XFileCancel_ :: CMEventTag 'Json XInfo_ :: CMEventTag 'Json XContact_ :: CMEventTag 'Json + XMember_ :: CMEventTag 'Json XDirectDel_ :: CMEventTag 'Json XGrpInv_ :: CMEventTag 'Json XGrpAcpt_ :: CMEventTag 'Json @@ -818,6 +821,8 @@ data CMEventTag (e :: MsgEncoding) where XGrpLinkReject_ :: CMEventTag 'Json XGrpLinkMem_ :: CMEventTag 'Json XGrpLinkAcpt_ :: CMEventTag 'Json + XGrpRelayInv_ :: CMEventTag 'Json + XGrpRelayAcpt_ :: CMEventTag 'Json XGrpMemNew_ :: CMEventTag 'Json XGrpMemIntro_ :: CMEventTag 'Json XGrpMemInv_ :: CMEventTag 'Json @@ -864,6 +869,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XFileCancel_ -> "x.file.cancel" XInfo_ -> "x.info" XContact_ -> "x.contact" + XMember_ -> "x.member" XDirectDel_ -> "x.direct.del" XGrpInv_ -> "x.grp.inv" XGrpAcpt_ -> "x.grp.acpt" @@ -871,6 +877,8 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpLinkReject_ -> "x.grp.link.reject" XGrpLinkMem_ -> "x.grp.link.mem" XGrpLinkAcpt_ -> "x.grp.link.acpt" + XGrpRelayInv_ -> "x.grp.relay.inv" + XGrpRelayAcpt_ -> "x.grp.relay.acpt" XGrpMemNew_ -> "x.grp.mem.new" XGrpMemIntro_ -> "x.grp.mem.intro" XGrpMemInv_ -> "x.grp.mem.inv" @@ -918,6 +926,7 @@ instance StrEncoding ACMEventTag where "x.file.cancel" -> XFileCancel_ "x.info" -> XInfo_ "x.contact" -> XContact_ + "x.member" -> XMember_ "x.direct.del" -> XDirectDel_ "x.grp.inv" -> XGrpInv_ "x.grp.acpt" -> XGrpAcpt_ @@ -925,6 +934,8 @@ instance StrEncoding ACMEventTag where "x.grp.link.reject" -> XGrpLinkReject_ "x.grp.link.mem" -> XGrpLinkMem_ "x.grp.link.acpt" -> XGrpLinkAcpt_ + "x.grp.relay.inv" -> XGrpRelayInv_ + "x.grp.relay.acpt" -> XGrpRelayAcpt_ "x.grp.mem.new" -> XGrpMemNew_ "x.grp.mem.intro" -> XGrpMemIntro_ "x.grp.mem.inv" -> XGrpMemInv_ @@ -968,6 +979,7 @@ toCMEventTag msg = case msg of XFileCancel _ -> XFileCancel_ XInfo _ -> XInfo_ XContact {} -> XContact_ + XMember {} -> XMember_ XDirectDel -> XDirectDel_ XGrpInv _ -> XGrpInv_ XGrpAcpt _ -> XGrpAcpt_ @@ -975,6 +987,8 @@ toCMEventTag msg = case msg of XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem _ -> XGrpLinkMem_ XGrpLinkAcpt {} -> XGrpLinkAcpt_ + XGrpRelayInv _ -> XGrpRelayInv_ + XGrpRelayAcpt _ -> XGrpRelayAcpt_ XGrpMemNew {} -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -1085,6 +1099,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do reqContent <- opt "content" let requestMsg = (,) <$> reqMsgId <*> reqContent pure XContact {profile, contactReqId, welcomeMsgId, requestMsg} + XMember_ -> XMember <$> p "profile" <*> p "newMemberId" XDirectDel_ -> pure XDirectDel XGrpInv_ -> XGrpInv <$> p "groupInvitation" XGrpAcpt_ -> XGrpAcpt <$> p "memberId" @@ -1092,6 +1107,8 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId" + XGrpRelayInv_ -> XGrpRelayInv <$> p "groupRelayInvitation" + XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" <*> opt "scope" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -1144,6 +1161,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId] XInfo profile -> o ["profile" .= profile] XContact {profile, contactReqId, welcomeMsgId, requestMsg} -> o $ ("contactReqId" .=? contactReqId) $ ("welcomeMsgId" .=? welcomeMsgId) $ ("msgId" .=? (fst <$> requestMsg)) $ ("content" .=? (snd <$> requestMsg)) $ ["profile" .= profile] + XMember {profile, newMemberId} -> o ["profile" .= profile, "newMemberId" .= newMemberId] XDirectDel -> JM.empty XGrpInv groupInv -> o ["groupInvitation" .= groupInv] XGrpAcpt memId -> o ["memberId" .= memId] @@ -1151,6 +1169,8 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId] + XGrpRelayInv groupRelayInv -> o ["groupRelayInvitation" .= groupRelayInv] + XGrpRelayAcpt relayLink -> o ["relayLink" .= relayLink] XGrpMemNew memInfo scope -> o $ ("scope" .=? scope) ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 131a66c465..9a007d85fe 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -135,21 +135,22 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- from GroupMember - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts diff --git a/src/Simplex/Chat/Store/Delivery.hs b/src/Simplex/Chat/Store/Delivery.hs index c1da436f04..f6e0a0c77c 100644 --- a/src/Simplex/Chat/Store/Delivery.hs +++ b/src/Simplex/Chat/Store/Delivery.hs @@ -161,11 +161,11 @@ getNextDeliveryTasks :: DB.Connection -> GroupInfo -> MessageDeliveryTask -> IO getNextDeliveryTasks db gInfo task = getWorkItems "message delivery task" getTaskIds (getMsgDeliveryTask_ db) (markDeliveryTaskFailed_ db) where - GroupInfo {groupId, useRelays} = gInfo + GroupInfo {groupId} = gInfo MessageDeliveryTask {jobScope, senderGMId} = task getTaskIds :: IO [Int64] getTaskIds - | isTrue useRelays = + | useRelays' gInfo = map fromOnly <$> DB.query db diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index a517a7365b..3254ae817a 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -30,6 +30,8 @@ module Simplex.Chat.Store.Direct createDirectConnection, createIncognitoProfile, createConnReqConnection, + createRelayMemberConnectionAsync, + updateConnLinkData, setPreparedGroupStartedConnection, getProfileById, getConnReqContactXContactId, @@ -153,10 +155,12 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> Maybe PreparedChatEntity -> ConnReqContact -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> Maybe PreparedChatEntity -> ConnReqContact -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe IncognitoProfile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection createConnReqConnection db userId acId preparedEntity_ cReq cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do currentTs <- getCurrentTime - customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile + customUserProfileId <- forM incognitoProfile $ \case + NewIncognito p -> createIncognitoProfile_ db userId currentTs p + ExistingIncognito LocalProfile {profileId = pId} -> pure pId let connStatus = ConnPrepared DB.execute db @@ -175,7 +179,9 @@ createConnReqConnection db userId acId preparedEntity_ cReq cReqHash sLnk xConta ) connId <- insertedRowId db case preparedEntity_ of - Just (PCEGroup gInfo _) -> updatePreparedGroup gInfo customUserProfileId currentTs + -- For relay groups, setPreparedGroupLinkInfo is called before the relay loop + Just (PCEGroup gInfo _) | not (useRelays' gInfo) -> + setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId currentTs _ -> pure () pure Connection @@ -213,16 +219,41 @@ createConnReqConnection db userId acId preparedEntity_ cReq cReqHash sLnk xConta Just (PCEContact Contact {contactId}) -> (ConnContact, Just contactId, Nothing, Just contactId) Just (PCEGroup _ GroupMember {groupMemberId}) -> (ConnMember, Nothing, Just groupMemberId, Just groupMemberId) Nothing -> (ConnContact, Nothing, Nothing, Nothing) - updatePreparedGroup GroupInfo {groupId, membership} customUserProfileId currentTs = do - DB.execute - db - "UPDATE groups SET via_group_link_uri = ?, via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ?" - (cReq, cReqHash, BI True, currentTs, groupId) - when (isJust customUserProfileId) $ - DB.execute - db - "UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE group_member_id = ?" - (customUserProfileId, currentTs, groupMemberId' membership) + +createRelayMemberConnectionAsync :: DB.Connection -> User -> GroupInfo -> GroupMember -> ShortLinkContact -> (CommandId, ConnId) -> SubscriptionMode -> IO () +createRelayMemberConnectionAsync db user@User {userId} gInfo GroupMember {groupMemberId} relayLink (cmdId, agentConnId) subMode = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, + group_member_id, via_short_link_contact, custom_user_profile_id, via_group_link, + created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, agentConnId, ConnNew, ConnMember, BI True) + :. (groupMemberId, relayLink, customUserProfileId_, BI True) + :. (currentTs, currentTs, BI (subMode == SMOnlyCreate)) + ) + connId <- insertedRowId db + setCommandConnId db user cmdId connId + where + customUserProfileId_ = localProfileId <$> incognitoMembershipProfile gInfo + +updateConnLinkData :: DB.Connection -> User -> Connection -> ConnReqContact -> ConnReqUriHash -> Maybe GroupLinkId -> VersionChat -> PQSupport -> IO () +updateConnLinkData db User {userId} Connection {connId} cReq cReqHash groupLinkId_ chatV pqSup = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET via_contact_uri = ?, via_contact_uri_hash = ?, group_link_id = ?, + conn_chat_version = ?, pq_support = ?, pq_encryption = ?, + updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (cReq, cReqHash, groupLinkId_, chatV, pqSup, pqSup, currentTs, userId, connId) setPreparedGroupStartedConnection :: DB.Connection -> GroupId -> IO () setPreparedGroupStartedConnection db groupId = do diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 338f42666e..8564354a97 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -41,7 +41,6 @@ module Simplex.Chat.Store.Groups createGroupRejectedViaLink, setGroupInvitationChatItemId, getGroup, - getGroupInfo, getGroupInfoByUserContactLinkConnReq, getGroupInfoViaUserShortLink, getGroupViaShortLinkToConnect, @@ -60,23 +59,38 @@ module Simplex.Chat.Store.Groups getGroupMemberById, getGroupMemberByIndex, getGroupMemberByMemberId, + getCreateUnknownGMByMemberId, getGroupMemberIdViaMemberId, getScopeMemberIdViaMemberId, getGroupMembers, getGroupMembersByIndexes, getSupportScopeMembersByIndexes, getGroupModerators, - getGroupRelays, + getGroupRelayMembers, getGroupMembersForExpiration, getGroupCurrentMembersCount, deleteGroupChatItems, deleteGroupMembers, cleanupHostGroupLinkConn, deleteGroup, + getInProgressGroups, getBaseGroupDetails, getContactGroupPreferences, getGroupInvitation, createNewContactMember, + createGroupRelayRecord, + getGroupRelayById, + getGroupRelayByGMId, + getGroupRelays, + createRelayForOwner, + getCreateRelayForMember, + createRelayConnection, + updateRelayStatus, + updateRelayStatusFromTo, + setRelayLinkAccepted, + setGroupInProgressDone, + createRelayRequestGroup, + updateRelayOwnStatusFromTo, createNewContactMemberAsync, createJoiningMember, getMemberJoinRequest, @@ -104,6 +118,7 @@ module Simplex.Chat.Store.Groups setMemberVectorRelationConnected, getMemberRelationsVector, createIntroReMember, + createIntroReMemberConn, createIntroToMemberContact, getMatchingContacts, getMatchingMembers, @@ -156,6 +171,7 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Char (toLower) import Data.Either (rights) +import Data.Functor (($>)) import Data.Int (Int64) import Data.List (partition, sortOn) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) @@ -165,6 +181,7 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Messages +import Simplex.Chat.Operators import Simplex.Chat.Protocol hiding (Binary) import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared @@ -173,7 +190,7 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -191,11 +208,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus, Maybe BoolInt) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. (Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. (Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked', Just isChatRelay) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. (Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked', isChatRelay) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. (Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -310,8 +327,8 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True} -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo -createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do +createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> Bool -> ExceptT StoreError IO GroupInfo +createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile useRelays = ExceptT $ do let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime @@ -327,11 +344,11 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc db [sql| INSERT INTO groups - (local_display_name, user_id, group_profile_id, enable_ntfs, + (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at) - VALUES (?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) + (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser customUserProfileId currentTs vr @@ -339,7 +356,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc pure GroupInfo { groupId, - useRelays = BoolDef False, + useRelays = BoolDef useRelays, + relayOwnStatus = Nothing, localDisplayName = ldn, groupProfile, localAlias = "", @@ -371,9 +389,9 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db vr user gId hostId <- getHostMemberId_ db user gId let GroupMember {groupMemberId, memberId, memberRole} = membership - MemberIdRole {memberId = invMemberId, memberRole = memberRole'} = invitedMember - liftIO . when (memberId /= invMemberId || memberRole /= memberRole') $ - DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (invMemberId, memberRole', groupMemberId) + MemberIdRole {memberId = invMemberId, memberRole = invMemberRole} = invitedMember + liftIO . when (memberId /= invMemberId || memberRole /= invMemberRole) $ + DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (invMemberId, invMemberRole, groupMemberId) gInfo' <- if p' == groupProfile then pure gInfo @@ -415,6 +433,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ ( GroupInfo { groupId, useRelays = BoolDef False, + relayOwnStatus = Nothing, localDisplayName, groupProfile, localAlias = "", @@ -498,8 +517,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing, - isChatRelay = BoolDef False + supportChat = Nothing } where memberChatVRange@(VersionRange minV maxV) = vr @@ -548,13 +566,17 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) -createPreparedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> ExceptT StoreError IO (GroupInfo, GroupMember) -createPreparedGroup db vr user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId = do +createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> VersionRangeChat -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> ExceptT StoreError IO (GroupInfo, GroupMember) +createPreparedGroup db gVar vr user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) - (groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing currentTs + (groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing useRelays Nothing currentTs hostMemberId <- insertHost_ currentTs groupId groupLDN - let userMember = MemberIdRole (MemberId $ encodeUtf8 groupLDN <> "_user_unknown_id") GRMember + userMemberId <- + if useRelays + then liftIO $ MemberId <$> encodedRandomBytes gVar 12 + else pure $ MemberId $ encodeUtf8 groupLDN <> "_user_unknown_id" + let userMember = MemberIdRole userMemberId GRMember membership <- createContactMemberInv_ db user groupId (Just hostMemberId) user userMember GCUserMember GSMemUnknown IBUnknown Nothing currentTs vr hostMember <- getGroupMember db vr user groupId hostMemberId when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember @@ -562,8 +584,9 @@ createPreparedGroup db vr user@User {userId, userContactId} groupProfile busines pure (g, hostMember) where insertHost_ currentTs groupId groupLDN = do - let memberId = MemberId $ encodeUtf8 groupLDN <> "_host_unknown_id" - hostProfile = profileFromName $ nameFromMemberId memberId + randHostId <- liftIO $ encodedRandomBytes gVar 12 + let memberId = MemberId $ encodeUtf8 groupLDN <> "_unknown_host_" <> randHostId + hostProfile = profileFromName $ nameFromBS randHostId (localDisplayName, profileId) <- createNewMemberProfile_ db user hostProfile currentTs indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ do @@ -750,7 +773,7 @@ createGroupViaLink' business membershipStatus = do currentTs <- liftIO getCurrentTime - (groupId, _groupLDN) <- createGroup_ db userId groupProfile Nothing business currentTs + (groupId, _groupLDN) <- createGroup_ db userId groupProfile Nothing business False Nothing currentTs hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact @@ -776,8 +799,8 @@ createGroupViaLink' ) insertedRowId db -createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe (CreatedLinkContact, Maybe SharedMsgId) -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO (GroupId, Text) -createGroup_ db userId groupProfile prepared business currentTs = ExceptT $ do +createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe (CreatedLinkContact, Maybe SharedMsgId) -> Maybe BusinessChatInfo -> Bool -> Maybe RelayStatus -> UTCTime -> ExceptT StoreError IO (GroupId, Text) +createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus currentTs = ExceptT $ do let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} = groupProfile withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do liftIO $ do @@ -792,10 +815,10 @@ createGroup_ db userId groupProfile prepared business currentTs = ExceptT $ do INSERT INTO groups (group_profile_id, local_display_name, user_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, - business_chat, business_member_id, customer_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + business_chat, business_member_id, customer_member_id, use_relays, relay_own_status) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. toPreparedGroupRow prepared :. businessChatInfoRow business) + ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. toPreparedGroupRow prepared :. businessChatInfoRow business :. (BI useRelays, relayOwnStatus)) groupId <- insertedRowId db pure (groupId, localDisplayName) @@ -903,6 +926,15 @@ deleteGroupProfile_ db userId groupId = |] (userId, groupId) +getInProgressGroups :: DB.Connection -> VersionRangeChat -> User -> UTCTime -> IO [GroupInfo] +getInProgressGroups db vr user@User {userId} createdAtCutoff = do + groupIds <- map fromOnly <$> + DB.query + db + "SELECT group_id FROM groups WHERE user_id = ? AND creating_in_progress = 1 AND created_at <= ?" + (userId, createdAtCutoff) + rights <$> mapM (runExceptT . getGroupInfo db vr user) groupIds + getBaseGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe Text -> IO [GroupInfo] getBaseGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do map (toGroupInfo vr userContactId []) @@ -1019,6 +1051,16 @@ getGroupMemberByMemberId db vr user GroupInfo {groupId} memberId = (groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?") (groupId, memberId) +getCreateUnknownGMByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Maybe ContactName -> ExceptT StoreError IO (GroupMember, Bool) +getCreateUnknownGMByMemberId db vr user gInfo memberId memberName = do + liftIO (runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case + Right m -> pure (m, False) + Left (SEGroupMemberNotFoundByMemberId _) -> do + let name = fromMaybe (nameFromMemberId memberId) memberName + m <- createNewUnknownGroupMember db vr user gInfo memberId name + pure (m, True) + Left e -> throwError e + getScopeMemberIdViaMemberId :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberId -> ExceptT StoreError IO GroupMemberId getScopeMemberIdViaMemberId db user g@GroupInfo {membership} sender scopeMemberId | scopeMemberId == memberId' membership = pure $ groupMemberId' membership @@ -1075,14 +1117,13 @@ getGroupModerators db vr user@User {userId, userContactId} GroupInfo {groupId} = (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") (userId, groupId, userContactId, GRModerator, GRAdmin, GROwner) --- TODO [channels fwd] retrieve relays based on knowledge about member from protocol, not role (isMemberRelay) -getGroupRelays :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] -getGroupRelays db vr user@User {userId, userContactId} GroupInfo {groupId} = do +getGroupRelayMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] +getGroupRelayMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do map (toContactMember vr user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND m.contact_id IS DISTINCT FROM ? AND m.member_role = ?") - (userId, groupId, userContactId, GRAdmin) + (userId, groupId, userContactId, GRRelay) getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do @@ -1166,8 +1207,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, memberChatVRange = peerChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing, - isChatRelay = BoolDef False + supportChat = Nothing } where insertMember_ = do @@ -1188,6 +1228,241 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, ) pure indexInGroup +createGroupRelayRecord :: DB.Connection -> GroupInfo -> GroupMember -> UserChatRelay -> ExceptT StoreError IO GroupRelay +createGroupRelayRecord db GroupInfo {groupId} GroupMember {groupMemberId} UserChatRelay {chatRelayId} = do + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + INSERT INTO group_relays + (group_id, group_member_id, chat_relay_id, relay_status, created_at, updated_at) + VALUES (?,?,?,?,?,?) + |] + (groupId, groupMemberId, chatRelayId, RSNew, currentTs, currentTs) + relayId <- liftIO $ insertedRowId db + getGroupRelayById db relayId + +getGroupRelayById :: DB.Connection -> Int64 -> ExceptT StoreError IO GroupRelay +getGroupRelayById db relayId = + ExceptT . firstRow toGroupRelay (SEGroupRelayNotFound relayId) $ + DB.query + db + (groupRelayQuery <> " WHERE group_relay_id = ?") + (Only relayId) + +getGroupRelayByGMId :: DB.Connection -> GroupMemberId -> ExceptT StoreError IO GroupRelay +getGroupRelayByGMId db groupMemberId = + ExceptT . firstRow toGroupRelay (SEGroupRelayNotFoundByMemberId groupMemberId) $ + DB.query + db + (groupRelayQuery <> " WHERE group_member_id = ?") + (Only groupMemberId) + +getGroupRelays :: DB.Connection -> GroupInfo -> IO [GroupRelay] +getGroupRelays db GroupInfo {groupId} = + map toGroupRelay + <$> DB.query + db + (groupRelayQuery <> " WHERE group_id = ?") + (Only groupId) + +groupRelayQuery :: Query +groupRelayQuery = + [sql| + SELECT group_relay_id, group_member_id, chat_relay_id, relay_status, relay_link + FROM group_relays + |] + +toGroupRelay :: (Int64, GroupMemberId, Int64, RelayStatus, Maybe ShortLinkContact) -> GroupRelay +toGroupRelay (groupRelayId, groupMemberId, userChatRelayId, relayStatus, relayLink) = + GroupRelay {groupRelayId, groupMemberId, userChatRelayId, relayStatus, relayLink} + +createRelayForOwner :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember +createRelayForOwner db vr gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {name} = do + currentTs <- liftIO getCurrentTime + let relayProfile = profileFromName name + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user relayProfile currentTs + groupMemberId <- createWithRandomId' db gVar $ \memId -> runExceptT $ do + indexInGroup <- getUpdateNextIndexInGroup_ db groupId + liftIO $ + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, indexInGroup, MemberId memId, GRRelay, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser, groupMemberId' membership) + :. (userId, localDisplayName, memProfileId, currentTs, currentTs) + ) + liftIO $ insertedRowId db + getGroupMemberById db vr user groupMemberId + +getCreateRelayForMember :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> ShortLinkContact -> ExceptT StoreError IO GroupMember +getCreateRelayForMember db vr gVar user@User {userId, userContactId} GroupInfo {groupId, localDisplayName = groupLDN} relayLink = + liftIO getGroupMemberByRelayLink >>= maybe createRelayMember pure + where + getGroupMemberByRelayLink = + maybeFirstRow (toContactMember vr user) $ + DB.query + db + (groupMemberQuery <> " WHERE m.group_id = ? AND m.relay_link = ?") + (groupId, relayLink) + createRelayMember = do + currentTs <- liftIO getCurrentTime + randRelayId <- liftIO $ encodedRandomBytes gVar 12 + let memberId = MemberId $ encodeUtf8 groupLDN <> "_unknown_relay_" <> randRelayId + relayProfile = profileFromName $ nameFromBS randRelayId + (localDisplayName, profileId) <- createNewMemberProfile_ db user relayProfile currentTs + indexInGroup <- getUpdateNextIndexInGroup_ db groupId + groupMemberId <- liftIO $ do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_profile_id, created_at, updated_at, relay_link + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, indexInGroup, memberId, GRRelay, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) + :. (userId, localDisplayName, profileId, currentTs, currentTs, relayLink) + ) + insertedRowId db + getGroupMember db vr user groupId groupMemberId + +createRelayConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection +createRelayConnection db vr user@User {userId} groupMemberId agentConnId connStatus chatV subMode = do + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, conn_status, conn_type, + group_member_id, conn_chat_version, to_subscribe, pq_support, pq_encryption, + created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, agentConnId, 0 :: Int, connStatus, ConnMember) + :. (groupMemberId, chatV, BI (subMode == SMOnlyCreate), PQSupportOff, PQSupportOff) + :. (currentTs, currentTs) + ) + connId <- liftIO $ insertedRowId db + getConnectionById db vr user connId + +updateRelayStatus :: DB.Connection -> GroupRelay -> RelayStatus -> IO GroupRelay +updateRelayStatus db relay@GroupRelay {groupRelayId} relayStatus = + updateRelayStatus_ db groupRelayId relayStatus $> relay {relayStatus} + +updateRelayStatusFromTo :: DB.Connection -> GroupRelay -> RelayStatus -> RelayStatus -> IO GroupRelay +updateRelayStatusFromTo db relay@GroupRelay {groupRelayId} fromStatus toStatus = do + maybeFirstRow fromOnly (DB.query db "SELECT relay_status FROM group_relays WHERE group_relay_id = ?" (Only groupRelayId)) >>= \case + Just status | status == fromStatus -> updateRelayStatus_ db groupRelayId toStatus $> relay {relayStatus = toStatus} + _ -> pure relay + +updateRelayStatus_ :: DB.Connection -> Int64 -> RelayStatus -> IO () +updateRelayStatus_ db relayId relayStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ?" (relayStatus, currentTs, relayId) + +setRelayLinkAccepted :: DB.Connection -> GroupRelay -> ShortLinkContact -> IO GroupRelay +setRelayLinkAccepted db relay@GroupRelay {groupRelayId, groupMemberId} relayLink = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_relays + SET relay_link = ?, relay_status = ?, updated_at = ? + WHERE group_relay_id = ? + |] + (relayLink, RSAccepted, currentTs, groupRelayId) + DB.execute + db + [sql| + UPDATE group_members + SET relay_link = ?, updated_at = ? + WHERE group_member_id = ? + |] + (relayLink, currentTs, groupMemberId) + pure relay {relayStatus = RSAccepted, relayLink = Just relayLink} + +setGroupInProgressDone :: DB.Connection -> GroupInfo -> IO () +setGroupInProgressDone db GroupInfo {groupId} = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?" + (currentTs, groupId) + +createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember) +createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange = do + currentTs <- liftIO getCurrentTime + -- Create group with placeholder profile + let Profile {displayName = fromMemberLDN} = fromMemberProfile + placeholderProfile = GroupProfile + { displayName = "relay_request_" <> fromMemberLDN, + fullName = "", + shortDescr = Nothing, + description = Nothing, + image = Nothing, + groupLink = Nothing, + groupPreferences = Nothing, + memberAdmission = Nothing + } + (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) currentTs + -- Store relay request data for recovery + liftIO $ setRelayRequestData_ groupId + ownerMemberId <- insertOwner_ currentTs groupId + let relayMember = MemberIdRole relayMemberId GRRelay + _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember GSMemAccepted IBUnknown Nothing currentTs vr + ownerMember <- getGroupMember db vr user groupId ownerMemberId + g <- getGroupInfo db vr user groupId + pure (g, ownerMember) + where + setRelayRequestData_ groupId = + DB.execute + db + [sql| + UPDATE groups + SET relay_request_inv_id = ?, + relay_request_group_link = ?, + relay_request_peer_chat_min_version = ?, + relay_request_peer_chat_max_version = ? + WHERE group_id = ? + |] + (Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, groupId) + insertOwner_ currentTs groupId = do + let MemberIdRole {memberId, memberRole} = fromMember + (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs + indexInGroup <- getUpdateNextIndexInGroup_ db groupId + liftIO $ do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + ) + insertedRowId db + +updateRelayOwnStatusFromTo :: DB.Connection -> GroupInfo -> RelayStatus -> RelayStatus -> IO GroupInfo +updateRelayOwnStatusFromTo db gInfo@GroupInfo {groupId} fromStatus toStatus = do + maybeFirstRow fromOnly (DB.query db "SELECT relay_own_status FROM groups WHERE group_id = ?" (Only groupId)) >>= \case + Just status | status == fromStatus -> updateRelayOwnStatus_ db gInfo toStatus $> gInfo {relayOwnStatus = Just toStatus} + _ -> pure gInfo + +updateRelayOwnStatus_ :: DB.Connection -> GroupInfo -> RelayStatus -> IO () +updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ?" (relayStatus, currentTs, groupId) + createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO () createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode = createWithRandomId' db gVar $ \memId -> runExceptT $ do @@ -1215,7 +1490,7 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo :. (minV, maxV) ) -createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) +createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) createJoiningMember db gVar @@ -1224,6 +1499,7 @@ createJoiningMember cReqChatVRange Profile {displayName, fullName, shortDescr, image, contactLink, preferences} cReqXContactId_ + cReqMemberId_ welcomeMsgId_ memberRole memberStatus = do @@ -1235,12 +1511,24 @@ createJoiningMember "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs) profileId <- liftIO $ insertedRowId db - createWithRandomId' db gVar $ \memId -> runExceptT $ do - insertMember_ ldn profileId (MemberId memId) currentTs - groupMemberId <- liftIO $ insertedRowId db - pure (groupMemberId, MemberId memId) + case cReqMemberId_ of + Just memberId -> do + checkMemberNotExists memberId + insertMember_ ldn profileId memberId currentTs + groupMemberId <- liftIO $ insertedRowId db + pure (groupMemberId, memberId) + Nothing -> + createWithRandomId' db gVar $ \memId -> runExceptT $ do + insertMember_ ldn profileId (MemberId memId) currentTs + groupMemberId <- liftIO $ insertedRowId db + pure (groupMemberId, MemberId memId) where VersionRange minV maxV = cReqChatVRange + -- TODO [relays] relay: TBC communicate rejection + checkMemberNotExists :: MemberId -> ExceptT StoreError IO () + checkMemberNotExists memberId = do + exists <- liftIO $ fromOnly . head <$> DB.query db "SELECT EXISTS (SELECT 1 FROM group_members WHERE group_id = ? AND member_id = ?)" (groupId, memberId) + when exists $ throwError SEDuplicateMemberId insertMember_ ldn profileId memberId currentTs = do indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ @@ -1488,8 +1776,7 @@ createNewGroupMember db user gInfo invitingMember memInfo@MemberInfo {profile} m memInvitedByGroupMemberId = Just $ groupMemberId' invitingMember, localDisplayName, memContactId = Nothing, - memProfileId, - isChatRelay = False + memProfileId } createNewMember_ db user gInfo newMember currentTs @@ -1517,8 +1804,7 @@ createNewMember_ memInvitedByGroupMemberId, localDisplayName, memContactId = memberContactId, - memProfileId = memberContactProfileId, - isChatRelay + memProfileId = memberContactProfileId } createdAt = do let invitedById = fromInvitedBy userContactId invitedBy @@ -1531,13 +1817,13 @@ createNewMember_ [sql| INSERT INTO group_members (group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, - member_restriction, is_chat_relay, invited_by, invited_by_group_member_id, + member_restriction, invited_by, invited_by_group_member_id, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty) - :. (memRestriction, BI isChatRelay, invitedById, memInvitedByGroupMemberId) + :. (memRestriction, invitedById, memInvitedByGroupMemberId) :. (userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) :. (minV, maxV) ) @@ -1563,8 +1849,7 @@ createNewMember_ memberChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing, - isChatRelay = BoolDef isChatRelay + supportChat = Nothing } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -1669,27 +1954,35 @@ getMemberRelationsVector db GroupMember {groupMemberId} = "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" (Only groupMemberId) -createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createIntroReMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> Maybe MemberRestrictions -> ExceptT StoreError IO GroupMember createIntroReMember db - user@User {userId} + user gInfo + memInfo@(MemberInfo _ _ _ memberProfile) + memRestrictions_ = do + currentTs <- liftIO getCurrentTime + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs + let memRestriction = restriction <$> memRestrictions_ + newMember = NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId} + createNewMember_ db user gInfo newMember currentTs + +createIntroReMemberConn :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> MemberInfo -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createIntroReMemberConn + db + user@User {userId} _host@GroupMember {memberContactId, activeConn} + reMember@GroupMember {groupMemberId} chatV - memInfo@(MemberInfo _ _ memChatVRange memberProfile) - memRestrictions_ + (MemberInfo _ _ memChatVRange _) (groupCmdId, groupAgentConnId) subMode = do let mcvr = maybe chatInitialVRange fromChatVRange memChatVRange cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn - memRestriction = restriction <$> memRestrictions_ currentTs <- liftIO getCurrentTime - (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs - let newMember = NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId, isChatRelay = False} - member <- createNewMember_ db user gInfo newMember currentTs - conn@Connection {connId = groupConnId} <- liftIO $ createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId chatV mcvr memberContactId cLevel currentTs subMode + conn@Connection {connId = groupConnId} <- liftIO $ createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr memberContactId cLevel currentTs subMode liftIO $ setCommandConnId db user groupCmdId groupConnId - pure (member :: GroupMember) {activeConn = Just conn} + pure (reMember :: GroupMember) {activeConn = Just conn} createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO () createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} chatV mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do @@ -1733,7 +2026,7 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs @@ -1751,14 +2044,16 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, db [sql| UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_link = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - (newName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, currentTs, userId, groupId) + ( (newName, fullName, shortDescr, description, image, groupLink) + :. (groupPreferences, memberAdmission, currentTs, userId, groupId) + ) updateGroup_ ldn currentTs = do DB.execute db @@ -1796,23 +2091,14 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName DB.query db [sql| - SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.preferences, gp.member_admission + SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_link, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? |] (Only groupId) - toGroupProfile (displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission) = - GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} - -getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo -getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do - chatTags <- getGroupChatTags db groupId - firstRow (toGroupInfo vr userContactId chatTags) (SEGroupNotFound groupId) $ - DB.query - db - (groupInfoQuery <> " WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?") - (groupId, userId, userContactId) + toGroupProfile (displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission) = + GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 91b9a74555..28ecbc829a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -676,7 +676,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts @@ -889,7 +889,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = baseParams = (userId, userId, CISRcvNew, userId, MCReport_, BI False) getPreviews = case clq of CLQFilters {favorite = False, unread = False} -> do - let q = baseQuery <> " WHERE g.user_id = ?" + let q = baseQuery <> " WHERE g.user_id = ? AND g.creating_in_progress = 0" p = baseParams :. Only userId queryWithPagination q p CLQFilters {favorite = True, unread = False} -> do @@ -897,7 +897,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = baseQuery <> " " <> [sql| - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.favorite = 1 |] p = baseParams :. Only userId @@ -907,7 +907,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = baseQuery <> " " <> [sql| - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] p = baseParams :. Only userId @@ -917,7 +917,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = baseQuery <> " " <> [sql| - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.favorite = 1 OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) |] @@ -929,7 +929,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = <> " " <> [sql| JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND ( LOWER(g.local_display_name) LIKE '%' || ? || '%' OR LOWER(gp.display_name) LIKE '%' || ? || '%' @@ -2966,7 +2966,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do i.forwarded_by_group_member_id, i.show_group_as_sender, -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -2974,20 +2974,18 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, - rm.member_status, rm.show_messages, rm.member_restriction, rm.is_chat_relay, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, + rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, - dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.is_chat_relay, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, + dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN group_members gsm ON gsm.group_member_id = i.group_scope_group_member_id - LEFT JOIN contact_profiles gsp ON gsp.contact_profile_id = COALESCE(gsm.member_profile_id, gsm.contact_profile_id) LEFT JOIN group_members m ON m.group_member_id = i.group_member_id LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260109_chat_relays.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260109_chat_relays.hs index 8b59053e91..4059008b02 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260109_chat_relays.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260109_chat_relays.hs @@ -12,35 +12,81 @@ m20260109_chat_relays = [r| CREATE TABLE chat_relays( chat_relay_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - address TEXT NOT NULL, + address BYTEA NOT NULL, name TEXT NOT NULL, domains TEXT NOT NULL, preset SMALLINT NOT NULL DEFAULT 0, tested SMALLINT, enabled SMALLINT NOT NULL DEFAULT 1, user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + deleted SMALLINT NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (now()), - updated_at TEXT NOT NULL DEFAULT (now()), - UNIQUE(user_id, address), - UNIQUE(user_id, name) + updated_at TEXT NOT NULL DEFAULT (now()) ); - CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id); +CREATE UNIQUE INDEX idx_chat_relays_user_id_address ON chat_relays(user_id, address); +CREATE UNIQUE INDEX idx_chat_relays_user_id_name ON chat_relays(user_id, name); ALTER TABLE users ADD COLUMN is_user_chat_relay SMALLINT NOT NULL DEFAULT 0; -ALTER TABLE group_members ADD COLUMN is_chat_relay SMALLINT NOT NULL DEFAULT 0; +ALTER TABLE groups + ADD COLUMN use_relays SMALLINT NOT NULL DEFAULT 0, + ADD COLUMN creating_in_progress SMALLINT NOT NULL DEFAULT 0, + ADD COLUMN relay_own_status TEXT, + ADD COLUMN relay_request_inv_id BYTEA, + ADD COLUMN relay_request_group_link BYTEA, + ADD COLUMN relay_request_peer_chat_min_version INTEGER, + ADD COLUMN relay_request_peer_chat_max_version INTEGER, + ADD COLUMN relay_request_failed SMALLINT DEFAULT 0, + ADD COLUMN relay_request_err_reason TEXT; + +ALTER TABLE group_profiles ADD COLUMN group_link BYTEA; + +CREATE TABLE group_relays( + group_relay_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + group_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + chat_relay_id BIGINT NOT NULL REFERENCES chat_relays ON DELETE CASCADE, + relay_status TEXT NOT NULL, + relay_link BYTEA, + created_at TEXT NOT NULL DEFAULT (now()), + updated_at TEXT NOT NULL DEFAULT (now()) +); +CREATE INDEX idx_group_relays_group_id ON group_relays(group_id); +CREATE UNIQUE INDEX idx_group_relays_group_member_id ON group_relays(group_member_id); +CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); + +ALTER TABLE group_members ADD COLUMN relay_link BYTEA; |] down_m20260109_chat_relays :: Text down_m20260109_chat_relays = T.pack [r| -ALTER TABLE group_members DROP COLUMN is_chat_relay; - ALTER TABLE users DROP COLUMN is_user_chat_relay; -DROP INDEX idx_chat_relays_user_id; +ALTER TABLE groups + DROP COLUMN use_relays, + DROP COLUMN creating_in_progress, + DROP COLUMN relay_own_status, + DROP COLUMN relay_request_inv_id, + DROP COLUMN relay_request_group_link, + DROP COLUMN relay_request_peer_chat_min_version, + DROP COLUMN relay_request_peer_chat_max_version, + DROP COLUMN relay_request_failed, + DROP COLUMN relay_request_err_reason; +ALTER TABLE group_profiles DROP COLUMN group_link; + +DROP INDEX idx_group_relays_group_id; +DROP INDEX idx_group_relays_group_member_id; +DROP INDEX idx_group_relays_chat_relay_id; +DROP TABLE group_relays; + +DROP INDEX idx_chat_relays_user_id; +DROP INDEX idx_chat_relays_user_id_address; +DROP INDEX idx_chat_relays_user_id_name; DROP TABLE chat_relays; + +ALTER TABLE group_members DROP COLUMN relay_link; |] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 3474d03ad9..f41f193c4d 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -360,13 +360,14 @@ ALTER TABLE test_chat_schema.chat_items ALTER COLUMN chat_item_id ADD GENERATED CREATE TABLE test_chat_schema.chat_relays ( chat_relay_id bigint NOT NULL, - address text NOT NULL, + address bytea NOT NULL, name text NOT NULL, domains text NOT NULL, preset smallint DEFAULT 0 NOT NULL, tested smallint, enabled smallint DEFAULT 1 NOT NULL, user_id bigint NOT NULL, + deleted smallint DEFAULT 0 NOT NULL, created_at text DEFAULT now() NOT NULL, updated_at text DEFAULT now() NOT NULL ); @@ -809,7 +810,7 @@ CREATE TABLE test_chat_schema.group_members ( member_welcome_shared_msg_id bytea, index_in_group bigint DEFAULT 0 NOT NULL, member_relations_vector bytea, - is_chat_relay smallint DEFAULT 0 NOT NULL + relay_link bytea ); @@ -837,7 +838,8 @@ CREATE TABLE test_chat_schema.group_profiles ( preferences text, description text, member_admission text, - short_descr text + short_descr text, + group_link bytea ); @@ -853,6 +855,30 @@ ALTER TABLE test_chat_schema.group_profiles ALTER COLUMN group_profile_id ADD GE +CREATE TABLE test_chat_schema.group_relays ( + group_relay_id bigint NOT NULL, + group_id bigint NOT NULL, + group_member_id bigint NOT NULL, + chat_relay_id bigint NOT NULL, + relay_status text NOT NULL, + relay_link bytea, + created_at text DEFAULT now() NOT NULL, + updated_at text DEFAULT now() NOT NULL +); + + + +ALTER TABLE test_chat_schema.group_relays ALTER COLUMN group_relay_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.group_relays_group_relay_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + CREATE TABLE test_chat_schema.group_snd_item_statuses ( group_snd_item_status_id bigint NOT NULL, chat_item_id bigint NOT NULL, @@ -909,7 +935,16 @@ CREATE TABLE test_chat_schema.groups ( conn_link_prepared_connection smallint DEFAULT 0 NOT NULL, via_group_link_uri bytea, summary_current_members_count bigint DEFAULT 0 NOT NULL, - member_index bigint DEFAULT 0 NOT NULL + member_index bigint DEFAULT 0 NOT NULL, + use_relays smallint DEFAULT 0 NOT NULL, + creating_in_progress smallint DEFAULT 0 NOT NULL, + relay_own_status text, + relay_request_inv_id bytea, + relay_request_group_link bytea, + relay_request_peer_chat_min_version integer, + relay_request_peer_chat_max_version integer, + relay_request_failed smallint DEFAULT 0, + relay_request_err_reason text ); @@ -1466,16 +1501,6 @@ ALTER TABLE ONLY test_chat_schema.chat_relays -ALTER TABLE ONLY test_chat_schema.chat_relays - ADD CONSTRAINT chat_relays_user_id_address_key UNIQUE (user_id, address); - - - -ALTER TABLE ONLY test_chat_schema.chat_relays - ADD CONSTRAINT chat_relays_user_id_name_key UNIQUE (user_id, name); - - - ALTER TABLE ONLY test_chat_schema.chat_tags ADD CONSTRAINT chat_tags_pkey PRIMARY KEY (chat_tag_id); @@ -1591,6 +1616,11 @@ ALTER TABLE ONLY test_chat_schema.group_profiles +ALTER TABLE ONLY test_chat_schema.group_relays + ADD CONSTRAINT group_relays_pkey PRIMARY KEY (group_relay_id); + + + ALTER TABLE ONLY test_chat_schema.group_snd_item_statuses ADD CONSTRAINT group_snd_item_statuses_pkey PRIMARY KEY (group_snd_item_status_id); @@ -1968,6 +1998,14 @@ CREATE INDEX idx_chat_relays_user_id ON test_chat_schema.chat_relays USING btree +CREATE UNIQUE INDEX idx_chat_relays_user_id_address ON test_chat_schema.chat_relays USING btree (user_id, address); + + + +CREATE UNIQUE INDEX idx_chat_relays_user_id_name ON test_chat_schema.chat_relays USING btree (user_id, name); + + + CREATE INDEX idx_chat_tags_chats_chat_tag_id ON test_chat_schema.chat_tags_chats USING btree (chat_tag_id); @@ -2240,6 +2278,18 @@ CREATE INDEX idx_group_profiles_user_id ON test_chat_schema.group_profiles USING +CREATE INDEX idx_group_relays_chat_relay_id ON test_chat_schema.group_relays USING btree (chat_relay_id); + + + +CREATE INDEX idx_group_relays_group_id ON test_chat_schema.group_relays USING btree (group_id); + + + +CREATE UNIQUE INDEX idx_group_relays_group_member_id ON test_chat_schema.group_relays USING btree (group_member_id); + + + CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON test_chat_schema.group_snd_item_statuses USING btree (chat_item_id); @@ -2919,6 +2969,21 @@ ALTER TABLE ONLY test_chat_schema.group_profiles +ALTER TABLE ONLY test_chat_schema.group_relays + ADD CONSTRAINT group_relays_chat_relay_id_fkey FOREIGN KEY (chat_relay_id) REFERENCES test_chat_schema.chat_relays(chat_relay_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.group_relays + ADD CONSTRAINT group_relays_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.group_relays + ADD CONSTRAINT group_relays_group_member_id_fkey FOREIGN KEY (group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.group_snd_item_statuses ADD CONSTRAINT group_snd_item_statuses_chat_item_id_fkey FOREIGN KEY (chat_item_id) REFERENCES test_chat_schema.chat_items(chat_item_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 87086f6bd4..c58099d2b0 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -26,6 +26,7 @@ module Simplex.Chat.Store.Profiles getUsers, setActiveUser, getUser, + getRelayUser, getUserIdByName, getUserByAConnId, getUserByASndFileId, @@ -58,6 +59,7 @@ module Simplex.Chat.Store.Profiles updateUserAddressSettings, getProtocolServers, getChatRelays, + getChatRelayById, insertProtocolServer, getUpdateServerOperators, getServerOperators, @@ -216,6 +218,11 @@ getUser db userId = ExceptT . firstRow toUser (SEUserNotFound userId) $ DB.query db (userQuery <> " WHERE u.user_id = ?") (Only userId) +getRelayUser :: DB.Connection -> ExceptT StoreError IO User +getRelayUser db = + ExceptT . firstRow toUser SERelayUserNotFound $ + DB.query_ db (userQuery <> " WHERE u.is_user_chat_relay = 1") + getUserIdByName :: DB.Connection -> UserName -> ExceptT StoreError IO Int64 getUserIdByName db uName = ExceptT . firstRow fromOnly (SEUserNotFoundByName uName) $ @@ -621,13 +628,25 @@ getChatRelays db User {userId} = [sql| SELECT chat_relay_id, address, name, domains, preset, tested, enabled FROM chat_relays - WHERE user_id = ? + WHERE user_id = ? AND deleted = 0 |] (Only userId) - where - toChatRelay :: (DBEntityId, ShortLinkContact, Text, Text, BoolInt, Maybe BoolInt, BoolInt) -> UserChatRelay - toChatRelay (chatRelayId, address, name, domains, BI preset, tested, BI enabled) = - UserChatRelay {chatRelayId, address, name, domains = T.splitOn "," domains, preset, tested = unBI <$> tested, enabled, deleted = False} + +toChatRelay :: (DBEntityId, ShortLinkContact, Text, Text, BoolInt, Maybe BoolInt, BoolInt) -> UserChatRelay +toChatRelay (chatRelayId, address, name, domains, BI preset, tested, BI enabled) = + UserChatRelay {chatRelayId, address, name, domains = T.splitOn "," domains, preset, tested = unBI <$> tested, enabled, deleted = False} + +getChatRelayById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO UserChatRelay +getChatRelayById db User {userId} relayId = + ExceptT . firstRow toChatRelay (SEUserChatRelayNotFound relayId) $ + DB.query + db + [sql| + SELECT chat_relay_id, address, name, domains, preset, tested, enabled + FROM chat_relays + WHERE user_id = ? AND chat_relay_id = ? AND deleted = 0 + |] + (userId, relayId) insertChatRelay :: DB.Connection -> User -> UTCTime -> NewUserChatRelay -> IO UserChatRelay insertChatRelay db User {userId} ts relay@UserChatRelay {address, name, domains, preset, tested, enabled} = do @@ -642,7 +661,7 @@ insertChatRelay db User {userId} ts relay@UserChatRelay {address, name, domains, RETURNING chat_relay_id |] (address, name, T.intercalate "," domains, BI preset, BI <$> tested, BI enabled, userId, ts, ts) - pure relay {chatRelayId = DBEntityId crId} + pure (relay :: NewUserChatRelay) {chatRelayId = DBEntityId crId} updateChatRelay :: DB.Connection -> UTCTime -> UserChatRelay -> IO () updateChatRelay db ts UserChatRelay {chatRelayId, address, name, domains, preset, tested, enabled} = @@ -907,7 +926,13 @@ setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, s | deleted -> pure Nothing | otherwise -> Just <$> insertChatRelay db user ts relay DBEntityId relayId - | deleted -> Nothing <$ DB.execute db "DELETE FROM chat_relays WHERE user_id = ? AND chat_relay_id = ? AND preset = ?" (userId, relayId, BI False) + | deleted -> do + -- If relay is referenced in group_relays, mark it as deleted instead of deleting + referenced <- fromOnly . head <$> DB.query db "SELECT EXISTS (SELECT 1 FROM group_relays WHERE chat_relay_id = ?)" (Only relayId) + if referenced + then DB.execute db "UPDATE chat_relays SET deleted = 1, updated_at = ? WHERE chat_relay_id = ?" (ts, relayId) + else DB.execute db "DELETE FROM chat_relays WHERE user_id = ? AND chat_relay_id = ? AND preset = ?" (userId, relayId, BI False) + pure Nothing | otherwise -> Just relay <$ updateChatRelay db ts relay createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () diff --git a/src/Simplex/Chat/Store/RelayRequests.hs b/src/Simplex/Chat/Store/RelayRequests.hs new file mode 100644 index 0000000000..04731d8ef3 --- /dev/null +++ b/src/Simplex/Chat/Store/RelayRequests.hs @@ -0,0 +1,104 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Simplex.Chat.Store.RelayRequests + ( hasPendingRelayRequests, + getNextPendingRelayRequest, + setRelayRequestErr, + ) +where + +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import Data.Time.Clock (getCurrentTime) +import Simplex.Chat.Store.Shared +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Protocol (InvitationId) +import Simplex.Messaging.Agent.Store.AgentStore (getWorkItem, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (firstRow') +import Simplex.Messaging.Version +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..)) +import Database.SQLite.Simple.QQ (sql) +#endif + +hasPendingRelayRequests :: DB.Connection -> IO Bool +hasPendingRelayRequests db = + fromOnly . head + <$> DB.query + db + [sql| + SELECT EXISTS ( + SELECT 1 + FROM groups + WHERE relay_own_status = ? + AND relay_request_failed = 0 + AND relay_request_err_reason IS NULL + LIMIT 1 + ) + |] + (Only RSInvited) + +getNextPendingRelayRequest :: DB.Connection -> IO (Either StoreError (Maybe (GroupId, RelayRequestData))) +getNextPendingRelayRequest db = + getWorkItem "relay request" getNextRequestGroupId getRelayRequestData (markRelayRequestFailed db) + where + getNextRequestGroupId :: IO (Maybe GroupId) + getNextRequestGroupId = + maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT group_id + FROM groups + WHERE relay_own_status = ? + AND relay_request_failed = 0 + AND relay_request_err_reason IS NULL + ORDER BY group_id ASC + LIMIT 1 + |] + (Only RSInvited) + getRelayRequestData :: GroupId -> IO (Either StoreError (GroupId, RelayRequestData)) + getRelayRequestData groupId = + firstRow' toRelayRequestData (SEGroupNotFound groupId) $ + DB.query + db + [sql| + SELECT + relay_request_inv_id, relay_request_group_link, + relay_request_peer_chat_min_version, relay_request_peer_chat_max_version + FROM groups + WHERE group_id = ? + |] + (Only groupId) + where + toRelayRequestData :: (Maybe InvitationId, Maybe ShortLinkContact, Maybe VersionChat, Maybe VersionChat) -> Either StoreError (GroupId, RelayRequestData) + toRelayRequestData = \case + (Just relayInvId, Just reqGroupLink, Just minV, Just maxV) -> + Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV}) + _ -> Left $ SEInternalError "missing relay request data" + +markRelayRequestFailed :: DB.Connection -> GroupId -> IO () +markRelayRequestFailed db groupId = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET relay_request_failed = 1, updated_at = ? WHERE group_id = ?" + (currentTs, groupId) + +setRelayRequestErr :: DB.Connection -> GroupId -> Text -> IO () +setRelayRequestErr db groupId errReason = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET relay_request_err_reason = ?, updated_at = ? WHERE group_id = ?" + (errReason, currentTs, groupId) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260109_chat_relays.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260109_chat_relays.hs index 677b373c0f..6ebfede31d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260109_chat_relays.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260109_chat_relays.hs @@ -5,39 +5,102 @@ module Simplex.Chat.Store.SQLite.Migrations.M20260109_chat_relays where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) +-- TODO [relays] TBC schema improvement - relay_link is duplicate on group_relays and group_members for owner +-- - chat_relays - user's list of chat relays to choose from (similar to protocol_servers) +-- - users.is_user_chat_relay - indicates that the user can serve as a chat relay +-- (TBC usage, e.g. agree to invitations to be relay) +-- - group_relays - group owner's list of relays for a group +-- - group_relays.relay_link - links for all relays of a group are included in GroupShortLinkData +-- - group_relays.relay_status - group owner's status for each relay (RelayStatus) +-- - group_relays.chat_relay_id - associates group_relays record with a chat_relays record, +-- chat_relays.deleted is to keep associated record if user removes chat relay from configuration, +-- but has group relays using it +-- - group_members.relay_link - relay link, saved on member record for user joining group +-- - groups.relay_own_status - indicates for a relay client that it is chat relay for the group (RelayStatus) +-- - groups.relay_request_* - relay request "work item" fields m20260109_chat_relays :: Query m20260109_chat_relays = [sql| CREATE TABLE chat_relays( chat_relay_id INTEGER PRIMARY KEY, - address TEXT NOT NULL, + address BLOB NOT NULL, name TEXT NOT NULL, domains TEXT NOT NULL, preset INTEGER NOT NULL DEFAULT 0, tested INTEGER, enabled INTEGER NOT NULL DEFAULT 1, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), - updated_at TEXT NOT NULL DEFAULT(datetime('now')), - UNIQUE(user_id, address), - UNIQUE(user_id, name) -); - + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id); +CREATE UNIQUE INDEX idx_chat_relays_user_id_address ON chat_relays(user_id, address); +CREATE UNIQUE INDEX idx_chat_relays_user_id_name ON chat_relays(user_id, name); ALTER TABLE users ADD COLUMN is_user_chat_relay INTEGER NOT NULL DEFAULT 0; -ALTER TABLE group_members ADD COLUMN is_chat_relay INTEGER NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN use_relays INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE groups ADD COLUMN creating_in_progress INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE groups ADD COLUMN relay_own_status TEXT; + +ALTER TABLE groups ADD COLUMN relay_request_inv_id BLOB; +ALTER TABLE groups ADD COLUMN relay_request_group_link BLOB; +ALTER TABLE groups ADD COLUMN relay_request_peer_chat_min_version INTEGER; +ALTER TABLE groups ADD COLUMN relay_request_peer_chat_max_version INTEGER; +ALTER TABLE groups ADD COLUMN relay_request_failed INTEGER DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_err_reason TEXT; + +ALTER TABLE group_profiles ADD COLUMN group_link BLOB; + +CREATE TABLE group_relays( + group_relay_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + chat_relay_id INTEGER NOT NULL REFERENCES chat_relays ON DELETE CASCADE, + relay_status TEXT NOT NULL, + relay_link BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; +CREATE INDEX idx_group_relays_group_id ON group_relays(group_id); +CREATE UNIQUE INDEX idx_group_relays_group_member_id ON group_relays(group_member_id); +CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); + +ALTER TABLE group_members ADD COLUMN relay_link BLOB; |] down_m20260109_chat_relays :: Query down_m20260109_chat_relays = [sql| -ALTER TABLE group_members DROP COLUMN is_chat_relay; - ALTER TABLE users DROP COLUMN is_user_chat_relay; -DROP INDEX idx_chat_relays_user_id; +ALTER TABLE groups DROP COLUMN use_relays; +ALTER TABLE groups DROP COLUMN creating_in_progress; + +ALTER TABLE groups DROP COLUMN relay_own_status; + +ALTER TABLE groups DROP COLUMN relay_request_inv_id; +ALTER TABLE groups DROP COLUMN relay_request_group_link; +ALTER TABLE groups DROP COLUMN relay_request_peer_chat_min_version; +ALTER TABLE groups DROP COLUMN relay_request_peer_chat_max_version; +ALTER TABLE groups DROP COLUMN relay_request_failed; +ALTER TABLE groups DROP COLUMN relay_request_err_reason; + +ALTER TABLE group_profiles DROP COLUMN group_link; + +DROP INDEX idx_group_relays_group_id; +DROP INDEX idx_group_relays_group_member_id; +DROP INDEX idx_group_relays_chat_relay_id; +DROP TABLE group_relays; + +DROP INDEX idx_chat_relays_user_id; +DROP INDEX idx_chat_relays_user_id_address; +DROP INDEX idx_chat_relays_user_id_name; DROP TABLE chat_relays; + +ALTER TABLE group_members DROP COLUMN relay_link; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 8f9df6aeb3..f5eae94ac2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -30,6 +30,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -82,6 +83,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -140,21 +142,22 @@ SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- from GroupMember - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts @@ -175,6 +178,21 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? + AND worker_scope = ? + AND job_scope_spec_tag IS NOT DISTINCT FROM ? + AND job_scope_include_pending IS NOT DISTINCT FROM ? + AND job_scope_support_gm_id IS NOT DISTINCT FROM ? + AND failed = 0 + AND task_status = ? + ORDER BY delivery_task_id ASC + +Plan: +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next_for_job_scope (group_id=? AND worker_scope=? AND job_scope_spec_tag=? AND job_scope_include_pending=? AND job_scope_support_gm_id=? AND failed=? AND task_status=?) + Query: SELECT delivery_task_id FROM delivery_tasks @@ -265,6 +283,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -299,6 +318,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -333,6 +353,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -490,6 +511,75 @@ Query: Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) +Query: + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + +Query: + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_profile_id, created_at, updated_at, relay_link + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + Query: INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, @@ -497,6 +587,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -531,6 +622,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -590,6 +682,16 @@ Query: Plan: SEARCH delivery_jobs USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT + relay_request_inv_id, relay_request_group_link, + relay_request_peer_chat_min_version, relay_request_peer_chat_max_version + FROM groups + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT COUNT(1) FROM ( @@ -864,7 +966,7 @@ Plan: SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? AND worker_scope=? AND failed=? AND task_status=?) Query: - SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.preferences, gp.member_admission + SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_link, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? @@ -873,6 +975,18 @@ Plan: SEARCH g USING INTEGER PRIMARY KEY (rowid=?) SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT group_id + FROM groups + WHERE relay_own_status = ? + AND relay_request_failed = 0 + AND relay_request_err_reason IS NULL + ORDER BY group_id ASC + LIMIT 1 + +Plan: +SCAN groups + Query: SELECT i.chat_item_id FROM chat_items i @@ -892,7 +1006,7 @@ Query: SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts @@ -1016,16 +1130,52 @@ Query: RETURNING chat_relay_id Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_chat_relay_id (chat_relay_id=?) + +Query: + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) Query: INSERT INTO group_members (group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, - member_restriction, is_chat_relay, invited_by, invited_by_group_member_id, + member_restriction, invited_by, invited_by_group_member_id, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1056,16 +1206,16 @@ Query: INSERT INTO groups (group_profile_id, local_display_name, user_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, - business_chat, business_member_id, customer_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + business_chat, business_member_id, customer_member_id, use_relays, relay_own_status) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO groups - (local_display_name, user_id, group_profile_id, enable_ntfs, + (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at) - VALUES (?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?) Plan: @@ -1121,7 +1271,7 @@ Query: i.forwarded_by_group_member_id, i.show_group_as_sender, -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -1129,20 +1279,18 @@ Query: ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, - rm.member_status, rm.show_messages, rm.member_restriction, rm.is_chat_relay, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, + rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, - dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.is_chat_relay, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, + dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN group_members gsm ON gsm.group_member_id = i.group_scope_group_member_id - LEFT JOIN contact_profiles gsp ON gsp.contact_profile_id = COALESCE(gsm.member_profile_id, gsm.contact_profile_id) LEFT JOIN group_members m ON m.group_member_id = i.group_member_id LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id @@ -1155,7 +1303,6 @@ Query: Plan: SEARCH i USING INTEGER PRIMARY KEY (rowid=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN -SEARCH gsm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH p USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH ri USING INDEX idx_chat_items_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) LEFT-JOIN @@ -1568,7 +1715,7 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_link = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups @@ -1580,6 +1727,26 @@ SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) LIST SUBQUERY 1 SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups + SET relay_request_inv_id = ?, + relay_request_group_link = ?, + relay_request_peer_chat_min_version = ?, + relay_request_peer_chat_max_version = ? + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, conn_status, conn_type, + group_member_id, conn_chat_version, to_subscribe, pq_support, pq_encryption, + created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + Query: INSERT INTO connections ( user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id, @@ -1613,6 +1780,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1639,6 +1807,13 @@ SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_mem SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) +Query: + INSERT INTO group_relays + (group_id, group_member_id, chat_relay_id, relay_status, created_at, updated_at) + VALUES (?,?,?,?,?,?) + +Plan: + Query: INSERT INTO msg_deliveries (message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts, created_at, updated_at, delivery_status) @@ -2237,7 +2412,7 @@ Query: ) ReportCount ON ReportCount.group_id = g.group_id JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND ( LOWER(g.local_display_name) LIKE '%' || ? || '%' OR LOWER(gp.display_name) LIKE '%' || ? || '%' @@ -2289,7 +2464,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.favorite = 1 OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) ORDER BY g.chat_ts DESC LIMIT ? @@ -2335,7 +2510,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: @@ -2380,7 +2555,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? Plan: @@ -2425,7 +2600,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) ORDER BY g.chat_ts DESC LIMIT ? Plan: @@ -2470,7 +2645,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.favorite = 1 AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: @@ -2515,7 +2690,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.favorite = 1 AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? Plan: @@ -2560,7 +2735,7 @@ Query: GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.favorite = 1 ORDER BY g.chat_ts DESC LIMIT ? Plan: @@ -2604,7 +2779,7 @@ Query: AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id>?) @@ -2646,7 +2821,7 @@ Query: AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? Plan: MATERIALIZE ChatStats SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id>?) @@ -2688,7 +2863,7 @@ Query: AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 GROUP BY group_id ) ReportCount ON ReportCount.group_id = g.group_id - WHERE g.user_id = ? ORDER BY g.chat_ts DESC LIMIT ? + WHERE g.user_id = ? AND g.creating_in_progress = 0 ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id>?) @@ -3065,6 +3240,21 @@ Query: Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=?) +Query: + SELECT EXISTS ( + SELECT 1 + FROM groups + WHERE relay_own_status = ? + AND relay_request_failed = 0 + AND relay_request_err_reason IS NULL + LIMIT 1 + ) + +Plan: +SCAN CONSTANT ROW +SCALAR SUBQUERY 1 +SCAN groups + Query: SELECT agent_conn_id FROM connections @@ -3230,7 +3420,15 @@ SEARCH chat_item_versions USING INDEX idx_chat_item_versions_chat_item_id (chat_ Query: SELECT chat_relay_id, address, name, domains, preset, tested, enabled FROM chat_relays - WHERE user_id = ? + WHERE user_id = ? AND chat_relay_id = ? AND deleted = 0 + +Plan: +SEARCH chat_relays USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT chat_relay_id, address, name, domains, preset, tested, enabled + FROM chat_relays + WHERE user_id = ? AND deleted = 0 Plan: SEARCH chat_relays USING INDEX idx_chat_relays_user_id (user_id=?) @@ -3364,6 +3562,30 @@ Query: Plan: SEARCH files USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT group_member_id + FROM group_members + WHERE group_id = ? + AND contact_id IS DISTINCT FROM ? + AND group_member_id IS DISTINCT FROM ? + AND member_status IN (?,?,?,?,?,?) + AND group_member_id > ? ORDER BY group_member_id ASC LIMIT ? +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id_index_in_group (group_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT group_member_id + FROM group_members + WHERE group_id = ? + AND contact_id IS DISTINCT FROM ? + AND group_member_id IS DISTINCT FROM ? + AND member_status IN (?,?,?,?,?,?) + ORDER BY group_member_id ASC LIMIT ? +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id_index_in_group (group_id=?) +USE TEMP B-TREE FOR ORDER BY + Query: SELECT group_member_id, group_snd_item_status, via_proxy FROM group_snd_item_statuses @@ -4664,6 +4886,14 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET relay_link = ?, updated_at = ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET show_messages = ?, updated_at = ? @@ -4699,6 +4929,14 @@ SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) LIST SUBQUERY 1 SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_relays + SET relay_link = ?, relay_status = ?, updated_at = ? + WHERE group_relay_id = ? + +Plan: +SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_snd_item_statuses SET group_snd_item_status = ?, updated_at = ? @@ -4858,15 +5096,16 @@ SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_me Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts @@ -4892,15 +5131,16 @@ SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts @@ -4919,15 +5159,16 @@ SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts @@ -4975,7 +5216,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5002,7 +5243,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5021,7 +5262,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5040,7 +5281,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5059,7 +5300,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5078,7 +5319,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5097,7 +5338,26 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.group_id = ? AND m.relay_link = ? +Plan: +SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5116,7 +5376,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5135,7 +5395,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -5152,6 +5412,25 @@ SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.user_id = ? AND m.group_id = ? AND m.contact_id IS DISTINCT FROM ? AND m.member_role = ? +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i @@ -5206,6 +5485,27 @@ Plan: SEARCH i USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) +Query: + SELECT group_relay_id, group_member_id, chat_relay_id, relay_status, relay_link + FROM group_relays + WHERE group_id = ? +Plan: +SEARCH group_relays USING INDEX idx_group_relays_group_id (group_id=?) + +Query: + SELECT group_relay_id, group_member_id, chat_relay_id, relay_status, relay_link + FROM group_relays + WHERE group_member_id = ? +Plan: +SEARCH group_relays USING INDEX idx_group_relays_group_member_id (group_member_id=?) + +Query: + SELECT group_relay_id, group_member_id, chat_relay_id, relay_status, relay_link + FROM group_relays + WHERE group_relay_id = ? +Plan: +SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT m.group_member_id, m.member_id, m.member_role, p.display_name, p.local_alias FROM group_members m @@ -5379,6 +5679,18 @@ SEARCH u USING INTEGER PRIMARY KEY (rowid=?) SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes, u.is_user_chat_relay + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + WHERE u.is_user_chat_relay = 1 +Plan: +SCAN u +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes, u.is_user_chat_relay @@ -5651,6 +5963,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) Query: DELETE FROM chat_relays WHERE user_id = ? AND chat_relay_id = ? AND preset = ? Plan: SEARCH chat_relays USING INTEGER PRIMARY KEY (rowid=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_chat_relay_id (chat_relay_id=?) Query: DELETE FROM commands WHERE user_id = ? AND command_id = ? Plan: @@ -5746,6 +6059,7 @@ SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=? Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? Plan: SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -5775,6 +6089,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -5804,6 +6119,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM groups WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_id (group_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_group_id (group_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_group_id (group_id=?) SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_group_id (group_id=?) @@ -6091,6 +6407,18 @@ SCAN CONSTANT ROW SCALAR SUBQUERY 1 SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +Query: SELECT EXISTS (SELECT 1 FROM group_members WHERE group_id = ? AND member_id = ?) +Plan: +SCAN CONSTANT ROW +SCALAR SUBQUERY 1 +SEARCH group_members USING COVERING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) + +Query: SELECT EXISTS (SELECT 1 FROM group_relays WHERE chat_relay_id = ?) +Plan: +SCAN CONSTANT ROW +SCALAR SUBQUERY 1 +SEARCH group_relays USING COVERING INDEX idx_group_relays_chat_relay_id (chat_relay_id=?) + Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ? Plan: SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_conditions_commit (conditions_commit=? AND server_operator_id=?) @@ -6223,6 +6551,10 @@ Query: SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0 OR ch Plan: SCAN groups +Query: SELECT group_id FROM groups WHERE user_id = ? AND creating_in_progress = 1 AND created_at <= ? +Plan: +SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) + Query: SELECT group_id FROM groups WHERE user_id = ? AND local_display_name = ? Plan: SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) @@ -6283,6 +6615,14 @@ Query: SELECT quota_err_counter FROM connections WHERE user_id = ? AND connectio Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT relay_own_status FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT relay_status FROM group_relays WHERE group_relay_id = ? +Plan: +SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6435,6 +6775,10 @@ Query: UPDATE contacts SET xcontact_id = ? WHERE contact_id = ? Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE delivery_jobs SET cursor_group_member_id = ?, updated_at = ? WHERE delivery_job_id = ? +Plan: +SEARCH delivery_jobs USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE delivery_jobs SET job_status = ?, job_err_reason = ?, updated_at = ? WHERE delivery_job_id = ? Plan: SEARCH delivery_jobs USING INTEGER PRIMARY KEY (rowid=?) @@ -6519,6 +6863,10 @@ Query: UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE g Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ? +Plan: +SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -6539,6 +6887,10 @@ Query: UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -6555,6 +6907,10 @@ Query: UPDATE groups SET members_require_attention=1 WHERE group_id=? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET relay_own_status = ?, 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=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 45da1dff77..82f7a3b38f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -122,7 +122,8 @@ CREATE TABLE group_profiles( preferences TEXT, description TEXT NULL, member_admission TEXT, - short_descr TEXT + short_descr TEXT, + group_link BLOB ) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -157,7 +158,16 @@ CREATE TABLE groups( conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0, via_group_link_uri BLOB, summary_current_members_count INTEGER NOT NULL DEFAULT 0, - member_index INTEGER NOT NULL DEFAULT 0, -- received + member_index INTEGER NOT NULL DEFAULT 0, + use_relays INTEGER NOT NULL DEFAULT 0, + creating_in_progress INTEGER NOT NULL DEFAULT 0, + relay_own_status TEXT, + relay_request_inv_id BLOB, + relay_request_group_link BLOB, + relay_request_peer_chat_min_version INTEGER, + relay_request_peer_chat_max_version INTEGER, + relay_request_failed INTEGER DEFAULT 0, + relay_request_err_reason TEXT, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -199,7 +209,7 @@ CREATE TABLE group_members( member_welcome_shared_msg_id BLOB, index_in_group INTEGER NOT NULL DEFAULT 0, member_relations_vector BLOB, - is_chat_relay INTEGER NOT NULL DEFAULT 0, + relay_link BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -727,18 +737,27 @@ CREATE TABLE connections_sync( ) STRICT; CREATE TABLE chat_relays( chat_relay_id INTEGER PRIMARY KEY, - address TEXT NOT NULL, + address BLOB NOT NULL, name TEXT NOT NULL, domains TEXT NOT NULL, preset INTEGER NOT NULL DEFAULT 0, tested INTEGER, enabled INTEGER NOT NULL DEFAULT 1, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), - updated_at TEXT NOT NULL DEFAULT(datetime('now')), - UNIQUE(user_id, address), - UNIQUE(user_id, name) -); + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; +CREATE TABLE group_relays( + group_relay_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + chat_relay_id INTEGER NOT NULL REFERENCES chat_relays ON DELETE CASCADE, + relay_status TEXT NOT NULL, + relay_link BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -1216,6 +1235,16 @@ CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items created_at ); CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id); +CREATE UNIQUE INDEX idx_chat_relays_user_id_address ON chat_relays( + user_id, + address +); +CREATE UNIQUE INDEX idx_chat_relays_user_id_name ON chat_relays(user_id, name); +CREATE INDEX idx_group_relays_group_id ON group_relays(group_id); +CREATE UNIQUE INDEX idx_group_relays_group_member_id ON group_relays( + group_member_id +); +CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 7f4a338376..15c2c4d41c 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -75,6 +75,7 @@ data ChatLockEntity data StoreError = SEDuplicateName | SEUserNotFound {userId :: UserId} + | SERelayUserNotFound | SEUserNotFoundByName {contactName :: ContactName} | SEUserNotFoundByContactId {contactId :: ContactId} | SEUserNotFoundByGroupId {groupId :: GroupId} @@ -102,6 +103,7 @@ data StoreError | SEInvalidMemberRelationUpdate | SEGroupWithoutUser | SEDuplicateGroupMember + | SEDuplicateMemberId | SEGroupAlreadyJoined | SEGroupInvitationNotFound | SENoteFolderAlreadyExists {noteFolderId :: NoteFolderId} @@ -150,6 +152,9 @@ data StoreError | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} | SEOperatorNotFound {serverOperatorId :: Int64} | SEUsageConditionsNotFound + | SEUserChatRelayNotFound {chatRelayId :: Int64} + | SEGroupRelayNotFound {groupRelayId :: Int64} + | SEGroupRelayNotFoundByMemberId {groupMemberId :: GroupMemberId} | SEInvalidQuote | SEInvalidMention | SEInvalidDeliveryTask {taskId :: Int64} @@ -657,22 +662,22 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe ShortLinkContact) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow -type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus, BoolInt) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) +type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (uiThemes, currentMembers, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupLink) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} + groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers} - in GroupInfo {groupId, useRelays = BoolDef False, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri} + in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case @@ -681,14 +686,13 @@ toPreparedGroup = \case _ -> Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_, BI isCRelay) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) = +toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) = let memberProfile = rowToLocalProfile profileRow memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - isChatRelay = BoolDef isCRelay supportChat = case supportChatTs_ of Just chatTs -> Just @@ -706,7 +710,7 @@ groupMemberQuery :: Query groupMemberQuery = [sql| SELECT - m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.is_chat_relay, + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, @@ -739,15 +743,16 @@ groupInfoQueryFields = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_link, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, + g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.is_chat_relay, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts @@ -835,6 +840,37 @@ addGroupChatTags db g@GroupInfo {groupId} = do chatTags <- getGroupChatTags db groupId pure (g :: GroupInfo) {chatTags} +getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo +getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do + chatTags <- getGroupChatTags db groupId + firstRow (toGroupInfo vr userContactId chatTags) (SEGroupNotFound groupId) $ + DB.query + db + (groupInfoQuery <> " WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?") + (groupId, userId, userContactId) + +-- Set group link info and optionally incognito profile before connecting to relays. +-- This is called once before connecting to relays, unlike createConnReqConnection -> setPreparedGroupLinkInfo_, +-- which is used in single-connection flows. +setPreparedGroupLinkInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Profile -> ExceptT StoreError IO GroupInfo +setPreparedGroupLinkInfo db vr user@User {userId} gInfo@GroupInfo {groupId} cReq cReqHash incognitoProfile = do + currentTs <- liftIO getCurrentTime + customUserProfileId <- liftIO $ mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile + liftIO $ setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId currentTs + getGroupInfo db vr user groupId + +setPreparedGroupLinkInfo_ :: DB.Connection -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Int64 -> UTCTime -> IO () +setPreparedGroupLinkInfo_ db GroupInfo {groupId, membership} cReq cReqHash customUserProfileId currentTs = do + DB.execute + db + "UPDATE groups SET via_group_link_uri = ?, via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ?" + (cReq, cReqHash, BI True, currentTs, groupId) + when (isJust customUserProfileId) $ + DB.execute + db + "UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE group_member_id = ?" + (customUserProfileId, currentTs, groupMemberId' membership) + setViaGroupLinkUri :: DB.Connection -> GroupId -> Int64 -> IO () setViaGroupLinkUri db groupId connId = do r <- diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 67b6779bdd..28896329f9 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -119,7 +119,7 @@ instance ToField AgentUserId where toField (AgentUserId uId) = toField uId aUserId :: User -> UserId aUserId User {agentUserId = AgentUserId uId} = uId --- TODO [chat relay] filter out chat relay users where necessary (e.g. loading list of users for UI) +-- TODO [relays] filter out chat relay users where necessary (e.g. loading list of users for UI) data User = User { userId :: UserId, agentUserId :: AgentUserId, @@ -450,6 +450,7 @@ type GroupId = Int64 data GroupInfo = GroupInfo { groupId :: GroupId, useRelays :: BoolDef, + relayOwnStatus :: Maybe RelayStatus, -- status of the relay itself related to the group localDisplayName :: GroupName, groupProfile :: GroupProfile, localAlias :: Text, @@ -472,6 +473,9 @@ data GroupInfo = GroupInfo } deriving (Eq, Show) +useRelays' :: GroupInfo -> Bool +useRelays' GroupInfo {useRelays} = isTrue useRelays + data BusinessChatType = BCBusiness -- used on the customer side | BCCustomer -- used on the business side @@ -732,6 +736,7 @@ data GroupProfile = GroupProfile shortDescr :: Maybe Text, -- short description limited to 160 characters description :: Maybe Text, -- this has been repurposed as welcome message image :: Maybe ImageData, + groupLink :: Maybe ShortLinkContact, groupPreferences :: Maybe GroupPreferences, memberAdmission :: Maybe GroupMemberAdmission } @@ -814,6 +819,15 @@ data GroupLinkRejection = GroupLinkRejection } deriving (Eq, Show) +-- sent by owner to relay when adding it to group +data GroupRelayInvitation = GroupRelayInvitation + { fromMember :: MemberIdRole, + fromMemberProfile :: Profile, + relayMemberId :: MemberId, + groupLink :: ShortLinkContact + } + deriving (Eq, Show) + data GroupRejectionReason = GRRLongName | GRRBlockedName @@ -949,11 +963,57 @@ data GroupMember = GroupMember memberChatVRange :: VersionRangeChat, createdAt :: UTCTime, updatedAt :: UTCTime, - supportChat :: Maybe GroupSupportChat, - isChatRelay :: BoolDef + supportChat :: Maybe GroupSupportChat } deriving (Eq, Show) +data GroupRelay = GroupRelay + { groupRelayId :: Int64, + groupMemberId :: GroupMemberId, + userChatRelayId :: Int64, -- ID of configured UserChatRelay + relayStatus :: RelayStatus, + relayLink :: Maybe ShortLinkContact + } + deriving (Eq, Show) + +data RelayStatus + = RSNew -- only for owner + | RSInvited + | RSAccepted + | RSActive + deriving (Eq, Show) + +data RelayRequestData = RelayRequestData + { relayInvId :: InvitationId, + reqGroupLink :: ShortLinkContact, + reqChatVRange :: VersionRangeChat + } + deriving (Eq, Show) + +relayStatusText :: RelayStatus -> Text +relayStatusText = \case + RSNew -> "new" + RSInvited -> "invited" + RSAccepted -> "accepted" + RSActive -> "active" + +instance TextEncoding RelayStatus where + textEncode = \case + RSNew -> "new" + RSInvited -> "invited" + RSAccepted -> "accepted" + RSActive -> "active" + textDecode = \case + "new" -> Just RSNew + "invited" -> Just RSInvited + "accepted" -> Just RSAccepted + "active" -> Just RSActive + _ -> Nothing + +instance FromField RelayStatus where fromField = fromTextField_ textDecode + +instance ToField RelayStatus where toField = toField . textEncode + data GroupSupportChat = GroupSupportChat { chatTs :: UTCTime, unread :: Int64, @@ -977,10 +1037,11 @@ groupMemberRef :: GroupMember -> GroupMemberRef groupMemberRef GroupMember {groupMemberId, memberProfile = p} = GroupMemberRef {groupMemberId, profile = fromLocalProfile p} --- TODO [channels fwd] knowledge whether member is a relay should come from protocol, not implicitly via role --- TODO - in channels members should directly connect only to relays -isMemberRelay :: GroupMember -> Bool -isMemberRelay GroupMember {memberRole} = memberRole == GRAdmin +isRelay :: GroupMember -> Bool +isRelay m = memberRole' m == GRRelay + +memberRole' :: GroupMember -> GroupMemberRole +memberRole' GroupMember {memberRole} = memberRole memberConn :: GroupMember -> Maybe Connection memberConn GroupMember {activeConn} = activeConn @@ -1032,8 +1093,7 @@ data NewGroupMember = NewGroupMember memInvitedByGroupMemberId :: Maybe GroupMemberId, localDisplayName :: ContactName, memProfileId :: Int64, - memContactId :: Maybe Int64, - isChatRelay :: Bool + memContactId :: Maybe Int64 } newtype MemberId = MemberId {unMemberId :: ByteString} @@ -1055,7 +1115,10 @@ instance ToJSON MemberId where toEncoding = strToJEncoding nameFromMemberId :: MemberId -> ContactName -nameFromMemberId = T.take 7 . safeDecodeUtf8 . B64.encode . unMemberId +nameFromMemberId = nameFromBS . unMemberId + +nameFromBS :: ByteString -> ContactName +nameFromBS = T.take 7 . safeDecodeUtf8 . B64.encode data InvitedBy = IBContact {byContactId :: Int64} | IBUser | IBUnknown deriving (Eq, Show) @@ -1790,6 +1853,8 @@ data CommandFunction | CFAcceptContact | CFAckMessage -- not used | CFDeleteConn -- not used + | CFSetShortLink + | CFGetShortLink deriving (Eq, Show) instance FromField CommandFunction where fromField = fromTextField_ textDecode @@ -1807,6 +1872,8 @@ instance TextEncoding CommandFunction where "accept_contact" -> Just CFAcceptContact "ack_message" -> Just CFAckMessage "delete_conn" -> Just CFDeleteConn + "set_short_link" -> Just CFSetShortLink + "get_short_link" -> Just CFGetShortLink _ -> Nothing textEncode = \case CFCreateConnGrpMemInv -> "create_conn" @@ -1818,6 +1885,8 @@ instance TextEncoding CommandFunction where CFAcceptContact -> "accept_contact" CFAckMessage -> "ack_message" CFDeleteConn -> "delete_conn" + CFSetShortLink -> "set_short_link" + CFGetShortLink -> "get_short_link" commandExpectedResponse :: CommandFunction -> AEvtTag commandExpectedResponse = \case @@ -1830,6 +1899,8 @@ commandExpectedResponse = \case CFAcceptContact -> t JOINED_ CFAckMessage -> t OK_ CFDeleteConn -> t OK_ + CFSetShortLink -> t LINK_ + CFGetShortLink -> t LDATA_ where t = AEvtTag SAEConn @@ -1946,6 +2017,10 @@ $(JQ.deriveJSON defaultJSON ''PendingContactConnection) $(JQ.deriveJSON defaultJSON ''GroupSupportChat) +$(JQ.deriveJSON (enumJSON $ dropPrefix "RS") ''RelayStatus) + +$(JQ.deriveJSON defaultJSON ''GroupRelay) + $(JQ.deriveJSON defaultJSON ''GroupMember) $(JQ.deriveJSON (enumJSON $ dropPrefix "MF") ''MsgFilter) @@ -1986,6 +2061,8 @@ $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkRejection) +$(JQ.deriveJSON defaultJSON ''GroupRelayInvitation) + $(JQ.deriveJSON defaultJSON ''IntroInvitation) $(JQ.deriveJSON defaultJSON ''MemberRestrictions) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 280fc32ea4..fafac46da8 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -6,13 +6,16 @@ module Simplex.Chat.Types.Shared where import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B +import Data.Text (Text) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$?>)) data GroupMemberRole - = GRObserver -- connects to all group members and receives all messages, can't send messages + = GRUnknown Text -- unknown role from a newer client + | GRRelay -- chat relay: forwards messages, can't send its own messages + | GRObserver -- connects to all group members and receives all messages, can't send messages | GRAuthor -- reserved, unused | GRMember -- + can send messages to all group members | GRModerator -- + moderate messages and block members (excl. Admins and Owners) @@ -32,14 +35,17 @@ instance TextEncoding GroupMemberRole where GRMember -> "member" GRAuthor -> "author" GRObserver -> "observer" - textDecode = \case - "owner" -> Just GROwner - "admin" -> Just GRAdmin - "moderator" -> Just GRModerator - "member" -> Just GRMember - "author" -> Just GRAuthor - "observer" -> Just GRObserver - r -> Nothing + GRRelay -> "relay" + GRUnknown t -> t + textDecode = Just . \case + "owner" -> GROwner + "admin" -> GRAdmin + "moderator" -> GRModerator + "member" -> GRMember + "author" -> GRAuthor + "observer" -> GRObserver + "relay" -> GRRelay + t -> GRUnknown t instance FromJSON GroupMemberRole where parseJSON = textParseJSON "GroupMemberRole" diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 897d21ce1d..4192bf5f7f 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -178,6 +178,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRUserContactLinkUpdated u UserContactLink {addressSettings} -> ttyUser u $ viewAddressSettings addressSettings 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 CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms -- CRGroupConversationsArchived u _g _conversations -> ttyUser u [] @@ -417,7 +418,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g CEvtContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CEvtBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] - CEvtGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CEvtGroupLinkConnecting u g m -> ttyUser u $ viewUserJoiningGroup g m CEvtBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CEvtUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] CEvtUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] @@ -459,7 +460,8 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} in ttyUser u [sShow connId <> ": END"] CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv] CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r - CEvtUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g + CEvtUserJoinedGroup u g m -> ttyUser u $ viewUserJoinedGroup g m + CEvtGroupLinkRelaysUpdated u g groupLink relays -> ttyUser u $ viewGroupLinkRelaysUpdated g groupLink relays CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m CEvtHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] CEvtHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] @@ -1149,6 +1151,20 @@ viewReceivedContactRequest c Profile {fullName, shortDescr} = "to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)" ] +viewGroupLinkRelaysUpdated :: GroupInfo -> GroupLink -> [GroupRelay] -> [StyledString] +viewGroupLinkRelaysUpdated g groupLink relays = + [ttyFullGroup g <> ": group link relays updated, current relays:"] + <> map showRelay relays + <> + [ "group link:", + plain $ maybe cReqStr strEncode shortLink + ] + where + showRelay GroupRelay {groupRelayId, relayStatus} = + " - relay id " <> sShow groupRelayId <> ": " <> plain (relayStatusText relayStatus) + GroupLink {connLinkContact = CCLink cReq shortLink} = groupLink + cReqStr = strEncode $ simplexChatContact cReq + viewGroupCreated :: GroupInfo -> Bool -> [StyledString] viewGroupCreated g testView = case incognitoMembershipProfile g of @@ -1159,12 +1175,21 @@ viewGroupCreated g testView = profile = fromLocalProfile localProfile message = [ "group " <> ttyFullGroup g <> " is created, your incognito profile for this group is " <> incognitoProfile' profile, - "to add members use " <> highlight ("/create link #" <> viewGroupName g) + instruction ] + instruction + | useRelays' g = relaysInstruction + | otherwise = "to add members use " <> highlight ("/create link #" <> viewGroupName g) Nothing -> [ "group " <> ttyFullGroup g <> " is created", - "to add members use " <> highlight ("/a " <> viewGroupName g <> " ") <> " or " <> highlight ("/create link #" <> viewGroupName g) + instruction ] + where + instruction + | useRelays' g = relaysInstruction + | otherwise = "to add members use " <> highlight ("/a " <> viewGroupName g <> " ") <> " or " <> highlight ("/create link #" <> viewGroupName g) + where + relaysInstruction = "wait for selected relay(s) to join, then you can invite members via group link" viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation g c = @@ -1176,12 +1201,22 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString] 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)"] -viewUserJoinedGroup :: GroupInfo -> [StyledString] -viewUserJoinedGroup g@GroupInfo {membership} = - case incognitoMembershipProfile g of - Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp) <> pendingApproval_] - Nothing -> [ttyGroup' g <> ": you joined the group" <> pendingApproval_] +viewUserJoiningGroup :: GroupInfo -> GroupMember -> [StyledString] +viewUserJoiningGroup g m + | isRelay m = [ttyGroup' g <> ": joining the group (connecting to relay " <> ttyMember m <> ")..."] + | otherwise = [ttyGroup' g <> ": joining the group..."] + +viewUserJoinedGroup :: GroupInfo -> GroupMember -> [StyledString] +viewUserJoinedGroup g@GroupInfo {membership} m + | isRelay membership = [ttyGroup' g <> ": you joined the group as relay"] + | otherwise = + case incognitoMembershipProfile g of + Just mp -> [ttyGroup' g <> ": you joined the group" <> connectedToRelay_ <> " incognito as " <> incognitoProfile' (fromLocalProfile mp) <> pendingApproval_] + Nothing -> [ttyGroup' g <> ": you joined the group" <> connectedToRelay_ <> pendingApproval_] where + connectedToRelay_ + | isRelay m = " (connected to relay " <> ttyMember m <> ")" + | otherwise = "" pendingApproval_ = case memberStatus membership of GSMemPendingApproval -> ", pending approval" GSMemPendingReview -> ", connecting to group moderators for admission to group" @@ -1990,7 +2025,9 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk groupSLinkData -> [grpLink "ok to connect"] <> [viewJSON groupSLinkData | testView] + GLPOk direct groupSLinkData -> + [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] + <> [viewJSON groupSLinkData] -- | testView] -- TODO [relays] disable link data output in cli (uncomment testView) GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 4722a0b299..7738e1ef9b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -53,7 +53,7 @@ import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion, sndAuthKeySMPClientVersion) +import Simplex.Messaging.Protocol (sndAuthKeySMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) diff --git a/tests/ChatTests/ChatRelays.hs b/tests/ChatTests/ChatRelays.hs index 3db731e261..2a45f79a73 100644 --- a/tests/ChatTests/ChatRelays.hs +++ b/tests/ChatTests/ChatRelays.hs @@ -5,6 +5,7 @@ import ChatTests.DBUtils import ChatTests.Utils import Test.Hspec hiding (it) +-- TODO [relays] test deleting relay (from configuration), referenced in group_relays. chatRelayTests :: SpecWith TestParams chatRelayTests = do describe "configure chat relays" $ do diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 16cf968437..fd7d3ae8c7 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -37,7 +37,6 @@ import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.DB (Binary (..)) -import Simplex.Messaging.Encoding.String import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport import Simplex.Messaging.Version @@ -229,19 +228,25 @@ chatGroupTests = do it "should remove support chat with member when member is removed" testScopedSupportMemberRemoved it "should remove support chat with member when user removes member" testScopedSupportUserRemovesMember it "should remove support chat with member when member leaves" testScopedSupportMemberLeaves - -- TODO [channels fwd] enable tests (requires communicating useRelays to members) -- TODO [channels fwd] add tests for channels - -- TODO - tests with multiple relays (all relays should deliver messages, members should deduplicate) -- TODO - tests with delivery loop over members restored after restart -- TODO - delivery in support scopes inside channels - xdescribe "channels" $ do + -- TODO - connect plans for relay groups + -- TODO - cancellation on failure to create relay group (for owner) + -- TODO - async retry connecting to relay (for members) + -- TODO - test relay privileges + describe "channels" $ do describe "relay delivery" $ do - it "should deliver messages to members" testChannelsRelayDeliver - describe "should deliver messages in a loop over members" $ do - it "number of recipients is multiple of bucket size (3/1)" (testChannelsRelayDeliverLoop 1) - it "number of recipients is NOT multiple of bucket size (3/2)" (testChannelsRelayDeliverLoop 2) - it "number of recipients is equal to bucket size (3/3)" (testChannelsRelayDeliverLoop 3) - it "sender should deduplicate their own messages" testChannelsSenderDeduplicateOwn + describe "single relay" $ do + it "should deliver messages to members" testChannels1RelayDeliver + describe "should deliver messages in a loop over members" $ do + it "number of recipients is multiple of bucket size (3/1)" (testChannels1RelayDeliverLoop 1) + it "number of recipients is NOT multiple of bucket size (3/2)" (testChannels1RelayDeliverLoop 2) + it "number of recipients is equal to bucket size (3/3)" (testChannels1RelayDeliverLoop 3) + it "sender should deduplicate their own messages" testChannelsSenderDeduplicateOwn + describe "multiple relays" $ do + it "2 relays: should deliver messages to members" testChannels2RelaysDeliver + it "should share same incognito profile with all relays" testChannels2RelaysIncognito testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = @@ -2495,12 +2500,12 @@ testPlanGroupLinkLeaveRejoin = threadDelay 100000 bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" _sLinkData <- getTermLine bob let gLinkSchema2 = linkAnotherSchema gLink bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" _sLinkData <- getTermLine bob bob ##> ("/c " <> gLink) @@ -3531,7 +3536,7 @@ testPlanGroupLinkKnown = gLink <- getGroupLink alice "team" GRMember True bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" _sLinkData <- getTermLine bob bob ##> ("/c " <> gLink) @@ -8356,107 +8361,198 @@ testScopedSupportMemberLeaves = testOpts { markRead = False } -testChannelsRelayDeliver :: HasCallStack => TestParams -> IO () -testChannelsRelayDeliver = - testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $ \alice bob cath dan eve -> do - createChannel5 alice bob cath dan eve GRObserver - alice #> "#team hi" - bob <# "#team alice> hi" - [cath, dan, eve] *<# "#team alice> hi [>>]" +testChannels1RelayDeliver :: HasCallStack => TestParams -> IO () +testChannels1RelayDeliver ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve - cath ##> "+1 #team hi" - cath <## "added 👍" - bob <# "#team cath> > alice hi" - bob <## " + 👍" - alice <# "#team cath> > alice hi" - alice <## " + 👍" - dan <# "#team cath> > alice hi" - dan <## " + 👍" - eve <# "#team cath> > alice hi" - eve <## " + 👍" + alice #> "#team hi" + bob <# "#team alice> hi" + [cath, dan, eve] *<# "#team alice> hi [>>]" + + cath ##> "+1 #team hi" + cath <## "added 👍" + bob <# "#team cath> > alice hi" + bob <## " + 👍" + alice <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + alice <# "#team cath> > alice hi" + alice <## " + 👍" + dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <# "#team cath> > alice hi" + dan <## " + 👍" + eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <# "#team cath> > alice hi" + eve <## " + 👍" + +createChannel1Relay :: String -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO () +createChannel1Relay gName owner relay cath dan eve = do + (shortLink, fullLink) <- prepareChannel1Relay gName owner relay + forM_ [cath, dan, eve] $ \member -> + memberJoinChannel gName [relay] shortLink fullLink member + +prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) +prepareChannel1Relay gName owner relay = do + rName <- userName relay + + relay ##> "/ad" + (relaySLink, _cLink) <- getContactLinks relay True + + owner ##> ("/relays name=" <> rName <> " " <> relaySLink) + owner <## "ok" + + owner ##> ("/public group relays=1 #" <> gName) + owner <## ("group #" <> gName <> " is created") + owner <## "wait for selected relay(s) to join, then you can invite members via group link" --- TODO [channels fwd] correctly setup channel with relay forwarding --- TODO - alice to create group as channel --- TODO - add bob as relay --- TODO - alice to manage group link, but members to connect to relay (bob) -createChannel5 :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO () -createChannel5 alice bob cath dan eve mRole = do - createGroup2 "team" alice bob - bob ##> ("/create link #team " <> T.unpack (textEncode mRole)) - gLink <- getGroupLink bob "team" mRole True - cath ##> ("/c " <> gLink) - cath <## "connection request sent!" - bob <## "cath (Catherine): accepting request to join group #team..." concurrentlyN_ - [ bob <## "#team: cath joined the group", - do - cath <## "#team: joining the group..." - cath <## "#team: you joined the group" - cath <## "#team: member alice (Alice) is connected", - do - alice <## "#team: bob added cath (Catherine) to the group (connecting...)" - alice <## "#team: new member cath is connected" - ] - dan ##> ("/c " <> gLink) - dan <## "connection request sent!" - bob <## "dan (Daniel): accepting request to join group #team..." - concurrentlyN_ - [ bob <## "#team: dan joined the group", - do - dan <## "#team: joining the group..." - dan <## "#team: you joined the group" - dan <## "#team: member alice (Alice) is connected" - dan <## "#team: member cath (Catherine) is connected", - do - alice <## "#team: bob added dan (Daniel) to the group (connecting...)" - alice <## "#team: new member dan is connected", - do - cath <## "#team: bob added dan (Daniel) to the group (connecting...)" - cath <## "#team: new member dan is connected" - ] - eve ##> ("/c " <> gLink) - eve <## "connection request sent!" - bob <## "eve (Eve): accepting request to join group #team..." - concurrentlyN_ - [ bob <## "#team: eve joined the group", - eve - <### [ "#team: joining the group...", - "#team: you joined the group", - "#team: member alice (Alice) is connected", - "#team: member cath (Catherine) is connected", - "#team: member dan (Daniel) is connected" - ], - do - alice <## "#team: bob added eve (Eve) to the group (connecting...)" - alice <## "#team: new member eve is connected", - do - cath <## "#team: bob added eve (Eve) to the group (connecting...)" - cath <## "#team: new member eve is connected", - do - dan <## "#team: bob added eve (Eve) to the group (connecting...)" - dan <## "#team: new member eve is connected" + [ do + owner <## ("#" <> gName <> ": group link relays updated, current relays:") + owner <## " - relay id 1: active" + owner <## "group link:" + _ <- getTermLine owner + pure (), + relay <## ("#" <> gName <> ": you joined the group as relay") ] -testChannelsRelayDeliverLoop :: HasCallStack => Int -> TestParams -> IO () -testChannelsRelayDeliverLoop deliveryBucketSize = - testChatCfg5 cfg aliceProfile bobProfile cathProfile danProfile eveProfile $ \alice bob cath dan eve -> do - createChannel5 alice bob cath dan eve GRObserver + owner ##> ("/show link #" <> gName) + getGroupLinks owner gName GRMember False - alice #> "#team hi" - bob <# "#team alice> hi" - [cath, dan, eve] *<# "#team alice> hi [>>]" +createChannel2Relays :: String -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO () +createChannel2Relays gName owner relay1 relay2 dan eve frank = do + (shortLink, fullLink) <- prepareChannel2Relays gName owner relay1 relay2 + forM_ [dan, eve, frank] $ \member -> + memberJoinChannel gName [relay1, relay2] shortLink fullLink member - cath ##> "+1 #team hi" - cath <## "added 👍" - bob <# "#team cath> > alice hi" - bob <## " + 👍" - alice <# "#team cath> > alice hi" - alice <## " + 👍" - dan <# "#team cath> > alice hi" - dan <## " + 👍" - eve <# "#team cath> > alice hi" - eve <## " + 👍" +prepareChannel2Relays :: String -> TestCC -> TestCC -> TestCC -> IO (String, String) +prepareChannel2Relays gName owner relay1 relay2 = do + r1Name <- userName relay1 + r2Name <- userName relay2 + + relay1 ##> "/ad" + (r1SLink, _cLink) <- getContactLinks relay1 True + relay2 ##> "/ad" + (r2SLink, _cLink) <- getContactLinks relay2 True + + owner ##> ("/relays name=" <> r1Name <> " " <> r1SLink <> " name=" <> r2Name <> " " <> r2SLink) + owner <## "ok" + + owner ##> ("/public group relays=1,2 #" <> gName) + owner <## ("group #" <> gName <> " is created") + owner <## "wait for selected relay(s) to join, then you can invite members via group link" + + concurrentlyN_ + [ owner + <### [ -- one relay connects + ConsoleString ("#" <> gName <> ": group link relays updated, current relays:"), + StartsWith " - relay id 1: ", + StartsWith " - relay id 2: ", + "group link:", + Predicate (const True), -- consume group link line + -- second relay connects + ConsoleString ("#" <> gName <> ": group link relays updated, current relays:"), + " - relay id 1: active", + " - relay id 2: active", + "group link:", + Predicate (const True) -- consume group link line + ], + relay1 <## ("#" <> gName <> ": you joined the group as relay"), + relay2 <## ("#" <> gName <> ": you joined the group as relay") + ] + + owner ##> ("/show link #" <> gName) + getGroupLinks owner gName GRMember False + +memberJoinChannel :: String -> [TestCC] -> String -> String -> TestCC -> IO () +memberJoinChannel gName relays shortLink fullLink member = do + mName <- userName member + mFullName <- showName member + relayNames <- mapM userName relays + + member ##> ("/_connect plan 1 " <> shortLink) + member <## "group link: ok to connect via relays" + groupSLinkData <- getTermLine member + + member ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " direct=off " <> groupSLinkData) + member <## ("#" <> gName <> ": group is prepared") + + member ##> "/_connect group #1" + member <## ("#" <> gName <> ": connection started") + concurrentlyN_ $ + [ member + <### concat + [ [ ConsoleString ("#" <> gName <> ": joining the group (connecting to relay " <> rName <> ")..."), + ConsoleString ("#" <> gName <> ": you joined the group (connected to relay " <> rName <> ")") + ] + | rName <- relayNames + ] + ] + <> [ do + relay <## (mFullName <> ": accepting request to join group #team...") + relay <## ("#" <> gName <> ": " <> mName <> " joined the group") + | relay <- relays + ] + +memberJoinChannelIncognito :: String -> [TestCC] -> String -> String -> TestCC -> IO String +memberJoinChannelIncognito gName relays shortLink fullLink member = do + relayNames <- mapM userName relays + + member ##> ("/_connect plan 1 " <> shortLink) + member <## "group link: ok to connect via relays" + groupSLinkData <- getTermLine member + + member ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " direct=off " <> groupSLinkData) + member <## ("#" <> gName <> ": group is prepared") + + member ##> "/_connect group #1 incognito=on" + memIncognito <- getTermLine member + member <## ("#" <> gName <> ": connection started incognito") + concurrentlyN_ $ + [ member + <### concat + [ [ ConsoleString ("#" <> gName <> ": joining the group (connecting to relay " <> rName <> ")..."), + ConsoleString ("#" <> gName <> ": you joined the group (connected to relay " <> rName <> ") incognito as " <> memIncognito) + ] + | rName <- relayNames + ] + ] + <> [ do + relay <## (memIncognito <> ": accepting request to join group #team...") + relay <## ("#" <> gName <> ": " <> memIncognito <> " joined the group") + | relay <- relays + ] + pure memIncognito + +testChannels1RelayDeliverLoop :: HasCallStack => Int -> TestParams -> IO () +testChannels1RelayDeliverLoop deliveryBucketSize ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + + alice #> "#team hi" + bob <# "#team alice> hi" + [cath, dan, eve] *<# "#team alice> hi [>>]" + + cath ##> "+1 #team hi" + cath <## "added 👍" + bob <# "#team cath> > alice hi" + bob <## " + 👍" + alice <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + alice <# "#team cath> > alice hi" + alice <## " + 👍" + dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <# "#team cath> > alice hi" + dan <## " + 👍" + eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <# "#team cath> > alice hi" + eve <## " + 👍" where cfg = testCfg {deliveryBucketSize} @@ -8466,8 +8562,8 @@ testChannelsSenderDeduplicateOwn ps = do withNewTestChat ps "cath" cathProfile $ \cath -> withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> do - withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> - createChannel5 alice bob cath dan eve GRMember + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> do + createChannel1Relay "team" alice bob cath dan eve -- chat relay bob is offline alice #> "#team 1" @@ -8477,8 +8573,8 @@ testChannelsSenderDeduplicateOwn ps = do cath #> "#team 5" dan #> "#team 6" - withTestChatCfg ps cfg "bob" $ \bob -> do - bob <## "subscribed 6 connections server localhost" + withTestChatCfgOpts ps cfg relayTestOpts "bob" $ \bob -> do + bob <## "subscribed 6 connections on server localhost" bob <### [ WithTime "#team alice> 1", WithTime "#team alice> 2", @@ -8488,25 +8584,31 @@ testChannelsSenderDeduplicateOwn ps = do WithTime "#team dan> 6" ] alice - <### [ WithTime "#team cath> 4 [>>]", + <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record cath", + "#team: bob forwarded a message from an unknown member, creating unknown member record dan", + WithTime "#team cath> 4 [>>]", WithTime "#team cath> 5 [>>]", WithTime "#team dan> 6 [>>]" ] cath - <### [ WithTime "#team alice> 1 [>>]", + <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record dan", + WithTime "#team alice> 1 [>>]", WithTime "#team alice> 2 [>>]", WithTime "#team alice> 3 [>>]", WithTime "#team dan> 6 [>>]" ] dan - <### [ WithTime "#team alice> 1 [>>]", + <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record cath", + WithTime "#team alice> 1 [>>]", WithTime "#team alice> 2 [>>]", WithTime "#team alice> 3 [>>]", WithTime "#team cath> 4 [>>]", WithTime "#team cath> 5 [>>]" ] eve - <### [ WithTime "#team alice> 1 [>>]", + <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record cath", + "#team: bob forwarded a message from an unknown member, creating unknown member record dan", + WithTime "#team alice> 1 [>>]", WithTime "#team alice> 2 [>>]", WithTime "#team alice> 3 [>>]", WithTime "#team cath> 4 [>>]", @@ -8515,3 +8617,77 @@ testChannelsSenderDeduplicateOwn ps = do ] where cfg = testCfg {deliveryWorkerDelay = 250000} + +testChannels2RelaysDeliver :: HasCallStack => TestParams -> IO () +testChannels2RelaysDeliver ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + withNewTestChat ps "frank" frankProfile $ \frank -> do + createChannel2Relays "team" alice bob cath dan eve frank + + alice #> "#team hi" + [bob, cath] *<# "#team alice> hi" + [dan, eve, frank] *<# "#team alice> hi [>>]" + + dan ##> "+1 #team hi" + dan <## "added 👍" + bob <# "#team dan> > alice hi" + bob <## " + 👍" + cath <# "#team dan> > alice hi" + cath <## " + 👍" + alice .<## " forwarded a message from an unknown member, creating unknown member record dan" + alice <# "#team dan> > alice hi" + alice <## " + 👍" + eve .<## " forwarded a message from an unknown member, creating unknown member record dan" + eve <# "#team dan> > alice hi" + eve <## " + 👍" + frank .<## " forwarded a message from an unknown member, creating unknown member record dan" + frank <# "#team dan> > alice hi" + frank <## " + 👍" + + -- remove below if default role is changed to observer + dan #> "#team hey" + [bob, cath] *<# "#team dan> hey" + [alice, eve, frank] *<# "#team dan> hey [>>]" + +testChannels2RelaysIncognito :: HasCallStack => TestParams -> IO () +testChannels2RelaysIncognito ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + danIncognito <- memberJoinChannelIncognito "team" [bob, cath] shortLink fullLink dan + forM_ [eve, frank] $ \member -> + memberJoinChannel "team" [bob, cath] shortLink fullLink member + + alice #> "#team hi" + [bob, cath] *<# "#team alice> hi" + dan ?<# "#team alice> hi [>>]" + [eve, frank] *<# "#team alice> hi [>>]" + + dan ##> "+1 #team hi" + dan <## "added 👍" + bob <# ("#team " <> danIncognito <> "> > alice hi") + bob <## " + 👍" + cath <# ("#team " <> danIncognito <> "> > alice hi") + cath <## " + 👍" + alice .<## (" forwarded a message from an unknown member, creating unknown member record " <> danIncognito) + alice <# ("#team " <> danIncognito <> "> > alice hi") + alice <## " + 👍" + eve .<## (" forwarded a message from an unknown member, creating unknown member record " <> danIncognito) + eve <# ("#team " <> danIncognito <> "> > alice hi") + eve <## " + 👍" + frank .<## (" forwarded a message from an unknown member, creating unknown member record " <> danIncognito) + frank <# ("#team " <> danIncognito <> "> > alice hi") + frank <## " + 👍" + + -- remove below if default role is changed to observer + dan ?#> "#team hey" + [bob, cath] *<# ("#team " <> danIncognito <> "> hey") + [alice, eve, frank] *<# ("#team " <> danIncognito <> "> hey [>>]") diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 3fdadc3b64..d4be2704a4 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2905,7 +2905,7 @@ testShortLinkJoinGroup = name <- userName cc sName <- showName cc cc ##> ("/_connect plan 1 " <> link) - cc <## "group link: ok to connect" + cc <## "group link: ok to connect directly" _sLinkData <- getTermLine cc cc ##> ("/c " <> link) cc <## "connection request sent!" @@ -3377,7 +3377,7 @@ testShortLinkPrepareGroup = testChat3 aliceProfile bobProfile cathProfile test alice ##> "/create link #team" (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3411,7 +3411,7 @@ testShortLinkPrepareGroup = testChat3 aliceProfile bobProfile cathProfile test alice <## "#team: bob left the group" cath <## "#team: bob left the group" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" void $ getTermLine bob testShortLinkPrepareGroupReject :: HasCallStack => TestParams -> IO () @@ -3422,7 +3422,7 @@ testShortLinkPrepareGroupReject = testChatCfg3 cfg aliceProfile bobProfile cathP alice ##> "/create link #team" (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3455,7 +3455,7 @@ testGroupShortLinkWelcome = testChat2 aliceProfile bobProfile test alice ##> "/create link #team" (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3488,7 +3488,7 @@ testShortLinkGroupRetry ps = testChatOpts2 opts' aliceProfile bobProfile test ps alice ##> "/create link #team" (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3703,7 +3703,7 @@ testShortLinkConnectPreparedGroupIncognito = testChat3 aliceProfile bobProfile c alice ##> "/create link #team" (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3747,7 +3747,7 @@ testShortLinkChangePreparedGroupUser = testChat3 aliceProfile bobProfile cathPro showActiveUser bob "bob (Bob)" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#team: group is prepared" @@ -3803,7 +3803,7 @@ testShortLinkChangePreparedGroupUserDuplicate = testChat3 aliceProfile bobProfil showActiveUser bob "robert" bob ##> ("/_connect plan 2 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData1 <- getTermLine bob bob ##> ("/_prepare group 2 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData1) bob <## "#team: group is prepared" @@ -3812,7 +3812,7 @@ testShortLinkChangePreparedGroupUserDuplicate = testChat3 aliceProfile bobProfil showActiveUser bob "bob (Bob)" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData2 <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData2) bob <## "#team: group is prepared" @@ -4075,7 +4075,7 @@ testShortLinkGroupChangeProfile = testChat3 aliceProfile bobProfile cathProfile cath <## "changed to #club" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#club: group is prepared" @@ -4113,7 +4113,7 @@ testShortLinkGroupChangeProfileReceived = testChat3 aliceProfile bobProfile cath alice <## "changed to #club" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "group link: ok to connect" + bob <## "group link: ok to connect directly" groupSLinkData <- getTermLine bob bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) bob <## "#club: group is prepared" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 756ee47727..b60e954478 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -75,6 +75,9 @@ danProfile = mkProfile "dan" "Daniel" Nothing eveProfile :: Profile eveProfile = mkProfile "eve" "Eve" Nothing +frankProfile :: Profile +frankProfile = mkProfile "frank" "Frank" Nothing + businessProfile :: Profile businessProfile = mkProfile "biz" "Biz Inc" Nothing diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 2332fa429c..d61f1350d5 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -107,7 +107,7 @@ testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile -testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} +testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, groupLink = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do