Latch

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

  1. A bot token from your box: /consent β†’ The chat bot. It starts ltk_, 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 an ltk_ token.
  2. 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.
  3. 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.

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.

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.

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:

What it posts on its own:

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.