From 8784b3f44ddca9bdae2063a93e98c9fb0b782453 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:17:27 +0400 Subject: [PATCH] rework api --- bots/api/COMMANDS.md | 1 - bots/src/API/Docs/Commands.hs | 2 + bots/src/API/Docs/Responses.hs | 2 +- .../types/typescript/src/commands.ts | 1 - src/Simplex/Chat/Controller.hs | 11 +- src/Simplex/Chat/Library/Commands.hs | 138 +++++++++--------- src/Simplex/Chat/Library/Subscriber.hs | 1 + src/Simplex/Chat/View.hs | 4 +- 8 files changed, 79 insertions(+), 81 deletions(-) diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index fe552cb839..f3bb915665 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -906,7 +906,6 @@ Create group. **Parameters**: - userId: int64 - incognito: bool -- useRelays: bool - groupProfile: [GroupProfile](./TYPES.md#groupprofile) **Syntax**: diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 3cfedd11e2..cff0e093a7 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -234,6 +234,7 @@ cliCommands = "MemberRole", "MuteUser", "NewGroup", + "NewPublicGroup", "QuitChat", "ReactToMessage", "RejectContact", @@ -363,6 +364,7 @@ undocumentedCommands = "APIHideUser", "APIImportArchive", "APIMuteUser", + "APINewPublicGroup", "APIPlanForwardChatItems", "APIPrepareContact", "APIPrepareGroup", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 87b7e8d07d..125b7eb915 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -160,7 +160,6 @@ undocumentedResponses = "CRGroupMemberSwitchAborted", "CRGroupMemberSwitchStarted", "CRGroupProfile", - "CRGroupRelaysAdded", "CRGroupUserChanged", "CRItemsReadForChat", "CRJoinedGroupMember", @@ -175,6 +174,7 @@ undocumentedResponses = "CRNtfConns", "CRNtfToken", "CRNtfTokenStatus", + "CRPublicGroupCreated", "CRQueueInfo", "CRRcvStandaloneFileCreated", "CRReactionMembers", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index e1866b5ef1..de6a7a7ce1 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -330,7 +330,6 @@ export namespace APIListMembers { export interface APINewGroup { userId: number // int64 incognito: boolean - useRelays: boolean groupProfile: T.GroupProfile } diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 3c637fb3b6..f721f97169 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -379,8 +379,6 @@ data ChatCommand | APIListMembers {groupId :: GroupId} | APIUpdateGroupProfile {groupId :: GroupId, groupProfile :: GroupProfile} | APICreateGroupLink {groupId :: GroupId, memberRole :: GroupMemberRole} - | APICreateRelayedGroupLink {groupId :: GroupId, autoChooseRelays :: Bool} -- TODO [relays] owner: TBC memberRole - | APIAddRelays {groupId :: GroupId, relayIds :: NonEmpty Int64} | APIGroupLinkMemberRole {groupId :: GroupId, memberRole :: GroupMemberRole} | APIDeleteGroupLink {groupId :: GroupId} | APIGetGroupLink {groupId :: GroupId} @@ -506,8 +504,11 @@ 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, useRelays :: Bool, groupProfile :: GroupProfile} - | NewGroup IncognitoEnabled Bool GroupProfile + | APINewGroup {userId :: UserId, incognito :: IncognitoEnabled, groupProfile :: GroupProfile} + | NewGroup IncognitoEnabled GroupProfile + -- TODO [relays] owner: TBC group link's default member role for APINewChannel + | APINewPublicGroup {userId :: UserId, incognito :: IncognitoEnabled, relayIds :: NonEmpty Int64, groupProfile :: GroupProfile} + | NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | AcceptMember GroupName ContactName GroupMemberRole @@ -681,6 +682,7 @@ data ChatResponse | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} | CRGroupCreated {user :: User, groupInfo :: GroupInfo} + | CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CRGroupMembers {user :: User, group :: Group} | CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} -- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]} @@ -745,7 +747,6 @@ data ChatResponse | CRGroupProfile {user :: User, groupInfo :: GroupInfo} | CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink} - | CRGroupRelaysAdded {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} | CRGroupLink {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink} | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} | CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index bbbd3768aa..9ad00fd2a3 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2238,21 +2238,58 @@ processChatCommand vr nm = \case chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction - 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 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: user should be prompted to choose/auto-choose relays for new group - -- TODO - can choose relays from APIGetUserServers -> CRUserServers, [UserOperatorServers] + APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do + gInfo <- newGroup user incognito gProfile False pure $ CRGroupCreated user gInfo - NewGroup incognito useRelays gProfile -> withUser $ \User {userId} -> - processChatCommand vr nm $ APINewGroup userId incognito useRelays gProfile + NewGroup incognito gProfile -> withUser $ \User {userId} -> + processChatCommand vr nm $ APINewGroup userId incognito gProfile + APINewPublicGroup userId incognito relayIds gProfile -> withUserId userId $ \user -> do + -- TODO [relays] owner: catch errors and clean up in case any step fails - delete group + -- TODO - create group record with "in_progress" field to hide from list in case app crashes, + -- TODO then pick up in cleanup manager + gInfo <- newGroup user incognito gProfile True + (gInfo', gLink, sLnk) <- newGroupLink user gInfo + relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) + groupRelays <- addRelays user gInfo' sLnk relays + pure $ CRPublicGroupCreated user gInfo' gLink groupRelays + where + newGroupLink :: User -> GroupInfo -> CM (GroupInfo, GroupLink, ShortLinkContact) + newGroupLink user gInfo@GroupInfo {groupProfile} = do + groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode + let userData = encodeShortLinkData $ GroupShortLinkData groupProfile + crClientData = encodeJSON $ CRDataGroup groupLinkId + -- TODO [relays] owner: prepare group link without initially creating on server + -- TODO - add link and owner key to group profile, sign profile + -- TODO - create group link on server with signed profile as data + -- vvv replace 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 + Just sl -> pure sl + Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" + let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk} + userData' = encodeShortLinkData $ GroupShortLinkData groupProfile' + -- same link with updated profile + _sLnk' <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm connId SCMContact userData' (Just crClientData)) + -- ^^^ to here ^^^ + gVar <- asks random + (gInfo', gLink) <- withFastStore $ \db -> do + gLink <- createGroupLink db gVar user gInfo connId ccLink' groupLinkId GRMember subMode + gInfo' <- updateGroupProfile db user gInfo groupProfile' + pure (gInfo', gLink) + pure (gInfo', gLink, sLnk) + -- auto-selection of enabled relays, currently unused + -- chooseRelays :: User -> CM [UserChatRelay] + -- chooseRelays user = do + -- -- TODO [relays] owner: more advanced relay selection strategy, e.g. pick from different operators + -- chatRelays <- withFastStore' (`getChatRelays` user) + -- let enabledRelays = filter (\UserChatRelay {enabled} -> enabled) chatRelays + -- selectedRelays <- take 3 <$> liftIO (shuffle enabledRelays) + -- when (null selectedRelays) $ throwChatError $ CEException "failed to select relays: no enabled relays configured" + -- pure selectedRelays + NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> + processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId @@ -2679,59 +2716,6 @@ processChatCommand vr nm = \case gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo gLink - APICreateRelayedGroupLink groupId autoChooseRelays -> withUser $ \user -> withGroupLock "createRelayedGroupLink" groupId $ do - gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db vr user groupId - assertUserGroupRole gInfo GROwner - groupLinkId <- GroupLinkId <$> drgRandomBytes 16 - subMode <- chatReadVar subscriptionMode - let userData = encodeShortLinkData $ GroupShortLinkData groupProfile - crClientData = encodeJSON $ CRDataGroup groupLinkId - -- TODO [relays] below to be replaced with: - -- 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 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 - Just sl -> pure sl - Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" - let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk} - userData' = encodeShortLinkData $ GroupShortLinkData groupProfile' - -- 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 - gInfo' <- updateGroupProfile db user gInfo groupProfile' - pure (gLink, gInfo') - if autoChooseRelays - then do - relays <- chooseRelays user - groupRelays <- addRelays user gInfo' sLnk relays - pure $ CRGroupRelaysAdded user gInfo' gLink groupRelays - else - pure $ CRGroupLinkCreated user gInfo' gLink - where - chooseRelays user = do - -- TODO [relays] owner: more advanced relay selection strategy, e.g. pick from different operators - chatRelays <- withFastStore' (`getChatRelays` user) - let enabledRelays = filter (\UserChatRelay {enabled} -> enabled) chatRelays - selectedRelays <- take 3 <$> liftIO (shuffle enabledRelays) - 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@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 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 gLnk@GroupLink {acceptMemberRole} <- withFastStore $ \db -> getGroupLink db user gInfo @@ -3553,6 +3537,18 @@ processChatCommand vr nm = \case groupId <- getGroupIdByName db user gName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) + newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> CM GroupInfo + newGroup user incognito gProfile@GroupProfile {displayName} useRelays = do + checkValidName displayName + gVar <- asks random + -- [incognito] generate incognito profile for group membership + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile useRelays + let cd = CDGroupSnd gInfo Nothing + createInternalChatItem user cd CIChatBanner (Just epochStart) + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo + pure gInfo sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo @@ -4653,8 +4649,10 @@ chatCommandP = ("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help db" <|> "/hd") $> ChatHelp HSDatabase, ("/help" <|> "/h") $> ChatHelp HSMain, - ("/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), + ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), + "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), + ("/public group") *> (NewPublicGroup <$> incognitoP <*> _strP <* A.space <* char_ '#' <*> groupProfile), + "/_public group " *> (APINewPublicGroup <$> A.decimal <*> incognitoOnOffP <*> _strP <* A.space <*> jsonP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)), "/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), @@ -4680,8 +4678,6 @@ chatCommandP = "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), - "/_create relayed link #" *> (APICreateRelayedGroupLink <$> A.decimal <*> (" auto_choose_relays=" *> onOffP)), - "/_add relays #" *> (APIAddRelays <$> A.decimal <*> _strP), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal), diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 3d6e35b5ac..ed61c6eb13 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -729,6 +729,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" XGrpRelayAcpt _relayLink -> do + liftIO $ print $ "XGrpRelayAcpt relayLink = " <> show _relayLink -- TODO [relays] owner: process relay acceptance -- TODO - * relay is invitee? other processing branch? -- TODO - * check processing client is owner, otherwise error diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index adbd86b9a0..c86f3aae03 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -177,6 +177,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRUserContactLinkUpdated u UserContactLink {addressSettings} -> ttyUser u $ viewAddressSettings addressSettings CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView + -- TODO [relays] CRPublicGroupCreated view - print group link, relays + CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms -- CRGroupConversationsArchived u _g _conversations -> ttyUser u [] @@ -241,8 +243,6 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g CRGroupLinkCreated u g gLink -> ttyUser u $ groupLink_ "Group link is created!" g gLink - -- TODO [relays] CRGroupRelaysAdded view - CRGroupRelaysAdded u g gLink _relays -> ttyUser u $ groupLink_ "Group link is created!" g gLink CRGroupLink u g gLink -> ttyUser u $ groupLink_ "Group link:" g gLink CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"]