No description
  • Python 99%
  • Dockerfile 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Pablo 2f53d77f3b
All checks were successful
Build and Publish Containers / build-and-push (push) Successful in 9s
Merge pull request '2.1.0 - new features' (#6) from staging into main
Reviewed-on: #6
2026-08-16 12:51:41 -05:00
.forgejo/workflows change build 2026-08-14 18:08:10 -05:00
bot feat: show card art in chat replies, and shorten the inline pause 2026-08-16 12:47:36 -05:00
deploy fix: point deploy manifests at the correct registry path 2026-08-14 17:54:35 -05:00
docs docs: record the shelved spoiler-alert design 2026-08-16 11:54:56 -05:00
legacy initial commit 2026-08-13 21:01:26 -05:00
scripts tooling: Forgejo issue client, and keep credentials out of the tree 2026-08-16 11:54:56 -05:00
.dockerignore tooling: Forgejo issue client, and keep credentials out of the tree 2026-08-16 11:54:56 -05:00
.env.example feat: show card art in chat replies, and shorten the inline pause 2026-08-16 12:47:36 -05:00
.gitignore tooling: Forgejo issue client, and keep credentials out of the tree 2026-08-16 11:54:56 -05:00
docker-compose.yml chore: point the local staging tag at 2.1.0 2026-08-16 12:38:10 -05:00
Dockerfile initial commit 2026-08-13 21:01:26 -05:00
README.md feat: show card art in chat replies, and shorten the inline pause 2026-08-16 12:47:36 -05:00
requirements.txt initial commit 2026-08-13 21:01:26 -05:00
VERSION feat: show card art in chat replies, and shorten the inline pause 2026-08-16 12:47:36 -05:00

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.