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
+28
View File
@@ -128,6 +128,34 @@ def level_progress(xp: int) -> dict:
}
SCORE_PRESTIGE = 5000
SCORE_HARVEST = 10
SCORE_COIN_DIVISOR = 20
SCORE_CI = 250
SCORE_PLOT = 200
SCORE_PERK = 120
SCORE_STREAK = 15
SCORE_STREAK_CAP = 30
def farm_score(farm: dict) -> int:
def value(key: str, default: int = 0) -> int:
raw = farm.get(key)
return int(raw) if raw is not None else default
perk_total = sum(value(f"perk_{perk.key}") for perk in PERKS)
return (
value("xp")
+ value("prestige") * SCORE_PRESTIGE
+ value("total_harvests") * SCORE_HARVEST
+ value("coins") // SCORE_COIN_DIVISOR
+ (value("ci_tier", 1) - 1) * SCORE_CI
+ (value("plot_count", STARTING_PLOTS) - STARTING_PLOTS) * SCORE_PLOT
+ perk_total * SCORE_PERK
+ min(value("streak"), SCORE_STREAK_CAP) * SCORE_STREAK
)
def unlocked_crops(level: int) -> list[Crop]:
return [crop for crop in CROPS if crop.min_level <= level]
+3 -1
View File
@@ -475,7 +475,7 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
def leaderboard(limit: int = 25) -> list[dict]:
farms = sorted(
_farms().find(),
key=lambda row: (int(row.get("level", 1)), int(row.get("xp", 0)), int(row.get("total_harvests", 0))),
key=economy.farm_score,
reverse=True,
)[:limit]
if not farms:
@@ -496,6 +496,8 @@ def leaderboard(limit: int = 25) -> list[dict]:
"xp": int(farm.get("xp", 0)),
"coins": int(farm.get("coins", 0)),
"total_harvests": int(farm.get("total_harvests", 0)),
"prestige": int(farm.get("prestige") or 0),
"score": economy.farm_score(farm),
}
)
return entries