Ctrl K
ComunidadGuías…

internal/app/bots/gramsrv_operator.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package bots

import (
	"context"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"

	adminapp "telesrv/internal/admin"
	"telesrv/internal/domain"
)

const gramsrvOperatorBotUserID int64 = 1250000023

type gramsrvAccountFreezeManager interface {
	AccountFreeze(context.Context, int64) (domain.AccountFreeze, bool, error)
	SetAccountFrozen(context.Context, adminapp.SetAccountFrozenRequest) (adminapp.CommandResult, error)
}

// WithGramsrvOperatorBot enables the in-process @gramsrv dialog for exact user IDs.
func WithGramsrvOperatorBot(chatIDs []int64, freezes gramsrvAccountFreezeManager) Option {
	return func(s *Service) {
		if freezes == nil {
			return
		}
		s.gramsrvFreezes = freezes
		s.gramsrvOperators = make(map[int64]bool, len(chatIDs))
		for _, id := range chatIDs {
			if id > 0 {
				s.gramsrvOperators[id] = true
			}
		}
	}
}

func (s *Service) respondAsGramsrvOperator(userID int64, body string) {
	if !s.gramsrvOperators[userID] {
		return
	}
	mu := s.serviceBotReplyLock(gramsrvOperatorBotUserID, userID)
	mu.Lock()
	defer mu.Unlock()
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	fields := strings.Fields(strings.TrimSpace(body))
	if len(fields) == 0 {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: gramsrvOperatorHelp()})
		return
	}
	command := strings.ToLower(strings.SplitN(strings.TrimPrefix(fields[0], "/"), "@", 2)[0])
	if command == "start" || command == "help" {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: gramsrvOperatorHelp()})
		return
	}
	if command != "account" && command != "freeze" && command != "unfreeze" || len(fields) != 2 {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: "Используй /account @username, /freeze @username или /unfreeze @username."})
		return
	}
	target, found, err := s.gramsrvOperatorTarget(ctx, fields[1])
	if err != nil {
		s.log.Error("gramsrv operator: lookup target", zap.Error(err))
		return
	}
	if !found {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: "Аккаунт не найден или недоступен для этой операции."})
		return
	}
	if target.ID == userID {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: "Свой аккаунт через бота заморозить нельзя."})
		return
	}
	reply, err := s.gramsrvOperatorAccountReply(ctx, userID, target)
	if err != nil {
		s.log.Error("gramsrv operator: account status", zap.Int64("target_user_id", target.ID), zap.Error(err))
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, botReply{Text: "Не удалось получить статус аккаунта."})
		return
	}
	s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, userID, reply)
}

func gramsrvOperatorHelp() string {
	return "Управление заморозкой аккаунтов Gramsrv.\n/account @username — статус и кнопка действия\n/freeze @username — открыть действие\n/unfreeze @username — открыть действие"
}

func (s *Service) gramsrvOperatorTarget(ctx context.Context, input string) (domain.User, bool, error) {
	if s.users == nil {
		return domain.User{}, false, nil
	}
	name := strings.TrimPrefix(strings.TrimSpace(input), "@")
	var target domain.User
	var found bool
	var err error
	if id, parseErr := strconv.ParseInt(name, 10, 64); parseErr == nil && id > 0 {
		target, found, err = s.users.ByID(ctx, id)
	} else if name != "" {
		target, found, err = s.users.ByUsername(ctx, name)
	}
	if !found || err != nil || target.ID <= 0 || target.Bot || domain.IsSystemUserID(target.ID) {
		return domain.User{}, false, err
	}
	return target, true, nil
}

func (s *Service) gramsrvOperatorAccountReply(ctx context.Context, userID int64, target domain.User) (botReply, error) {
	freeze, found, err := s.gramsrvFreezes.AccountFreeze(ctx, target.ID)
	if err != nil {
		return botReply{}, err
	}
	frozen := found && freeze.Frozen
	token := s.verifyOptionToken()
	state := domain.BotChatState{BotUserID: gramsrvOperatorBotUserID, UserID: userID,
		Command: "gramsrv", Step: "confirm", Draft: map[string]string{
			"target": strconv.FormatInt(target.ID, 10),
			"name":   target.Username,
			"frozen": strconv.FormatBool(frozen),
			"token":  token,
		}}
	if err := s.bots.UpsertBotChatState(ctx, state); err != nil {
		return botReply{}, err
	}
	status, action := "активен", "🔒 Заморозить"
	if frozen {
		status, action = "заморожен", "🔓 Разморозить"
	}
	label := fmt.Sprintf("@%s", target.Username)
	if target.Username == "" {
		label = strconv.FormatInt(target.ID, 10)
	}
	return botReply{Text: fmt.Sprintf("%s (ID %d): %s.", label, target.ID, status),
		ReplyMarkup: &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline,
			Inline: [][]domain.MarkupButton{{{Type: domain.MarkupButtonCallback, Text: action, Data: []byte("gs:" + token)}}}}}, nil
}

