mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-14 06:18:06 +00:00
feat(config): resolve the bot username from BOT_USERNAME or getMe
The default sticker pack name and the lol User-Agent now follow whichever bot runs the code, so a bot account change needs no code edit. The default pack becomes miti99_by_<bot username>.
This commit is contained in:
1 parent
c18006fabc
commit
79104ef3d7
16 files changed
+198
-41
No files matched your search
+7
-2
@@ -21,12 +21,17 @@ OWNER_ID=
|
||||
# Comma-separated admin Telegram user ids (renamed from ADMIN_USER_IDS).
|
||||
ADMIN_IDS=
|
||||
|
||||
# Bot's Telegram username, without "@". Unset = asked from Telegram (getMe)
|
||||
# once at startup. Used for the default sticker pack name and the lol
|
||||
# User-Agent. Set it to skip that call or to pin the value.
|
||||
BOT_USERNAME=
|
||||
|
||||
# Sticker set /addsticker writes to. Every user contributes to this one pack.
|
||||
# MUST end in _by_<this bot username>: Telegram requires that suffix on sets a
|
||||
# bot creates and refuses to edit sets it did not create, so the suffix is what
|
||||
# proves the pack is manageable. Created automatically, owned by OWNER_ID, on
|
||||
# the first /addsticker if it does not exist yet. Unset = the default below.
|
||||
STICKER_PACK_NAME=miti99_by_miti99bot
|
||||
# the first /addsticker if it does not exist yet. Unset = miti99_by_<bot username>.
|
||||
STICKER_PACK_NAME=
|
||||
|
||||
# SOURCE_COMMIT (commit SHA) is read at startup for the deploynotify owner DM.
|
||||
# Do NOT set it here. Coolify provides it at runtime. Keep "Include Source
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
func TestResolveBotUsername(t *testing.T) {
|
||||
t.Run("configured value wins without calling getMe", func(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
if got := resolveBotUsername(context.Background(), rb.Bot, "envbot"); got != "envbot" {
|
||||
t.Errorf("got %q, want envbot", got)
|
||||
}
|
||||
for _, c := range rb.Sent() {
|
||||
if c.Method == "getMe" {
|
||||
t.Error("getMe called although BOT_USERNAME was set")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to getMe", func(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
rb.StubMethod("getMe", `{"id":1,"is_bot":true,"username":"apibot"}`)
|
||||
if got := resolveBotUsername(context.Background(), rb.Bot, ""); got != "apibot" {
|
||||
t.Errorf("got %q, want apibot", got)
|
||||
}
|
||||
})
|
||||
|
||||
// RecordingBot answers an unstubbed method with a bare `true`, which cannot
|
||||
// decode into a User: the failure must yield "" rather than stop startup.
|
||||
t.Run("getMe failure yields empty", func(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
if got := resolveBotUsername(context.Background(), rb.Bot, ""); got != "" {
|
||||
t.Errorf("got %q, want empty", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadConfig_BotUsername(t *testing.T) {
|
||||
t.Setenv("PORT", "8080")
|
||||
for in, want := range map[string]string{"": "", "examplebot": "examplebot", " @examplebot ": "examplebot"} {
|
||||
t.Setenv("BOT_USERNAME", in)
|
||||
if got := loadConfig().BotUsername; got != want {
|
||||
t.Errorf("BOT_USERNAME=%q: got %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-1
@@ -17,6 +17,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/cron"
|
||||
"github.com/tiennm99/miti99bot/internal/deploynotify"
|
||||
"github.com/tiennm99/miti99bot/internal/log"
|
||||
@@ -164,8 +166,10 @@ func main() {
|
||||
log.Fatal("telegram bot init failed", "err", err)
|
||||
}
|
||||
|
||||
botUsername := resolveBotUsername(rootCtx, b, cfg.BotUsername)
|
||||
reg, err := modules.Build(cfg.Modules, factories(), provider, modules.BuildOptions{
|
||||
Bot: b,
|
||||
Bot: b,
|
||||
BotUsername: botUsername,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("module registry build failed", "err", err)
|
||||
@@ -355,11 +359,36 @@ func buildProvider(ctx context.Context, cfg config) (storage.Provider, func(), e
|
||||
}
|
||||
}
|
||||
|
||||
// botUsernameTimeout bounds the startup getMe. It is best-effort, so a slow
|
||||
// Telegram must not hold up the rest of startup for long.
|
||||
const botUsernameTimeout = 10 * time.Second
|
||||
|
||||
// resolveBotUsername returns the bot's username: BOT_USERNAME when set,
|
||||
// otherwise one getMe call. A failed getMe is logged and yields "" rather than
|
||||
// stopping startup; modules that need the username then learn it lazily or
|
||||
// do without.
|
||||
func resolveBotUsername(ctx context.Context, b *bot.Bot, configured string) string {
|
||||
if configured != "" {
|
||||
log.Info("bot username", "username", configured, "source", "BOT_USERNAME")
|
||||
return configured
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, botUsernameTimeout)
|
||||
defer cancel()
|
||||
me, err := b.GetMe(ctx)
|
||||
if err != nil || me == nil || me.Username == "" {
|
||||
log.Warn("getMe failed; bot username unknown until a module asks again", "err", err)
|
||||
return ""
|
||||
}
|
||||
log.Info("bot username", "username", me.Username, "source", "getMe")
|
||||
return me.Username
|
||||
}
|
||||
|
||||
// config is the process configuration read from the environment by loadConfig.
|
||||
type config struct {
|
||||
Port string
|
||||
TelegramBotToken string
|
||||
SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify
|
||||
BotUsername string // optional; empty = ask Telegram via getMe at startup
|
||||
Modules []string
|
||||
BotOwnerID int64
|
||||
AdminUserIDs map[int64]bool
|
||||
@@ -392,6 +421,7 @@ func loadConfig() config {
|
||||
Port: port,
|
||||
TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"],
|
||||
SourceCommit: envMap["SOURCE_COMMIT"],
|
||||
BotUsername: strings.TrimPrefix(strings.TrimSpace(envMap["BOT_USERNAME"]), "@"),
|
||||
Modules: splitCSV(envMap["MODULES"]),
|
||||
BotOwnerID: parseInt64(envMap["OWNER_ID"]),
|
||||
AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]),
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ services:
|
||||
# ADMIN_IDS: ${ADMIN_IDS:-} # CSV of admin Telegram user ids; empty = none
|
||||
# LOL_PANDASCORE_TOKEN: ${LOL_PANDASCORE_TOKEN:-} # PandaScore token — SECRET; empty = /lol* fetches fail
|
||||
# GOLD_VNAPP_API_KEY: ${GOLD_VNAPP_API_KEY:-} # VNAppMob key — SECRET; empty = fetched and cached in Mongo
|
||||
# STICKER_PACK_NAME: ${STICKER_PACK_NAME:-miti99_by_miti99bot} # /addsticker set
|
||||
# BOT_USERNAME: ${BOT_USERNAME:-} # bot username without "@"; empty = getMe at startup
|
||||
# STICKER_PACK_NAME: ${STICKER_PACK_NAME:-} # /addsticker set; empty = miti99_by_<bot username>
|
||||
# LOG_LEVEL: ${LOG_LEVEL:-info} # debug|info|warn|error
|
||||
|
||||
# --- Fixed by this stack (not Coolify settings) ---
|
||||
|
||||
@@ -34,7 +34,8 @@ Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in.
|
||||
| `MODULES` | optional | CSV; empty = all modules, including any added later |
|
||||
| `OWNER_ID` | optional | Telegram user id for owner-only commands, the deploy DM, and the `/addsticker` pack owner. Unset = owner-only commands are denied and `/addsticker` refuses |
|
||||
| `ADMIN_IDS` | optional | CSV of Telegram user ids for admin-only commands |
|
||||
| `STICKER_PACK_NAME` | optional | set `/addsticker` writes to; default `miti99_by_miti99bot`. See [sticker packs](sticker-packs.md) |
|
||||
| `BOT_USERNAME` | optional | the bot's Telegram username, without `@`; unset = asked from Telegram (`getMe`) once at startup. Used for the default sticker pack name and the lol User-Agent |
|
||||
| `STICKER_PACK_NAME` | optional | set `/addsticker` writes to; default `miti99_by_<bot username>`. See [sticker packs](sticker-packs.md) |
|
||||
| `LOL_PANDASCORE_TOKEN` | optional | PandaScore API token for the lol module (free tier) — secret, never logged; without it every `/lol*` fetch fails (stale cache may still serve briefly) |
|
||||
| `RENDERER_URL` | leave unset | base URL of the animation renderer; fixed by `compose.yml` to the bundled renderer (`http://renderer:3000`), so a Coolify value is ignored |
|
||||
| `LOG_LEVEL` | optional | `debug`, `info` (default), `warn`, or `error`; logs are JSON on stdout |
|
||||
|
||||
@@ -24,7 +24,7 @@ Single-shot: one message replying to the media to add. No conversation state.
|
||||
|
||||
| Env | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `STICKER_PACK_NAME` | `miti99_by_miti99bot` | The Telegram set to write to |
|
||||
| `STICKER_PACK_NAME` | `miti99_by_<bot username>` | The Telegram set to write to |
|
||||
| `OWNER_ID` | — | Must be the account that **owns** that set |
|
||||
|
||||
`OWNER_ID` is reused rather than given a sticker-specific twin because
|
||||
@@ -36,6 +36,9 @@ The caller's identity is used nowhere. That is what makes the command stateless:
|
||||
no records, no keys, no per-user locks, and no ownership checks. It is also why
|
||||
`/addsticker` needs no per-user storage.
|
||||
|
||||
The bot's username comes from `BOT_USERNAME` when set, otherwise from one
|
||||
`getMe` call at startup (retried on the first `/addsticker` if that failed).
|
||||
|
||||
`STICKER_PACK_NAME` **must end in `_by_<this bot's username>`** — Telegram
|
||||
requires that suffix on every set a bot creates, and refuses to let a bot edit
|
||||
any set it did not create. A name without the suffix therefore cannot be this
|
||||
@@ -49,7 +52,7 @@ before any download or API call:
|
||||
|
||||
**The pack creates itself on first use.** If the set does not exist, the first
|
||||
`/addsticker` creates it — owned by `OWNER_ID`, titled with the slug half of the
|
||||
name (`miti99_by_miti99bot` → *miti99*), and seeded with the sticker that
|
||||
name (`miti99_by_examplebot` → *miti99*), and seeded with the sticker that
|
||||
triggered it, because Telegram cannot create an empty set. The reply says
|
||||
*"Created the shared pack with this sticker."*
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ const (
|
||||
// credential itself.
|
||||
tokenEnv = "LOL_PANDASCORE_TOKEN" // #nosec G101
|
||||
|
||||
userAgent = "miti99bot/0.1 (https://t.me/miti99bot)"
|
||||
// userAgentProduct leads the User-Agent; Client.userAgent appends the
|
||||
// running bot's t.me link when its username is known.
|
||||
userAgentProduct = "miti99bot/0.1"
|
||||
// pageSize is PandaScore's per_page maximum; 100 covers a dense day in
|
||||
// one request and a full week in a couple.
|
||||
pageSize = 100
|
||||
@@ -247,8 +249,18 @@ type CacheStore = storage.DocStore[cacheRecord]
|
||||
// default HTTP client; tests inject a custom HTTP client (typically pointing
|
||||
// at httptest.Server).
|
||||
type Client struct {
|
||||
HTTP *http.Client
|
||||
URL string // override for tests; empty falls back to apiURL
|
||||
HTTP *http.Client
|
||||
URL string // override for tests; empty falls back to apiURL
|
||||
BotUsername string // running bot's username for the User-Agent; may be empty
|
||||
}
|
||||
|
||||
// userAgent identifies the caller to PandaScore, linking the running bot when
|
||||
// its username is known.
|
||||
func (c *Client) userAgent() string {
|
||||
if c.BotUsername == "" {
|
||||
return userAgentProduct
|
||||
}
|
||||
return userAgentProduct + " (https://t.me/" + c.BotUsername + ")"
|
||||
}
|
||||
|
||||
// httpClient returns the client to use, or a sensible default.
|
||||
@@ -296,7 +308,7 @@ func (c *Client) fetchEventsPage(ctx context.Context, from, to time.Time, page i
|
||||
// stay safe to log.
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("User-Agent", c.userAgent())
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -422,3 +422,12 @@ func TestTruncate(t *testing.T) {
|
||||
t.Errorf("truncate = %q, want 'a lon...'", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientUserAgent(t *testing.T) {
|
||||
if got := (&Client{}).userAgent(); got != "miti99bot/0.1" {
|
||||
t.Errorf("no username: got %q", got)
|
||||
}
|
||||
if got := (&Client{BotUsername: "examplebot"}).userAgent(); got != "miti99bot/0.1 (https://t.me/examplebot)" {
|
||||
t.Errorf("with username: got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func New(deps modules.Deps) modules.Module {
|
||||
subscribers: storage.Typed[subscription.Doc](deps.Store),
|
||||
pushDate: storage.Typed[subscription.DayDoc](deps.Store),
|
||||
cache: storage.Typed[cacheRecord](deps.Store),
|
||||
client: &Client{},
|
||||
client: &Client{BotUsername: deps.BotUsername},
|
||||
}
|
||||
return modules.Module{
|
||||
Commands: []modules.Command{
|
||||
|
||||
@@ -117,6 +117,10 @@ type Deps struct {
|
||||
Store storage.Collection // the module's own collection; build typed views with storage.Typed[T]
|
||||
Registry *Registry // populated by Build; safe to capture but read-only at module use
|
||||
Bot *bot.Bot // nil-safe: only crons that fan-out (lol daily push) need it
|
||||
// BotUsername is the bot's Telegram username without "@", from BOT_USERNAME
|
||||
// or getMe at startup. Empty when neither was available; consumers must
|
||||
// cope (sticker resolves it lazily, lol omits it from its User-Agent).
|
||||
BotUsername string
|
||||
}
|
||||
|
||||
// Factory constructs a Module from its Deps. Deps are passed directly (instead
|
||||
|
||||
@@ -159,7 +159,8 @@ func (r *Registry) Crons() []Cron {
|
||||
// Factory's Deps. Adding new optional deps here keeps Build's signature
|
||||
// stable as the dep list grows.
|
||||
type BuildOptions struct {
|
||||
Bot *bot.Bot
|
||||
Bot *bot.Bot
|
||||
BotUsername string
|
||||
}
|
||||
|
||||
// Build constructs a Registry from the requested module names. The Provider
|
||||
@@ -220,9 +221,10 @@ func Build(enabled []string, factories map[string]Factory, provider storage.Prov
|
||||
}
|
||||
|
||||
moduleDeps := Deps{
|
||||
Store: provider.Collection(name),
|
||||
Registry: reg,
|
||||
Bot: opts.Bot,
|
||||
Store: provider.Collection(name),
|
||||
Registry: reg,
|
||||
Bot: opts.Bot,
|
||||
BotUsername: opts.BotUsername,
|
||||
}
|
||||
mod := factory(moduleDeps)
|
||||
// A factory that hardcodes its own Name is a bug: the registry key is
|
||||
|
||||
@@ -53,9 +53,10 @@ type stickerSource struct {
|
||||
// no ownership to check. That is what keeps the command stateless.
|
||||
//
|
||||
// The resolver is built here and captured by the handler so the bot's username
|
||||
// is fetched at most once per process rather than once per invocation.
|
||||
func addStickerCommand() modules.Command {
|
||||
resolver := &botUsernameResolver{}
|
||||
// is fetched at most once per process rather than once per invocation. It
|
||||
// starts from botUsername, which is empty when startup could not learn it.
|
||||
func addStickerCommand(botUsername string) modules.Command {
|
||||
resolver := &botUsernameResolver{username: botUsername}
|
||||
return modules.Command{
|
||||
Name: "addsticker",
|
||||
Visibility: modules.VisibilityPublic,
|
||||
@@ -93,6 +94,9 @@ func handleAddSticker(ctx context.Context, b *bot.Bot, update *models.Update, re
|
||||
if err != nil {
|
||||
return replyErr(ctx, b, msg, "util_addsticker_username", err)
|
||||
}
|
||||
if pack.Name == "" {
|
||||
pack.Name = defaultPackName(username)
|
||||
}
|
||||
title, err := packTitle(pack.Name, username)
|
||||
if err != nil {
|
||||
return replyMisconfigured(ctx, b, msg, "util_addsticker_pack_name", err, packNotBotOwnedRefusal)
|
||||
|
||||
@@ -94,8 +94,10 @@ func TestAddSticker_NonOwnerWritesToConfiguredPack(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSticker_DefaultsToMiti99Pack(t *testing.T) {
|
||||
rb := installAddSticker(t, "", "miti99bot")
|
||||
// The default pack follows the running bot: its name ends in the username
|
||||
// getMe reports, so the same build works under any bot token.
|
||||
func TestAddSticker_DefaultPackFollowsBotUsername(t *testing.T) {
|
||||
rb := installAddSticker(t, "", "otherbot")
|
||||
|
||||
rb.Bot.ProcessUpdate(context.Background(), stickerReply(999, "", "src", ""))
|
||||
|
||||
@@ -103,8 +105,36 @@ func TestAddSticker_DefaultsToMiti99Pack(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("no addStickerToSet call; got %+v", rb.Sent())
|
||||
}
|
||||
if got := call.Form["name"]; got != "miti99_by_miti99bot" {
|
||||
t.Errorf("name = %q, want the default pack", got)
|
||||
if got := call.Form["name"]; got != "miti99_by_otherbot" {
|
||||
t.Errorf("name = %q, want the default pack for @otherbot", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A username known at startup (BOT_USERNAME or the startup getMe) is used as
|
||||
// is: the command must not spend a getMe call re-learning it.
|
||||
func TestAddSticker_SeededUsernameSkipsGetMe(t *testing.T) {
|
||||
t.Setenv("OWNER_ID", "999")
|
||||
t.Setenv("STICKER_PACK_NAME", "")
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
reg, err := modules.Build([]string{"sticker"},
|
||||
map[string]modules.Factory{"sticker": sticker.New},
|
||||
storage.NewMemoryProvider(), modules.BuildOptions{BotUsername: "seededbot"})
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
modules.Install(rb.Bot, reg, modules.Auth{})
|
||||
|
||||
rb.Bot.ProcessUpdate(context.Background(), stickerReply(999, "", "src", ""))
|
||||
|
||||
if _, ok := callTo(rb, "getMe"); ok {
|
||||
t.Error("getMe called although the username was seeded")
|
||||
}
|
||||
call, ok := callTo(rb, "addStickerToSet")
|
||||
if !ok {
|
||||
t.Fatalf("no addStickerToSet call; got %+v", rb.Sent())
|
||||
}
|
||||
if got := call.Form["name"]; got != "miti99_by_seededbot" {
|
||||
t.Errorf("name = %q, want the default pack for @seededbot", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
// same handle to Build and to any startup task, matching lol/coin/stock.
|
||||
const CollectionName = "sticker"
|
||||
|
||||
// New is the module Factory. It takes no Deps: the command reads its pack from
|
||||
// the environment and keeps no state, so the collection this module is handed
|
||||
// goes unused.
|
||||
func New(_ modules.Deps) modules.Module {
|
||||
// New is the module Factory. Only Deps.BotUsername is used: the command reads
|
||||
// its pack from the environment and keeps no state, so the collection this
|
||||
// module is handed goes unused.
|
||||
func New(deps modules.Deps) modules.Module {
|
||||
return modules.Module{
|
||||
Commands: []modules.Command{
|
||||
addStickerCommand(),
|
||||
addStickerCommand(deps.BotUsername),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,10 @@ const (
|
||||
// not exist yet is created by the first successful /addsticker.
|
||||
stickerPackNameEnv = "STICKER_PACK_NAME"
|
||||
|
||||
// defaultStickerPackName is the shared pack used when the env is unset.
|
||||
defaultStickerPackName = "miti99_by_miti99bot"
|
||||
// defaultStickerPackSlug names the shared pack used when the env is unset.
|
||||
// The full name is this slug plus "_by_<bot_username>", so the default
|
||||
// follows whichever bot runs the code.
|
||||
defaultStickerPackSlug = "miti99"
|
||||
|
||||
// stickerPackOwnerEnv reuses the bot-wide owner setting rather than
|
||||
// introducing a second variable: AddStickerToSet needs the *set owner's*
|
||||
@@ -74,7 +76,7 @@ const (
|
||||
|
||||
// stickerPack is the resolved target of /addsticker.
|
||||
type stickerPack struct {
|
||||
Name string // Telegram set name, e.g. "miti99_by_miti99bot"
|
||||
Name string // Telegram set name, e.g. "miti99_by_examplebot"; empty until defaulted
|
||||
OwnerID int64 // the account the set belongs to; AddStickerToSet demands it
|
||||
}
|
||||
|
||||
@@ -82,16 +84,15 @@ type stickerPack struct {
|
||||
// Internal, not user-facing: nothing the caller does fixes a misconfiguration.
|
||||
var errNoPackOwner = errors.New("util: sticker pack owner ID unset")
|
||||
|
||||
// loadStickerPack resolves the shared pack from the environment.
|
||||
// loadStickerPack resolves the shared pack from the environment. Name is left
|
||||
// empty when STICKER_PACK_NAME is unset: the default depends on the bot's
|
||||
// username, which the caller fills in with defaultPackName once resolved.
|
||||
//
|
||||
// Read per invocation rather than captured at startup, matching how gold and
|
||||
// lol read their credentials: it keeps the command testable with t.Setenv and
|
||||
// costs nothing next to the API calls that follow.
|
||||
func loadStickerPack() (stickerPack, error) {
|
||||
name := strings.TrimSpace(os.Getenv(stickerPackNameEnv))
|
||||
if name == "" {
|
||||
name = defaultStickerPackName
|
||||
}
|
||||
ownerID, err := strconv.ParseInt(strings.TrimSpace(os.Getenv(stickerPackOwnerEnv)), 10, 64)
|
||||
if err != nil || ownerID == 0 {
|
||||
return stickerPack{}, errNoPackOwner
|
||||
@@ -99,6 +100,11 @@ func loadStickerPack() (stickerPack, error) {
|
||||
return stickerPack{Name: name, OwnerID: ownerID}, nil
|
||||
}
|
||||
|
||||
// defaultPackName is the shared pack's name for a bot with this username.
|
||||
func defaultPackName(botUsername string) string {
|
||||
return defaultStickerPackSlug + setNameSuffix + botUsername
|
||||
}
|
||||
|
||||
// stickerShareLink is the public URL of a sticker set.
|
||||
func stickerShareLink(setName string) string {
|
||||
return "https://t.me/addstickers/" + setName
|
||||
@@ -150,9 +156,10 @@ func packTitle(name, botUsername string) (string, error) {
|
||||
|
||||
// botUsernameResolver caches the bot's username.
|
||||
//
|
||||
// The bot starts with bot.WithSkipGetMe(), so nothing populates a username
|
||||
// until this asks. Failures are never cached: a transient GetMe error must not
|
||||
// disable /addsticker for the process's lifetime.
|
||||
// main seeds it from BOT_USERNAME or a startup getMe. The bot starts with
|
||||
// bot.WithSkipGetMe(), so when neither produced a username nothing populates
|
||||
// one until this asks. Failures are never cached: a transient GetMe error must
|
||||
// not disable /addsticker for the process's lifetime.
|
||||
type botUsernameResolver struct {
|
||||
mu sync.Mutex
|
||||
username string
|
||||
|
||||
@@ -106,15 +106,15 @@ func TestPackTitle_AtNameLengthCap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadStickerPack(t *testing.T) {
|
||||
t.Run("defaults the name and requires an owner", func(t *testing.T) {
|
||||
t.Run("leaves the name to the default and requires an owner", func(t *testing.T) {
|
||||
t.Setenv("OWNER_ID", "42")
|
||||
t.Setenv("STICKER_PACK_NAME", "")
|
||||
pack, err := loadStickerPack()
|
||||
if err != nil {
|
||||
t.Fatalf("loadStickerPack: %v", err)
|
||||
}
|
||||
if pack.Name != defaultStickerPackName {
|
||||
t.Errorf("name = %q, want %q", pack.Name, defaultStickerPackName)
|
||||
if pack.Name != "" {
|
||||
t.Errorf("name = %q, want empty until the username fills the default", pack.Name)
|
||||
}
|
||||
if pack.OwnerID != 42 {
|
||||
t.Errorf("ownerID = %d, want 42", pack.OwnerID)
|
||||
|
||||
Reference in new issue
Block a user