Self-hosting
The chat bot (Matrix)
A small program beside your box that reminds you in a Matrix room of your own, relays the two things the box says may leave, and takes a handful of commands from you β only what its own token allows, and never in a room anybody else could read.
On this page
The chat bot is you, relayed: a small program that runs beside your box, reads what is due
today, reminds you in a Matrix room, tells you what somebody else did to your clock, and takes
a few commands β !status, !today, !pause β without opening the app. It lives in the
repository at tools/matrixbot/latch_matrixbot.py. It is one file and Python's standard
library only: no Matrix SDK, nothing to install, and it is not in the container.
Important
Whoever runs the homeserver can read everything the bot posts β and could send commands as you. The bot cannot encrypt, so its room is plaintext on that server (and in its backups). Use a homeserver you run or trust, and keep the room to you and the bot. That is also why the box β not the bot β decides what the bot may read, and why recite, log and hygiene are off until you switch them on.
What you need
- A bot token from your box:
/consentβ The chat bot. It startsltk_, is shown once, and reaches only the bot's own routes. Never put your own API token in the bot β it refuses anything that is not anltk_token. - A Matrix account for the bot β its own, not yours (the bot refuses to run as its owner: it would obey its own posts). Log it in once and keep its access token.
- A room with you and the bot in it, set up so the bot will talk there:
- join rule invite (or knock) β never public or "restricted";
- history not world readable;
- not encrypted β the bot has no end-to-end encryption, and it will not post plaintext
into a room you meant to be private;
- no more members than you allow (
max_members, 2 for you and the bot).
The config file
A JSON file that only you can read (chmod 600 β the bot refuses anything group- or
world-readable, before it reads a byte, because the file holds two tokens):
{
"latch_url": "http://127.0.0.1:8770",
"latch_token": "ltk_β¦",
"hs_url": "https://matrix.example",
"hs_token": "β¦the bot account's access tokenβ¦",
"room": "!abcdefghijk:matrix.example",
"owner": "@you:matrix.example",
"max_members": 2,
"poll_seconds": 30,
"state_path": "/home/you/.local/state/latch-matrixbot.json"
}
| Key | What it is |
|---|---|
latch_url |
your box β this machine (127.0.0.1 or localhost); see below for another one |
latch_token |
the ltk_β¦ bot token from /consent |
hs_url |
your homeserver's base address β no path, no query; https unless it is this machine (plain http elsewhere carries the bot's token and every message in the clear β it warns) |
hs_token |
the bot account's access token |
room |
the room's ID (!β¦:server) β not an alias, which can be pointed somewhere else later |
owner |
your Matrix ID β the only one the bot takes commands from |
max_members |
the most people the room may hold (2β100) before the bot stops |
poll_seconds |
how long each wait for news lasts (5β300) |
state_path |
where it keeps its place (created 0600); the folder must exist |
Every key is required, and an unknown key is refused (it is usually a typo).
Another machine. By default latch_url must be this machine: the bot's token then never
crosses a network, and a box that is not the one you meant cannot answer it. To run the bot
elsewhere, start it with --allow-remote-latch β and use https, or its token travels in the
clear (it warns).
Running it
chmod 600 ~/.config/latch/matrixbot.json
python3 tools/matrixbot/latch_matrixbot.py --config ~/.config/latch/matrixbot.json
--once runs one round and exits (handy for a first test), -v logs every request's status
line. The log never carries a message body, a text from your box or a token.
The first run is history. The first time it starts with a new state file it answers
nothing already in the room β a command you typed last week is not acted on today β and it
starts the box's news at its end rather than posting your whole log. From then on it keeps its
place in state_path; if it cannot save its place there (a full disk, a read-only folder), it
acts on nothing until it can.
It exits with 0 when stopped, 2 when it refuses (the config, a room rule, a token the
box or the homeserver no longer accepts), and 1 when a --once round could not finish β
the box or the homeserver was away, or what it had to say could not all be sent. Running on,
it waits at most poll_seconds for news each round and starts a round at most once a second.
Under a supervisor, restart on failure but not on 2 β a 2 needs you to look. For systemd:
[Service]
ExecStart=/usr/bin/python3 /path/to/latch/tools/matrixbot/latch_matrixbot.py --config %h/.config/latch/matrixbot.json
Restart=on-failure
RestartPreventExitStatus=2
The room is checked all the time
At startup and on every sync the bot checks that the room is still invite or knock, not
world readable, not encrypted, no bigger than max_members, that you and it are both still in
it, that the homeserver still accepts its token, and that your box still answers its token on
/api/v1/bot/today.
- While it runs, it checks every change one at a time, in the order it happened, so a rule broken and put back between two syncs β a stranger who joins, reads and leaves; history world readable for a minute β still counts. When more happened than one sync carries (a busy room, a homeserver that was away), the homeserver sends only the net result, where a visitor who came and went does not show; the bot then reads back what it skipped and checks that too. If the homeserver will not let it, it stops.
- When it starts, it checks the room as it is then. It does not replay what happened while it was stopped β a new room's own making, or a stranger who came and went while it was off β it cannot have posted anything then, and the room as it stands is what it would post into.
The moment a check fails it stops: it answers nothing in that sync β not even a command sent before the change, and not after a restart either β posts nothing it had waiting, and exits 2. Fix the room and start it again; a room that is still broken is refused at startup.
Commands
Only from you, only as plain text starting with !, only fresh β a command more than fifteen
minutes old, sent while the bot was away, is not acted on (it says how many it skipped) β and
each at most once.
Anything else you say is never acted on, but it is not met with silence either: status,
/today or Pause? gets Did you mean !status?, and other small talk gets the way to !help
β at most once a quarter of an hour, and not right after it answered you (a thanks is not a
question). It never repeats or logs what you wrote.
| Command | What it does |
|---|---|
!status |
the lock in one line: locked or not, hidden, paused, collared, open training, an event's day |
!today |
what is due β reflection, mantra, check-in β today's question, and your AI's line |
!remind on Β· !remind off |
the bot's own reminders here (on to start with) |
!pause |
the safeword: every grant suspended at once. Always there. Resuming is only in the app, on Consent |
!recite |
recite today's mantra, as now β only if you switched on Recite and log for you |
!log <habit> [minutes] |
log a habit by its short name, as now β only if you switched on Recite and log for you |
!ask <anything> |
talk to your AI Handler β only if you switched on Talk to your AI Handler here (below) |
!hygiene Β· !hygiene emergency |
out for hygiene, as now β maintenance inside a scheduled slot, an emergency window otherwise (emergency skips the slot). Stops the clock. Only if you switched on Hygiene from the room (below) |
!relocked |
locked back up, as now β only if you switched on Hygiene from the room |
!help |
the list β also the answer to anything else |
There is no !unlock, and there never will be: the key is yours, in the app. Whether recite
and log work is decided by your box, on the bot's token β switched off, the box refuses them
and the bot answers with the help and how to switch them on β whatever else was wrong with
the command (it asks the box first, without writing anything). The time on the clock is only in
!status if you switched on Show the clock, and never while a lock is hidden.
Hygiene from the room
!hygiene and !relocked are the dashboard's π card, said in the room. They move your clock β
out stops it, back in starts it β so they have their own switch, Hygiene from the room on
Consent β The chat bot, off until you turn it on: Recite and log for you does not open them.
- Only ever now. The room cannot backdate a window or a relock. Locked back up earlier than you said? Say so in the app, where the π button takes a when.
- The emergency door is never refused, here or anywhere. Outside a maintenance slot,
!hygieneis an emergency window;!hygiene emergencyasks for one inside a slot too. - Nothing about your lock's time. The answer names the window's length (20 min), and
!relockedhow long you were out and how far past the window β never time left, never on a hidden lock. - Locked back up? When a window runs out and you have not said you are back in, the bot asks,
once for that window, as a message that rings (while reminders are on).
!statusand!todaysay you are out for hygiene while you are. A window run well past its time is a π Long soak mark.
Talking to your AI Handler
!ask <anything> is a turn of your AI Handler's chat β the same conversation as
the AI page, seeing everything you granted it (your reflections, your kit, the clock) β and its
answer comes back here, as a message that rings. It is off until you switch on Talk to your
AI Handler here on Consent β The chat bot; switched off, !ask says so and asks nothing.
- One question at a time. The model can take a minute; ask again before it answers and the
bot says it is still on the last one. Meanwhile it keeps reading the room: a
!pausebehind a question is acted on at once, never queued behind the model. - The safeword still wins. An answer written knowing something a pause takes back is not posted if you paused while it was thinking β it waits on the AI page. The bot checks again just before it posts.
- A hidden lock's time never reaches the room: chat is never shown it, and an answer that talks about time while one runs is posted masked.
- If the model is away, the bot says so with the rules line, as the AI page does; if the AI is switched off or the day's calls are used up, it says that. A question the bot was still waiting on when it stopped is not asked again β its turn is on the AI page.
The bot logs that it ran !ask, never what you asked or what came back.
Exactly what it relays
The bot says its own sentences, filled in from what your box's two bot routes give it β codes, counts and flags β plus the only two kinds of text those routes carry:
- today's reflection question β your library's or a playset's; one somebody else set for today follows their rules (your AI's own question never travels);
- a line your AI said that it marked safe to repeat, while Relay your AI Handler's lines is on β and a Handler's line only if you switch Relay a Handler's lines on, labelled with the kind its signer declared, a person or an AI. Either line leaves the room the moment its voice may no longer speak: the safeword, its Say a line revoked, the AI switched off, a pack's collar taken off. A Handler who can see your hidden clock never has a line repeated while that lock stays hidden.
What it posts on its own:
- a reminder, once a day each, when your reflection, check-in or mantra is due (worked out as if the health module did not exist β a hard day never reaches the room). Reminders are ordinary messages, so your client notifies you; everything else is a notice, which most clients show quietly. The reflection is due from midnight, but its reminder waits until four hours before the hour it is due by (Reflection due by on the Handler's settings β 22:00 unless you changed it, so from 18:00), on your box's clock;
- a challenge, the moment your Handler asks for proof (if you switched
challenges on): πΈ Proof, now: write OTTER 47 where
the camera can see itβ¦ β an ordinary message, so it rings, and it follows
!remindlike the reminders do. One that was answered or ran out before the bot saw it is never posted late;!todaysays πΈ Proof asked for while one is open; - news from your box: time added, a freeze or training set by somebody else (who β your AI, your Handler β never the training's title or their reason); a badge (its title only when it was earned by your own game, otherwise a badge β and only when the room is shown the number it states anyway: First Latch and a count of locks always; Long Haul, A Year Under and a playset's locked on N days only with Show the clock; a count of training, reflections, check-ins or spins β Good Dog, Said Out Loud, Still Here, Let the Wheel Decide β and the streak and photo ones never, because the room gets today's duties as done or not and the clock as time left, never those counts; a playset's joined and granted say no number and are named; its finished says how many days you were locked, so only with Show the clock, and never when it also says how many days you reflected); being collared β with the kind your Handler signed they are, said as their word, never as a fact the bot checked β or joining a pack (never a name); a pause, a lock, an unlock. A burst becomes a few lines and β¦and N more.
Withheld means never. A line the box would not relay when the bot read it β its relay switch
off, a hidden clock in it, a piece of your kit named β is not relayed later, whatever changes:
switching the relay on, renaming the kit, the lock ending. While you are paused no voice's
line reaches the room at all, not even one said just before. And anything that has to wait β the
two seconds between messages, the hourly cap, a homeserver's bad minute β is checked again, by
your box, just before it goes: a line you switched off, paused or named in the meantime (even
while the bot is still sending) is dropped for good. An answer to !status or !today is built
when it is sent, so it is as of then β !today reads today's line under the switches as they are
at that moment.
It never carries your words, anybody's name, a training title, your kit, your health or a
hidden lock's clock, and a refusal never reaches the room β except your AI Handler's answers
to !ask, which carry whatever you let your AI see, because you switched that on. It posts at
most one message every two seconds and thirty an hour.
What it does not do
No end-to-end encryption (so its room must not be encrypted), no photos or voice, no resume, no unlock, no back-dating (the box refuses a time from the bot), and no second copy of your data: everything it knows it reads from your box, when it needs it.
When it stops
| It says | What to do |
|---|---|
config_mode: β¦ chmod 600 it |
the config is readable by others: chmod 600 it |
latch_url: β¦ is not this machine |
run it beside the box, or pass --allow-remote-latch |
latch_probe: β¦ answered 401 |
the bot token was revoked β make a new one on /consent |
latch_probe: β¦ answered 403 |
that is not a bot token (your own API token?) |
latch_probe: β¦ answered 404 |
your box has no bot routes β update Latch |
latch_probe: β¦ answered 503 |
your box is a hosted instance: its API is off, and a bot cannot run there |
whoami: β¦ |
the homeserver no longer accepts the bot's token, or it is logged in as you |
not_in_room: β¦ |
the bot's account is not in room, or the homeserver will not let it post there |
history_visibility, join_rules, encrypted, members, owner_absent, bot_left |
a room rule broke β fix the room, then start it again |
gap: β¦ |
more happened between two syncs than the bot could read back (or the homeserver would not let it) β look at the room, then start it again |
sync: β¦ answered 4xx |
the homeserver refused the bot's very first sync β check hs_url |
| the homeserver refused the bot's sync position (a warning) | nothing to do: it starts over as history (after the bot moved to another homeserver, or the homeserver was restored) |
| I could not reach your box just now | the box was away for that command; say it again |
could not save the state file |
its folder is full or read-only; it acts on nothing until it can save its place again |
After restoring your box from a backup, stop the bot and delete its state_path before you
start it again: the restored box counts its news from an earlier point, and a bot that kept its
place would say nothing until the box caught up with it. The next run is history.
This page also ships inside the app, at /guide β so your own instance always serves the guide for the version you are running, with the internet unplugged. Get Latch Β· Something wrong here? Tell me.