signal-bot/DOCS-signal-envelopes.md
Krzysztof kuhy Rudnicki 5d76adf4db feat: watch several groups, and answer only where you were asked
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 15:03:24 +02:00

2.5 KiB

Signal envelope shapes

Reference for the two JSON frames signal-cli-rest-api pushes down ws://…/v1/receive/{number}. signal_bot/models.py flattens both into one IncomingMessage; these are the shapes it flattens.

From another account: dataMessage

{
  "envelope": {
    "source": "NAME",
    "sourceNumber": "PHONE_NUMBER",
    "sourceUuid": "SOURCE_ID",
    "sourceName": "USER_DEFINED_NAME",
    "sourceDevice": 69,
    "timestamp": 1719177917717,
    "dataMessage": {
      "timestamp": 1719177917717,
      "message": "MESSAGE_CONTENT",
      "expiresInSeconds": 0,
      "viewOnce": false,
      "sticker": { "packId": "166dd1b5…", "stickerId": 6 },
      "groupInfo": { "groupId": "ID_OF_GROUP", "type": "DELIVER" }
    }
  },
  "account": "BOT_ACCOUNT_PHONE_NUMBER"
}

From the bot's own account: syncMessage.sentMessage

Because the bot runs as a linked device, the account's own outgoing messages come back as sync messages. Same fields, one level deeper.

{
  "envelope": {
    "sourceUuid": "SOURCE_ID",
    "sourceName": "USER_DEFINED_NAME",
    "timestamp": 1719177728639,
    "syncMessage": {
      "sentMessage": {
        "destination": "DEST_NAME",
        "destinationNumber": "DEST_PHONE_NUMBER",
        "timestamp": 1719177728639,
        "message": "MESSAGE_CONTENT",
        "sticker": { "packId": "166dd1b5…", "stickerId": 6 },
        "groupInfo": { "groupId": "ID_OF_GROUP", "type": "DELIVER" }
      }
    }
  },
  "account": "BOT_ACCOUNT_PHONE_NUMBER"
}

The trap this caused

The original bot read envelope.dataMessage.sticker even when the frame was a syncMessage. Its own stickers therefore never matched the sticker filter and were counted as messages. Four consecutive commits (f1931b8, b325126, 62352e5, d35aa59) fought the symptom.

models.py resolves the payload first — dataMessage or syncMessage.sentMessage — and reads sticker, reaction, message and groupInfo off whichever it found. tests/test_models.py asserts both shapes.

No groupInfo means a direct message

A frame without groupInfo arrived in a one-to-one conversation. A linked device receives every one of those on the account, so counting.watched_group rejects them explicitly rather than relying on a falsy comparison. See the scoping scenario in fake_signal/scenarios/.

Frames the bot ignores

Receipts, typing indicators and anything else with no dataMessage and no syncMessage.sentMessage parse to None and are dropped.