111 lines
4.2 KiB
Go
111 lines
4.2 KiB
Go
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)
|
||
}
|