This commit is contained in:
tamakyi
2026-10-19 02:25:19 +08:00
parent 4592b6ef24
commit 12a37e726f
14 changed files with 315 additions and 5 deletions
+82 -1
View File
@@ -18,6 +18,8 @@ import (
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/mail"
)
// 本文件提供站方处置能力。项目刻意不做管理后台,因此运营动作全部落在 CLI:
@@ -38,8 +40,10 @@ var Restrict = &cli.Command{
&cli.StringFlag{Name: "domain", Usage: "Custom domain of the user"},
&cli.StringFlag{Name: "level", Value: string(db.RestrictionL1), Usage: "Restriction level: L1 / L2 / L3 / L4"},
&cli.IntFlag{Name: "days", Value: 7, Usage: "Effective days, counted from now"},
&cli.StringFlag{Name: "reason", Usage: "Reason recorded for audit (required)"},
&cli.StringFlag{Name: "reason", Usage: "Reason recorded for audit (required, not shown to the user)"},
&cli.BoolFlag{Name: "yes", Usage: "Skip confirmation"},
&cli.BoolFlag{Name: "force", Usage: "Required when --days exceeds 90 (abnormal duration)"},
&cli.BoolFlag{Name: "no-notify", Usage: "Do not email the restriction notice to the user"},
},
Action: runRestrict,
}
@@ -119,6 +123,12 @@ func runRestrict(ctx *cli.Context) error {
if days <= 0 {
return errors.New("--days must be positive")
}
// 不设硬上限(确实存在长期封禁场景),但超过 90 天必须显式加 --force:
// 这类值几乎都是手误(多打一个 0),靠 --yes 挡不住重复执行的历史命令。
if days > restrictDaysForceThreshold && !ctx.Bool("force") {
return errors.Errorf("--days %d 超过 %d 天,属于异常时长;确认无误请加 --force 重试",
days, restrictDaysForceThreshold)
}
reason := strings.TrimSpace(ctx.String("reason"))
if reason == "" {
return errors.New("--reason is required (recorded for audit)")
@@ -152,6 +162,23 @@ func runRestrict(ctx *cli.Context) error {
fmt.Println("当前处置: 无")
}
fmt.Printf("本次处置: %s,%d 天,原因:%s\n", level, days, reason)
if days > restrictDaysForceThreshold {
fmt.Printf("⚠️ 超长处置:%d 天,到期 %s(常规处置为 7–30 天)\n",
days, time.Now().AddDate(0, 0, days).Format("2006-01-02 15:04"))
}
// 二次确认:异常时长时,即使已给 --force,交互模式下还要再输入一次级别。
// 脚本里 `--force --yes` 两个显式开关本身就是两次确认,不再额外拦截。
if days > restrictDaysForceThreshold && !ctx.Bool("yes") {
logrus.Warn("这是超长处置。请再次输入级别名(如 L3)确认:")
var confirmLevel string
if _, err := fmtScan(&confirmLevel); err != nil {
return errors.Wrap(err, "read confirmation")
}
if strings.TrimSpace(confirmLevel) != level {
return errors.New("已取消(级别不匹配)")
}
}
if !ctx.Bool("yes") {
logrus.Warn("输入大写 YES 确认执行(该操作可经 unrestrict 撤销):")
@@ -182,9 +209,63 @@ func runRestrict(ctx *cli.Context) error {
}).Info("处置已生效")
fmt.Printf("已生效,到期时间 %s(到期自动解除,无需手工操作)\n",
restriction.ExpiresAt.Format("2006-01-02 15:04"))
if !ctx.Bool("no-notify") {
notifyRestrictionOwner(user, level, restriction.ExpiresAt)
}
return nil
}
// restrictDaysForceThreshold 超过这个天数需要 --force。不做硬上限:长期封禁是真实需求。
const restrictDaysForceThreshold = 90
// notifyRestrictionOwner 把处置结果通知箱主(按他自己的界面语言)。
//
// 为什么必须通知:处置是平台单方面限制用户功能,不告知就变成「暗箱」,
// 用户既无法自查也无法申诉。邮件里只给「级别 + 到期时间 + 规则链接」,
// 不引用 --reason(那是运维审计备注),也不引用被举报内容原文。
//
// 发送失败只告警、不回滚:处置已经写入,邮件不可用不应让运营者误以为没生效。
func notifyRestrictionOwner(user *db.User, level string, expires time.Time) {
if user.Email == "" {
fmt.Println("(该账号没有邮箱,跳过处置通知)")
return
}
lang := user.Language
if lang == "" {
lang = conf.App.DefaultLang
}
levelDesc := i18n.T(lang, "restriction.level."+level)
notice := mail.RestrictionNotice{
To: user.Email,
Heading: i18n.T(lang, "mail.restriction.heading"),
Greeting: i18n.T(lang, "mail.restriction.greeting", user.Name),
Body: i18n.T(lang, "mail.restriction.body", user.Domain, level, levelDesc, expires.Format("2006-01-02 15:04")),
Note: i18n.T(lang, "mail.restriction.note"),
RulesText: i18n.T(lang, "mail.restriction.rules"),
RulesURL: strings.TrimRight(conf.App.ExternalURL, "/") + "/rules",
Appeal: i18n.T(lang, "mail.restriction.appeal"),
}
// 加超时:SMTP 不可达时 gomail 的裸 Dial 会一直等到系统 TCP 超时(可达两分钟),
// 运营者站在终端前会以为命令卡死。超时只影响通知,不影响已写入的处置。
done := make(chan error, 1)
go func() { done <- mail.SendRestrictionNoticeMail(notice) }()
select {
case err := <-done:
if err != nil {
logrus.WithError(err).WithField("user_id", user.ID).Warn("处置通知邮件发送失败(处置已生效)")
fmt.Println("⚠️ 通知邮件发送失败(处置已生效)。请检查 [mail] 配置,或用 --no-notify 跳过通知。")
return
}
case <-time.After(20 * time.Second):
logrus.WithField("user_id", user.ID).Warn("处置通知邮件发送超时(处置已生效)")
fmt.Println("⚠️ 通知邮件发送超时(处置已生效)。请检查 SMTP 配置。")
return
}
fmt.Printf("已发送处置通知邮件至 %s(语言 %s)\n", user.Email, i18n.Resolve(lang))
}
func runUnrestrict(ctx *cli.Context) error {
database, err := openDatabase()
if err != nil {