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)
|
|||
|
|
}
|