Commit Graph

51 Commits

Author SHA1 Message Date
c9a90eb48a 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
0c42a6367c feat: make the bot actually reply on Signal, and gate every way it went silent
Some checks are pending
CI / check (push) Waiting to run
First live reply, as its own registered account. `!cat` and `!dog` from the
phone return images -- verified in the production shape (the bot *invited to
an already-existing group*) and under a systemd user unit.

Four failures, each of which left the bot looking perfectly healthy:

- Invited is not joined. Signal only adds an account outright if the adder
  holds its profile key; with no profile the add becomes a pending invite,
  and signal-cli cannot accept one addressed to a phone number ("Cannot find
  service ID for self"). Group listed, websocket up, /healthz OK, zero
  messages. register_bot.sh now sets the profile *before* the add, and
  startup refuses on `member: false`.
- MODE=json-rpc-native is a GraalVM build with no AWT: text sends work,
  every attachment 400s with "Can't load library: awt". Both mode guards now
  demand exactly json-rpc, and start_container replaces a wrong-mode
  container instead of reusing it.
- The API spells one group two ways -- /v1/groups returns `group.<base64>`,
  envelopes carry what that decodes to -- so the scoping gate rejected the
  bot's own group. e2e.sh now models both spellings; reverting the fix fails
  4 scenarios, which is why the offline suite missed it before.
- BIND_PORT in .env was dead: __main__ read os.getenv before anything loaded
  a .env. The bind address comes from Config now.

A taken port used to be uvicorn logging the collision and exiting 0, which
every supervisor reads as success. portcheck.py refuses to start, exits
non-zero, and names the holder by reading /proc directly -- no subprocess, no
PATH assumptions, testable against a fake proc tree. Default port is 8137:
not 8000, and below the ephemeral range so the kernel cannot take it.

The bot now runs as signal-bot.service (Restart=always, linger checked rather
than assumed, StartLimitBurst so a config error fails visibly instead of
looping). Verified by SIGKILL and by re-running a command under the unit.

100% branch coverage, 230 tests, all gates green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 14:37:27 +02:00
53e231ee14 feat: register the bot as its own Signal account, no phone app needed
Some checks are pending
CI / check (push) Waiting to run
Shelter cannot work on this phone, and it would not have helped anyway.

It cannot work: setting a Device Owner makes Android enable
no_add_managed_profile, no_add_private_profile and no_add_clone_profile on
every user, which kills Shelter, Island, Private Space and app cloning
alike. Verified on-device rather than inferred -- `pm create-user
--profileOf 0 --managed` returns "Cannot add user: no_add_managed_profile is
enabled". This is the Android 11 COMP deprecation, enforced in three places
in DevicePolicyManagerService and unchanged through Android 16.

It would not have helped: a second Signal *app* is not a second Signal
*account*. Every account needs its own phone number, so the second number
was always the real requirement -- and once you have one, signal-cli can
register it as a PRIMARY device with no phone app, no QR scan and no work
profile at all. That is strictly better than both the linked-device setup
and the Shelter plan: the bot cannot read personal DMs and never posts as
the account owner.

scripts/register_bot.sh drives that flow. It gates on signal-cli >= 0.14.7
rather than hoping: stable container tag 0.100 bundles 0.14.5, and on
2026-03-06 Signal made the post-quantum ratchet mandatory and de-registered
every account whose client lacked it -- a stale client is how a bot dies
silently months later. Verified that the recommended CI tag really does
bundle 0.14.7 instead of assuming it.

Rehearsed offline, not written blind. fake_signal grew /v1/accounts,
/v1/register/{number}, /v1/register/{number}/verify/{token} and
/v1/groups/{number}, so the whole runbook including every error path
(missing captcha, stray verify, wrong code, unregistered number, empty group
list, out-of-range and non-numeric picks) runs against the fake. The picker
was proven to resolve the group the user actually pointed at.

Prompts now read from $SIGNAL_TTY instead of /dev/tty directly. A hard-coded
/dev/tty made the interactive path untestable without a real terminal; the
seam is the same idea as the injected clock and transport on the Python side,
and is what let the picker be rehearsed at all.

live_smoke.sh and register_bot.sh share scripts/lib_signal.sh so the two
cannot drift, and shellcheck now runs with -x so the sourced lib is actually
linted from its callers.

201 tests, 100% branch coverage, all 23 gates green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 13:41:13 +02:00
9193360461 feat: gate suppressions mechanically instead of relying on discipline
Some checks are pending
CI / check (push) Waiting to run
This repo has zero `# noqa` and zero `# type: ignore`, and the only reason it
stays that way is a gate. Nothing was enforcing it: testsAndMisc has a
no-noqa pre-commit hook and signal-bot did not, so the rule lived entirely in
whoever was editing. During this rewrite a suppression was reflexively typed
three times before being caught by hand, which is exactly the failure a hook
makes impossible.

Blocked in both places: a pygrep pre-commit hook, and a CI step mirroring it
so a commit made without hooks installed cannot land one on main. Verified
by canary: a file containing `# noqa: F401` exits 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 13:09:16 +02:00
749ad28f4b fix: pin setuptools to 84.0.0, closing the dependabot alert
The build-system pin was setuptools==80.9.0, which GHSA flags for an sdist
MANIFEST.in exclusion bypass via Unicode NFC/NFD collision on macOS APFS/HFS+
(fixed in 83.0.0). Build-backend requirements are a real dependency surface
and the freshness sweep had not covered them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 13:08:02 +02:00
afbc95a11b fix: make shellcheck agree between local and CI, and fix the group picker
CI failed on shellcheck where the local hook passed: the runner's shellcheck
reports SC2015 (`A && B || C` is not if-then-else) and mine did not. Both now
run with --severity=style so a version skew cannot pass locally and fail in
CI. The cleanup traps are plain ifs, which is what the finding was about --
`kill` on an already-dead pid makes B fail and C run while A is true.

live_smoke.sh choose_group had two bugs that only surface when it is actually
run, neither reachable from the offline suite:
- it fetched /v1/groups twice, once to display and once to resolve the picked
  index, so an unstable order would hand back a different group than the one
  shown. Fetched once into a variable now.
- `read -r -p` wrote its prompt to stdout, which the caller captures with
  $(...), so the prompt text would have been swallowed into the group id.
  Prompt and listing both go to stderr; stdout carries only the id.
Also validates that the pick is a number and in range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 13:07:28 +02:00
9299aa50a6 feat: rewrite the bot so it runs, and gate it like the other repos
The bot did not work. `run.sh` started a FastAPI app whose
`@app.on_event("startup")` awaited `asyncio.gather()` on two infinite
tasks, so uvicorn never finished starting up and no HTTP was ever served;
the readme's `python main.py` was a no-op with no `__main__` block and no
`uvicorn.run()` call. All three CI workflows were red, one referencing an
`environment.yml` that does not exist.

Rewritten as a package behind an injectable transport seam, which is what
makes the offline environment possible.

Bugs fixed:
- startup deadlock: lifespan spawns the loops instead of awaiting them
- sticker filter read dataMessage.sticker even on syncMessage envelopes, so
  the account's own stickers were never filtered (f1931b8, b325126, 62352e5,
  d35aa59 all fought this symptom)