func (s *Service) onGramsrvOperatorCallback(ctx context.Context, query domain.BotCallbackQuery) (domain.BotCallbackAnswer, bool, error) {
	if !s.gramsrvOperators[query.UserID] {
		return domain.BotCallbackAnswer{Alert: true, Message: "Доступ закрыт."}, true, nil
	}
	mu := s.serviceBotReplyLock(gramsrvOperatorBotUserID, query.UserID)
	mu.Lock()
	defer mu.Unlock()
	state, found, err := s.bots.GetBotChatState(ctx, gramsrvOperatorBotUserID, query.UserID)
	if err != nil {
		return domain.BotCallbackAnswer{}, true, err
	}
	if !found || state.Command != "gramsrv" || state.Step != "confirm" ||
		string(query.Data) != "gs:"+state.Draft["token"] || state.Draft["token"] == "" {
		return domain.BotCallbackAnswer{Alert: true, Message: "Кнопка устарела. Открой /account снова."}, true, nil
	}
	targetID, err := strconv.ParseInt(state.Draft["target"], 10, 64)
	if err != nil || targetID <= 0 || targetID == query.UserID {
		return domain.BotCallbackAnswer{Alert: true, Message: "Некорректная цель."}, true, nil
	}
	target, ok, err := s.gramsrvOperatorTarget(ctx, strconv.FormatInt(targetID, 10))
	if err != nil || !ok {
		return domain.BotCallbackAnswer{Alert: true, Message: "Аккаунт недоступен."}, true, err
	}
	current, exists, err := s.gramsrvFreezes.AccountFreeze(ctx, targetID)
	if err != nil {
		return domain.BotCallbackAnswer{}, true, err
	}
	expected := state.Draft["frozen"] == "true"
	if (exists && current.Frozen) != expected {
		return domain.BotCallbackAnswer{Alert: true, Message: "Статус уже изменился. Открой /account снова."}, true, nil
	}
	next := !expected
	request := adminapp.SetAccountFrozenRequest{CommandMeta: adminapp.CommandMeta{
		CommandID: fmt.Sprintf("gramsrv-bot:%d:%d:%s", query.UserID, query.ID, state.Draft["token"]),
		Actor:     fmt.Sprintf("gramsrv-bot:%d", query.UserID),
		Reason:    "operator bot action",
	}, UserID: targetID, Frozen: next}
	if next {
		request.Until = time.Unix(math.MaxInt32, 0).UTC()
		request.AppealURL = strings.TrimRight(s.publicBaseURL, "/") + "/"
		if request.AppealURL == "/" {
			request.AppealURL = "https://telesrv.net/"
		}
	}
	result, err := s.gramsrvFreezes.SetAccountFrozen(ctx, request)
	if err != nil {
		s.log.Error("gramsrv operator: set account freeze", zap.Int64("target_user_id", targetID), zap.Error(err))
		return domain.BotCallbackAnswer{Alert: true, Message: "Не удалось изменить статус аккаунта."}, true, nil
	}
	state.Step = "done"
	if err := s.bots.UpsertBotChatState(ctx, state); err != nil {
		s.log.Error("gramsrv operator: save callback state", zap.Error(err))
	}
	if !result.AlreadyExecuted {
		s.gramsrvOperatorNotify(ctx, query.UserID, target, next)
	}
	reply, err := s.gramsrvOperatorAccountReply(ctx, query.UserID, target)
	if err == nil {
		s.sendServiceBotReply(ctx, gramsrvOperatorBotUserID, query.UserID, reply)
	}
	return domain.BotCallbackAnswer{Message: "Готово."}, true, nil
}

func (s *Service) gramsrvOperatorNotify(ctx context.Context, actorID int64, target domain.User, frozen bool) {
	actor := strconv.FormatInt(actorID, 10)
	if user, found, err := s.users.ByID(ctx, actorID); err == nil && found && user.Username != "" {
		actor = "@" + user.Username
	}
	name := fmt.Sprintf("@%s", target.Username)
	if target.Username == "" {
		name = strconv.FormatInt(target.ID, 10)
	}
	action := "разморозил"
	if frozen {
		action = "заморозил"
	}
	text := fmt.Sprintf("%s %s аккаунт %s (ID %d).", actor, action, name, target.ID)
	sendCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
	defer cancel()
	for id := range s.gramsrvOperators {
		s.sendServiceBotReply(sendCtx, gramsrvOperatorBotUserID, id, botReply{Text: text})
	}
}