Codync
免費、開源的 1:1 替代方案,取代 Grok Bot 與 Muse。從 iPhone、Mac 或 Linux 桌面將 Claude Code、Codex、Cursor 與 40+ 程式碼代理人作為持續機器人傳訊。群組聊天、主題串…
AI 翻譯
尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
Telegram bot delivering GitHub, GitLab, Gitea, Forgejo and GitVerse repository events to group chats in real time.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A Telegram bot that delivers GitHub, GitLab, Gitea, Forgejo and GitVerse
repository events to group
chats in real time. Everything is driven by inline keyboards in a private chat with the bot;
/start is the only command.
A live instance runs at @g0thubbot — send it /start
to see the interface before deploying your own.
Architecture: a single Go binary. The GitHub webhook verifies the HMAC (other
hosts: their per-connection secret, see internal/forge),
deduplicates by delivery id, finds the matching integrations, and writes rows
into a Postgres outbox. A pool of workers drains the outbox, renders each event
as Telegram HTML, and sends it with retries and backoff.
19 kinds: push, pull_request, pull_request_review,
pull_request_review_comment, issues, issue_comment, commit_comment,
release, star, fork, create, delete, gollum, member, public,
deployment, deployment_status, check_suite, workflow_run.
GitLab projects get 9 kinds of their own: push, tag_push,
merge_request, issue, note (comments on merge requests, issues, commits
and snippets), pipeline, release, wiki_page, deployment.
Gitea, Forgejo and GitVerse repositories share one payload format and get 9
kinds: push, create, delete, pull_request, pull_request_review,
issues, issue_comment, release, wiki.
Every kind is toggled per integration, plus three presets: everything, the
important ones, nothing. Filters match on author, branch, label, and action.
star fires once per user per repository, forever. When a chat hits the
CHAT_PER_MINUTE ceiling, the excess collapses into a digest.
Connecting a repository to a chat and changing its settings is limited to administrators of that chat. Rights are asked of Telegram at the moment of the action (cached for a minute), not at the moment the screen is drawn. Viewing is covered by the same check: the chat screen and the event, filter, and health screens are all behind it.
Disconnecting is narrower than that. A repository is unplugged only by the administrator who connected it, or by the chat's owner — one administrator cannot remove another's integration, and the owner is what keeps a chat from being stuck with an integration whose author has left.
Telegram offers bots no way to list a forum's topics, so the picker is built
from what the bot has seen: a topic created while it was in the chat, a rename,
and any message posted inside a topic. A topic older than the bot becomes
visible as soon as someone sends /start inside it — commands reach the bot
whatever its privacy setting, ordinary messages only with privacy mode off.
Failing that, the picker can create a topic itself, which needs the
Manage topics admin right.
| Variable | Purpose |
|---|---|
BOT_TOKEN | bot token from @BotFather |
BOT_USERNAME | bot username without @ |
DATABASE_URL | postgres://… connection string (SQLite is not supported) |
GITHUB_APP_ID, GITHUB_SLUG | GitHub App id and slug |
GITHUB_PRIVATE_KEY_PATH | path to the App's .pem private key |
GITHUB_WEBHOOK_SECRET | GitHub App webhook secret |
PUBLIC_URL | external address the reverse proxy points at |
SECRET_KEY | AES-GCM key encrypting installation tokens — 32 random bytes in base64, generate with openssl rand -base64 32 |
CHAT_PER_MINUTE, WORKERS | delivery pace and worker count |
ADMIN_IDS | comma-separated Telegram user ids that get the admin panel (/admin): referral links, broadcasts, anonymous statistics |
Configuration is environment variables only. The full list with defaults lives
in .env.example: copy it to .env and the binary reads it at startup. Real
environment variables always win over the file.
These hosts have no App to install, so a connection is a webhook. Other platforms in the bot lists them; picking one mints a secret and shows what to paste into the repository's (or group's) webhook settings. How the secret is used differs per host:
| Host | Webhook URL | Secret goes into |
|---|---|---|
| GitLab | https://<host>/hook/gitlab (the old /gl/webhook still works) | Secret token |
| Gitea | https://<host>/hook/gitea/<connection> | Secret (HMAC signature) |
| Forgejo, Codeberg | https://<host>/hook/forgejo/<connection> | Secret (HMAC signature) |
| GitVerse | https://<host>/hook/gitverse | Authorization header |
GitVerse is built on Gitea and is parsed as Gitea; it only reaches ports 80
and 443, so PUBLIC_URL must use one of them.
For GitLab specifically, in the project's (or group's) Settings → Webhooks:
https://<host>/hook/gitlabGitLab gives no way to list what a webhook covers, so a project appears in the
bot once it has sent its first event — the webhook's Test button is
enough. One token serves any number of projects and groups, on gitlab.com or
a self-managed instance: no GitLab API access is needed, only that GitLab can
reach PUBLIC_URL. A connection is removed from the bot itself (Delete
connection), which stops delivery for every chat it feeds and makes GitLab's
further deliveries fail with 401. The same holds for the other hosts, with
their own "test delivery" button.
https://<host>/gh/webhook, secret: the value of
GITHUB_WEBHOOK_SECRET.https://<host>/github/setup, with "Redirect on update" checked.push,
pull_request, issues, installation..pem private key and put it in deploy/secrets/github-app.pem
with mode 0600.The install link is handed out by the bot and carries a single-use state
token valid for an hour; that token, and nothing else, decides whose
installation it is. An installation is bound to a user once — changing the
owner means removing and re-installing the App.
A public App also needs a homepage URL and a privacy policy URL in its GitHub
settings. With @BotFather, set the description, the about text, and a picture;
there is one command, /start.
cd deploy
cp .env.example .env # fill in DATABASE_URL, BOT_TOKEN, …
docker compose up -d
The database is external by default: DATABASE_URL points at your own cluster,
with sslmode=require if it is reachable outside a private network. The schema
migrates itself at startup, so that user needs DDL rights on the database. The
bundled Postgres is an option for a self-contained deployment:
docker compose -f docker-compose.yml -f docker-compose.local-db.yml up -d
The bot listens on 127.0.0.1:8080 and is published by a reverse proxy — see
deploy/Caddyfile.example. GET /healthz pings Postgres and answers 503 when
the database is unreachable; the container healthcheck uses the same endpoint.
An alternative to a proxy on a public IP: cloudflared holds an outbound
connection to Cloudflare, the server has no inbound ports, and its address is
visible to neither GitHub nor a scanner. Requires a domain on Cloudflare.
cloudflared tunnel login
cloudflared tunnel create ghnotify
cloudflared tunnel route dns ghnotify notify.example.com
cp ~/.cloudflared/<TUNNEL-ID>.json deploy/cloudflared/
cp deploy/cloudflared/config.example.yml deploy/cloudflared/config.yml
Put your tunnel id and host into config.yml; only /gh/webhook,
/gl/webhook, /hook/... and /github/setup are exposed, everything else is
a 404. Then:
docker compose -f docker-compose.yml -f docker-compose.cloudflared.yml up -d
PUBLIC_URL is the same https://notify.example.com used in the tunnel route.
Telegram updates arrive over long polling anyway, so the only inbound traffic
is the webhooks, and the tunnel covers those paths entirely.
Run exactly one instance. Telegram updates are fetched by long polling, and
two copies would fight over getUpdates. Migrations take an advisory lock at
startup, so a simultaneous restart will not corrupt the database, but the
process cannot be scaled horizontally.
A janitor sweeps hourly so the tables that only grow stay bounded: delivered outbox rows after an hour (the per-chat rate valve looks back sixty seconds), failed ones after 7 days, callback keys after 7 days, GitHub delivery ids after 7 days, and audit records after 90 days. Pending outbox rows are never swept at any age. An expired callback key is not an error: the button lands the user on the home screen instead.
Every user-visible string lives in internal/i18n/locales/*.yaml and is
embedded in the binary. Five locales: English (the base and the fallback),
Russian, Spanish, German, Portuguese.
The private interface's language comes from Telegram's language_code on the
first /start and can be changed from the Settings screen. A notification's
language is the language of the integration's owner, so two integrations in one
chat can speak differently. A test in internal/i18n enforces that every locale
carries the same set of keys and placeholders.
make test # one throwaway Postgres container for the whole run
make test-direct # no Makefile: testcontainers boots Postgres itself
Each test gets its own database inside the shared container, so make test
starts exactly one Postgres rather than one per test.