Admin¶
For a deployment whose clients bring their own bot, the admin is the interface: bots are added
there, tokens rotated there, a noisy client throttled there, and a client who left switched off
there. Dynamic bots is what the rows mean and how a container hears about
them; this page is the interface over them. Three models are registered — bots, profiles and queues — from
AppConfig.ready(), so nothing is read at import.
Registration needs no setting: a web container that sends nothing and records nothing still has to be able to configure the bots the other containers serve.
The token is write-only¶
The token field renders nothing and posts a new value. Empty means keep, so a credential can be rotated by somebody who cannot read the one being replaced.
What a page shows instead is a mask — the identity, which is not a secret, and eight dots for the half that is. The identity is read from the token rather than typed: it is what every queued message, feed row and log line names a bot by, and a mis-keyed digit is a row that serves somebody else's bot.
A new token is stored through TOKEN_STORAGE, like every other write — see
Tokens.
Reading one is a page of its own. Next to the mask is show it, which asks for a
confirmation and then writes a bot.token_revealed row into the event feed: the bot, and who
asked. So "who saw this token" has an answer during the incident where somebody has to ask it —
and a credential is never in a changeform, a browser history or a screenshot of one by accident.
Fieldsets are permission boundaries¶
| section | what is on it |
|---|---|
| Bot | the label a person reads, and the identity |
| Credentials | the mask and the write-only token field. Behind view_telegrambot_token |
| Placement | the profile it inherits from, and the queue it publishes to |
| Limits, Updates, Delivery, Bot behaviour | one line per setting this bot may decide, collapsed until you open them |
| State | whether it is switched on, and what the supervisor last said about it |
Adding a bot needs that permission too: a new bot has no token to keep, so adding one is setting a credential. And the field is absent from the form a user without it gets, not just from the page — a section that is merely unrendered still posts back, and a token accepted from somebody who may not read one is the bot given away.
view_telegrambot_token follows the precedent the event feed set with
view_telegramevent_payload: being allowed to switch a bot off is a different question from
being allowed to read what sends as it. Without it the credentials section is not rendered at
all rather than masked — a section that renders is a section that posts back, and a token field
left in place would let a user who cannot read the credential replace it.
Overriding a setting is a decision, so it is a checkbox¶
Each setting on those four sections is a pair: a box that says this bot decides it, and the value. Next to the box is what the bot would use if you left it alone, and which layer that comes from:
MAX_RETRIES Inherited: 5 — from the profile 'vip'.
RATE_LIMIT Inherited: {'overall_per_second': 30, ...} — from the deployment defaults.
The checkbox exists because overrides is sparse by key presence, not by value:
RATE_LIMIT: None is a real setting — it switches pacing off — so "inherit" cannot be spelled
as an empty field or a null. Unticking a box hands the setting back to the profile.
Values are judged by the package's own checks: a MAX_RETRIES of zero is refused on the page
because E012 refuses it at boot. One law, two consumers — a second copy of the rules in the
form is how a page comes to accept a configuration the deployment then refuses to start with.
Two settings are credentials — WEBHOOK_SECRET, and REDIS_URL because it carries the
broker's password — so what the page prints for them is 'set' or 'not set' and the layer,
never the value, and their pairs are only offered to a user who may see this bot's token.
The values are judged together, and against the profile this submission chose rather than the
one the row held: W004 decides whether a BLPOP_TIMEOUT will be honoured from
HEARTBEAT_INTERVAL and the transport's deadline, so a setting judged one at a time — or under
the old profile — is refused while the configuration is right. A bot being added is resolved by
the identity in the token being submitted, because a bot's own environment variables are keyed
by it.
TOKEN, QUEUE, ENABLED and DEFAULT_KWARGS are not on those sections: the first three
have a place of their own on the page — the credentials section, the queue picker and the
switch — and the fourth is a callable, which no form can hold. Settings the process owns are
absent too, and E053 is what reports one written into a row anyway.
What the list is for¶
The bot changelist is meant to be scanned rather than opened row by row:
| column | the question it answers |
|---|---|
| pool | where this bot's work is done — the axis a container is started with |
| own settings | which settings this bot decides for itself, because a bot behaving unlike its neighbours is usually carrying an old override |
| serving | whether anything is serving it: switched on, and not quarantined |
Renaming a queue reaches the bots pointed at it within a poll: the providers watch the three tables a resolved bot reads, so the rename is a change they can see rather than one that waits for a restart.
Filters cover the switch, the profile, the queue and the pool; Switch on and Switch off are actions, so five hundred clients are one statement rather than five hundred saves. A profile or a queue shows how many bots are on it, as a link to exactly those bots.
The request never talks to Telegram¶
Saving a row moves its watermark and publishes a notice; a supervisor reads it and acts, seconds
later. Nothing here calls getMe or setWebhook, which is what makes an action over five hundred
selected rows return immediately instead of holding a request open for five hundred round trips.
The three actions that would need the network write an intent into the row instead:
| action | what it asks for |
|---|---|
| Check the token with Telegram | getMe — the cheapest way to find out whether a pasted credential works |
| Register the webhook | setWebhook, from the URL and secret that bot's settings resolve to |
| Remove the webhook | deleteWebhook, which a bot being switched off needs before Telegram stops posting to a URL that answers 404 |
Something with an event loop carries it out and writes one line back — the asked for column on the list, and four read-only fields on the State section. Run it beside the bot containers:
An intent is a person waiting for an answer, so --watch polls in seconds. Each one is claimed
with a compare-and-set before it is carried out, so two containers reading the same table cannot
both call Telegram for one bot, and an operator who asks for something else in the meantime keeps
their newer question.
The claim is a lease: the asking stays on the row until an answer is written, so a container
killed mid-intent loses the work rather than taking the only record of the request with it — the
next pass picks it up once the claim lapses. And the answer is written only by the claim still
held, so a worker slow enough to finish after that cannot replace a newer answer; it says so in
the log instead. Nothing is scheduled twice: manage.py tgbot_intents appears once on
Deployment. What is written back is redacted the way a feed row is: aiogram puts the API
URL into its messages, and the URL carries the token.
Edits that would strand a backlog are refused¶
Pointing a bot at a different queue leaves whatever is in the old one with nobody to take it: a consumer serves the queues it was told about, and after the move nothing points there. So the edit is refused while that queue still holds messages, and the message says what to do first — let it drain, or switch the bot off and drain it deliberately.
Judged by the queue each configuration resolves to, not by the picker alone: a bot with no queue of its own takes one from its profile or the deployment's defaults, and moving that one strands a backlog like any other.
A queue the transport cannot be reached to read is not a queue with messages in it: the edit
goes through, because a page that refused every change while Redis blinked would be worse than
the rare mistake. That is the same trade the supervisor makes about a provider it could not read.
A transport that cannot be built — a BROKER naming nothing importable, a driver that is not
installed — is refused instead: nobody can read that queue at all, then or later.
This is a guard, not a guarantee. The depth read and the save are two steps, so a message
published between them lands in the queue the bot is leaving. Closing that would mean holding a
lock across every bot.send() in the deployment for an edit somebody makes twice a year. What
the check catches is the ordinary mistake — moving a client with a visible backlog — and the
order with no race in it is the one the message names: switch the bot off, let the queue drain,
then move it.
quarantine_reason and quarantined_until are the supervisor's own writing, read-only here: they
say what happened to a bot, and a person clearing the text would not clear the condition. The
switch a person reaches for is enabled.
Switching a bot on or off moves its updated_at, which is the watermark a supervisor polls —
so the change lands within a poll rather than at the next restart.