Telegram catalogue of my projects, sites and bots: @ssrjkk_bot.
The bot lives in this repository under bot/, next to the projects hub
it catalogues. It is a Cloudflare Worker. Project data lives in projects.json, the
bundle in cloudflare-worker/worker.js is built from src/worker.js with esbuild, and the worker keeps
language preferences, the star cache and anonymous usage events in KV and D1.
Every project entry can carry a repository, a live site, a Telegram bot and a screenshot. Cards render whatever is present, so a project without a public site does not get a dead button.
Commands:
| Command | What it does |
|---|---|
/start |
Menu with projects, sites, bots, links and language switch |
/projects |
Three project cards per page plus stack facets, each card opens a detail view |
/news |
Pushes, releases and pull requests from the public profile feed |
/sites |
URL buttons for all project sites |
/bots |
URL buttons for all Telegram bots |
/search <query> |
Search by name, stack or description, RU and EN |
/top |
Top three projects by GitHub stars |
/links |
GitHub, Telegram, hub and bot catalogue |
/channels |
Telegram channels |
/about |
Short profile |
/feedback <text> |
Forward the text to the owner |
/stats |
Owner only: users, messages, opens, inline picks, week bars, top projects, referrers |
/broadcast <text> |
Owner only: send text to every user the bot has seen |
/myid |
Chat ID |
/help |
Command list |
The bot also answers inline queries: projects with screenshots come back as photo results, the rest as articles with site, repository and bot buttons.
/projects ends with a facet row built from the lang field of the catalogue — the six most common stacks,
each filter paging inside itself — so a new technology shows up in the filter without a code change. /news
renders pushes, releases, branch and tag creation and deletion, and pull requests from the public profile
event feed, one line per repository and action, and caches the rendered descriptors — not the raw events —
in KV. Events draw from the same 60 requests per hour GitHub budget as the star counts, so an entry stays
readable for a day after its 15 minutes of freshness: a rate limit or a 202 from a feed that is still being
generated cannot blank the section. Unauthenticated event payloads carry no commit list, so a push line names
the branch rather than a commit count. Every detail card has a share button opening t.me/share/url with
that project's own ?start=p_ link. /setup registers the command menu once per language scope, so Telegram
autocompletes /news in Russian or English according to the client language. /feedback <text> delivers the
note to the owner chat with the author's ID, HTML-escaped and cut to 900 characters; without OWNER_ID it
says it did not send anything.
https://t.me/ssrjkk_bot?start=p_<name> opens that project's card directly, and
?start=ref_<user id> credits the inviter in /stats once and never the invitee. Every card on the
hub carries a ?start=p_ link, and each detail card in the bot has an invite
button that builds a personal ?start=ref_ link for the reader. npm run check fails if a hub link points
at a project the catalogue does not have.
projects.json is the only place to edit content: github, hub, bot (the catalogue
bot itself, used to build invite links), projects, channels and links. Each project entry:
{
"name": "wake",
"title": "Wake",
"lang": "Python, TS",
"stars": 0,
"desc": "Russian description",
"desc_en": "English description",
"repo": "https://github.com/ssrjkk/wake",
"site": "https://wake-7k6.pages.dev",
"bot": "https://t.me/waketrade_bot",
"shot": "https://raw.githubusercontent.com/ssrjkk/ssrjkk/main/assets/shots/wake.jpg"
}site, bot and shot are optional. Screenshots are served from the profile repository on main, which
is reachable by Telegram without a token. stars is the fallback value — the worker refreshes real counts
from the GitHub API and caches them for an hour; when the API answers nothing for a project (rate limit on
an unauthenticated IP) the card keeps stars instead of showing 0. "private": true keeps the repository
link out of public cards while the entry stays in the catalogue. A project's repo must equal
https://github.com/ssrjkk/<name> — the bundle check enforces it.
After editing, rebuild and run the checks from bot/:
npm ci
npm run build
npm run check
npm test
npm run coveragenpm run check verifies the data shape, every URL, the screenshot host, unique project names, that the
bundle contains the current data, the wrangler bindings and secrets, that no token was committed, that the
README only documents commands and workflows that exist, and that every ?start=p_ link on the hub opens a
project the catalogue has.
Node 20 or newer. All commands run from bot/ (from the repository root: cd bot).
npm ci
npm run build # src/worker.js + projects.json -> cloudflare-worker/worker.js
npm test # node:test suite against the built bundle, Telegram and GitHub are mocked
npm run coverage # same suite, plus the numeric coverage gate CI fails onnpm run coverage measures the shipped bundle and scripts/smoke-prod.mjs and exits non-zero below 99.5%
lines, 96.5% branches and 97% functions; the suite currently reports 99.69 / 97.15 / 97.44. It runs against
the bundle that is deployed: Telegram, GitHub, KV and D1 are mocked, and fetch is replaced before the first
test with a stub that throws, so a case that forgets its mock fails loudly instead of reaching the network
from CI. The language and star caches are reset between the cases that care about them, and a catalogue built
from a temporary projects.json exercises the shapes the real one does not have yet — a project without a
screenshot, a private repository.
One gate reads the interface instead of the code: src/worker.js, the built bundle and every string of both
language packs must be free of emoji. The menu is spelled in words, and a pictograph in a button label or a
heading fails the suite with the file, the key and the code point it found.
npm run check is held to the same standard: 48 mutations of the data, the bundle, the configuration, the
README and the hub cards prove the gate rejects a blank title, a missing lang, a repeated name, a screenshot
outside the profile repository, a stale bundle, an unused wrangler binding, a planted Telegram or GitHub
token, a bundle that is not a worker module, and a README or hub card promising something the repository does
not have.
What no test reaches is what the data gate forbids: every project carries a title, a lang and a screenshot,
so displayName, stackTokens and the inline article branch keep their fallbacks. They are proven on the
fixture catalogue rather than deleted — deploying without npm run check must not print an empty name.
The worker expects four secrets, set with wrangler secret put and never committed:
BOT_TOKEN— from @BotFatherOWNER_ID— Telegram user ID for/stats,/broadcastand the target of/feedback, the bot reports it with/myidSETUP_KEY— query key for the one-time/setupcallWEBHOOK_SECRET— value Telegram sends inX-Telegram-Bot-Api-Secret-Token
Bindings in cloudflare-worker/wrangler.toml:
- KV
BOT_KV— language preferences, star cache, news cache - D1
BOT_DB—events (user_id, kind, value, ts), wherekindismsg,cmd,search,open(a project card),nav(a menu section or astack:<tech>filter),lang,inline(an inline result picked in another chat),feedback(a note delivered to the owner) andref(who invited this user)
From the repository root:
cd bot
npm run build
cd cloudflare-worker
npx wrangler deployThen point Telegram at the worker once:
https://<worker-host>/setup?key=<SETUP_KEY>
/setup calls setWebhook with the secret header and uploads the command list for both language scopes.
GET /health returns OK; GET / returns the catalogue size as JSON.
A reply is not dropped because Telegram was busy: a 429 or a 502/503/504 is repeated up to three
times, waiting the parameters.retry_after the API asks for, with the whole pause capped at three seconds —
a webhook handler that hangs costs more than a lost message. A 500 is never repeated: the send may already
have happened, and a retry would put a duplicate in the chat.
npm run smoke # WORKER_URL overrides the deployed worker it asksThree questions to the production worker: GET /health answers OK, GET / reports the same projects,
sites and bots counts as projects.json, and POST /webhook without the secret header answers 403.
A transport error or a 5xx is retried three times, because a new version can still be spreading across the
edge; any other status is an answer, so it is not retried. Every check reports what it saw and any red one
exits non-zero — .github/workflows/bot-prod-smoke.yml runs it hourly and on demand. The checks are unit
tested against a mocked worker, including the three ways they are supposed to fail.
Paths relative to the repository root:
bot/src/worker.js worker source
bot/projects.json project, site, bot and link data
bot/cloudflare-worker/worker.js built bundle (committed, deployed as is)
bot/cloudflare-worker/wrangler.toml
bot/scripts/check-bundle.mjs data and bundle checks
bot/scripts/smoke-prod.mjs live checks against the deployed worker
bot/tests/worker.test.mjs node:test suite
bot/tests/smoke.test.mjs node:test suite for the smoke checks
CI for the bot is .github/workflows/bot-ci.yml (build, bundle diff, check, test), scoped to bot/, and
.github/workflows/bot-prod-smoke.yml (the live smoke above, hourly and on demand).
CodeQL runs through GitHub's default setup for the repository, so there is no committed CodeQL workflow.
The hub's own live-status workflow is separate.
@ssrjkk_bot — каталог моих проектов, сайтов и ботов в Telegram. Воркер на
Cloudflare Workers лежит в этом репозитории в bot/, рядом с хабом. Данные в projects.json,
сборка esbuild, состояние в KV и D1.
Команды: /projects — карточки проектов со скриншотами и ссылками, внизу строка фасетов по стеку (шесть
самых частых технологий из projects.lang, пагинация остаётся внутри фильтра), /news — пуши, релизы и
pull request из публичной ленты профиля, по строке на пару «репозиторий — событие»; запись свежа 15 минут
и живёт в KV сутки, поэтому лимит GitHub не обнуляет раздел, /sites — все сайты, /bots — все боты,
/search — поиск по
названию, стеку и описанию, /top — топ по звёздам, /links и /channels — ссылки и каналы,
/feedback <текст> — сообщение владельцу (экранируется и режется до 900 символов), /stats — статистика для
владельца: пользователи, сообщения, открытия карточек, выборы из inline, отзывы, неделя по дням, топ
проектов и кто кого пригласил. Меню команд регистрируется отдельно для ru и en, поэтому подсказки Telegram
переведены.
Deep-ссылки: https://t.me/ssrjkk_bot?start=p_<name> сразу открывает карточку проекта,
?start=ref_<user id> один раз засчитывает пригласившего. С карточек хаба ведут ссылки ?start=p_,
а в карточке бота есть «Поделиться» с той же ссылкой проекта и «Пригласить» с личной ?start=ref_.
npm run check ловит расхождение: имя проекта в ссылке хаба должно быть в каталоге.
Правка контента: изменить projects.json, затем из bot/:
npm run build && npm run check && npm test && npm run coverage.
Секреты (BOT_TOKEN, OWNER_ID, SETUP_KEY, WEBHOOK_SECRET) задаются через wrangler secret put и в
репозиторий не попадают. Деплой: cd bot && npm run build, затем npx wrangler deploy из
bot/cloudflare-worker.
Ответ бота не теряется из-за лимита Telegram: 429 и 502/503/504 повторяются до трёх раз с паузой из
parameters.retry_after, суммарно не дольше трёх секунд — висящий вебхук дороже потерянного сообщения. 500
не повторяем: под ним сообщение могло уже уйти, а повтор — это дубликат в чате.
Смоук по проду: npm run smoke (WORKER_URL меняет адрес) задаёт живому воркеру три вопроса — /health
отвечает OK, GET / отдаёт те же счётчики, что и projects.json, POST /webhook без секрета отвечает
403. Сетевой сбой и 5xx повторяются трижды (версия могла ещё не разъехаться по edge), другой статус — это
ответ, его не повторяют. Любая красная проверка выходит кодом 1;
.github/workflows/bot-prod-smoke.yml гоняет смоук раз в час и по кнопке, а сами проверки покрыты тестами,
включая три способа, которыми они должны падать.
Покрытие — числовой порог, а не «посмотрели и согласились»: npm run coverage меряет задеплоенный бандл и
scripts/smoke-prod.mjs и выходит кодом 1 ниже 99.5% строк, 96.5% веток и 97% функций; сейчас прогон даёт
99.69 / 97.15 / 97.44. Тесты гоняют против того же бандла, что уезжает на Cloudflare: Telegram, GitHub, KV и
D1 замоканы, а fetch до первой проверки подменён заглушкой, которая бросает ошибку, — тест, забывший
двойника, падает сразу и с адресом запроса, а не уходит в сеть из CI. Кеш языка и кеш звёзд сбрасываются
между проверками, которым важно начало TTL, а ветки, которых в текущем каталоге нет (проект без скриншота,
приватный репозиторий), прогоняются на сборке из временного projects.json теми же флагами esbuild.
Одна проверка читает не код, а сам интерфейс: в src/worker.js, в собранном бандле и в каждой строке обоих
языковых пакетов не должно остаться эмодзи. Меню набирается словами, и картинка в подписи кнопки или в
заголовке роняет прогон с указанием файла, ключа и найденного code point.
npm run check проверяется так же: 48 мутаций данных, бандла, конфигурации, README и карточек хаба
доказывают, что гейт отклоняет пустой title, отсутствующий lang, повтор имени, скриншот не из профильного
репозитория, устаревший бандл, неиспользуемую привязку wrangler, подложенный токен Telegram или GitHub,
бандл без обработчика по умолчанию и документ или карточку, обещающие то, чего в репозитории нет.
До чего не доводит живой каталог — то, что запрещает гейт данных: у каждого проекта есть title, lang и
скриншот, поэтому displayName, stackTokens и article-ветка inline-ответов сохраняют свои фолбэки. Они
подтверждены на фикстуре, а не удалены: деплой в обход npm run check не должен печатать пустое имя.
SSRJKK — GitHub · Telegram · Projects hub · @ssrjkk_bot