mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:07:53 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
119 lines
4.1 KiB
Markdown
119 lines
4.1 KiB
Markdown
# 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`
|
|
|
|
```json
|
|
{
|
|
"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"
|
|
}
|
|
```
|
|
|
|
## Somebody changed the membership: `groupInfo.type` is `UPDATE`
|
|
|
|
The only frame that names *who* added the bot to a group, which is what
|
|
`signal_bot/trust.py` needs. It carries no `message` at all, so it counts
|
|
toward nothing and answers nothing; `source`/`sourceNumber` is the person
|
|
who made the change. Captured live on 2026-08-29:
|
|
|
|
```json
|
|
{
|
|
"envelope": {
|
|
"source": "+48123456789",
|
|
"sourceNumber": "+48123456789",
|
|
"sourceUuid": "b1c2d3e4-…",
|
|
"sourceName": "kuhy",
|
|
"sourceDevice": 1,
|
|
"timestamp": 1788006851503,
|
|
"dataMessage": {
|
|
"timestamp": 1788006851503,
|
|
"message": null,
|
|
"groupInfo": { "groupId": "Y29uZmlnLWdy/…", "groupName": "testbot", "type": "UPDATE" }
|
|
}
|
|
},
|
|
"account": "+420111222333"
|
|
}
|
|
```
|
|
|
|
Note `source` here is a **phone number**, not the display name this document
|
|
used to show for `dataMessage`. Both spellings have been observed, which is
|
|
why `models.py` reads the sender's number from `sourceNumber` and nothing
|
|
trusts `source`.
|
|
|
|
## From the bot's own account: `syncMessage.sentMessage`
|
|
|
|
When the account is deployed as a *linked device*, its own outgoing messages
|
|
come back as sync messages. Same fields, one level deeper.
|
|
|
|
> **This deployment never receives them.** The bot registers its own number as
|
|
> its own primary device and has no linked devices, so Signal sends it no copy
|
|
> of its own traffic. The consequence to know before you try to test anything:
|
|
> the bot **cannot self-trigger a command**. Posting `!cat` from the bot's
|
|
> number into a group does nothing whatsoever — a human has to type it, and
|
|
> `journalctl --user -u signal-bot.service` showing `running command …` is how
|
|
> you confirm it landed. The parsing below stays because the linked-device
|
|
> deployment is still supported, not because it fires here.
|
|
|
|
```json
|
|
{
|
|
"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 would receive 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.
|