- Python 99%
- Dockerfile 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build and Publish Containers / build-and-push (push) Successful in 9s
Reviewed-on: #6 |
||
| .forgejo/workflows | ||
| bot | ||
| deploy | ||
| docs | ||
| legacy | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| requirements.txt | ||
| VERSION | ||
mtg-tbot
A Magic: The Gathering assistant for Telegram. Looks up cards, picks printings, shows prices and legalities, answers rules questions from the Comprehensive Rules, and reports what decks people are actually playing.
Python 3.12 · python-telegram-bot 21 (async) · Docker · Kubernetes via the bjw-s app-template chart.
What it does
Card lookup, in any chat it's in
[[lightning bolt]] card image, prices, legalities, reference links
((deathrite shaman)) oracle text and official rulings
{MSC} what a set is and where its cards are playable
Several per message work. Names are fuzzy-matched, so [[lightnig bolt]] still
finds Lightning Bolt. When a name is ambiguous — [[bolt]] matches dozens of
cards — the bot offers the closest ten as buttons, and tapping one posts that
card.
Set codes share the brace syntax with mana symbols, so a code must be three to
six characters with at least one letter — {R}, {10} and {100} are left
alone. An unrecognised code is ignored rather than answered, so a stray
{CHAOS} mid-conversation stays quiet; /set is explicit and always replies.
Inline mode — from any chat, without adding the bot there
@yourbot lightning bolt search; full Scryfall syntax works
@yourbot t:instant c:r cmc=1 ...including queries like this
@yourbot !p shivan dragon one row per printing, listed by set
@yourbot !r birds of paradise oracle text and rulings
@yourbot !s msc set details
@yourbot !q trample Comprehensive Rules lookup
Inline results are lists rather than grids of images, so each row is readable while scrolling; the art still arrives with the message. The printing picker leads with the set code and set name, then collector number and price, so you can pick the right version instead of squinting at near-identical thumbnails.
Results are cards you can actually play. Tokens, emblems, art series, oversized
pieces and Jumpstart front cards are filtered out everywhere — searching
lightning used to lead with a front card literally named "Lightning", and
Lightning Bolt listed nine gold-bordered display printings among its real ones.
Names that begin with what you typed come first: lightning leads with
Lightning Angel and Lightning Axe rather than Arc Lightning and Ball Lightning,
which is what Scryfall's alphabetical ordering gives on its own.
Telegram sends a query on every keystroke, so searches wait
INLINE_DEBOUNCE_SECONDS (1 by default) and drop anything the typist has
already superseded — otherwise "lightning bolt" would cost fourteen searches.
Rules
/rule 702.2b exact rule, verbatim
/rules trample full-text search across rules and glossary
/rules "double strike" match the phrase exactly
/rules blood moon names a card? the rules behind what it does
/ask does deathtouch kill a planeswalker?
Quoting a query matches those words adjacently; unquoted, every word must
appear before the search loosens. When nothing matches and the query names a
card, the bot answers the question actually being asked — the Comprehensive
Rules almost never mention cards by name, so blood moon finds nothing
literally, but its type line and text map onto Nonbasic Land, Enchantment and
Mountain. Cards with keyword abilities lead with those: Questing Beast returns
vigilance, haste and deathtouch.
/rule and /rules are deterministic lookups against a local index — no model,
no cost, no chance of invention. /ask adds a natural-language answer and only
works when an API key is configured (see below).
Metagame
/meta modern most-played archetypes, with meta share and decklist links
/set msc set details, by code
Available for standard, pioneer, modern, legacy, vintage, pauper, explorer, historic, alchemy, premodern, peasant, extended, block, highlander, canadian. mtgtop8 publishes no archetype breakdown for Commander, and the bot says so rather than returning an empty list.
How the rules engine works
The Comprehensive Rules (3,162 rules plus a 739-entry glossary) are pulled from the Academy Ruins API at image build time and written into a SQLite FTS5 index baked into the container. Every deploy therefore ships whatever CR was current when it was built.
/ask is retrieval-augmented generation, not a trained model. The passages
relevant to the question are retrieved from that index and passed to Claude as
context, with instructions to cite rule numbers and to say so when the passages
don't settle the question. This matters: a fine-tuned model would freeze the
rules at training time and start drifting the next time Wizards updates them,
and would have no way to cite its source. Rebuilding the index is free and
takes seconds.
If ANTHROPIC_API_KEY is unset, the answerer is never constructed and /ask
reports that it's disabled. Everything else works unchanged.
Configuration
Every value is read from the environment — nothing is read from a file on disk,
so the same image runs under docker compose and in Kubernetes, and running a
second instance means a second Secret, not a second build. See
.env.example for the full list.
| Variable | Required | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN |
yes | From @BotFather |
ANTHROPIC_API_KEY |
no | Enables /ask; omit to run rules search only |
LLM_MODEL |
no | Default claude-opus-5; claude-haiku-4-5 is ~5x cheaper |
LLM_EFFORT |
no | low by default — these are short, grounded answers |
MAX_CARDS_PER_MESSAGE |
no | Default 5 |
ALLOWED_CHAT_IDS |
no | Comma-separated allow-list; empty means any chat |
Local development
cp .env.example .env # fill in TELEGRAM_BOT_TOKEN
docker compose up --build
Without Docker:
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python -m bot.rules_engine.index data/rules.db # build the rules index once
RULES_DB_PATH=data/rules.db python -m bot.main
Deployment
Push to main and the Forgejo workflow builds
the image and pushes it to git.liniz.net tagged both latest and the contents
of VERSION. Kubernetes install and upgrade steps are in
deploy/README.md.
The deployment runs a single replica with the Recreate strategy, because
Telegram permits exactly one long-polling consumer per bot token. Long polling
also means no Service, no Ingress and no TLS certificate — the bot only makes
outbound connections. There is no database and no volume: the rules index is in
the image and the card and metagame caches are in memory, so pods are safe to
kill at any time. Liveness is checked against a heartbeat file the bot touches
every minute, which catches a wedged polling loop that a process-alive check
would miss.
Layout
bot/
main.py startup, handler registration, heartbeat
config.py environment-backed settings
scryfall.py async Scryfall client with TTL cache
formatting.py HTML rendering for Telegram
handlers/ cards, inline, rules, top8, misc
rules_engine/ CR index builder, FTS search, RAG answerer
top8/ mtgtop8 scraper
deploy/helm/ bjw-s app-template values
legacy/ the previous bot, kept for reference (not deployed)
Credits
Card data from Scryfall. Rules via Academy Ruins. Metagame data from mtgtop8. Not affiliated with Wizards of the Coast.