package notify import ( "context" "log/slog" "sync" "building-mgmt/internal/telegram" ) // Bots — выбор бота для пользователя: собственный токен приоритетнее // глобального TELEGRAM_BOT_TOKEN. Именно так нотификатор решает, через // кого слать, — интерфейс, чтобы в тестах подставить карту без поллеров. type Bots interface { BotFor(userID string) telegram.API } // Registry — боты пользователей и их личные поллеры. // // Роль процесса здесь одна: на каждый сохранённый токен нужен свой // длинный поллинг (getUpdates у Telegram — очередь на бота, чужой её // не заберёт). Реестр владеет дочерними контекстами от parent, поэтому // остановка процесса гасит все поллеры разом (defer tgCancel в cmd/api). // // Потокобезопасен: BotFor вызывают нотификатор и хендлеры из разных // горутин, Register/Unregister — в запросах на сохранение и удаление // токена. type Registry struct { parent context.Context store Store // global — бот сервера (TELEGRAM_BOT_TOKEN); может быть выключенным, // тогда BotFor отдаёт его же, а вызывающий проверяет Enabled(). global telegram.API log *slog.Logger mu sync.Mutex users map[string]*userBot } // userBot — один личный бот: клиент API плюс отмена его поллера. type userBot struct { bot telegram.API cancel context.CancelFunc } // NewRegistry создаёт реестр поверх родительского контекста. func NewRegistry(ctx context.Context, store Store, global telegram.API, log *slog.Logger) *Registry { return &Registry{ parent: ctx, store: store, global: global, log: log, users: make(map[string]*userBot), } } // BotFor — бот пользователя: свой токен, иначе глобальный (может быть // выключенным — проверяет вызывающий). func (r *Registry) BotFor(userID string) telegram.API { r.mu.Lock() defer r.mu.Unlock() if u, ok := r.users[userID]; ok { return u.bot } return r.global } // Register поднимает поллер личного бота. Повторный вызов (пользователь // заменил токен) сначала гасит прежний поллер: две горутины не должны // тянуть одну очередь. func (r *Registry) Register(userID string, bot telegram.API) { if bot == nil || !bot.Enabled() { return } r.mu.Lock() defer r.mu.Unlock() if old, ok := r.users[userID]; ok { old.cancel() } ctx, cancel := context.WithCancel(r.parent) r.users[userID] = &userBot{bot: bot, cancel: cancel} // Имя бота на старте процесса ещё пустое: getMe не делаем, чтобы не // тормозить запуск (оно лежит в БД и в статусе). Поле добавляем // только когда оно есть — пустое bot="" лишь путает в логе. attrs := []any{"user_id", userID} if name := bot.Username(); name != "" { attrs = append(attrs, "bot", name) } r.log.Info("telegram user bot registered", attrs...) go RunPoller(ctx, r.store, bot, r.log) } // Unregister гасит поллер и забывает бота: дальше BotFor отдаёт // глобального (или выключенного, если его нет). func (r *Registry) Unregister(userID string) { r.mu.Lock() defer r.mu.Unlock() if u, ok := r.users[userID]; ok { u.cancel() delete(r.users, userID) r.log.Info("telegram user bot removed", "user_id", userID) } } // Users — сколько личных ботов сейчас поднято (для лога на старте). func (r *Registry) Users() int { r.mu.Lock() defer r.mu.Unlock() return len(r.users) }