Send Markdown. Get native Telegram rich text.
A deliberately small Go bot built directly on Telegram Bot API 10.1.
Quick Start · Usage · How It Works · Operations · Configuration
Telegram Bot API 10.1 can render structured Rich Markdown natively. makeitMD keeps the product boundary as small as possible: send source text in a private chat and receive Telegram's native rendering immediately.
| Principle | makeitMD approach |
|---|---|
| Exact input | Passes the submitted string unchanged as rich_message.markdown |
| Native rendering | Uses Telegram sendRichMessage, not a custom parser |
| Minimal interaction | /start only, one panel: two tabs and the language, no settings |
| Private operations | Statistics and source audit stay operator-only |
| Direct integration | Calls the Bot API over HTTP without a Telegram library |
Result: one text in, one native rich message out.
| Channel | Version | Meaning |
|---|---|---|
| Stable | v0.1.0 |
First public release |
| Production | v0.1.0 |
Runs as makeitmd-bot with healthy DB and Telegram polling |
You need Go 1.26.5+, Docker, a Telegram token from
BotFather, and access to the Asterfield shared
core-postgres database.
cp .env.example .env
$EDITOR .env
docker compose up --buildBefore the first start, apply core/migrations/003_makeitmd.sql, provision the
makeitmd_core password, and ensure the external Docker network core_net
exists.
- Open @makeitMD_bot.
- Send
/start. - Send Markdown source text.
- Receive the native Telegram Rich Markdown message.
The greeting is intentionally short:
Send me Markdown. I’ll render it.
/start also opens the panel: How it works, About — the running
version, the rendering the bot uses, the repository, the licence and the admin —
and Language. Back returns to the greeting, and every screen edits the
same message rather than adding another one. There is nothing else to configure,
so there is no settings screen.
The panel speaks the sixteen languages the Asterfield bots share. It follows the language your Telegram client reports until you pick one, and the language you pick is stored once for the whole family: the other bots answer in it too. Follow Telegram on that screen hands the decision back to your client.
Everything else the bot says is a rendered message. In a group makeitMD renders
nothing: /start there is registered as an ephemeral command, so the answer —
a link back to the private chat — is visible only to the person who sent it.
Rich Markdown supports headings, nested styles, lists, task lists, tables, quotes, code blocks, details, links, formulas, and other structured content. Telegram currently limits one rich message to 32,768 UTF-8 characters.
Telegram getUpdates
|
v
core.touch identity/presence
|
v
store exact source as received
|
v
sendRichMessage(markdown: exact source)
|
+--> sent -> lifetime statistics
+--> failed -> short syntax error
The durable Telegram offset advances only after handling an update. Replayed
updates do not resend conversions already marked sent.
Global Telegram identity and presence live in core.person and
core.presence. Private makeitMD data lives in the isolated makeitmd schema:
makeitmd.conversions— exact decoded source, raw input message and entities, every outbound Markdown/result attempt, final Telegram result, delivery state, character count, and byte count.makeitmd.user_stats— lifetime per-user conversion totals independent of raw-text retention.makeitmd.runtime_state— durable Telegram update offset.makeitmd.schema_migrations— applied domain migration ledger.
Raw sent and failed conversions expire after 90 days by default. Unresolved
received rows remain available for recovery. No statistics are shown to users.
GET /healthzchecks PostgreSQL and Telegram polling freshness and returns version, commit, build date, and pending/failed conversion counts.GET /metricsexposes private-data-free Prometheus counters.- Docker marks the container unhealthy when
/healthzfails. - Telegram GET requests retry transport, 429, and 5xx failures with jitter.
- POST requests retry only when Telegram explicitly returns
retry_after, avoiding unsafe duplicate sends after an ambiguous transport failure. - Transport errors are sanitized before logging so the bot token cannot appear through an embedded Bot API URL.
| Variable | Required | Default | Purpose |
|---|---|---|---|
TELEGRAM_BOT_TOKEN |
yes | — | Telegram Bot API token |
DATABASE_URL |
yes | — | makeitmd_core connection to shared core-postgres |
AUTO_MIGRATE |
no | true |
Apply makeitMD migrations on startup |
MIGRATIONS_DIR |
no | ./migrations |
SQL migration directory |
HTTP_ADDR |
no | :8080 |
Internal health and metrics listener |
CONVERSION_RETENTION |
no | 2160h |
Retention for terminal raw conversion rows |
LOG_LEVEL |
no | info |
Reserved log level |
BUILD_VERSION |
no | dev |
Version stamped into health and logs |
BUILD_COMMIT |
no | none |
Source revision |
BUILD_DATE |
no | unknown |
UTC build timestamp |
deploy/ws04/compose.yaml is the production stack. It pulls the image the
release workflow published to GHCR and never builds one:
# in the stack directory, alongside .env
MAKEITMD_IMAGE=ghcr.io/freshlabdev/makeitmd@sha256:<digest> docker compose up -dPin MAKEITMD_IMAGE by digest rather than tag. A digest names one exact build,
so a rollback is a one-line change with nothing to rebuild, and docker inspect
on the running container answers which commit it came from. BUILD_VERSION /
BUILD_COMMIT / BUILD_DATE are baked in by the release workflow; they only
need setting for a local build.
go mod verify
go test -race ./...
go vet ./...
govulncheck ./...
docker compose config
docker build -t makeitmd:local .CI repeats module verification, build, race tests, vet, vulnerability scanning, Docker build, and Compose validation. See versioning, release process, and the full changelog.