{"id":51144227,"url":"https://github.com/asm0dey/conference-notifier-bot","last_synced_at":"2026-06-26T01:30:46.264Z","repository":{"id":366918259,"uuid":"1278442261","full_name":"asm0dey/conference-notifier-bot","owner":"asm0dey","description":"Telegram bot that reminds you about Java conference CFP (Call for Papers) deadlines — urgency markers, map pins, GraalVM native image, Docker","archived":false,"fork":false,"pushed_at":"2026-06-23T21:54:10.000Z","size":176,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T01:30:41.292Z","etag":null,"topics":["alpaquita","bot","call-for-papers","cfp","conferences","db-scheduler","docker","graalvm","h2-database","java","kotlin","ksp","ktor","native-image","telegram","telegram-bot"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"0bsd","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/asm0dey.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-23T19:48:27.000Z","updated_at":"2026-06-23T21:46:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/asm0dey/conference-notifier-bot","commit_stats":null,"previous_names":["asm0dey/conference-notifier-bot"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/asm0dey/conference-notifier-bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asm0dey%2Fconference-notifier-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asm0dey%2Fconference-notifier-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asm0dey%2Fconference-notifier-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asm0dey%2Fconference-notifier-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/asm0dey","download_url":"https://codeload.github.com/asm0dey/conference-notifier-bot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asm0dey%2Fconference-notifier-bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34799570,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpaquita","bot","call-for-papers","cfp","conferences","db-scheduler","docker","graalvm","h2-database","java","kotlin","ksp","ktor","native-image","telegram","telegram-bot"],"created_at":"2026-06-26T01:30:42.165Z","updated_at":"2026-06-26T01:30:46.249Z","avatar_url":"https://github.com/asm0dey.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Conference CFP Notifier Bot\n\nA Telegram bot that watches [javaconferences.org](https://javaconferences.org/conferences.json)\nand tells you (in a DM or a channel) when conference Call-for-Papers deadlines are coming up,\nso you don't miss a submission window.\n\n## What it sends\n\nTwo kinds of **push** notifications, delivered as you go:\n\n- **CFP opened** — a one-time heads-up the first time a conference's CFP is seen open.\n- **Closing soon** — a reminder every day during the final 7 days before the CFP closes,\n  through the close day itself.\n\nEach message carries a **deadline-urgency marker** and, when the conference has coordinates,\na **clickable Google-Maps location** plus a **native Telegram map pin**:\n\n```\n🔴 ⏰ CFP closes TODAY: KotlinConf\n📍 Copenhagen, Denmark          ← tap → Google Maps (a map pin also arrives)\n⏳ Deadline 5 June 2026\n➡️ https://sessionize.com/kotlinconf\n```\n\n| Marker | Meaning            |\n|--------|--------------------|\n| 🔴     | closes today / tomorrow |\n| 🟠     | 2–3 days left      |\n| 🟡     | 4–7 days left      |\n| 🟢     | more than a week   |\n\n## Commands\n\n| Command  | What it does |\n|----------|--------------|\n| `/start` | Registers the current chat for notifications (works in a DM and in a channel). |\n| `/check` | Runs the deadline check immediately (handy for verifying wiring). |\n| `/active` | Sends a separate message for **every** currently-open CFP, right now. |\n\n`/active` is a **pull** view — your source of truth for \"what's open?\" — to complement the\ntimely pushes. It enqueues one message per open CFP and delivers them reliably even past\nTelegram's per-chat rate limit (see [How it works](#how-it-works)).\n\nThe bot registers these as a Telegram **command menu** on startup, so clients autocomplete\nthem (and show a menu button) when you type `/`.\n\n## Setup\n\n1. Create a bot with [@BotFather](https://t.me/BotFather) and copy the token.\n2. Put the token in a gitignored `.env` (see [Secrets](#secrets)) and run:\n\n   ```bash\n   set -a; . ./.env; set +a\n   ./gradlew run\n   ```\n\n3. **Register a private chat:** DM your bot `/start`.\n4. **Register a channel:** add the bot to the channel as an admin, then post `/start`\n   in the channel. The bot stores the channel's chat id and posts there.\n\n## Configuration (env vars)\n\n| Variable     | Default          | Meaning |\n|--------------|------------------|---------|\n| `BOT_TOKEN`  | (required)       | Telegram bot token from @BotFather |\n| `DB_PATH`    | `./data/cfpbot`  | H2 file path (state + scheduler survive restarts) |\n| `CHECK_HOUR` | `9`              | Hour of day (0–23, server local time) for the daily check |\n\n## Secrets\n\nThe bot token lives in a gitignored `.env` file. Load it without printing it:\n\n```bash\nset -a; . ./.env; set +a\n./gradlew run\n```\n\nNever commit `.env` (or `.envrc`) or paste the token into any tracked file. Telegram API\nerror URLs embed the token, so the bot redacts errors to the exception class name only.\n\n## How it works\n\nState (registered chats + which reminders have been sent) lives in an embedded **H2**\ndatabase. The daily check is a pure recompute from the live feed plus that state, so\nrestarts never reset progress and a missed day self-heals on the next run. The schedule\nis persisted by [db-scheduler](https://github.com/kagkarlsson/db-scheduler), which also\nruns any missed execution on startup.\n\n**First-seen-open semantics:** the \"CFP opened\" notice fires once, the first time a CFP is\nseen open. A chat that registers *after* a CFP was already open won't get that CFP's opened\nmessage — but still gets the final-week daily reminders, and `/active` always lists everything\ncurrently open.\n\n**`/active` send queue.** `/active` can produce dozens of messages, and Telegram rate-limits\n~1 message/sec **per chat**. So instead of fire-and-forget, `/active` writes each message to a\npersisted `send_queue` table and drains it:\n\n- Items are claimed atomically with `FOR UPDATE SKIP LOCKED`, so the inline drain and a\n  recurring drain never double-send (even though they can run concurrently).\n- On a per-chat rate limit, only that chat is backed off; other chats keep delivering. The\n  failing item is re-queued and retried; it's dropped only after 5 real delivery attempts.\n- A recurring `drain-queue` task runs every 2 minutes and delivers whatever is left — so the\n  remainder arrives within ~2 minutes even if Telegram throttles the first burst.\n\nThe timely **push** notifications (opened / closing-today) are sent **directly**, never through\nthe queue, so an urgent \"closes TODAY\" is never delayed.\n\nRun the tests with `./gradlew test`.\n\n## Native image (GraalVM)\n\nBuild a standalone native executable (starts fast, no JVM needed to run). Requires a\nGraalVM-capable JDK — e.g. [Liberica NIK](https://bell-sw.com/liberica-native-image-kit/)\n(`sdk install java 25.0.3.r25-nik`). The build reads `GRAALVM_HOME`/`JAVA_HOME`\n(`graalvmNative { toolchainDetection = false }`):\n\n```bash\nGRAALVM_HOME=/path/to/liberica-nik ./gradlew nativeCompile\n```\n\nOutput: `build/native/nativeCompile/cfpbot` (~90 MB). Run it like the jar — same env vars:\n\n```bash\nset -a; . ./.env; set +a            # load BOT_TOKEN without printing it\nDB_PATH=./data/cfpbot ./build/native/nativeCompile/cfpbot\n```\n\nVerified in the native image: boot, H2 + HikariCP, db-scheduler (daily check + queue drain),\nKSP command registration (`/start`, `/check`, `/active`), and long-polling all run cleanly.\n\n### Reachability metadata\n\nGraalVM needs reflection/serialization hints for telegram-bot's runtime serializer lookups.\nThey live in `src/main/resources/META-INF/native-image/cfpbot/reachability-metadata.json`,\ncaptured with the native-image tracing agent and committed. This covers the startup +\nlong-poll paths. The **inbound command handling and outbound message-send paths were not\nexercised when the metadata was captured** (they need live Telegram interaction), so the first\ntime you use them in the native binary you may hit a `Serializer ... not found` / reflection\nerror. If so, top up the metadata by running the JVM app under the agent while actually using\nthe bot, then rebuild:\n\n```bash\n./gradlew installDist\nset -a; . ./.env; set +a\nJAVA_HOME=/path/to/liberica-nik \\\nJAVA_OPTS=\"-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/cfpbot\" \\\n  build/install/conference-notifier-bot/bin/conference-notifier-bot\n# DM the bot /start, /check, /active (and post in a channel), then Ctrl-C\nGRAALVM_HOME=/path/to/liberica-nik ./gradlew nativeCompile\n```\n\n## Docker (native, on Alpaquita, non-root)\n\nA multi-stage [`Dockerfile`](Dockerfile) builds a **musl** native binary in a BellSoft\nLiberica NIK image and ships it on a tiny `bellsoft/alpaquita-linux-base:stream-musl`\nruntime (the binary links musl libc + libz dynamically, both present in that base). The\ncontainer runs as a **non-root** user and keeps its H2 database in a mounted `/data` volume.\n\n```bash\ndocker build -t cfpbot:native .\n```\n\nRun — load the token from your local `.env`, bind-mount a DB directory you own, and run as\nyour own UID/GID so the database files on the host stay owned by you:\n\n```bash\nmkdir -p ./data\ndocker run --rm \\\n  --env-file .env \\\n  --user \"$(id -u):$(id -g)\" \\\n  -v \"$PWD/data:/data\" \\\n  cfpbot:native\n```\n\n- `--env-file .env` — secrets stay out of the image; `.env` is also excluded from the build\n  context by [`.dockerignore`](.dockerignore).\n- `--user \"$(id -u):$(id -g)\"` + a **bind mount** — the process writes the H2 files as your\n  host user, so `./data/cfpbot.mv.db` is owned and readable by you, no root anywhere.\n- `DB_PATH` defaults to `/data/cfpbot` inside the container; override with `-e DB_PATH=...`\n  (keep it under `/data`). `CHECK_HOUR` defaults to `9`.\n\nOr with Compose ([`compose.yaml`](compose.yaml)) — set your ids once and it wires env, the\nvolume, and the user:\n\n```bash\nUID=$(id -u) GID=$(id -g) DB_DIR=./data docker compose up --build\n```\n\n## CI \u0026 releases\n\n- **CI** ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs `./gradlew test` on every\n  pull request.\n- **Release** ([`.github/workflows/release.yml`](.github/workflows/release.yml)) runs on **every\n  push to `main`**: it runs the tests, then (only if green) increments a single integer version\n  from the latest `vN` tag (first release is `v1`), builds and pushes the Docker image to\n  `ghcr.io/\u003cowner\u003e/\u003crepo\u003e` tagged with **both `N` and `latest`**, and publishes a GitHub\n  Release — tagged `vN`, with auto-generated notes and the native Linux binary attached.\n\nPublishing uses the built-in `GITHUB_TOKEN` (no extra secrets). The ghcr package may start out\nprivate — make it public in the repo's package settings to pull anonymously. Since every `main`\npush publishes (docs included), add `paths-ignore` to the release workflow if you want to skip\ndocs-only changes.\n\n## Tech stack\n\nKotlin 2.3 / JDK 21 · Gradle (Kotlin DSL) · [vendelieu/telegram-bot](https://github.com/vendelieu/telegram-bot)\n+ KSP · Ktor client · kotlinx-serialization · H2 + HikariCP · db-scheduler · Kotest · GraalVM native-image.\n\n## License\n\n[0BSD](LICENSE) (BSD Zero Clause) — do anything you like, no attribution required, no warranty.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasm0dey%2Fconference-notifier-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasm0dey%2Fconference-notifier-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasm0dey%2Fconference-notifier-bot/lists"}