From e58aa1625e8c397384d7bb8f0bb5117c71a731d8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:01:46 +0400 Subject: [PATCH] wip --- bots/api/COMMANDS.md | 6 +++--- bots/src/API/Docs/Commands.hs | 2 +- .../simplex-chat-client/types/typescript/src/commands.ts | 2 +- packages/simplex-chat-client/typescript/src/client.ts | 2 +- packages/simplex-chat-nodejs/src/api.ts | 3 ++- packages/simplex-chat-python/src/simplex_chat/api.py | 1 + .../simplex-chat-python/src/simplex_chat/types/_commands.py | 2 +- src/Simplex/Chat/Library/Commands.hs | 4 ++-- src/Simplex/Chat/Library/Subscriber.hs | 3 +++ src/Simplex/Chat/Messages/Batch.hs | 5 +---- 10 files changed, 16 insertions(+), 14 deletions(-) diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 5bf7064ec6..463dd4088b 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -289,15 +289,15 @@ Send messages. **Syntax**: ``` -/_send [ live=on][ ttl=] json +/_send [ live=on][ ttl=][ sign=on] json ``` ```javascript -'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript +'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + (signMessages ? ' sign=on' : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript ``` ```python -'/_send ' + ChatRef_cmd_string(sendRef) + (' live=on' if liveMessage else '') + ((' ttl=' + str(ttl)) if ttl is not None else '') + ' json ' + json.dumps(composedMessages) # Python +'/_send ' + ChatRef_cmd_string(sendRef) + (' live=on' if liveMessage else '') + ((' ttl=' + str(ttl)) if ttl is not None else '') + (' sign=on' if signMessages else '') + ' json ' + json.dumps(composedMessages) # Python ``` **Responses**: diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index ddfc817fa2..3aa054af0c 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -86,7 +86,7 @@ chatCommandsDocsData = ), ( "Message commands", "Commands to send, update, delete, moderate messages and set message reactions", - [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems", "CRChatCmdError"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> " json " <> Json "composedMessages"), + [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems", "CRChatCmdError"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> OnOffParam "sign" "signMessages" (Just False) <> " json " <> Json "composedMessages"), ( "APIUpdateChatItem", [], "Update message.", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 1656b54f68..3f4e3ad7ee 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -97,7 +97,7 @@ export namespace APISendMessages { export type Response = CR.NewChatItems | CR.ChatCmdError export function cmdString(self: APISendMessages): string { - return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) + return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + (self.signMessages ? ' sign=on' : '') + ' json ' + JSON.stringify(self.composedMessages) } } diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index 12fa150fc4..10c4ee0a81 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -121,7 +121,7 @@ export class ChatClient { async apiSendMessages(chatType: T.ChatType, chatId: number, messages: T.ComposedMessage[]): Promise { const r = await this.sendChatCmd( - CC.APISendMessages.cmdString({sendRef: {chatType, chatId}, composedMessages: messages, liveMessage: false}) + CC.APISendMessages.cmdString({sendRef: {chatType, chatId}, composedMessages: messages, liveMessage: false, signMessages: false}) ) if (r.type === "newChatItems") return r.chatItems throw new ChatCommandError("unexpected response", r) diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 0d3339df9a..77c87d899b 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -423,7 +423,8 @@ export class ChatApi { CC.APISendMessages.cmdString({ sendRef, composedMessages: messages, - liveMessage + liveMessage, + signMessages: false }) ) if (r.type === "newChatItems") return r.chatItems diff --git a/packages/simplex-chat-python/src/simplex_chat/api.py b/packages/simplex-chat-python/src/simplex_chat/api.py index ef37e28384..e5b885fc17 100644 --- a/packages/simplex-chat-python/src/simplex_chat/api.py +++ b/packages/simplex-chat-python/src/simplex_chat/api.py @@ -193,6 +193,7 @@ class ChatApi: "sendRef": send_ref, "composedMessages": messages, "liveMessage": live_message, + "signMessages": False, } ) ) diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index 882faa0180..a7f4a56465 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -85,7 +85,7 @@ class APISendMessages(TypedDict): def APISendMessages_cmd_string(self: APISendMessages) -> str: - return '/_send ' + T.ChatRef_cmd_string(self['sendRef']) + (' live=on' if self['liveMessage'] else '') + ((' ttl=' + str(self.get('ttl'))) if self.get('ttl') is not None else '') + ' json ' + json.dumps(self['composedMessages']) + return '/_send ' + T.ChatRef_cmd_string(self['sendRef']) + (' live=on' if self['liveMessage'] else '') + ((' ttl=' + str(self.get('ttl'))) if self.get('ttl') is not None else '') + (' sign=on' if self['signMessages'] else '') + ' json ' + json.dumps(self['composedMessages']) APISendMessages_Response = CR.NewChatItems | CR.ChatCmdError diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 8394d43d84..1e2fb9f541 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -839,10 +839,10 @@ processChatCommand cxt nm = \case SMDRcv -> False itemsMsgIds :: [CChatItem c] -> [SharedMsgId] itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) - -- per-item self/history delete signer: sign iff the target item was held signed (preserves self-delete deniability) + -- history delete always signs (attributable owner action); self-delete signs iff the target was held signed (deniability) delEventSigned :: GroupInfo -> Maybe GroupChatScopeInfo -> Bool -> CChatItem 'CTGroup -> Maybe (Maybe MsgSigning, ChatMsgEvent 'Json) delEventSigned gInfo chatScopeInfo onlyHistory (CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId, msgSigned}}) = - (\msgId -> let evt = XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) onlyHistory in (groupMsgSigning (isJust msgSigned) gInfo evt, evt)) <$> itemSharedMsgId + (\msgId -> let evt = XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) onlyHistory in (groupMsgSigning (onlyHistory || isJust msgSigned) gInfo evt, evt)) <$> itemSharedMsgId APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds -- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 7ec9f822d4..560e41003b 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2351,6 +2351,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | senderRole < GRModerator -> do messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e pure Nothing + -- a forged unsigned moderation would pre-censor a not-yet-received post via CIModeration; require verified (relay moderation always signs) + | useRelays' gInfo && msgSigned /= Just MSSVerified -> + messageError ("x.msg.del: unverified moderation of message not yet received, " <> tshow e) $> Nothing | otherwise -> case scope_ of Just (MSMember scopeMemberId) -> withStore $ \db -> do diff --git a/src/Simplex/Chat/Messages/Batch.hs b/src/Simplex/Chat/Messages/Batch.hs index 987c0b40bc..81861aad74 100644 --- a/src/Simplex/Chat/Messages/Batch.hs +++ b/src/Simplex/Chat/Messages/Batch.hs @@ -105,10 +105,7 @@ batchDeliveryTasks1 _vr maxLen = toResult . foldl' addToBatch ([], [], [], 0, 0) -- | Encode a batch element for relay groups: >[/]. encodeFwdElement :: GrpMsgForward -> VerifiedMsg 'Json -> ByteString -encodeFwdElement fwd@GrpMsgForward {fwdSender} verifiedMsg - | FwdChannel <- fwdSender, isJust signedMsg_ = - error "encodeFwdElement: signed message must forward as FwdMember, not FwdChannel" - | otherwise = ">" <> smpEncode fwd <> encodeBatchElement signedMsg_ msgBody +encodeFwdElement fwd verifiedMsg = ">" <> smpEncode fwd <> encodeBatchElement signedMsg_ msgBody where (_, signedMsg_, msgBody) = verifiedMsgParts verifiedMsg