- no websocket reconnect: a container restart killed the bot silently
- blocking `requests` inside async functions stalled the event loop
- every counted message DMed the whole tally to the bot itself
- images were written to CWD under a filename taken from the remote URL
- one global 10s cooldown shared across all users and commands
- dead code: RECEIVE_URL, get_attachments (no call site), USER_MESSAGE_COUNT
- .env.sample declared PRIMARY/SECONDARY_PHONE_NUMBER and MODE, none read

Scoping is now an enforced invariant. A linked device receives every DM and
every group on the account; GROUP_ID was the only thing narrowing that and
defaulted to "". Config refuses to start unscoped, direct messages are
dropped explicitly rather than by a falsy check, and a scenario asserts that
DMs and other groups produce zero outbound messages.

Offline environment: fake_signal/ serves the real protocol surface, so the
unmodified bot connects to it by pointing API_BASE_URL at it. scripts/e2e.sh
boots both, replays 7 scenarios and exits non-zero on mismatch. A local
Signal server is not achievable (signal-cli compiles its endpoints and trust
store in; Signal-Server needs FoundationDB), so faking this boundary is the
only way to test every feature without an account.

Transport pinned to bbernhard/signal-cli-rest-api:0.100 with
MODE=json-rpc-native. The readme pinned 0.167-dev; the dev tag series counts
separately, so that was older than 0.100, not newer. /v1/receive is only a
WebSocket in a json-rpc mode, so the mode is verified at startup.

