mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
fix: improve cleanup rule display
This commit is contained in:
@@ -317,6 +317,21 @@ func TestBotAdminCommandsManageDevicePolicy(t *testing.T) {
|
||||
if !found {
|
||||
t.Fatalf("cleanup rule not added; reply=%q rules=%+v", reply.Text, cfg.AccountCleanupRules)
|
||||
}
|
||||
|
||||
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup_rule add account_age_grace new_7d 7")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg = loadBotConfig(ctx, repos)
|
||||
found = false
|
||||
for _, rule := range cfg.AccountCleanupRules {
|
||||
if rule.ID == "new_7d" && rule.Type == "account_age_grace" && rule.MinCount == 7 {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("cleanup shorthand rule not added; reply=%q rules=%+v", reply.Text, cfg.AccountCleanupRules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCleanupRulesCanBeDeletedUntilEmpty(t *testing.T) {
|
||||
@@ -347,6 +362,38 @@ func TestBotCleanupRulesCanBeDeletedUntilEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCleanupRuleListInfersDaysAndHidesDuplicateNames(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)
|
||||
}
|
||||
if err := repos.Setting.Set(ctx, SettingAccountCleanupRules, `[
|
||||
{"id":"login_7d","name":"login_7d","type":"recent_login","enabled":true,"window_days_min":1,"window_days_max":5,"min_count":1},
|
||||
{"id":"new_7d","name":"new_7d","type":"account_age_grace","enabled":true,"window_days_min":1,"window_days_max":1,"min_count":1}
|
||||
]`); 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_rule list")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, bad := range []string{"login_7d</code> · login_7d", "new_7d</code> · new_7d", "5 天内登录", "新号宽限 1 天"} {
|
||||
if strings.Contains(reply.Text, bad) {
|
||||
t.Fatalf("rule list still contains bad fragment %q: %s", bad, reply.Text)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"login_7d", "7 天内登录", "new_7d", "新号宽限 7 天"} {
|
||||
if !strings.Contains(reply.Text, want) {
|
||||
t.Fatalf("rule list missing %q: %s", want, reply.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotRegistrationCommandUsesOpenRegQuota(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos, bot := newBotTestService(t)
|
||||
|
||||
@@ -3,12 +3,20 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
cleanupRuleWindowDaysRE = regexp.MustCompile(`(?i)(?:^|[_-])(\d+)[_-](\d+)\s*d(?:$|[_-])`)
|
||||
cleanupRuleSingleDayRE = regexp.MustCompile(`(?i)(?:^|[_-])(\d+)\s*d(?:$|[_-])`)
|
||||
cleanupRuleHoursRE = regexp.MustCompile(`(?i)(?:^|[_-])(\d+(?:\.\d+)?)\s*h(?:$|[_-])`)
|
||||
cleanupRuleNumberRE = regexp.MustCompile(`(?i)(?:^|[_-])(\d+)(?:$|[_-])`)
|
||||
)
|
||||
|
||||
// Bot / 设备管控相关的设置键。全部存储在 settings 表,由管理员通过
|
||||
// Telegram Bot 命令调整。带安全默认值:所有"自动删号"策略默认关闭。
|
||||
const (
|
||||
@@ -187,6 +195,7 @@ func normalizeCleanupRules(rules []accountCleanupRule) []accountCleanupRule {
|
||||
if r.Name == "" {
|
||||
r.Name = r.ID
|
||||
}
|
||||
inferCleanupRuleValuesFromID(&r)
|
||||
if r.WindowDaysMin < 1 {
|
||||
r.WindowDaysMin = 1
|
||||
}
|
||||
@@ -206,3 +215,76 @@ func normalizeCleanupRules(rules []accountCleanupRule) []accountCleanupRule {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inferCleanupRuleValuesFromID(rule *accountCleanupRule) {
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(rule.ID + "_" + rule.Name))
|
||||
switch rule.Type {
|
||||
case "watch_hours":
|
||||
if minDays, maxDays := cleanupRuleWindowDays(key); minDays > 0 && maxDays > 0 {
|
||||
rule.WindowDaysMin = minDays
|
||||
rule.WindowDaysMax = maxDays
|
||||
} else if days := cleanupRuleSingleDay(key); days > 0 {
|
||||
rule.WindowDaysMin = days
|
||||
rule.WindowDaysMax = days
|
||||
}
|
||||
if hours := cleanupRuleHours(key); hours > 0 {
|
||||
rule.MinHours = hours
|
||||
}
|
||||
case "recent_login":
|
||||
if days := cleanupRuleSingleDay(key); days > 0 {
|
||||
rule.WindowDaysMax = days
|
||||
}
|
||||
case "account_age_grace":
|
||||
if days := cleanupRuleSingleDay(key); days > 0 {
|
||||
rule.MinCount = days
|
||||
}
|
||||
case "signin_streak":
|
||||
if n := cleanupRuleTrailingNumber(key); n > 0 {
|
||||
rule.MinCount = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupRuleWindowDays(value string) (int, int) {
|
||||
if m := cleanupRuleWindowDaysRE.FindStringSubmatch(value); len(m) >= 3 {
|
||||
minDays, _ := strconv.Atoi(m[1])
|
||||
maxDays, _ := strconv.Atoi(m[2])
|
||||
if maxDays < minDays {
|
||||
maxDays = minDays
|
||||
}
|
||||
return minDays, maxDays
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func cleanupRuleSingleDay(value string) int {
|
||||
if m := cleanupRuleSingleDayRE.FindStringSubmatch(value); len(m) >= 2 {
|
||||
days, _ := strconv.Atoi(m[1])
|
||||
return days
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func cleanupRuleHours(value string) float64 {
|
||||
if m := cleanupRuleHoursRE.FindStringSubmatch(value); len(m) >= 2 {
|
||||
hours, _ := strconv.ParseFloat(m[1], 64)
|
||||
return hours
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func cleanupRuleTrailingNumber(value string) int {
|
||||
matches := cleanupRuleNumberRE.FindAllStringSubmatch(value, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0
|
||||
}
|
||||
last := matches[len(matches)-1]
|
||||
if len(last) < 2 {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.Atoi(last[1])
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -1309,30 +1309,32 @@ func parseCleanupRuleCommand(args []string) (accountCleanupRule, error) {
|
||||
rule := accountCleanupRule{
|
||||
Type: strings.ToLower(strings.TrimSpace(args[0])),
|
||||
ID: strings.TrimSpace(args[1]),
|
||||
Name: strings.TrimSpace(args[1]),
|
||||
Enabled: true,
|
||||
WindowDaysMin: 3,
|
||||
WindowDaysMax: 5,
|
||||
MinHours: 6,
|
||||
MinCount: 1,
|
||||
}
|
||||
if len(args) > 2 {
|
||||
rule.Name = strings.TrimSpace(args[2])
|
||||
}
|
||||
switch rule.Type {
|
||||
case "watch_hours":
|
||||
if len(args) >= 6 {
|
||||
rule.WindowDaysMin, _ = strconv.Atoi(args[3])
|
||||
rule.WindowDaysMax, _ = strconv.Atoi(args[4])
|
||||
rule.MinHours, _ = strconv.ParseFloat(args[5], 64)
|
||||
name, values := cleanupRuleNameAndValues(args[2:], 3)
|
||||
rule.Name = name
|
||||
if len(values) >= 3 {
|
||||
rule.WindowDaysMin, _ = strconv.Atoi(values[0])
|
||||
rule.WindowDaysMax, _ = strconv.Atoi(values[1])
|
||||
rule.MinHours, _ = strconv.ParseFloat(values[2], 64)
|
||||
}
|
||||
case "recent_login":
|
||||
if len(args) >= 4 {
|
||||
rule.WindowDaysMax, _ = strconv.Atoi(args[3])
|
||||
name, values := cleanupRuleNameAndValues(args[2:], 1)
|
||||
rule.Name = name
|
||||
if len(values) >= 1 {
|
||||
rule.WindowDaysMax, _ = strconv.Atoi(values[0])
|
||||
}
|
||||
case "signin_streak", "account_age_grace":
|
||||
if len(args) >= 4 {
|
||||
rule.MinCount, _ = strconv.Atoi(args[3])
|
||||
name, values := cleanupRuleNameAndValues(args[2:], 1)
|
||||
rule.Name = name
|
||||
if len(values) >= 1 {
|
||||
rule.MinCount, _ = strconv.Atoi(values[0])
|
||||
}
|
||||
default:
|
||||
return accountCleanupRule{}, fmt.Errorf("不支持的规则类型:%s", rule.Type)
|
||||
@@ -1344,6 +1346,25 @@ func parseCleanupRuleCommand(args []string) (accountCleanupRule, error) {
|
||||
return normalized[0], nil
|
||||
}
|
||||
|
||||
func cleanupRuleNameAndValues(args []string, numericCount int) (string, []string) {
|
||||
if len(args) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(args) >= numericCount && cleanupRuleValuesAreNumeric(args[:numericCount]) {
|
||||
return "", args
|
||||
}
|
||||
return strings.TrimSpace(args[0]), args[1:]
|
||||
}
|
||||
|
||||
func cleanupRuleValuesAreNumeric(values []string) bool {
|
||||
for _, value := range values {
|
||||
if _, err := strconv.ParseFloat(strings.TrimSpace(value), 64); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func formatCleanupRules(rules []accountCleanupRule) string {
|
||||
if len(rules) == 0 {
|
||||
return "<b>保号规则</b>\n\n暂无规则。"
|
||||
@@ -1352,21 +1373,55 @@ func formatCleanupRules(rules []accountCleanupRule) string {
|
||||
sb.WriteString("<b>保号规则</b>\n")
|
||||
for i, r := range rules {
|
||||
state := map[bool]string{true: "启用", false: "停用"}[r.Enabled]
|
||||
sb.WriteString(fmt.Sprintf("\n%d. <code>%s</code> · %s · %s · %s", i+1, r.ID, r.Name, cleanupRuleTypeLabel(r.Type), state))
|
||||
switch r.Type {
|
||||
case "watch_hours":
|
||||
sb.WriteString(fmt.Sprintf(" · %d~%d 天 %.1f 小时", r.WindowDaysMin, r.WindowDaysMax, r.MinHours))
|
||||
case "recent_login":
|
||||
sb.WriteString(fmt.Sprintf(" · %d 天内登录", r.WindowDaysMax))
|
||||
case "signin_streak":
|
||||
sb.WriteString(fmt.Sprintf(" · 连续签到 %d 天", r.MinCount))
|
||||
case "account_age_grace":
|
||||
sb.WriteString(fmt.Sprintf(" · 新号宽限 %d 天", r.MinCount))
|
||||
detail := cleanupRuleDetail(r)
|
||||
parts := []string{
|
||||
fmt.Sprintf("\n%d. <code>%s</code>", i+1, r.ID),
|
||||
}
|
||||
if shouldShowCleanupRuleName(r, detail) {
|
||||
parts = append(parts, r.Name)
|
||||
}
|
||||
parts = append(parts, cleanupRuleTypeLabel(r.Type), state)
|
||||
if detail != "" {
|
||||
parts = append(parts, detail)
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " · "))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func shouldShowCleanupRuleName(r accountCleanupRule, detail string) bool {
|
||||
name := strings.TrimSpace(r.Name)
|
||||
if name == "" || strings.EqualFold(name, r.ID) {
|
||||
return false
|
||||
}
|
||||
if detail != "" && strings.EqualFold(name, detail) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cleanupRuleDetail(r accountCleanupRule) string {
|
||||
switch r.Type {
|
||||
case "watch_hours":
|
||||
return fmt.Sprintf("%d~%d 天 %s 小时", r.WindowDaysMin, r.WindowDaysMax, formatRuleHours(r.MinHours))
|
||||
case "recent_login":
|
||||
return fmt.Sprintf("%d 天内登录", r.WindowDaysMax)
|
||||
case "signin_streak":
|
||||
return fmt.Sprintf("连续签到 %d 天", r.MinCount)
|
||||
case "account_age_grace":
|
||||
return fmt.Sprintf("新号宽限 %d 天", r.MinCount)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatRuleHours(hours float64) string {
|
||||
if hours == float64(int(hours)) {
|
||||
return strconv.Itoa(int(hours))
|
||||
}
|
||||
return fmt.Sprintf("%.1f", hours)
|
||||
}
|
||||
|
||||
func cleanupRuleTypeLabel(t string) string {
|
||||
switch t {
|
||||
case "watch_hours":
|
||||
|
||||
Reference in New Issue
Block a user