diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index f3bb915665..fe552cb839 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -906,6 +906,7 @@ Create group. **Parameters**: - userId: int64 - incognito: bool +- useRelays: bool - groupProfile: [GroupProfile](./TYPES.md#groupprofile) **Syntax**: diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 0c173df77c..cbe8b9f891 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) @@ -2134,6 +2136,7 @@ MemberSupport: **Record type**: - groupId: int64 - useRelays: bool +- relayOwnStatus: [RelayStatus](#relaystatus)? - localDisplayName: string - groupProfile: [GroupProfile](#groupprofile) - localAlias: string @@ -2219,6 +2222,7 @@ Known: - updatedAt: UTCTime - supportChat: [GroupSupportChat](#groupsupportchat)? - isChatRelay: bool +- relayData: [GroupRelay](#grouprelay)? --- @@ -2329,10 +2333,21 @@ Known: - shortDescr: string? - description: string? - image: string? +- groupLink: string? - groupPreferences: [GroupPreferences](#grouppreferences)? - memberAdmission: [GroupMemberAdmission](#groupmemberadmission)? +--- + +## GroupRelay + +**Record type**: +- groupRelayId: int64 +- relayStatus: [RelayStatus](#relaystatus) +- relayLink: string? + + --- ## GroupShortLinkData @@ -3041,6 +3056,17 @@ NewMemberPendingReview: - type: "newMemberPendingReview" +--- + +## RelayStatus + +**Enum type**: +- "new" +- "invited" +- "accepted" +- "active" + + --- ## ReportReason diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 73e427fa03..8c7e7f1976 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -280,6 +280,7 @@ chatTypesDocsData = (sti @GroupPreference, STRecord, "", [], "", ""), (sti @GroupPreferences, STRecord, "", [], "", ""), (sti @GroupProfile, STRecord, "", [], "", ""), + (sti @GroupRelay, STRecord, "", [], "", ""), (sti @GroupShortLinkData, STRecord, "", [], "", ""), (sti @GroupSummary, STRecord, "", [], "", ""), (sti @GroupSupportChat, STRecord, "", [], "", ""), @@ -319,6 +320,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, "", [], "", ""), @@ -466,6 +468,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 @@ -511,6 +514,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/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index de6a7a7ce1..e1866b5ef1 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -330,6 +330,7 @@ export namespace APIListMembers { export interface APINewGroup { userId: number // int64 incognito: boolean + useRelays: boolean groupProfile: T.GroupProfile } diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 993ca789b3..8f80c179e7 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 @@ -2511,6 +2512,7 @@ export interface GroupMember { updatedAt: string // ISO-8601 timestamp supportChat?: GroupSupportChat isChatRelay: boolean + relayData?: GroupRelay } export interface GroupMemberAdmission { @@ -2585,10 +2587,17 @@ export interface GroupProfile { shortDescr?: string description?: string image?: string + groupLink?: string groupPreferences?: GroupPreferences memberAdmission?: GroupMemberAdmission } +export interface GroupRelay { + groupRelayId: number // int64 + relayStatus: RelayStatus + relayLink?: string +} + export interface GroupShortLinkData { groupProfile: GroupProfile } @@ -3443,6 +3452,13 @@ export namespace RcvGroupEvent { } } +export enum RelayStatus { + New = "new", + Invited = "invited", + Accepted = "accepted", + Active = "active", +} + export enum ReportReason { Spam = "spam", Content = "content", diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 8d70bd3bdd..b2654416b9 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -172,6 +172,7 @@ newChatController expireCIThreads <- TM.emptyIO expireCIFlags <- TM.emptyIO cleanupManagerAsync <- newTVarIO Nothing + relayChecksAsync <- newTVarIO Nothing timedItemThreads <- TM.emptyIO chatActivated <- newTVarIO True showLiveItems <- newTVarIO False @@ -213,6 +214,7 @@ newChatController expireCIThreads, expireCIFlags, cleanupManagerAsync, + relayChecksAsync, timedItemThreads, chatActivated, showLiveItems, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bca2131d86..03193a6f82 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -250,6 +250,7 @@ data ChatController = ChatController expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), + relayChecksAsync :: TVar (Maybe (Async ())), chatActivated :: TVar Bool, timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, @@ -391,7 +392,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 @@ -503,8 +504,8 @@ data ChatCommand | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text} | UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text} | ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text} - | APINewGroup {userId :: UserId, incognito :: IncognitoEnabled, groupProfile :: GroupProfile} - | NewGroup IncognitoEnabled GroupProfile + | APINewGroup {userId :: UserId, incognito :: IncognitoEnabled, useRelays :: Bool, groupProfile :: GroupProfile} + | NewGroup IncognitoEnabled Bool GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | AcceptMember GroupName ContactName GroupMemberRole diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 89fd410502..e3d462b7dc 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -210,6 +210,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 relayChecksAsync + readTVarIO relayAsync >>= \case + Nothing -> do + a <- Just <$> async (void $ runExceptT $ runRelayChecks relayUser) + atomically $ writeTVar relayAsync a + _ -> pure () startExpireCIs user = whenM shouldExpireChats $ do startExpireCIThread user setExpireCIFlag user True @@ -220,6 +229,28 @@ startChatController mainApp enableSndFiles = do ttlCount <- getChatTTLCount db user pure $ ttl > 0 || ttlCount > 0 +-- startExpireCIThread :: User -> CM' () +-- startExpireCIThread user@User {userId} = do +-- expireThreads <- asks expireCIThreads +-- atomically (TM.lookup userId expireThreads) >>= \case +-- Nothing -> do +-- a <- Just <$> async runExpireCIs +-- atomically $ TM.insert userId a expireThreads +-- _ -> pure () +-- where +-- runExpireCIs = do +-- delay <- asks (initialCleanupManagerDelay . config) +-- liftIO $ threadDelay' delay +-- interval <- asks $ ciExpirationInterval . config +-- forever $ do +-- flip catchAllErrors' (eToView') $ do +-- expireFlags <- asks expireCIFlags +-- atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry +-- lift waitChatStartedAndActivated +-- ttl <- withStore' (`getChatItemTTL` user) +-- expireChatItems user ttl False +-- liftIO $ threadDelay' interval + getConnsToSub :: User -> CM [ConnId] getConnsToSub user = withFastStore' $ \db -> do @@ -1195,8 +1226,8 @@ processChatCommand vr nm = \case withFastStore' $ \db -> deleteGroup db user gInfo pure $ CRGroupDeletedUser user gInfo where - getRecipients gInfo@GroupInfo {useRelays} - | isTrue useRelays = do + getRecipients gInfo + | useRelays' gInfo = do relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo pure (relays, relays) | otherwise = do @@ -1630,8 +1661,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 = getGroupRelays db vr user gInfo | otherwise = getGroupMembers db vr user gInfo _ -> throwCmdError "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do @@ -2032,9 +2063,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 -- TODO [certs rcv] (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) Nothing IKPQOn subMode @@ -2228,19 +2259,26 @@ 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 + APINewGroup userId incognito useRelays 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 + 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 + -- TODO [relays] owner: create group using relays + -- TODO - prepare group link + -- TODO - add link, owner key to group profile, sign + -- TODO - create group link on server, use signed profile as data + -- TODO - choose chat relays: auto or user action (new api in case of latter) + -- TODO - send contact requests to relays + -- TODO - create relay member connections, relay records, relay status: RSInvited pure $ CRGroupCreated user gInfo - NewGroup incognito gProfile -> withUser $ \User {userId} -> - processChatCommand vr nm $ APINewGroup userId incognito gProfile + NewGroup incognito useRelays gProfile -> withUser $ \User {userId} -> + processChatCommand vr nm $ APINewGroup userId incognito useRelays 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 @@ -2589,8 +2627,8 @@ 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 + getRecipients user gInfo + | useRelays' gInfo = do relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo pure (relays, relays) | otherwise = do @@ -3371,7 +3409,7 @@ processChatCommand vr nm = \case sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p') where getRecipients - | isTrue (useRelays gInfo') = withFastStore' $ \db -> getGroupRelays db vr user gInfo' + | useRelays' gInfo' = withFastStore' $ \db -> getGroupRelays db vr user gInfo' | otherwise = do ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo' pure $ filter memberCurrentOrPending ms @@ -3627,6 +3665,19 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do + -- TODO [relays] member: connect to relays + -- TODO - get ContactLinkData.relays data + -- TODO - see decodeShortLinkData -> linkUserData', retrieve relays in addition to userData + -- TODO - if relay list is non-empty, connect to relays + -- TODO - or, base on group profile? add useRelays/group type to group link data? (e.g. "channel") + -- TODO note: + -- TODO this is Connection Plan api - we're not connecting here yet; + -- TODO options: + -- TODO - save relay links on prepared group record, connect to them in APIConnectPreparedGroup + -- TODO - only mark group as `useRelays`, repeat retrieving link data in APIConnectPreparedGroup + -- TODO retreiving relays at point of conenctions seems better, as arbitrary time + -- TODO can pass between creating prepared group from plan and connecting to it, + -- TODO during which relays can change. (cReq, cData) <- getShortLinkConnReq user l' groupSLinkData_ <- liftIO $ decodeShortLinkData cData plan <- groupJoinRequestPlan user cReq groupSLinkData_ @@ -4257,6 +4308,16 @@ cleanupManager = do let cutoffTs = addUTCTime (-(14 * nominalDay)) ts withStore' (`deleteOldProbes` cutoffTs) +runRelayChecks :: User -> CM () +runRelayChecks user = do + -- TODO [relays] relay: periodically check served groups + -- TODO - get groups where user is chat relay + -- TODO - retrive group link data, check presence of relay link + -- TODO - if relay link is present, update relay status to RSActive + -- TODO - if relay link is absent and staus was RSActive -> update to new "Removed" status? + -- TODO - * recovery for joining served group (add relay protocol) - also in this thread? + pure () + expireChatItems :: User -> Int64 -> Bool -> CM () expireChatItems user@User {userId} globalTTL sync = do currentTs <- liftIO getCurrentTime @@ -4537,8 +4598,8 @@ chatCommandP = ("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help db" <|> "/hd") $> ChatHelp HSDatabase, ("/help" <|> "/h") $> ChatHelp HSMain, - ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), - "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), + ("/group" <|> "/g") *> (NewGroup <$> incognitoP <*> (" use_relays=" *> onOffP <|> pure False) <* A.space <* char_ '#' <*> groupProfile), + "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <*> (" use_relays=" *> onOffP <|> pure False) <* 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)), @@ -4800,7 +4861,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 b9fd0c7ada..c500cf9223 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1018,7 +1018,7 @@ acceptBusinessJoinRequestAsync 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 @@ -1456,8 +1456,8 @@ 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 (isMemberRelay membership) = do unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" withFastStore' $ \db -> getGroupRelays db vr user gInfo | otherwise = case scopeInfo of @@ -1994,9 +1994,9 @@ 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 @@ -2109,7 +2109,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 @@ -2117,7 +2117,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 diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 75eb208962..afa7c448ea 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -220,7 +220,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 -> getGroupRelays 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') @@ -728,6 +728,15 @@ 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 -> do + -- TODO [relays] owner: process relay acceptance + -- TODO - * relay is invitee? other processing branch? + -- TODO - * check processing client is owner, otherwise error + -- TODO - update relay record with relay link, relay status: RSAccepted + -- TODO - update group link (add relay link) + -- TODO - agent async setConnShortLink api; agent api to allow setting ContactLinkData.relays + -- TODO - on group link updated: relay status: RSActive (can share group link with members) + pure () _ -> messageError "CONF from invited member must have x.grp.acpt" GCHostMember -> case chatMsgEvent of @@ -800,6 +809,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> prepared unless (memberPending membership || isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo'' m'' GCInviteeMember -> do + -- TODO [relays] relay: don't introduce new member to other members (gInfo', mStatus) <- if not (memberPending m) then do @@ -1140,6 +1150,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case chatMsgEvent of XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport + XGrpRelayInv groupLink -> relayContactRequest groupLink -- TODO show/log error, other events in contact request _ -> pure () MERR _ err -> do @@ -1308,6 +1319,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise -> do mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p xContactId_ rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason + relayContactRequest :: ShortLinkContact -> CM () + relayContactRequest groupLink = do + -- TODO [relays] relay: process contact request to server group + -- TODO - retrieve group link data, validate group profile, verify owner's signature + -- TODO - create group record, relay status: RSInvited + -- TODO - create relay link (async) + -- TODO - new user contact link referencing this group + -- TODO - link data: relay key for group, relay identity (profile, certificate, relay identity key) + -- TODO - accept request - send XGrpRelayAcpt to owner (continuation on link created) + -- TODO - create owner member connection, relay status: RSAccepted + -- TODO - * duplicate requests can be deduplicated by group link + pure () memberCanSend :: GroupMember -> @@ -2813,8 +2836,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO [channels fwd] base on differentiation between groups and channels isUserGrpFwdRelay :: GroupInfo -> Bool - isUserGrpFwdRelay GroupInfo {useRelays, membership = membership@GroupMember {memberRole}} - | isTrue useRelays = isMemberRelay membership + isUserGrpFwdRelay gInfo@GroupInfo {membership = membership@GroupMember {memberRole}} + | useRelays' gInfo = isMemberRelay membership | otherwise = memberRole >= GRAdmin xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) @@ -3095,7 +3118,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 -> getGroupRelays db vr user gInfo | otherwise = withStore' $ \db -> getGroupMembers db vr user gInfo startDeliveryTaskWorkers :: CM () @@ -3219,7 +3242,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, cursorGMId_ = startingCursor} = job sendBodyToMembers :: CM () sendBodyToMembers - | isTrue (useRelays gInfo) = -- channel + | useRelays' gInfo = -- channel case jobScope of -- there's no member review in channels, so job spec includePending is ignored DJSGroup {} -> do 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 a523edbdc6..88da258813 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -328,8 +328,8 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json - XGrpRelayInv :: ConnLinkContact -> ChatMsgEvent 'Json - XGrpRelayAcpt :: ConnLinkContact -> ChatMsgEvent 'Json + XGrpRelayInv :: ShortLinkContact -> ChatMsgEvent 'Json + XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 2da8f183e7..57a86afa09 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -135,26 +135,30 @@ 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.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, + mu.is_chat_relay, NULL, NULL, NULL, -- from GroupMember - m.group_member_id, m.group_id, 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.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 + 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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN group_relays r ON r.group_relay_id = m.group_relay_id JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id diff --git a/src/Simplex/Chat/Store/Delivery.hs b/src/Simplex/Chat/Store/Delivery.hs index 30fffac0e7..49102656e8 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/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index ddeb320212..f970c2e8fd 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -190,11 +190,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe Int64, 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 Int64, 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) :. (Maybe BoolInt, Maybe Int64, Maybe RelayStatus, Maybe ShortLinkContact) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, 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, 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 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 isChatRelay, groupRelayId, relayStatus, relayLink)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, 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) :. (isChatRelay, groupRelayId, relayStatus, relayLink)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -309,8 +309,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 @@ -326,11 +326,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, 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, 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 @@ -338,7 +338,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 = "", @@ -414,6 +415,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ ( GroupInfo { groupId, useRelays = BoolDef False, + relayOwnStatus = Nothing, localDisplayName, groupProfile, localAlias = "", @@ -481,7 +483,8 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe createdAt, updatedAt = createdAt, supportChat = Nothing, - isChatRelay = BoolDef False + isChatRelay = BoolDef False, + relayData = Nothing } where memberChatVRange@(VersionRange minV maxV) = vr @@ -1100,7 +1103,8 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, createdAt, updatedAt = createdAt, supportChat = Nothing, - isChatRelay = BoolDef False + isChatRelay = BoolDef False, + relayData = Nothing } where insertMember_ = @@ -1457,14 +1461,14 @@ createNewMember_ db [sql| INSERT INTO group_members - (group_id, member_id, member_role, member_category, member_status, member_restriction, is_chat_relay, invited_by, invited_by_group_member_id, + (group_id, member_id, member_role, member_category, member_status, 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) + peer_chat_min_version, peer_chat_max_version, is_chat_relay) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, memberCategory, memberStatus, memRestriction, BI isChatRelay, invitedById, memInvitedByGroupMemberId) + ( (groupId, memberId, memberRole, memberCategory, memberStatus, memRestriction, invitedById, memInvitedByGroupMemberId) :. (userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) - :. (minV, maxV) + :. (minV, maxV, BI isChatRelay) ) groupMemberId <- insertedRowId db pure @@ -1488,7 +1492,8 @@ createNewMember_ createdAt, updatedAt = createdAt, supportChat = Nothing, - isChatRelay = BoolDef isChatRelay + isChatRelay = BoolDef isChatRelay, + relayData = Nothing } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -1820,14 +1825,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} + toGroupProfile (displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission) = + GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index df5b800c65..98c7abceea 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -675,12 +675,14 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, 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 + 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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN contacts c ON m.contact_id = c.contact_id LEFT JOIN chat_items i ON i.user_id = m.user_id AND i.group_id = m.group_id @@ -2999,35 +3001,39 @@ 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, -- quoted ChatItem 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.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, + rm.is_chat_relay, rr.group_relay_id, rr.relay_status, rr.relay_link, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, 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 + 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, + dbm.is_chat_relay, dbr.group_relay_id, dbr.relay_status, dbr.relay_link 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id) + LEFT JOIN group_relays rr ON rr.group_relay_id = rm.group_relay_id LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id) + LEFT JOIN group_relays dbr ON dbr.group_relay_id = dbm.group_relay_id WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id = ? |] (userId, groupId, itemId) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs index c8d05401d9..d2c8f41a8c 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs @@ -10,7 +10,7 @@ import Database.SQLite.Simple.QQ (sql) -- (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 (GroupRelayStatus) +-- - 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 @@ -19,8 +19,7 @@ import Database.SQLite.Simple.QQ (sql) -- receiving event to member connection, owner can match it to the relay; -- TBC inverse association - from group_relays to group_members? -- - TBC also inverse link from group_relays to group_members? (group_relays.group_member_id) --- - groups.relay_status_self - indicates for a relay client that it is chat relay for the group (GroupRelayStatus) --- - connections.group_member_id_messaging - secondary connection for a group member in relayed group +-- - groups.relay_own_status - indicates for a relay client that it is chat relay for the group (RelayStatus) m20251018_chat_relays :: Query m20251018_chat_relays = [sql| @@ -43,6 +42,12 @@ CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id); ALTER TABLE users ADD COLUMN is_user_chat_relay INTEGER NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN use_relays INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE groups ADD COLUMN relay_own_status 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, @@ -57,11 +62,6 @@ ALTER TABLE group_members ADD COLUMN is_chat_relay INTEGER NOT NULL DEFAULT 0; ALTER TABLE group_members ADD COLUMN group_relay_id INTEGER REFERENCES group_relays ON DELETE SET NULL; CREATE INDEX idx_group_members_group_relay_id ON group_members(group_relay_id); - -ALTER TABLE groups ADD COLUMN relay_status_self TEXT; - -ALTER TABLE connections ADD COLUMN group_member_id_messaging INTEGER REFERENCES group_members ON DELETE CASCADE; -CREATE INDEX idx_connections_group_member_id_messaging ON connections(group_member_id_messaging); |] down_m20251018_chat_relays :: Query @@ -72,6 +72,12 @@ DROP TABLE chat_relays; ALTER TABLE users DROP COLUMN is_user_chat_relay; +ALTER TABLE groups DROP COLUMN use_relays; + +ALTER TABLE groups DROP COLUMN relay_own_status; + +ALTER TABLE group_profiles DROP COLUMN group_link; + DROP INDEX idx_group_relays_group_id; DROP INDEX idx_group_relays_chat_relay_id; DROP TABLE group_relays; @@ -80,9 +86,4 @@ ALTER TABLE group_members DROP COLUMN is_chat_relay; DROP INDEX idx_group_members_group_relay_id; ALTER TABLE group_members DROP COLUMN group_relay_id; - -ALTER TABLE groups DROP COLUMN relay_status_self; - -DROP INDEX idx_connections_group_member_id_messaging; -ALTER TABLE connections DROP COLUMN group_member_id_messaging; |] 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 df158b4a5c..90335f167f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -106,26 +106,30 @@ 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.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, + mu.is_chat_relay, NULL, NULL, NULL, -- from GroupMember - m.group_member_id, m.group_id, 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.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 + 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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN group_relays r ON r.group_relay_id = m.group_relay_id JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id @@ -137,6 +141,7 @@ Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH g USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN 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=?) @@ -817,7 +822,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 = ? @@ -845,12 +850,14 @@ Query: SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, 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 + 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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN contacts c ON m.contact_id = c.contact_id LEFT JOIN chat_items i ON i.user_id = m.user_id AND i.group_id = m.group_id @@ -861,6 +868,7 @@ Query: Plan: SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) LEFT-JOIN Query: @@ -969,6 +977,7 @@ 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 @@ -1015,9 +1024,9 @@ Plan: Query: INSERT INTO groups - (local_display_name, user_id, group_profile_id, enable_ntfs, + (use_relays, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at) - VALUES (?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?) Plan: @@ -1073,48 +1082,54 @@ Query: i.forwarded_by_group_member_id, i.show_group_as_sender, -- GroupMember m.group_member_id, m.group_id, 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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, -- quoted ChatItem 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.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, + rm.is_chat_relay, rr.group_relay_id, rr.relay_status, rr.relay_link, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, 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 + 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, + dbm.is_chat_relay, dbr.group_relay_id, dbr.relay_status, dbr.relay_link 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id) + LEFT JOIN group_relays rr ON rr.group_relay_id = rm.group_relay_id LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id) + LEFT JOIN group_relays dbr ON dbr.group_relay_id = dbm.group_relay_id WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id = ? 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 r 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 SEARCH rm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH rp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH rr USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH dbm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH dbp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH dbr USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: SELECT @@ -1653,9 +1668,9 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: INSERT INTO group_members - (group_id, member_id, member_role, member_category, member_status, member_restriction, is_chat_relay, invited_by, invited_by_group_member_id, + (group_id, member_id, member_role, member_category, member_status, 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) + peer_chat_min_version, peer_chat_max_version, is_chat_relay) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4979,18 +4994,20 @@ 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.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 + 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, + mu.is_chat_relay, NULL, NULL, NULL FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5013,18 +5030,20 @@ 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.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 + 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, + mu.is_chat_relay, NULL, NULL, NULL FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5040,18 +5059,20 @@ 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.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 + 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, + mu.is_chat_relay, NULL, NULL, NULL FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5096,16 +5117,18 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) @@ -5119,120 +5142,139 @@ SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) LIST SUBQUERY 1 SCAN chat_items USING COVERING INDEX idx_chat_items_group_member_id SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ? Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.member_category = ? Plan: SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.member_id = ? Plan: SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id WHERE m.group_member_id = ? AND m.user_id = ? Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_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 NULL OR m.contact_id != ?) Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_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 NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?) Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN Query: @@ -5678,6 +5720,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: @@ -5831,6 +5874,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=?) @@ -6216,7 +6260,7 @@ SEARCH chat_items USING INTEGER PRIMARY KEY (rowid>?) Query: SELECT count(1) FROM group_members Plan: -SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id +SCAN group_members USING COVERING INDEX idx_group_members_group_relay_id Query: SELECT count(1) FROM pending_group_messages Plan: diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index c47fd1a556..2870e8e761 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 ); CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -156,7 +157,9 @@ CREATE TABLE groups( request_shared_msg_id BLOB, conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0, via_group_link_uri BLOB, - summary_current_members_count INTEGER NOT NULL DEFAULT 0, -- received + summary_current_members_count INTEGER NOT NULL DEFAULT 0, + use_relays INTEGER NOT NULL DEFAULT 0, + relay_own_status TEXT, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -197,6 +200,7 @@ CREATE TABLE group_members( member_xcontact_id BLOB, member_welcome_shared_msg_id BLOB, is_chat_relay INTEGER NOT NULL DEFAULT 0, + group_relay_id INTEGER REFERENCES group_relays ON DELETE SET NULL, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -732,11 +736,19 @@ CREATE TABLE chat_relays( 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) ); +CREATE TABLE group_relays( + group_relay_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + chat_relay_id INTEGER NOT NULL REFERENCES chat_relays ON DELETE CASCADE, + relay_status TEXT NOT NULL, + relay_link BLOB +); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -1201,6 +1213,9 @@ CREATE INDEX idx_connections_to_subscribe ON connections( to_subscribe ); CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id); +CREATE INDEX idx_group_relays_group_id ON group_relays(group_id); +CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); +CREATE INDEX idx_group_members_group_relay_id ON group_members(group_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 7fb3f72951..df51ac90f1 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -654,22 +654,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 ConnLinkContact) :. (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 = (Int64, 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 = (Int64, 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) :. (BoolInt, Maybe Int64, Maybe RelayStatus, Maybe ShortLinkContact) 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 @@ -678,14 +678,13 @@ toPreparedGroup = \case _ -> Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, 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, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs) :. (BI isCRelay, groupRelayId_, relayStatus_, relayLink)) = 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 @@ -697,22 +696,28 @@ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, lastMsgFromMemberTs = supportChatLastMsgFromMemberTs } _ -> Nothing + isChatRelay = BoolDef isCRelay + relayData = case (groupRelayId_, relayStatus_) of + (Just groupRelayId, Just relayStatus) -> Just GroupRelay {groupRelayId, relayStatus, relayLink} + _ -> Nothing in GroupMember {..} groupMemberQuery :: Query groupMemberQuery = [sql| SELECT - m.group_member_id, m.group_id, 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.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, + m.is_chat_relay, r.group_relay_id, r.relay_status, r.relay_link, 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 group_relays r ON r.group_relay_id = m.group_relay_id LEFT JOIN connections c ON c.group_member_id = m.group_member_id |] @@ -731,23 +736,26 @@ toBusinessChatInfo _ = Nothing groupInfoQuery :: Query groupInfoQuery = groupInfoQueryFields <> " " <> groupInfoQueryFrom +-- membership "member" never references group_relays, therefore `NULL, NULL, NULL` which avoids extra join groupInfoQueryFields :: Query 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.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 + 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, + mu.is_chat_relay, NULL, NULL, NULL |] groupInfoQueryFrom :: Query diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 9a8b252364..69f21a2bd4 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -118,7 +118,7 @@ instance ToField AgentUserId where toField (AgentUserId uId) = toField uId aUserId :: User -> UserId aUserId User {agentUserId = AgentUserId uId} = uId --- TODO [chat relays] 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, @@ -449,6 +449,7 @@ type GroupId = Int64 data GroupInfo = GroupInfo { groupId :: GroupId, useRelays :: BoolDef, + relayOwnStatus :: Maybe RelayStatus, localDisplayName :: GroupName, groupProfile :: GroupProfile, localAlias :: Text, @@ -467,11 +468,13 @@ data GroupInfo = GroupInfo customData :: Maybe CustomData, groupSummary :: GroupSummary, membersRequireAttention :: Int, - viaGroupLinkUri :: Maybe ConnReqContact, - relayOwnStatus :: Maybe GroupRelayStatus + viaGroupLinkUri :: Maybe ConnReqContact } 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,9 +735,9 @@ 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 ConnLinkContact, groupPreferences :: Maybe GroupPreferences, - memberAdmission :: Maybe GroupMemberAdmission, - groupLink :: Maybe ConnLinkContact + memberAdmission :: Maybe GroupMemberAdmission } deriving (Eq, Show) @@ -963,24 +966,37 @@ data GroupMember = GroupMember } deriving (Eq, Show) --- TODO [chat relays] review; consider where to use it: --- TODO - GroupMember? (now) --- TODO - separate list of relays in GroupInfo? --- TODO - only on request? data GroupRelay = GroupRelay { groupRelayId :: Int64, - relayStatus :: GroupRelayStatus, - relayLink :: ConnLinkContact + relayStatus :: RelayStatus, + relayLink :: Maybe ShortLinkContact } deriving (Eq, Show) -data GroupRelayStatus - = GRSNew -- only for owner - | GRSInvited - | GRSAccepted - | GRSActive +data RelayStatus + = RSNew -- only for owner + | RSInvited + | RSAccepted + | RSActive deriving (Eq, Show) +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, @@ -2016,7 +2032,7 @@ $(JQ.deriveJSON defaultJSON ''PendingContactConnection) $(JQ.deriveJSON defaultJSON ''GroupSupportChat) -$(JQ.deriveJSON (enumJSON $ dropPrefix "GRS") ''GroupRelayStatus) +$(JQ.deriveJSON (enumJSON $ dropPrefix "RS") ''RelayStatus) $(JQ.deriveJSON defaultJSON ''GroupRelay) 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