Repo standards: ruff select=ALL, mypy --strict, 189 tests at 100% branch
coverage, 250-line cap, exact pins at newest stable, shared gates delegated
to ~/utils, one CI workflow replacing three red ones. No noqa, no
type: ignore, no coverage omits anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 13:05:42 +02:00
0075c4de82 feat: made it uvicorn fast api compliant 2024-10-11 19:27:15 +02:00
d35aa59a25 fix: do not count sticker messages 2024-10-05 10:57:31 +02:00
kuhyx
bc8bc307ef
added sticker to the sample message 2024-10-05 10:53:23 +02:00
64414450e8 feat: print when message will be counted 2024-10-05 10:22:16 +02:00
62352e532f fix: restore additional should count check 2024-10-05 10:18:02 +02:00
3c79bfb4eb fix: add should_count to message count inside send to group 2024-10-05 10:15:34 +02:00
0205627e26 fix: remove automatic message count whenever something gets send to group 2024-10-05 10:12:37 +02:00
b325126042 feat: added explanation why bot is not countnig message 2024-10-05 10:08:51 +02:00
f1931b800c feat: stickers no longer count towards messages rankngs 2024-10-05 10:01:28 +02:00
ae2a16c47d feat: traps are back 8) 2024-09-24 23:11:14 +02:00
kuhyx
6f322e0282
Update readme.md 2024-09-24 22:38:10 +02:00
cd606cf452 chore: added env sample file 2024-09-24 22:36:55 +02:00
470cc14c24 fix: updated readme, stopped traps untiull fixed 2024-09-24 22:19:58 +02:00
ef0c46f64d feat: made run sh into executable script 2024-09-24 19:37:59 +02:00
5bc6e5014f feat: make bot react only to commands with "!" 2024-06-24 23:34:58 +02:00
369332debf feat: add 10 seconds delay before sending command 2024-06-24 23:29:34 +02:00
3b7fe604dc fix: removed unnecessary async in is_message_reaction 2024-06-24 21:12:12 +02:00
e8efcfffcb feat: ignore reaction messages 2024-06-24 21:10:10 +02:00
6a897a80a2 feat: more pies edge cases 2024-06-24 21:04:18 +02:00
14aa602161 feat: added common name to send message 2024-06-24 20:43:31 +02:00
062bf9abcc feat: make message counting work for group (hopefully) 2024-06-24 20:22:05 +02:00
6ec89d8e34 feat: count messages send by id 2024-06-24 20:18:51 +02:00
807a0ef9f5 fix: message_value != None 2024-06-24 19:47:40 +02:00
55c4bf653a feat: sendding commands works for emoji messages 2024-06-24 19:47:23 +02:00
08099d7067 feat: show error tpye when trigger command fails 2024-06-24 19:34:53 +02:00
0f74208e4b feat: try and catch errors when trigger command 2024-06-24 19:31:31 +02:00
10c728552b feat: even more names for cats and dogs 2024-06-24 19:22:10 +02:00
520e5c44e3 feat: made it work on group messages again 2024-06-24 18:21:26 +01:00
f2a7793c2d feat: added nohup to gitignore 2024-06-24 18:18:05 +01:00
824c79dbf7 Merge branch 'main' of https://github.com/kuhyx/signal-bot 2024-06-24 19:13:29 +02:00
deb54e4c49 feat: added additional commands for cats and dogs to nerf traps 2024-06-24 19:13:14 +02:00
kuhyx
e1264ce58f
Removed sensitive signal server console log 2024-06-24 12:27:52 +02:00
kuhyx
f32cecd78a
Create SECURITY.md 2024-06-24 12:26:18 +02:00
kuhyx
38f69c69b9
Create pylint.yml 2024-06-24 12:25:43 +02:00
kuhyx
c04621d634
Create python-package-conda.yml 2024-06-24 12:21:00 +02:00
kuhyx
d016c62510
Create python-app.yml 2024-06-24 12:20:47 +02:00
kuhyx
bcd3e6f275
Update readme.md 2024-06-24 12:20:18 +02:00
kuhyx
2930f5494e
Update readme.md 2024-06-24 12:19:18 +02:00
14d6807119 feat: added traps command 2024-06-24 12:05:31 +02:00
bd3bb4a4cb feat: !pies command 2024-06-24 00:19:43 +02:00
44f965c16b feat: made script more extensible 2024-06-23 23:47:46 +02:00
09e811a2b7 fix: self sending !kot works now 2024-06-23 23:03:20 +02:00
dd11f33d21 feat: send kot image after receiving !kot 2024-06-23 22:09:55 +02:00