feat(telegram): promo bot + channel-chat moderation gate
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Add a second standalone promo bot to the bot container (answers /start with a localized message + a URL button into the main bot's Mini App) and gate write access in a channel's linked discussion chat: grant on join when the Telegram user is registered and neither admin-suspended nor holding a new chat_muted role, and revoke/grant on the matching moderation change for a member currently in the chat. Eligibility (registered AND NOT suspended AND NOT chat_muted; the game suspension dominates) is resolved once in the backend and reached two ways: the bot's join-time unary ResolveChatEligibility over the existing mTLS bot-link, and a backend chat_access_changed event -> gateway -> ChatGate command (idempotent; a temporary-block-expiry sweeper may over-emit). The bot guards the block/unblock path with getChatMember, since bots cannot list members. A web_app button cannot open another bot's Mini App (it signs initData with the sending bot's token), so the promo button is a t.me ?startapp URL reusing the UI's VITE_TELEGRAM_LINK. The bot must be a chat admin with the restrict-members right and chat_member in its allowed updates. No schema change: chat_muted reuses the data-driven account_roles table.
This commit is contained in:
@@ -103,6 +103,54 @@ func TestLoadBotRequired(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadBotChatAndPromo verifies the moderated-chat id and the promo-bot
|
||||
// configuration parse, the @-prefix is stripped from the username, and a promo token
|
||||
// without a username/link is rejected.
|
||||
func TestLoadBotChatAndPromo(t *testing.T) {
|
||||
t.Run("parsed", func(t *testing.T) {
|
||||
setBotRequired(t)
|
||||
t.Setenv("TELEGRAM_CHAT_ID", "-100222")
|
||||
t.Setenv("TELEGRAM_PROMO_BOT_TOKEN", "promo-token")
|
||||
t.Setenv("TELEGRAM_BOT_USERNAME", "@ScrabbleBot")
|
||||
t.Setenv("TELEGRAM_BOT_LINK", "https://t.me/ScrabbleBot/app")
|
||||
c, err := LoadBot()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadBot: %v", err)
|
||||
}
|
||||
if c.ChatID != -100222 {
|
||||
t.Errorf("ChatID = %d, want -100222", c.ChatID)
|
||||
}
|
||||
if c.PromoBotToken != "promo-token" {
|
||||
t.Errorf("PromoBotToken = %q", c.PromoBotToken)
|
||||
}
|
||||
if c.BotUsername != "ScrabbleBot" {
|
||||
t.Errorf("BotUsername = %q, want the leading @ stripped", c.BotUsername)
|
||||
}
|
||||
if c.BotLinkURL != "https://t.me/ScrabbleBot/app" {
|
||||
t.Errorf("BotLinkURL = %q", c.BotLinkURL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("promo token requires username and link", func(t *testing.T) {
|
||||
setBotRequired(t)
|
||||
t.Setenv("TELEGRAM_PROMO_BOT_TOKEN", "promo-token")
|
||||
if _, err := LoadBot(); err == nil {
|
||||
t.Fatal("LoadBot: expected an error for a promo token without username/link")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
setBotRequired(t)
|
||||
c, err := LoadBot()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadBot: %v", err)
|
||||
}
|
||||
if c.PromoBotToken != "" || c.ChatID != 0 {
|
||||
t.Errorf("defaults: promo=%q chat=%d, want empty/0", c.PromoBotToken, c.ChatID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestLoadRejectsUnsupportedExporter verifies an exporter outside the supported set
|
||||
// fails validation (the validator path).
|
||||
func TestLoadRejectsUnsupportedExporter(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user