Company knowledge — right in your Telegram chat
An employee messages the bot in a DM — Query Engine answers under their own permissions, with links to sources. Everything the web chat can do, without leaving the messenger.
webhook
answer as a message
Telegram is one more thin window into the same conversation:
Query Engine does the thinking and searching under the
employee's identity, Telegram merely carries the message in and the answer
out. Slack's twin — differing in one thing: the conversation is cut not by
a thread, but by the /new command.
One conversation model across all surfaces →
Admin registers a bot with @BotFather once — the
bot token and webhook secret land in telegram_settings, like SMTP in
Email. The screen is
platform settings, a single toggle turns the bot on.
Achilles generates the webhook secret itself at setWebhook —
the admin doesn't enter it, unlike the signing secret in Slack.
The sender is resolved to an account via
identity_mapping
(source='telegram') — the sign-in identity. Telegram doesn't
expose an email, so there's no auto-match: only a
link code
— whoever signed in on the web gets a code and returns it to the bot. Only an admin creates
the account
(by invitation) —
knowing the bot grants no access.
Public bot: an unlinked user gets a neutral refusal,
code guessing is rate-limited.
From there on it's only user_id → role → ACL.
A DM has no threads:
the /new command opens a new conversation, all other messages continue the current one. /new is an adapter command; it
never enters the conversation history. Context is truncated by tokens — the engine's shared
mechanism.
The conversation lives in conversations (key chat_id in
meta), the adapter holds the active one via a pointer in Redis. Groups ·
@mention · topics — v2.
The webhook receives the update, acknowledges immediately, and processing goes to the
interactive
queue, the answer comes back as a message. It survives restarts and retries.
A repeated update is dropped by update_id; the public endpoint is
rate-limited.
What's the status of release 4.2 and who owns the ajax bug?
Release 4.2 is in regression testing, ship date planned for Friday. The ajax bug is owned by Dina Safina1.
/new — start a new conversation
The answer is scoped to Maxim's permissions: anything he can't see won't appear in it. The
next message continues the conversation, /new opens a fresh one.
First contact — linking the account with a code:
Hi! What's the status of release 4.2?
To continue, confirm your account: sign in at achilles.local/link and send me the code. After that I'll answer under your permissions.
K7P2-9XQ4
Done — linked to maxim@company.com. Ask away
The bot replies the same way to everyone — known user or stranger — never revealing that an instance exists. The code is visible only in the signed-in browser and comes back through the bot — a forwarded link can't hijack the account. Linking screen →
source='telegram' → sign-in identity bridge
chat_id
/new resets it
| id | BigInteger | PKCHECK | always 1 · singleton |
| bot_token_enc | Text | NULL |
bot token from BotFather (12345:ABC…), AES-256-GCM ciphertext
|
| webhook_secret_enc | Text | NULL |
webhook secret (X-Telegram-Bot-Api-Secret-Token header) ·
Achilles generates it itself · ciphertext
|
| bot_username | Text | NULL | bot's @handle for the UI · from getMe |
| enabled | Boolean | NOT NULLDEFAULT | master switch · DEFAULT false · off → the bot stays silent |
| last_test_ok | Boolean | NULL | last delivery verdict: token live and webhook registered — not just a valid token |
| last_test_at | DateTime(tz) | NULL | when it was tested · feeds the status chip on the screen |
| created_at | DateTime(tz) | DEFAULT | server_default=now() |
| updated_at | DateTime(tz) | DEFAULT |
server_default=now() · trigger set_updated_at()
|
is_available() — a model property, not a column:
enabled and bot_token_enc ·
webhook_secret_enc filled in. Shares its core with slack_settings;
differing in one thing — we generate the webhook secret rather than the provider issuing it.
Honest enable. Turning the bot on is atomic: Achilles calls
setWebhook and the switch stays on only if Telegram accepts it —
a refusal (revoked token) or an unreachable address rolls enabled back and
answers with the reason, never a green switch with no delivery behind it. Telegram
delivers only to a public HTTPS address, so a
localhost/private PUBLIC_BASE_URL is rejected up front. "Test
connection" reads the same truth — getMe plus getWebhookInfo —
so a live token whose webhook isn't registered reads as not connected.
Checks: webhook secretidentity resolutionupdate_id dedup/new = new conversationACL pass-throughunlinked usercode guessing limit
The webhook is anonymous by design: the lock is the secret token in the X-Telegram-Bot-Api-Secret-Token header — which Achilles generated at setWebhook — and the rate-limit window is fail-closed.
identity_mapping → user_id bridge, shared
link_tokens, link-code protection.
→ Auth / Security
/new.
→ Query Engine
/api/v1/telegram/* paths on the shared surface map.
→ HTTP API