mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:37:03 +01:00
main
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d13ae42590 |
A switchboard: any command off, off until, or on its own cooldown
One row per live command on a new Commands tab of the management page,
each with an on/off switch, an optional "off until" moment and a cooldown
that overrides the bot's default for that command alone. Every switch is
live: the bot keeps them in its own SQLite (command_settings, a permanent
table) and reads the row on every attempt, so nothing goes through
/_reload and nothing restarts.
A command that is off answers "BEEP BOOP !kot WYŁĄCZONE." (with "DO HH:MM"
for a timed switch) rather than going quiet, so nobody reports it broken;
the refusal is filed as refused_off, which the analytics count with the
other refusals, and it neither starts the cooldown nor counts as a use.
The bot grows GET /_commands and PUT /_commands/{name}; the management app
proxies them under /api/commands and passes the bot's 404/503 through
unwrapped. !cisza and !odcisz carry no cooldown and the page says so
instead of accepting a number the bot would ignore (test_wiring_ungated
keeps that set honest against the real table).
Proven end to end: scripts/e2e.sh now brackets a PUT with switch_off.yaml
and switch_on.yaml, the same actor refused and then served, with no reload
between them. Also bumps vitest and @vitest/coverage-v8 to 5.0.0 and
@types/react-dom to 19.2.7, which the freshness gate asked for.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| 405541be9b |
release: 0.2.6 «Stephanus I» — dodano !analityka
One row per command attempt, not two aggregates written only on success. command_uses holds last-use only and daily_uses counts successes; both are rate-limiter state and both are written by store.record_use, which the dispatcher calls only when a handler returns cleanly. A gate refusal and a crash returned before it, so a command that failed every time looked exactly like one nobody ran, and nothing knew which command a counted message was. command_events now takes every attempt with its outcome, its duration and the item it served; upstream_calls takes every outbound request made while serving one. The two writes stay separate on purpose -- recording a refusal as a use is what slid the cooldown window forward every time somebody was told to wait -- and test_dispatcher_outcomes asserts last_used does not move. Backfilled from journald joined against message_events, with daily_uses filling the residual so the two sources cannot double-count. Two things were wrong on the first attempt and both were found by measuring rather than by reasoning: the window has to be symmetric, because sent_at_ms comes from the sender's phone and the journal stamp from this host (194 of 299 lines landed *before* their own message, by up to 3.2 s); and matching has to be a global closest-pair assignment, because per-row greedy cannot separate two commands seconds apart. Together those took the dropped rows from 137 to zero, and recovered 418 invocations back to 29 August. Also: !analityka in the group, twelve Polish lines, naming at most three people and only ones whose display name has been seen; an Analytics tab with Nivo charts; and continuous latency measurement, which answers the open question in DOCS-latency.md -- a text send is 235 ms and an image send about 2.2 s, so the upload really is where the time goes. EPHEMERAL_TABLES was doing two unrelated jobs and is now two tuples: what a schema bump rebuilds, and what leaving a group clears. daily_uses left the first (it is a backfill source now) and stayed in the second, and _drop_reshaped took over the repair a bump used to do for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4QTp3h5Q3MvfrwGV8xqPf |
|||
| 6ec746fad2 |
feat: a word the bot does not know gets an answer, and gets kept
An unrecognised "!word" was dropped in silence, which made a typo
indistinguishable from the bot being down and threw away the best available
list of commands worth writing. Three things now happen at the one place in
dispatcher.py that used to be a bare `return`.
A near miss is corrected -- "!cta" asks whether you meant "!kot". The
distance is optimal string alignment rather than difflib's ratio: a
transposition is the typo people actually make, it costs one edit here, and
it shreds the matching blocks a ratio counts ("!cta"/"!cat" scores 0.75 but
"!cta"/"!kot" scores 0.50, so no cutoff separates them on principle).
A word with no near miss points at !zaproponuj, which files an idea from
inside the group. Either way the word and the message are recorded, and the
management page grows an Ideas tab that lists them.
Three things stay silent, each for its own reason. "!kot ladny" is a real
trigger with words after it and always has been silent. "!!" is punctuation.
And a command claimed in some *other* group is neither answered nor recorded
-- the album's triggers are real people's names, and pack_scoping.yaml says
it out loud: a refusal would itself announce that the album exists. The
existence check is therefore global, which is what pack_add already asks.
Measured rather than guessed, against the shipped aliases.json:
- a minimum candidate length, because "!ja" is one edit from ~40 of the 676
two-letter words somebody could type; 69 of them got a suggestion before
the rule and 18 after.
- the two-edit budget starts at six characters, not five. At five it put
four pairs of *different* commands within reach of each other --
!pies/!popes, !days/!cats, !dzis/!dni, !mnie/!dni -- and costs nothing to
give up, because every real typo this was written for is one edit away.
test_no_two_commands_are_within_reach_of_each_other fails if a future
alias reopens the gap.
Both new tables are keyed on the envelope's own sent_at_ms, never the wall
clock: signal-cli replays undelivered frames on reconnect, and a clock-keyed
table collects a duplicate row per reconnect instead of ignoring one.
The branch is gated on should_count -- which excludes the bot's own envelope,
and that is the loop guard, since handle() checks is_own nowhere else -- and
wrapped in its own try/except, because it runs above the one that wraps a
handler and Bot.listen has no exception handling of its own.
Suggestions live in SQLite, not in a tracked file, and the export goes to the
state directory. check_personal_data learns the names it refuses from the
live database, so it catches a name somebody has typed and cannot catch a
misspelling of one -- and an unrecognised-command corpus is made of
misspellings. A group-writable file in a public tree is both a leak and a
wedge. The export therefore carries
deliberate !zaproponuj notes in full and unrecognised words as counts only;
bodies stay on the loopback-only page.
Verified on the live bot: restarted, both tables created with every existing
row count unchanged, !cta and !zaproponuj !pogoda delivered through
/_selftest, both rows read back out of the live database, and the journal
names neither the word nor the body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01416TegJ3sa8xL5qGGgrygP
|
|||
| dc1995b558 |
feat: the message event log, and the nightly report built on it
The tally was one integer per person, deleted after each post. Nothing dated survived it, so weeks, months, records, streaks and weekday averages were all unanswerable. This replaces it with an append-only per-message log holding metadata only -- never a message body -- and rebuilds the nightly report on top. What changes for the group: a photo posted without a caption now counts (it was worth zero), stickers count, the bot's own messages no longer do, people who said nothing appear with a 0, and the report covers the previous full calendar day in Europe/Warsaw instead of "since the last post" -- which used to merge two days whenever a group went quiet. Notable decisions, each with the trap it avoids: - Events and people are PERMANENT_TABLES: neither forget_group nor a future SCHEMA_VERSION bump may drop them. Both paths used to cost a day's counter and would now cost the entire history, and forget_group fires on a listing that merely omits members. - is_own comes from which envelope branch carried the payload, not from comparing identities. The account is spelled as a uuid on some frames and a +E.164 number on others, so any comparison misses one of them. - The roster is stashed in refresh(), not _adopt(), which returns early once the watched set stops changing -- i.e. always, in the steady state. - The roster matches on uuid OR number because /v1/groups spells kuhy as +48123456789 while his messages carry an ACI uuid. Joining on uuid alone would have listed him twice every night, once with 0. - Members whose display name we have never seen are counted, not named: their only spellings are a uuid or a phone number. - (group, uuid, sent_at_ms) is the primary key, so a websocket redelivery after a reconnect cannot inflate the counts. - Days come from a stored Warsaw date string, never a timestamp range: 2026-03-29 is 23 hours long and 2026-10-25 is 25. - Polish number agreement is a real rule with a 12-14 exception, so "44 znaki" and "45 znaków" are both spelled correctly. config.py and startup.py were two lines under the 250-line cap and both had to grow, so config_env and startup_retry come out of them first, as pure moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom |
|||
| f92502d18b |
feat: name every release after a pope, and tell the group when one lands
Release n is pope n, so the version is a fact about the ordinal rather than a judgement about the size of a change. The constraint that fixes the scheme is Jan Pawel II at 2.13.7: eight patches per minor with majors opening at ordinals 33 and 153 gives (264 - 153) = 13 * 8 + 7 exactly, and both earlier majors happen to divide evenly by eight too, so no bracket needs a fudge factor. The anchors are not arbitrary -- 33 is Sylwester I, pope through the Edict of Milan and Nicaea, and 153 is Wiktor II, the first pope elected after the 1054 schism. popes.json is scraped by tool/gen_popes.py rather than recalled, because antipopes, Benedykt IX's three separate reigns and the Stefan II question all shift the count and being wrong by one moves Jan Pawel II off 2.13.7 silently. The generator refuses to write a list that is not 267 contiguous entries with Jan Pawel II at 264. A second, independent parse of the same page agreed on every ordinal, which is the only reason to believe either. On startup each watched group is told about the releases it has not seen. A group with no record has seen none of them, so the first announcement starts at 0.0.0 "Swiety Piotr Apostol" rather than skipping to whatever is current -- watching the ladder start at Peter is the point of naming releases after popes. A group added years from now would otherwise be handed the whole backlog, so the message is capped at ten releases and says how many it left out. The version is recorded before the message is sent: with Restart=always and RestartSec=5, a crash between the two would re-announce on every restart, and a missed announcement is far cheaper than a looping one. DOCS-changelog.md is the single source -- the bot parses it, so there is no second machine-readable copy to drift. It is DOCS-, not CHANGELOG.md, because the md-naming gate allows four namespaces and that is not one of them. Deliberately NOT bumping SCHEMA_VERSION for the new group_meta table: _apply_schema drops every table on a bump, so bumping to add a table would have thrown away the live tallies to gain nothing. Verified against the real database -- group_meta appeared and all four counts survived the restart. The schema moves to its own module so store.py stays under the 250-line cap, and /healthz now names the build, because there was previously no way to ask a running bot which version it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom |