axe/internal/notify/registry.go

111 lines
4.2 KiB
Go
Raw Normal View History

2026-10-18 19:07:25 +00:00
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)
}