diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index cbe8b9f891..a2de11aa94 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2344,6 +2344,7 @@ Known: **Record type**: - groupRelayId: int64 +- userChatRelayId: int64 - relayStatus: [RelayStatus](#relaystatus) - relayLink: string? @@ -3588,6 +3589,14 @@ OperatorNotFound: UsageConditionsNotFound: - type: "usageConditionsNotFound" +UserChatRelayNotFound: +- type: "userChatRelayNotFound" +- chatRelayId: int64 + +GroupRelayNotFound: +- type: "groupRelayNotFound" +- groupRelayId: int64 + InvalidQuote: - type: "invalidQuote" diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 8f80c179e7..0246d3e736 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2594,6 +2594,7 @@ export interface GroupProfile { export interface GroupRelay { groupRelayId: number // int64 + userChatRelayId: number // int64 relayStatus: RelayStatus relayLink?: string } @@ -3812,6 +3813,8 @@ export type StoreError = | StoreError.ProhibitedDeleteUser | StoreError.OperatorNotFound | StoreError.UsageConditionsNotFound + | StoreError.UserChatRelayNotFound + | StoreError.GroupRelayNotFound | StoreError.InvalidQuote | StoreError.InvalidMention | StoreError.InvalidDeliveryTask @@ -3897,6 +3900,8 @@ export namespace StoreError { | "prohibitedDeleteUser" | "operatorNotFound" | "usageConditionsNotFound" + | "userChatRelayNotFound" + | "groupRelayNotFound" | "invalidQuote" | "invalidMention" | "invalidDeliveryTask" @@ -4274,6 +4279,16 @@ export namespace StoreError { type: "usageConditionsNotFound" } + export interface UserChatRelayNotFound extends Interface { + type: "userChatRelayNotFound" + chatRelayId: number // int64 + } + + export interface GroupRelayNotFound extends Interface { + type: "groupRelayNotFound" + groupRelayId: number // int64 + } + export interface InvalidQuote extends Interface { type: "invalidQuote" } diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 87d513e87e..1db555f4c7 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2690,7 +2690,7 @@ processChatCommand vr nm = \case -- TODO - prepare group link (without creating on server) -- TODO - add link, owner key to group profile, sign -- TODO - create group link on server, use signed profile as data - -- vvv + -- vvv FROM HERE vvv (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink sLnk <- case toShortLinkContact ccLink' of @@ -2700,7 +2700,7 @@ processChatCommand vr nm = \case userData' = encodeShortLinkData $ GroupShortLinkData groupProfile' -- same link with updated profile _sLnk' <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm connId SCMContact userData' (Just crClientData)) - -- ^^^ + -- ^^^ TO HERE ^^^ gVar <- asks random (gLink, gInfo') <- withFastStore $ \db -> do gLink <- createGroupLink db gVar user gInfo connId ccLink' groupLinkId GRMember subMode @@ -2709,7 +2709,7 @@ processChatCommand vr nm = \case if autoChooseRelays then do relays <- chooseRelays user - groupRelays <- addRelays user gInfo' relays + groupRelays <- addRelays user gInfo' sLnk relays pure $ CRGroupRelaysAdded user gInfo' gLink groupRelays else pure $ CRGroupLinkCreated user gInfo' gLink @@ -2722,12 +2722,15 @@ processChatCommand vr nm = \case when (null selectedRelays) $ throwChatError $ CEException "failed to select relays: no enabled relays configured" pure selectedRelays APIAddRelays groupId relayIds -> withUser $ \user -> withGroupLock "addRelays" groupId $ do - (gInfo, gLink) <- withFastStore $ \db -> do + (gInfo, gLink@GroupLink {connLinkContact}) <- withFastStore $ \db -> do gInfo <- getGroupInfo db vr user groupId gLink <- getGroupLink db user gInfo pure (gInfo, gLink) + sLnk <- case toShortLinkContact connLinkContact of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "failed to add relays: no short link in group link" relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) - groupRelays <- addRelays user gInfo relays + groupRelays <- addRelays user gInfo sLnk relays pure $ CRGroupRelaysAdded user gInfo gLink groupRelays APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId @@ -3571,13 +3574,45 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) - addRelays :: User -> GroupInfo -> [UserChatRelay] -> CM [GroupRelay] - addRelays _user _gInfo _relays = do - -- TODO [relays] owner: send contact requests to relays - -- TODO - create relay member connections, relay records (group_relays), relay status: RSNew - -- TODO - send requests to relays: INV message - XGrpRelayInv, relay status: RSInvited - -- TODO - agent joinConnectionAsync for contact links (currently prohibited) - pure [] + addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [GroupRelay] + addRelays user gInfo@GroupInfo {membership} groupSLink relays = + forM relays $ \relay -> addRelay relay + where + addRelay :: UserChatRelay -> CM GroupRelay + addRelay relay@UserChatRelay {address} = do + -- TODO [relays] owner: can update relay profile from data retrieved via getConnShortLink + (cReq, _cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) address + lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + Just (agentV, _) -> do + let chatV = agentToChatVersion agentV + gVar <- asks random + subMode <- chatReadVar subscriptionMode + -- TODO [relays] owner: replace with async join (joinConnectionAsync currently prohibited for contact links) + -- TODO - or make "add relays" api retriable, via prepared connection + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + (relayMember, conn, groupRelay) <- withStore $ \db -> do + groupRelay <- createGroupRelayRecord db gInfo relay + relayMember <- createRelayMemberRecord db vr gVar user gInfo relay groupRelay + conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode + pure (relayMember, conn, groupRelay) + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + GroupMember {memberRole = relayRole, memberId = relayMemberId} = relayMember + relayInv = GroupRelayInvitation { + fromMember = MemberIdRole userMemberId userRole, + fromMemberProfile = membershipProfile, + invitedMember = MemberIdRole relayMemberId relayRole, + groupLink = groupSLink + } + dm <- encodeConnInfo $ XGrpRelayInv relayInv + (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode + let newConnStatus = if sqSecured then ConnSndReady else ConnJoined + groupRelay' <- withFastStore' $ \db -> do + void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus + updateRelayStatusFromTo db groupRelay RSNew RSInvited + pure groupRelay' drgRandomBytes :: Int -> CM ByteString drgRandomBytes n = asks random >>= atomically . C.randomBytes n privateGetUser :: UserId -> CM User diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 692cfe0920..a6f0caf813 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1150,7 +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 + XGrpRelayInv groupRelayInv -> relayContactRequest groupRelayInv -- TODO show/log error, other events in contact request _ -> pure () MERR _ err -> do @@ -1319,8 +1319,8 @@ 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 + relayContactRequest :: GroupRelayInvitation -> CM () + relayContactRequest _groupRelayInv = 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 diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 88da258813..264baa9f54 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -328,7 +328,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json - XGrpRelayInv :: ShortLinkContact -> ChatMsgEvent 'Json + XGrpRelayInv :: GroupRelayInvitation -> ChatMsgEvent 'Json XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json @@ -1100,7 +1100,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId" - XGrpRelayInv_ -> XGrpRelayInv <$> p "groupLink" + XGrpRelayInv_ -> XGrpRelayInv <$> p "groupRelayInvitation" XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" <*> opt "scope" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" @@ -1161,7 +1161,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId] - XGrpRelayInv groupLink -> o ["groupLink" .= groupLink] + XGrpRelayInv groupRelayInv -> o ["groupRelayInvitation" .= groupRelayInv] XGrpRelayAcpt relayLink -> o ["relayLink" .= relayLink] XGrpMemNew memInfo scope -> o $ ("scope" .=? scope) ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 57a86afa09..08dfd4645d 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -149,13 +149,13 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do 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, + mu.is_chat_relay, NULL, 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.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 + m.is_chat_relay, r.group_relay_id, r.chat_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 diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 715741676e..cfa5c3cb45 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -74,6 +74,11 @@ module Simplex.Chat.Store.Groups getContactGroupPreferences, getGroupInvitation, createNewContactMember, + createGroupRelayRecord, + getGroupRelayById, + createRelayMemberRecord, + createRelayConnection, + updateRelayStatusFromTo, createNewContactMemberAsync, createJoiningMember, getMemberJoinRequest, @@ -156,6 +161,7 @@ import Crypto.Random (ChaChaDRG) import Data.Bifunctor (second) import Data.Char (toLower) import Data.Either (rights) +import Data.Functor (($>)) import Data.Int (Int64) import Data.List (partition, sortOn) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) @@ -165,6 +171,7 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Messages +import Simplex.Chat.Operators import Simplex.Chat.Protocol hiding (Binary) import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared @@ -190,11 +197,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 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) +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 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') :. (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 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, chatRelayId, 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, chatRelayId, relayStatus, relayLink)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -1122,6 +1129,95 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, :. (minV, maxV) ) +createGroupRelayRecord :: DB.Connection -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupRelay +createGroupRelayRecord db GroupInfo {groupId} UserChatRelay {chatRelayId} = do + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + INSERT INTO group_relays + (group_id, chat_relay_id, relay_status, created_at, updated_at) + VALUES (?,?,?,?,?) + |] + (groupId, chatRelayId, RSNew, currentTs, currentTs) + relayId <- liftIO $ insertedRowId db + getGroupRelayById db relayId + +getGroupRelayById :: DB.Connection -> Int64 -> ExceptT StoreError IO GroupRelay +getGroupRelayById db relayId = + ExceptT . firstRow toGroupRelay (SEGroupRelayNotFound relayId) $ + DB.query + db + [sql| + SELECT group_relay_id, chat_relay_id, chat_relay_id, relay_status, relay_link + FROM group_relays + WHERE group_relay_id = ? + |] + (Only relayId) + where + toGroupRelay :: (Int64, Int64, RelayStatus, Maybe ShortLinkContact) -> GroupRelay + toGroupRelay (groupRelayId, userChatRelayId, relayStatus, relayLink) = + GroupRelay {groupRelayId, userChatRelayId, relayStatus, relayLink} + +-- TODO [relays] TBC role, category, relay profile +-- TODO - GCInviteeMember -> GCRelayMember? +-- TODO - GRMember -> GRRelay? +-- TODO - create 1 profile per relay, link to chat_relays? +createRelayMemberRecord :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> GroupRelay -> ExceptT StoreError IO GroupMember +createRelayMemberRecord db vr gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {name} GroupRelay {groupRelayId} = do + currentTs <- liftIO getCurrentTime + let relayProfile = profileFromName name + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user relayProfile currentTs + groupMemberId <- createWithRandomId gVar $ \memId -> do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_profile_id, created_at, updated_at, + is_chat_relay, group_relay_id + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, MemberId memId, GRMember, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser, groupMemberId' membership) + :. (userId, localDisplayName, memProfileId, currentTs, currentTs) + :. (BI True, groupRelayId) + ) + insertedRowId db + getGroupMemberById db vr user groupMemberId + +createRelayConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection +createRelayConnection db vr user@User {userId} groupMemberId agentConnId connStatus chatV subMode = do + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, conn_status, conn_type, + group_member_id, conn_chat_version, to_subscribe, pq_support, pq_encryption, + created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, agentConnId, 0 :: Int, connStatus, ConnMember) + :. (groupMemberId, chatV, BI (subMode == SMOnlyCreate), PQSupportOff, PQSupportOff) + :. (currentTs, currentTs) + ) + connId <- liftIO $ insertedRowId db + getConnectionById db vr user connId + +updateRelayStatusFromTo :: DB.Connection -> GroupRelay -> RelayStatus -> RelayStatus -> IO GroupRelay +updateRelayStatusFromTo db relay@GroupRelay {groupRelayId} fromStatus toStatus = do + maybeFirstRow fromOnly (DB.query db "SELECT relay_status FROM group_relays WHERE group_relay_id = ?" (Only groupRelayId)) >>= \case + Just status | status == fromStatus -> updateRelayStatus_ db groupRelayId toStatus $> relay {relayStatus = toStatus} + _ -> pure relay + +updateRelayStatus_ :: DB.Connection -> Int64 -> RelayStatus -> IO () +updateRelayStatus_ db relayId relayStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ?" (relayStatus, currentTs, relayId) + createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO () createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode = createWithRandomId gVar $ \memId -> do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 98c7abceea..62541773d4 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -679,7 +679,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe 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 + m.is_chat_relay, r.group_relay_id, r.chat_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 @@ -3005,7 +3005,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do 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, + m.is_chat_relay, r.group_relay_id, r.chat_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 @@ -3014,14 +3014,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do 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, + rm.is_chat_relay, rr.group_relay_id, rr.chat_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.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.is_chat_relay, dbr.group_relay_id, dbr.relay_status, dbr.relay_link + dbm.is_chat_relay, dbr.group_relay_id, dbr.chat_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 m ON m.group_member_id = i.group_member_id 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 d2c8f41a8c..17ec38ace1 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251018_chat_relays.hs @@ -53,7 +53,9 @@ CREATE TABLE group_relays( 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 + relay_link BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); 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); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 2870e8e761..f1611da074 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -747,7 +747,9 @@ CREATE TABLE group_relays( 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 + relay_link BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index c0ded28bd2..05dddc61eb 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -148,6 +148,7 @@ data StoreError | SEOperatorNotFound {serverOperatorId :: Int64} | SEUsageConditionsNotFound | SEUserChatRelayNotFound {chatRelayId :: Int64} + | SEGroupRelayNotFound {groupRelayId :: Int64} | SEInvalidQuote | SEInvalidMention | SEInvalidDeliveryTask {taskId :: Int64} @@ -657,7 +658,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe ShortLinkContact) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow -type GroupMemberRow = (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 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 Int64, Maybe RelayStatus, Maybe ShortLinkContact) type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) @@ -679,7 +680,7 @@ toPreparedGroup = \case _ -> Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -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)) = +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_, chatRelayId_, relayStatus_, relayLink)) = let memberProfile = rowToLocalProfile profileRow memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ @@ -698,8 +699,8 @@ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, } _ -> Nothing isChatRelay = BoolDef isCRelay - relayData = case (groupRelayId_, relayStatus_) of - (Just groupRelayId, Just relayStatus) -> Just GroupRelay {groupRelayId, relayStatus, relayLink} + relayData = case (groupRelayId_, chatRelayId_, relayStatus_) of + (Just groupRelayId, Just userChatRelayId, Just relayStatus) -> Just GroupRelay {groupRelayId, userChatRelayId, relayStatus, relayLink} _ -> Nothing in GroupMember {..} @@ -711,7 +712,7 @@ groupMemberQuery = 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, + m.is_chat_relay, r.group_relay_id, r.chat_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, @@ -737,7 +738,7 @@ toBusinessChatInfo _ = Nothing groupInfoQuery :: Query groupInfoQuery = groupInfoQueryFields <> " " <> groupInfoQueryFrom --- membership "member" never references group_relays, therefore `NULL, NULL, NULL` which avoids extra join +-- membership "member" never references group_relays, therefore `NULL, NULL, NULL, NULL` which avoids extra join groupInfoQueryFields :: Query groupInfoQueryFields = [sql| @@ -756,7 +757,7 @@ groupInfoQueryFields = 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 + mu.is_chat_relay, NULL, NULL, NULL, NULL |] groupInfoQueryFrom :: Query diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 1b06c27cdd..af7bc41747 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -820,8 +820,9 @@ data GroupLinkRejection = GroupLinkRejection data GroupRelayInvitation = GroupRelayInvitation { fromMember :: MemberIdRole, + fromMemberProfile :: Profile, invitedMember :: MemberIdRole, - groupProfile :: GroupProfile + groupLink :: ShortLinkContact } deriving (Eq, Show) @@ -968,6 +969,7 @@ data GroupMember = GroupMember data GroupRelay = GroupRelay { groupRelayId :: Int64, + userChatRelayId :: Int64, -- ID of configured UserChatRelay relayStatus :: RelayStatus, relayLink :: Maybe ShortLinkContact } @@ -2076,6 +2078,8 @@ $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkRejection) +$(JQ.deriveJSON defaultJSON ''GroupRelayInvitation) + $(JQ.deriveJSON defaultJSON ''IntroInvitation) $(JQ.deriveJSON defaultJSON ''MemberRestrictions)