# Commands The copy that matters is not this file: `!pomoc` prints the same list **in the group**, in Polish, one line of description per command. That version is built from the command registry at run time and cannot drift, because `CommandRegistry.register` takes the description as a required argument and `help_command.render_help` prints any command its `GROUPS` table forgot to file anyway, under no heading. This file is the trigger reference — every spelling that resolves, which `!pomoc` deliberately does not print. | Command | Aliases | |---|---| | cat | `!kot` `!koty` `!kots` `!cat` `!cats` `!meow` `!miau` + styled variants | | dog | `!pies` `!psy` `!dog` `!dogs` `!woof` `!szczek` + styled variants | | gecko | `!gekon` `!gekony` `!geko` `!gecko` `!geckon` `!geckos` | | art | `!sztuka` `!sztukę` `!sztuki` `!art` | | poem | `!wiersz` `!wiersze` `!wiersza` `!wierszyk` `!poem` `!poems` `!poezja` `!poezje` `!poetry` | | trap | `!pułapka` `!pulapka` `!pułapki` `!pulapki` `!pułapkę` `!pulapke` `!trap` `!traps` `!trapy` `!trapper` `!sidła` `!sidla` `!sidło` `!sidlo` `!wnyk` `!wnyki` `!potrzask` `!zasadzka` `!klatka` `!samołówka` `!samolowka` `!beartrap` `!bear_trap` `!snare` `!snares` `!mousetrap` | | pope | `!papież` `!papiez` `!papieze` `!papieża` `!papa` `!pope` `!popes` `!pontifex` + styled variants | | stats_today | `!staty` `!statystyki` `!stats` `!dziś` `!dzis` | | stats_yesterday | `!wczoraj` `!yesterday` | | stats_week | `!tydzień` `!tydzien` `!week` | | stats_month | `!miesiąc` `!miesiac` `!month` | | stats_year | `!rok` `!year` | | stats_records | `!rekordy` `!rekord` `!records` | | stats_crown | `!korona` `!crown` | | stats_weekdays | `!dni` `!days` | | stats_me | `!ja` `!moje` `!mnie` | | command_stats | `!analityka` `!command_stats` `!commandstats` `!analytics` `!uzycie` `!użycie` `!ranking` | | help | `!pomoc` `!pomocy` `!help` `!komendy` `!commands` | | random | `!losowe` `!losuj` `!random` `!rand` | | add_photo | `!dodaj` `!dodaj_zdjecie` `!dodajzdjecie` `!add_photo` `!addphoto` | | suggest | `!zaproponuj` `!propozycja` `!sugestia` `!suggest` `!pomysl` | | good_bot | `!good bot` `!dobry bot` `!goodbot` `!dobrybot` `!good_bot` `!dobry_bot` | | food_today | `!co_jadł` `!co_jadl` `!cojadł` `!cojadl` `!jedzenie` `!dieta` + the operator's own in `aliases_custom.json` | | workout_today | `!co_ćwiczył` `!co_cwiczyl` `!coćwiczył` `!cocwiczyl` `!trening` `!workout` + the operator's own in `aliases_custom.json` | | mute | `!cisza` `!cicho` `!mute` | | unmute | `!odcisz` `!wracaj` | Aliases live in `signal_bot/aliases.json`, not in source, because they are data: adding one should never mean touching Python. Two constraints on a new one. It is subtracted from the personal-data gate's word list by `_repo_vocabulary()`, so it must not be name-shaped; and `tests/test_nearest.py` fails if it lands within edit distance of a *different* command's trigger, because the near-miss suggestion below would then be a coin toss. `!poems` is the one trigger in the table that is only just legal: it sits two edits from `!pies`, and passes solely because `!poem` is one edit away and wins the tie. Deleting `!poem` would fail the suite at `!poems`, nowhere near where the change was made. The operator can add words of their own without a commit: the Commands tab of the management page writes `aliases_custom.json` in the bot's state directory (`CUSTOM_ALIASES_FILE` names another path), keyed by command name, and the bot appends them to the shipped list when it builds the table. They are extra spellings only -- `!pomoc` keeps showing the shipped one -- and they go through the same clash check as `aliases.json`, so a word another command already answers to is refused, as is a name no command has. That is what makes the page's save a reload: the bot proves the file before it is kept. The tagger refuses to give a person a trigger that is already somebody's custom alias, for the same reason it refuses the shipped ones. `good_bot` is the one two-word trigger. It works because `fold()` keeps inner spaces and the registry matches the whole text before it looks at the first word, so `!good bot` resolves and `!good` alone does not. Two spaces between the words resolve to nothing, which is what the one-word spellings are for. It is also the one command that answers with no message: it puts an emoji on the compliment itself (`POST /v1/reactions/`), drawn from a small set, and says nothing in the group. `food_today` and `workout_today` read two logs other programs keep -- diet_guard's `food_log.json` and screen_locker's `log.json`, at those programs' default paths unless `FOOD_LOG_FILE` / `WORKOUT_LOG_FILE` say otherwise -- and print the current Warsaw day out of them, fresh on every call. The spelling the group actually uses for each has a surname in it, so it is a custom alias in the state directory and never appears in this repository. Food prints every entry and the day's total, never the daily budget, which diet_guard keeps on the machine on purpose. Workouts print every field an entry carries except a reservation phone number, a screenshot path and the sync record id. ## A word nothing claims An unrecognised `!word` used to be dropped in silence, which made a typo indistinguishable from the bot being down. Now it is answered, and kept: - within one edit (two, from six characters up) of a real trigger, the bot asks whether that is what you meant — `!cta` → *chodziło o !kot?* The distance is optimal string alignment rather than `difflib`'s ratio, because a transposition costs one edit there and shreds the matching blocks a ratio counts. - otherwise it points at `!zaproponuj`, which files the idea. - either way the word and the message go into `unknown_commands`, which the management page shows. That is the list of commands worth writing next. Three things stay silent, and each for its own reason. `!kot ładny` is a real trigger with words after it, and always has been silent. `!!` and `!?` are punctuation, not a mistyped command. And a command scoped to some *other* group — the album's, whose triggers are real people's names — is neither answered nor recorded, because a refusal would announce that the album exists. `random` exists **only in the photo album's own groups**, and only on an installation that has an album at all — everywhere else it is not a command, not refused and never logged. It takes arguments, and those arguments are resolved to pseudonym keys by the registry before any handler, log line or `command_uses` row sees them. See `DOCS-photo-pack.md`. `mute` (`!cisza [1-10]`) silences the bot in **that group only**, for the minutes asked or ten by default, and ten at most. It confirms once and then answers nothing: no commands, no near-miss suggestion, and the nightly report is held back (not marked, so the next run still says that day). Messages are still counted. Anybody may ask -- ten quiet minutes is the whole downside. `unmute` (`!odcisz`) is the one command that runs through a mute, and only for `TRUSTED_INVITERS`; anyone else is refused without a reply, because a refusal is a reply. With `TRUSTED_INVITERS` unset, anybody may. There is no `!unmute` alias: it sits one edit from `!mute`, inside the near-miss budget. `add_photo` (`!dodaj alfa wakacje` + an attachment) exists in the same groups as `random` and under the same silence elsewhere. It does **not** put the photo in the album: it strips it and queues it for approval on the management page (`./web.sh`), and answers with a count — `czeka na zatwierdzenie: 1`. Only an approved photo is retrievable, and it is from the moment it is approved. No cooldown, no size cap, no daily limit. `DOCS-add-photo.md`. Any command can be **switched off** from the management page (Commands tab), outright or until a moment, and given its own cooldown. The bot reads the switch on every attempt, so it is live without a reload; a command that is off answers `BEEP BOOP !kot WYŁĄCZONE.` (or `… WYŁĄCZONE DO 14:01.`, Warsaw time) rather than going quiet, and the refusal neither starts the cooldown nor counts as a use. `!pomoc` still lists it. `DOCS-web-ui.md` has the page, and the same row is where a custom alias is typed. ## Why `!pomoc` shows the first alias and not a trigger `Command.triggers` is a `frozenset` of **folded skeletons** — `folding.py` runs NFKC, strips diacritics and maps Cyrillic/Greek/small-caps/leet lookalikes onto ASCII, so that `!gecko` typed on a Russian keyboard still resolves. Everything that makes those triggers robust also makes them unprintable: the set has no order, `!papież` survives only as `!papiez`, and the styled variants come out as zalgo. So `Command.primary` holds the first *raw* trigger instead, which `aliases.json` lists as the canonical Polish spelling for every command. Two consequences worth remembering, both from `folding.py`: - A command name can never contain a digit — `fold` maps `0134578` onto letters, so `!7dni` would resolve as `!tdni`. - The styled Unicode aliases in the table above are redundant now; folding covers them. They are kept because removing an alias can only take something away.