mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:07:29 +01:00
feat: block binaries from a public repo, and write down the real attachment frame
Two halves of the same ask: the message database is private and must stay that way. The database was already gitignored, and the event table is pinned by a test that asserts its exact column set so no message body can be quietly added. What was missing was any defence against a binary reaching this *public* repo at all -- and the proof it was missing was sitting in the tree: three `bottles/**/indexeddb.sqlite` files, committed in "register the bot as its own Signal account". Their contents turned out to be harmless (one row, 120 bytes, a userPreferences store from developer.android.com), but the path into history was real. So the shared gate from ~/utils is wired in, the three files are untracked, and .gitignore now blocks *.sqlite3 along with its -wal and -shm companions rather than only the one filename. The second half is the attachment envelope, captured from the live API rather than assumed -- which is what 0.0.3 taught. A caption-less PNG posted in the real group confirmed `attachments[].contentType`, and settled something that was not guessable: a photo with no caption sends `message: null`. That is exactly the frame `should_count` used to value at zero. Verified end to end on production data, not a fixture: the frame at 19:31:16 was recorded as media_kind=image, media_count=1, char_len=0, filed under 2026-08-30 hour 19. The doc also records the fields nothing reads yet, and one open question left deliberately open: `caption` is a field on the attachment and was null here, so whether a captioned photo puts its text in `message` or in `attachments[].caption` has not been observed. If it is the latter, char_len is recording 0 for those. DEBUG stays on until a `caption: str` settles it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
This commit is contained in:
parent
50f6a587c2
commit
0f824c441e
9
.gitignore
vendored
9
.gitignore
vendored
@ -166,3 +166,12 @@ cython_debug/
|
||||
*.jpg
|
||||
*.gif
|
||||
nohup.out
|
||||
|
||||
# The message database is private and never leaves this machine. A live
|
||||
# SQLite database is three files, so all three are named.
|
||||
*.sqlite3
|
||||
*.sqlite3-wal
|
||||
*.sqlite3-shm
|
||||
|
||||
# Wine/Bottles scratch dir, never part of the bot.
|
||||
bottles/
|
||||
|
||||
@ -45,6 +45,16 @@ repos:
|
||||
language: pygrep
|
||||
types: [python]
|
||||
|
||||
# This repository is public and holds a private message database. The
|
||||
# database is gitignored, but three binary SQLite files reached the
|
||||
# history anyway before this gate existed, so .gitignore is evidently
|
||||
# not the whole defence. Exceptions go in .binary-allowlist.
|
||||
- id: no-binaries
|
||||
name: no binary or image files
|
||||
entry: scripts/check_no_binaries.sh
|
||||
language: script
|
||||
pass_filenames: true
|
||||
|
||||
# The shared 250-line cap. Checks only the files in the commit, so an
|
||||
# unrelated commit never breaks. No baseline, no allowlist -- those are
|
||||
# suppressions. The exemption list lives with the checker in ~/utils so
|
||||
|
||||
@ -28,6 +28,52 @@ Reference for the two JSON frames `signal-cli-rest-api` pushes down
|
||||
}
|
||||
```
|
||||
|
||||
## A message with an attachment
|
||||
|
||||
Captured from the live API on 2026-08-30 with `LOG_LEVEL=DEBUG`, which makes
|
||||
the dispatcher log each frame's shape (keys and types, never values -- see
|
||||
`signal_bot/envelope_shape.py`). This is a caption-less PNG posted in the real
|
||||
group, reproduced exactly as the logger described it:
|
||||
|
||||
```
|
||||
{account: str, envelope: {
|
||||
dataMessage: {
|
||||
attachments: [{caption: null, contentType: 'image/png', filename: null,
|
||||
height: int, id: str, size: int, uploadTimestamp: int,
|
||||
width: int} x1],
|
||||
expiresInSeconds: int,
|
||||
groupInfo: {groupId: str, groupName: str, revision: int, type: 'DELIVER'},
|
||||
isExpirationUpdate: bool,
|
||||
message: null,
|
||||
timestamp: int,
|
||||
viewOnce: bool},
|
||||
serverDeliveredTimestamp: int, serverReceivedTimestamp: int,
|
||||
source: str, sourceDevice: int, sourceName: str,
|
||||
sourceNumber: str, sourceUuid: str, timestamp: int}}
|
||||
```
|
||||
|
||||
Three things this settles, none of which were guessable:
|
||||
|
||||
- `attachments` is a list of objects and the type lives in `contentType`. That
|
||||
is what `models._attachments` reads and what `counting.media_kind` maps.
|
||||
- **A caption-less photo sends `message: null`**, not `""` and not an absent
|
||||
key. Before 0.1.0 `should_count` ended in `bool(message.text)`, so every one
|
||||
of these was silently worth nothing.
|
||||
- Read them off the *resolved* payload from `_payload()`, never off
|
||||
`envelope["dataMessage"]` -- see "The trap this caused" below.
|
||||
|
||||
Fields nothing reads yet, recorded so the next person does not have to capture
|
||||
them again: `expiresInSeconds`, `isExpirationUpdate`, `viewOnce`,
|
||||
`groupInfo.groupName`, `groupInfo.revision`, and the attachment's `caption`,
|
||||
`filename`, `width`, `height`, `size` and `uploadTimestamp`.
|
||||
|
||||
**Open question, deliberately not assumed.** `caption` is a field on the
|
||||
attachment, and it was `null` here because this photo had none. Whether a
|
||||
photo *with* a caption puts the text in `message` or in `attachments[].caption`
|
||||
has not been observed yet. `char_len` currently measures `message` only, so if
|
||||
it turns out to be the latter, captioned photos are recording a length of 0.
|
||||
Leave `LOG_LEVEL=DEBUG` on and watch for `caption: str` to settle it.
|
||||
|
||||
## Somebody changed the membership: `groupInfo.type` is `UPDATE`
|
||||
|
||||
The only frame that names *who* added the bot to a group, which is what
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
scripts/check_no_binaries.sh
Executable file
26
scripts/check_no_binaries.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Thin delegate to the shared gate in ~/utils.
|
||||
#
|
||||
# Copying the logic here is what lets one repo's idea of the rule drift from
|
||||
# every other repo's, so this script only locates the shared checker and
|
||||
# forwards its arguments.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_no_binaries.sh"
|
||||
|
||||
main() {
|
||||
if [[ ! -x "$SHARED_GATE" ]]; then
|
||||
echo "Error: shared gate not found at $SHARED_GATE" >&2
|
||||
echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2
|
||||
echo " UTILS_ROOT to where it lives." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bash "$SHARED_GATE" "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Reference in New Issue
Block a user