mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
Harden Telegram bot cleanup commands
This commit is contained in:
@@ -418,6 +418,108 @@ func TestBotCleanupRulesCanBeDeletedUntilEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCleanupRunPreviewsBeforeConfirm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos, bot := newBotTestService(t)
|
||||
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
||||
if err := repos.User.Create(ctx, admin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now()
|
||||
old := now.Add(-30 * 24 * time.Hour)
|
||||
stale := &model.User{Username: "stale", PasswordHash: "x", Role: "user", IsActive: true}
|
||||
stale.CreatedAt = old
|
||||
stale.LastLoginAt = &old
|
||||
recent := &model.User{Username: "recent", PasswordHash: "x", Role: "user", IsActive: true}
|
||||
recent.CreatedAt = old
|
||||
recent.LastLoginAt = &now
|
||||
newUser := &model.User{Username: "newbie", PasswordHash: "x", Role: "user", IsActive: true}
|
||||
newUser.CreatedAt = now
|
||||
for _, user := range []*model.User{stale, recent, newUser} {
|
||||
if err := repos.User.Create(ctx, user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := repos.Setting.Set(ctx, SettingAccountCleanupEnabled, "true"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Setting.Set(ctx, SettingAccountCleanupKeepMode, "any"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Setting.Set(ctx, SettingAccountCleanupRules, `[
|
||||
{"id":"login_7d","name":"最近登录","type":"recent_login","enabled":true,"window_days_max":7},
|
||||
{"id":"new_7d","name":"新号宽限","type":"account_age_grace","enabled":true,"min_count":7}
|
||||
]`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
||||
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
||||
|
||||
reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup run")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(reply.Text, "当前只是预览") || !strings.Contains(reply.Text, "stale") || !strings.Contains(reply.Text, "/cleanup run confirm") {
|
||||
t.Fatalf("cleanup run should preview candidates and confirmation command, got %q", reply.Text)
|
||||
}
|
||||
if got, _ := repos.User.FindByID(ctx, stale.ID); got == nil {
|
||||
t.Fatal("cleanup preview must not delete the stale user")
|
||||
}
|
||||
|
||||
reply, err = bot.executeCommand(ctx, channel, msg, "/deleted")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(reply.Text, "当前只是预览") {
|
||||
t.Fatalf("/deleted alias should preview only, got %q", reply.Text)
|
||||
}
|
||||
if got, _ := repos.User.FindByID(ctx, stale.ID); got == nil {
|
||||
t.Fatal("/deleted preview alias must not delete users")
|
||||
}
|
||||
|
||||
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup run confirm")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(reply.Text, "已清理 <b>1</b>") {
|
||||
t.Fatalf("cleanup confirm should delete exactly one stale user, got %q", reply.Text)
|
||||
}
|
||||
if got, _ := repos.User.FindByID(ctx, stale.ID); got != nil {
|
||||
t.Fatal("stale user should be deleted after explicit confirmation")
|
||||
}
|
||||
for _, user := range []*model.User{recent, newUser, admin} {
|
||||
if got, _ := repos.User.FindByID(ctx, user.ID); got == nil {
|
||||
t.Fatalf("%s should be kept by保号 rules/protection", user.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCleanupConfirmRequiresEnabledRules(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos, bot := newBotTestService(t)
|
||||
user := &model.User{Username: "viewer", PasswordHash: "x", Role: "user", IsActive: true}
|
||||
user.CreatedAt = time.Now().Add(-30 * 24 * time.Hour)
|
||||
if err := repos.User.Create(ctx, user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Setting.Set(ctx, SettingAccountCleanupEnabled, "true"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
||||
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
||||
|
||||
reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup run confirm")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(reply.Text, "没有启用的保号规则") {
|
||||
t.Fatalf("cleanup confirm without rules should be blocked, got %q", reply.Text)
|
||||
}
|
||||
if got, _ := repos.User.FindByID(ctx, user.ID); got == nil {
|
||||
t.Fatal("cleanup confirm without enabled rules must not delete users")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCleanupRuleListInfersDaysAndHidesDuplicateNames(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos, bot := newBotTestService(t)
|
||||
|
||||
@@ -219,11 +219,42 @@ func (s *DeviceService) SweepAccountCleanup(ctx context.Context) (int, error) {
|
||||
if !cfg.AccountCleanupEnabled {
|
||||
return 0, nil
|
||||
}
|
||||
users, err := s.repo.User.List(ctx)
|
||||
candidates, err := s.accountCleanupCandidates(ctx, cfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
removed := 0
|
||||
for _, candidate := range candidates {
|
||||
s.notify(ctx, candidate.UserID, fmt.Sprintf("⛔️ 账号 <b>%s</b> 未满足保号规则,已被清理。\n规则结果:%s\n如需恢复请联系管理员。", candidate.Username, candidate.Details))
|
||||
s.log.Warn("account cleanup: deleting account", zap.String("user", candidate.Username), zap.String("details", candidate.Details))
|
||||
_ = s.repo.UserDevice.DeleteByUser(ctx, candidate.UserID)
|
||||
if err := s.repo.User.Delete(ctx, candidate.UserID); err == nil {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
type accountCleanupCandidate struct {
|
||||
UserID string
|
||||
Username string
|
||||
Details string
|
||||
}
|
||||
|
||||
func (s *DeviceService) PreviewAccountCleanup(ctx context.Context) ([]accountCleanupCandidate, error) {
|
||||
cfg := loadBotConfig(ctx, s.repo)
|
||||
if !cfg.AccountCleanupEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
return s.accountCleanupCandidates(ctx, cfg)
|
||||
}
|
||||
|
||||
func (s *DeviceService) accountCleanupCandidates(ctx context.Context, cfg botConfig) ([]accountCleanupCandidate, error) {
|
||||
users, err := s.repo.User.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates := make([]accountCleanupCandidate, 0)
|
||||
for i := range users {
|
||||
u := &users[i]
|
||||
if s.isProtected(ctx, u) || !u.IsActive {
|
||||
@@ -233,14 +264,13 @@ func (s *DeviceService) SweepAccountCleanup(ctx context.Context) (int, error) {
|
||||
if keep {
|
||||
continue
|
||||
}
|
||||
s.notify(ctx, u.ID, fmt.Sprintf("⛔️ 账号 <b>%s</b> 未满足保号规则,已被清理。\n规则结果:%s\n如需恢复请联系管理员。", u.Username, details))
|
||||
s.log.Warn("account cleanup: deleting account", zap.String("user", u.Username), zap.String("details", details))
|
||||
_ = s.repo.UserDevice.DeleteByUser(ctx, u.ID)
|
||||
if err := s.repo.User.Delete(ctx, u.ID); err == nil {
|
||||
removed++
|
||||
}
|
||||
candidates = append(candidates, accountCleanupCandidate{
|
||||
UserID: u.ID,
|
||||
Username: u.Username,
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
return removed, nil
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// KickDevice marks a device as kicked so the next request from it is rejected
|
||||
|
||||
@@ -449,4 +449,22 @@ func TestTelegramCommandFiltering(t *testing.T) {
|
||||
t.Fatalf("%s should be supported so group slash commands get feedback", cmd)
|
||||
}
|
||||
}
|
||||
for _, cmd := range []string{"/restart", "/update_bot", "/coins", "/red", "/white_channel", "/config"} {
|
||||
if telegramSupportedCommand(cmd) {
|
||||
t.Fatalf("%s should not be treated as supported until it has a real Mgo implementation", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramSupportedCommandSetMatchesRegistry(t *testing.T) {
|
||||
_, bot := newBotTestService(t)
|
||||
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
||||
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "admin"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
||||
for _, def := range bot.telegramCommandDefinitions(t.Context(), channel, msg) {
|
||||
for _, alias := range def.Aliases {
|
||||
if !telegramSupportedCommand(alias) {
|
||||
t.Fatalf("registered command %s must be in telegramSupportedCommandSet", alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ func telegramPrivateMessageForUser(msg *TelegramMessage) *TelegramMessage {
|
||||
}
|
||||
|
||||
func telegramGroupPrivateAdminHint() string {
|
||||
return "管理命令请私聊 Bot 使用 <code>/menu</code> 或对应管理员命令,避免在群组公开管理面板。"
|
||||
return "群组内不展示管理面板;管理员可在已绑定群组直接发送文本管理命令,涉及账号凭据的操作仍请私聊 Bot。"
|
||||
}
|
||||
|
||||
func telegramGroupPrivateUserHint(action string) string {
|
||||
@@ -479,7 +479,7 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage)
|
||||
if telegramIsGroupChat(msg.Chat.Type) {
|
||||
adminHint := ""
|
||||
if s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||
adminHint = "\n\n管理员命令和管理面板请私聊 Bot 使用,避免在群组公开。"
|
||||
adminHint = "\n\n管理员可在已绑定群组直接发送文本管理命令;管理面板和账号凭据操作请私聊 Bot。"
|
||||
}
|
||||
return "<b>MediaStationGo 群组可用命令</b>\n\n" +
|
||||
"<b>/menu</b> — 打开群组自助菜单\n" +
|
||||
@@ -523,7 +523,9 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage)
|
||||
"<b>/unbind 用户1 用户2</b> — 批量解绑 Telegram 绑定(管理员)\n" +
|
||||
"<b>/unbind_duplicates</b> / <b>/unbind_inactive 天数</b> — 清理重复/无效绑定或久未登录绑定(管理员)\n" +
|
||||
"<b>/antishare on play=3 login=3 warn=2</b> — 防共享策略(管理员)\n" +
|
||||
"<b>/cleanup on|off|run</b> — 保号规则开关/巡检(管理员)\n" +
|
||||
"<b>/cleanup run</b> — 预览保号清理候选(管理员)\n" +
|
||||
"<b>/cleanup run confirm</b> — 确认清理候选账号(管理员)\n" +
|
||||
"<b>/cleanup on|off</b> — 保号规则开关(管理员)\n" +
|
||||
"<b>/cleanup_mode any|all|count 2</b> — 保号模式(管理员)\n" +
|
||||
"<b>/cleanup_rule list|add|edit|修改|del|enable|disable</b> — Mgo 保号规则(管理员)\n" +
|
||||
"<b>/ban 用户名</b> / <b>/unban 用户名</b> — 禁用/解禁用户(管理员)\n" +
|
||||
@@ -532,6 +534,12 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage)
|
||||
"<b>/search 关键词</b> — 搜索媒体库\n" +
|
||||
"<b>/downloads</b> — 下载列表\n" +
|
||||
"<b>/stats</b> — 媒体库统计\n\n" +
|
||||
"<b>Mgo 兼容 Sakura 管理命令:</b>\n" +
|
||||
"用户:<code>/ucr</code> <code>/uinfo</code> <code>/rmemby</code> <code>/only_rm_record</code> <code>/renewall</code>\n" +
|
||||
"审计:<code>/userip</code> <code>/auditip</code> <code>/auditdevice</code> <code>/auditclient</code> <code>/udeviceid</code>\n" +
|
||||
"清理:<code>/syncunbound</code> <code>/syncgroupm</code> <code>/check_ex</code> <code>/deleted</code> <code>/low_activity</code>\n" +
|
||||
"权限:<code>/embyadmin</code> <code>/banall</code> <code>/unbanall</code> <code>/prouser</code> <code>/revuser</code> <code>/embylibs_blockall</code> <code>/embylibs_unblockall</code>\n" +
|
||||
"运维:<code>/proadmin</code> <code>/revadmin</code> <code>/backup_db</code> <code>/restore_from_db</code>\n\n" +
|
||||
"<b>自动推送事件:</b>\n" +
|
||||
"• 订阅命中新资源\n" +
|
||||
"• 下载任务完成\n" +
|
||||
|
||||
@@ -167,7 +167,11 @@ func TestTelegramGroupHidesAdminPanelFromRegularUsers(t *testing.T) {
|
||||
|
||||
func TestTelegramGroupAdminMenuDoesNotExposeButtonsInGroup(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, bot := newBotTestService(t)
|
||||
repos, bot := newBotTestService(t)
|
||||
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
||||
if err := repos.User.Create(ctx, admin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"group_chat_id":"-100123","admin_user_ids":"9001"}`}
|
||||
msg := &TelegramMessage{
|
||||
From: TelegramUser{ID: 9001, Username: "admin", FirstName: "Admin"},
|
||||
@@ -189,6 +193,17 @@ func TestTelegramGroupAdminMenuDoesNotExposeButtonsInGroup(t *testing.T) {
|
||||
if !strings.Contains(reply.Text, "请私聊 Bot") || telegramReplyHasButtonPrefix(reply, "adm_") {
|
||||
t.Fatalf("group admin callback should not render admin panel publicly: %#v", reply)
|
||||
}
|
||||
|
||||
reply, err := bot.executeCommand(ctx, channel, msg, "/users")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(reply.Text, "用户管理") {
|
||||
t.Fatalf("bound group admin text command should run, got %q", reply.Text)
|
||||
}
|
||||
if len(reply.Buttons) != 0 {
|
||||
t.Fatalf("group admin text command must not expose inline buttons publicly: %#v", reply.Buttons)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramPollingChannelHintWinsForPrivateMessages(t *testing.T) {
|
||||
|
||||
@@ -134,9 +134,6 @@ func (s *TelegramBotService) telegramCommandDefinitions(ctx context.Context, cha
|
||||
{Aliases: []string{"/revuser"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||
return s.cmdSakuraProtectedUser(ctx, args, false), nil
|
||||
}},
|
||||
{Aliases: []string{"/restart", "/update_bot", "/paolu", "/bindall_id", "/sync_favorites", "/coins", "/score", "/coinsall", "/coinsclear", "/red", "/srank", "/white_channel", "/rev_white_channel", "/unban_channel", "/config"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) {
|
||||
return s.cmdSakuraUnsupported("Mgo 兼容命令", "这些命令涉及外部 Bot 自身运维/积分红包/皮套人管理,已在 MediaStationGo 中由权限、通知渠道、设备策略替代。"), nil
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,15 +167,18 @@ func (s *TelegramBotService) executeCommand(ctx context.Context, channel *model.
|
||||
return telegramCommandReply{Text: fmt.Sprintf("未知命令: %s\n\n输入 /help 查看可用命令列表。", cmd)}, nil
|
||||
}
|
||||
if telegramIsGroupChat(msg.Chat.Type) && !def.GroupAllowed {
|
||||
if def.AdminOnly && s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: telegramGroupPrivateAdminHint()}, nil
|
||||
if !def.AdminOnly || !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{}, nil
|
||||
}
|
||||
return telegramCommandReply{}, nil
|
||||
}
|
||||
if def.AdminOnly && !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) {
|
||||
return telegramCommandReply{Text: def.AdminOnlyText}, nil
|
||||
}
|
||||
return def.Handle(args)
|
||||
reply, err := def.Handle(args)
|
||||
if telegramIsGroupChat(msg.Chat.Type) && def.AdminOnly && !def.GroupAllowed {
|
||||
reply.Buttons = nil
|
||||
}
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func telegramSupportedCommand(cmd string) bool {
|
||||
@@ -204,9 +204,7 @@ var telegramSupportedCommandSet = map[string]struct{}{
|
||||
"/check_ex": {}, "/deleted": {}, "/low_activity": {}, "/uranks": {}, "/days_ranks": {}, "/week_ranks": {},
|
||||
"/embyadmin": {}, "/unbanall": {}, "/banall": {}, "/embylibs_unblockall": {}, "/embylibs_blockall": {},
|
||||
"/extraembylibs_unblockall": {}, "/extraembylibs_blockall": {}, "/proadmin": {}, "/revadmin": {},
|
||||
"/backup_db": {}, "/restore_from_db": {}, "/restart": {}, "/update_bot": {}, "/paolu": {}, "/bindall_id": {}, "/sync_favorites": {}, "/config": {},
|
||||
"/coins": {}, "/score": {}, "/coinsall": {}, "/coinsclear": {}, "/red": {}, "/srank": {}, "/prouser": {}, "/revuser": {},
|
||||
"/white_channel": {}, "/rev_white_channel": {}, "/unban_channel": {},
|
||||
"/backup_db": {}, "/restore_from_db": {}, "/prouser": {}, "/revuser": {},
|
||||
}
|
||||
|
||||
type telegramBotCommand struct {
|
||||
@@ -254,7 +252,7 @@ func telegramAdminBotCommandMenu() []telegramBotCommand {
|
||||
telegramBotCommand{Command: "downloads", Description: "下载列表(管理员)"},
|
||||
telegramBotCommand{Command: "stats", Description: "媒体库统计(管理员)"},
|
||||
telegramBotCommand{Command: "users", Description: "用户管理(管理员)"},
|
||||
telegramBotCommand{Command: "cleanup", Description: "开启关闭或执行保号巡检(管理员)"},
|
||||
telegramBotCommand{Command: "cleanup", Description: "保号清理预览/确认(管理员)"},
|
||||
telegramBotCommand{Command: "cleanup_mode", Description: "设置保号规则匹配模式(管理员)"},
|
||||
telegramBotCommand{Command: "cleanup_rule", Description: "Mgo保号规则管理(管理员)"},
|
||||
)
|
||||
|
||||
@@ -1125,7 +1125,7 @@ func (s *TelegramBotService) protectReason(ctx context.Context, userID string) s
|
||||
func (s *TelegramBotService) replyDevicePolicy(ctx context.Context) telegramCommandReply {
|
||||
cfg := loadBotConfig(ctx, s.repo)
|
||||
text := fmt.Sprintf(
|
||||
"<b>设备策略</b>\n\n① 防共享:<b>%s</b>\n 并发播放上限 %d / 登录客户端上限 %d;超限会禁用账号,管理员可解禁。\n 设备指纹异常警告 %d 次后禁用账号。\n\n② Mgo 保号规则:<b>%s</b>\n 保号模式:%s;需要满足 %d 条;启用规则 %d 条。\n\n<b>命令:</b>\n<code>/antishare on play=3 login=3 warn=2</code>\n<code>/cleanup on|off|run</code>\n<code>/cleanup_mode any|all|count 2</code>\n<code>/cleanup_rule list|add|edit|修改|del|enable|disable</code>\n\n策略默认关闭;清理前会先通过 Bot 通知用户;管理员/受保护账号永不自动处理。",
|
||||
"<b>设备策略</b>\n\n① 防共享:<b>%s</b>\n 并发播放上限 %d / 登录客户端上限 %d;超限会禁用账号,管理员可解禁。\n 设备指纹异常警告 %d 次后禁用账号。\n\n② Mgo 保号规则:<b>%s</b>\n 保号模式:%s;需要满足 %d 条;启用规则 %d 条。\n\n<b>命令:</b>\n<code>/antishare on play=3 login=3 warn=2</code>\n<code>/cleanup run</code> 预览候选\n<code>/cleanup run confirm</code> 确认清理\n<code>/cleanup on|off</code>\n<code>/cleanup_mode any|all|count 2</code>\n<code>/cleanup_rule list|add|edit|修改|del|enable|disable</code>\n\n策略默认关闭;清理前会先预览候选;管理员/受保护账号永不自动处理。",
|
||||
onOff(cfg.AntiShareEnabled), cfg.MaxConcurrentPlay, cfg.MaxLoggedClients, cfg.WarnThreshold,
|
||||
onOff(cfg.AccountCleanupEnabled), cleanupModeLabel(cfg.AccountCleanupKeepMode), cfg.AccountCleanupRequiredCount, countEnabledCleanupRules(cfg.AccountCleanupRules))
|
||||
return telegramCommandReply{
|
||||
@@ -1197,21 +1197,72 @@ func (s *TelegramBotService) cmdCleanup(ctx context.Context, args []string) tele
|
||||
return telegramCommandReply{Text: "关闭失败:" + err.Error()}
|
||||
}
|
||||
return s.replyDevicePolicy(ctx)
|
||||
case "run", "sweep", "巡检":
|
||||
case "run", "sweep", "巡检", "preview", "预览":
|
||||
device := s.device
|
||||
if device == nil {
|
||||
device = NewDeviceService(s.log, s.repo)
|
||||
}
|
||||
removed, err := device.SweepAccountCleanup(ctx)
|
||||
if err != nil {
|
||||
return telegramCommandReply{Text: "巡检失败:" + err.Error()}
|
||||
if len(args) > 1 && isCleanupConfirmArg(args[1]) {
|
||||
cfg := loadBotConfig(ctx, s.repo)
|
||||
if !cfg.AccountCleanupEnabled {
|
||||
return telegramCommandReply{Text: "保号规则未开启,不会清理账号。"}
|
||||
}
|
||||
if countEnabledCleanupRules(cfg.AccountCleanupRules) == 0 {
|
||||
return telegramCommandReply{Text: "没有启用的保号规则,不会清理账号。"}
|
||||
}
|
||||
removed, err := device.SweepAccountCleanup(ctx)
|
||||
if err != nil {
|
||||
return telegramCommandReply{Text: "确认清理失败:" + err.Error()}
|
||||
}
|
||||
return telegramCommandReply{Text: fmt.Sprintf("保号规则确认清理完成,已清理 <b>%d</b> 个账号。", removed)}
|
||||
}
|
||||
return telegramCommandReply{Text: fmt.Sprintf("保号规则巡检完成,清理 <b>%d</b> 个账号。", removed)}
|
||||
candidates, err := device.PreviewAccountCleanup(ctx)
|
||||
if err != nil {
|
||||
return telegramCommandReply{Text: "巡检预览失败:" + err.Error()}
|
||||
}
|
||||
return telegramCommandReply{Text: s.formatCleanupPreview(ctx, candidates)}
|
||||
default:
|
||||
return telegramCommandReply{Text: "用法:<code>/cleanup on|off|run</code>"}
|
||||
return telegramCommandReply{Text: "用法:<code>/cleanup on|off</code>、<code>/cleanup run</code> 预览、<code>/cleanup run confirm</code> 确认清理"}
|
||||
}
|
||||
}
|
||||
|
||||
func isCleanupConfirmArg(arg string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(arg)) {
|
||||
case "confirm", "yes", "delete", "确认", "清理", "删除":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) formatCleanupPreview(ctx context.Context, candidates []accountCleanupCandidate) string {
|
||||
cfg := loadBotConfig(ctx, s.repo)
|
||||
if !cfg.AccountCleanupEnabled {
|
||||
return "保号规则未开启,不会清理账号。"
|
||||
}
|
||||
if countEnabledCleanupRules(cfg.AccountCleanupRules) == 0 {
|
||||
return "没有启用的保号规则,不会清理账号。"
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "保号规则预览完成:没有需要清理的账号。"
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("<b>保号规则预览</b>\n\n将清理候选:<b>%d</b> 个账号。\n当前只是预览,未删除任何账号。\n\n", len(candidates)))
|
||||
limit := len(candidates)
|
||||
if limit > 10 {
|
||||
limit = 10
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
candidate := candidates[i]
|
||||
sb.WriteString(fmt.Sprintf("%d. <b>%s</b>\n%s\n", i+1, escapeHTML(candidate.Username), escapeHTML(candidate.Details)))
|
||||
}
|
||||
if len(candidates) > limit {
|
||||
sb.WriteString(fmt.Sprintf("……另有 %d 个候选未展示。\n", len(candidates)-limit))
|
||||
}
|
||||
sb.WriteString("\n确认无误后再执行:<code>/cleanup run confirm</code>")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *TelegramBotService) cmdCleanupMode(ctx context.Context, args []string) telegramCommandReply {
|
||||
if len(args) == 0 {
|
||||
return telegramCommandReply{Text: "用法:<code>/cleanup_mode any</code>、<code>/cleanup_mode all</code> 或 <code>/cleanup_mode count 2</code>"}
|
||||
|
||||
Reference in New Issue
Block a user