feat: add telegram notification channel with outbox service and per-user preferences

Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
This commit is contained in:
2026-06-22 20:52:02 +00:00
parent a9d0814c47
commit e0e64c8d9e
36 changed files with 720 additions and 78 deletions
+18 -12
View File
@@ -126,7 +126,7 @@ The **Code Farm** (`/game`) is a cooperative idle game in the spirit of Farmvill
- **Refactor (prestige).** At level 10 you can refactor: the farm resets (coins, level, CI, extra plots, perks) in exchange for a permanent +25% coin bonus that stacks with every refactor.
- **Visit and water friends.** Open another member's farm at `/game/farm/{username}` and water their growing builds to speed them up - you earn coins for helping, and the owner sees the help live. This is the social loop that makes the game cooperative.
- **Steal a harvest.** A ready build on someone else's farm can be stolen once a 60-second protection window passes - the owner gets that grace period to harvest it first. A successful steal pays the thief half the build's coin value (the owner loses the whole build) and earns the **Cat Burglar** badge; the victim gets the **Robbed** badge and a live notification that someone raided their farm (the thief is never named). Stealing pays coins only, so the leaderboard stays earned by real farming. This is the competitive counterpart to watering.
- **Leaderboard.** Top farmers are ranked by level, XP, and total harvests, shown alongside your own farm.
- **Leaderboard.** Top farmers are ranked by a composite achievement score that weighs every factor the game tracks - refactor (prestige) count, XP, lifetime harvests, current coins, CI tier, plots bought, perk levels, and login streak - so total accomplishment decides position rather than just the current post-refactor cycle. The score is shown alongside your own farm next to each player's level.
The farm refreshes live over the pub/sub bus (a watered build appears on the owner's screen at once) and every plot countdown ticks client-side. Every endpoint also answers JSON, and Devii can play the game on the member's behalf via the `game_*` tools (`game_state`, `game_plant`, `game_harvest`, `game_buy_plot`, `game_upgrade_ci`, `game_water`, `game_steal`, `game_view_farm`, `game_leaderboard`). See the API reference group **Code Farm**.
@@ -715,7 +715,7 @@ installable Progressive Web App. Push uses only standard libraries (`cryptograph
### Events
Every event flows through a single funnel - `create_notification()` in `utils.py` -
which delivers on two independent channels, in-app and web push, each gated by the
which delivers on three independent channels, in-app, web push and Telegram, each gated by the
recipient's preferences (see "Configurable notifications" below). Whenever the in-app
channel delivers, the recipient's open browser also raises a live, click-through toast
in real time, bridged onto the in-process pub/sub bus by a lock-owner relay:
@@ -741,17 +741,23 @@ return `404`/`410` are soft-deleted.
Every notification type can be turned on or off per channel, per user. The **Notifications**
tab on a profile page (`/profile/{username}?tab=notifications`, visible to the profile owner
and to admins) shows one row per type with two checkboxes - **In-app** and **Push** - saved
individually as you toggle them (`POST /profile/{username}/notifications`). A "Reset to
defaults" button clears all of a user's overrides (`POST /profile/{username}/notifications/reset`).
and to admins) shows one row per type with three checkboxes - **In-app**, **Push** and
**Telegram** - saved individually as you toggle them (`POST /profile/{username}/notifications`).
A "Reset to defaults" button clears all of a user's overrides
(`POST /profile/{username}/notifications/reset`). The Telegram column is disabled until the
user pairs Telegram from the profile Telegram panel; once paired, opting a type in delivers
that notification to the user's Telegram chat.
Defaults are opt-out: a type/channel a user never touched is enabled. Admins set the
platform-wide default for each type/channel on `/admin/notifications`
(`POST /admin/notifications`); a default applies only to users who have not made an explicit
choice. Resolution is: user override, else admin default, else on. Preferences are stored in
the `notification_preferences` table (per `user_uid` + `notification_type`, soft-deletable)
and enforced inside `create_notification()`: the in-app row is written only when the in-app
channel is enabled, and `push.notify_user` is scheduled only when the push channel is enabled.
Defaults are opt-out for in-app and push (a type/channel a user never touched is enabled) and
opt-in for Telegram (every type is off by default). Admins set the platform-wide default for
each type/channel on `/admin/notifications` (`POST /admin/notifications`); a default applies
only to users who have not made an explicit choice. Resolution is: user override, else admin
default, else the channel fallback. Preferences are stored in the `notification_preferences`
table (per `user_uid` + `notification_type`, soft-deletable) and enforced inside
`create_notification()`: the in-app row is written only when the in-app channel is enabled,
`push.notify_user` is scheduled only when the push channel is enabled, and a Telegram message
is queued (to the `telegram_outbox`, drained on the service-lock owner where the bot runs)
only when the Telegram channel is enabled and the user is paired.
### VAPID keys