fix v14
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -432,6 +432,20 @@ var enDict = map[string]string{
|
||||
"restricted.title": "This question box is temporarily unavailable",
|
||||
"restricted.desc": "This question box has been suspended for violating community guidelines.",
|
||||
|
||||
// 处置通知邮件(发给箱主,四语言)
|
||||
"mail.restriction.heading": "Your question box has been restricted",
|
||||
"mail.restriction.greeting": "Hi %s,",
|
||||
"mail.restriction.body": "Your question box @%s has been restricted at level %s (%s) until %s.",
|
||||
"mail.restriction.note": "Some features are unavailable during the restriction. It lifts automatically on expiry, with no action needed.",
|
||||
"mail.restriction.rules": "Read the community rules and enforcement policy",
|
||||
"mail.restriction.appeal": "If you believe this is a mistake, contact the site operator through the footer link and we will review it.",
|
||||
|
||||
// 处置级别名称(邮件与文案共用)
|
||||
"restriction.level.L1": "Stop distribution",
|
||||
"restriction.level.L2": "Narrow publishing",
|
||||
"restriction.level.L3": "Freeze publishing",
|
||||
"restriction.level.L4": "Suspend page",
|
||||
|
||||
// 举报(单题页,公开访客可用)
|
||||
"report.entry": "Report this content",
|
||||
"report.submit": "Submit",
|
||||
|
||||
@@ -432,6 +432,20 @@ var jaDict = map[string]string{
|
||||
"restricted.title": "この質問箱は一時的に利用できません",
|
||||
"restricted.desc": "この質問箱はコミュニティガイドライン違反のため、公開を停止しています。",
|
||||
|
||||
// 处置通知邮件(发给箱主,四语言)
|
||||
"mail.restriction.heading": "質問箱が制限されました",
|
||||
"mail.restriction.greeting": "%s さん、こんにちは。",
|
||||
"mail.restriction.body": "あなたの質問箱 @%s は制限されました。レベル %s(%s)、%s まで有効です。",
|
||||
"mail.restriction.note": "制限期間中は一部の機能が利用できません。期限が来ると自動的に解除されます。",
|
||||
"mail.restriction.rules": "コミュニティルールと対応方針を見る",
|
||||
"mail.restriction.appeal": "誤りだと思われる場合は、フッターの「管理人に連絡」からご連絡ください。確認のうえ対応します。",
|
||||
|
||||
// 处置级别名称(邮件与文案共用)
|
||||
"restriction.level.L1": "配信停止",
|
||||
"restriction.level.L2": "公開の縮小",
|
||||
"restriction.level.L3": "投稿の凍結",
|
||||
"restriction.level.L4": "アクセス停止",
|
||||
|
||||
// 举报(单题页,公开访客可用)
|
||||
"report.entry": "この内容を報告する",
|
||||
"report.submit": "送信",
|
||||
|
||||
@@ -432,6 +432,20 @@ var zhCNDict = map[string]string{
|
||||
"restricted.title": "提问箱暂时不可访问",
|
||||
"restricted.desc": "该提问箱因违反社区规范,已暂停对外访问。",
|
||||
|
||||
// 处置通知邮件(发给箱主,四语言)
|
||||
"mail.restriction.heading": "你的提问箱已被限制",
|
||||
"mail.restriction.greeting": "%s,你好:",
|
||||
"mail.restriction.body": "你的提问箱 @%s 已被限制,级别 %s(%s),有效期至 %s。",
|
||||
"mail.restriction.note": "限制期间部分功能不可用,到期后自动解除,无需手工操作。",
|
||||
"mail.restriction.rules": "查看社区规则与处置说明",
|
||||
"mail.restriction.appeal": "如果你认为处置有误,请通过站点页脚的「找站长」联系我们,我们会复核。",
|
||||
|
||||
// 处置级别名称(邮件与文案共用)
|
||||
"restriction.level.L1": "停止分发",
|
||||
"restriction.level.L2": "收窄公开",
|
||||
"restriction.level.L3": "冻结发布",
|
||||
"restriction.level.L4": "暂停访问",
|
||||
|
||||
// 举报(单题页,公开访客可用)
|
||||
"report.entry": "举报这条内容",
|
||||
"report.submit": "提交",
|
||||
|
||||
@@ -432,6 +432,20 @@ var zhTWDict = map[string]string{
|
||||
"restricted.title": "提問箱暫時無法存取",
|
||||
"restricted.desc": "該提問箱因違反社群規範,已暫停對外存取。",
|
||||
|
||||
// 处置通知邮件(发给箱主,四语言)
|
||||
"mail.restriction.heading": "你的提問箱已被限制",
|
||||
"mail.restriction.greeting": "%s,你好:",
|
||||
"mail.restriction.body": "你的提問箱 @%s 已被限制,級別 %s(%s),有效期至 %s。",
|
||||
"mail.restriction.note": "限制期間部分功能無法使用,到期後自動解除,無需手動操作。",
|
||||
"mail.restriction.rules": "檢視社群規則與處置說明",
|
||||
"mail.restriction.appeal": "如果你認為處置有誤,請透過頁尾的「找站長」聯絡我們,我們會複核。",
|
||||
|
||||
// 处置级别名称(邮件与文案共用)
|
||||
"restriction.level.L1": "停止分發",
|
||||
"restriction.level.L2": "收窄公開",
|
||||
"restriction.level.L3": "凍結發佈",
|
||||
"restriction.level.L4": "暫停存取",
|
||||
|
||||
// 举报(单题页,公开访客可用)
|
||||
"report.entry": "檢舉這則內容",
|
||||
"report.submit": "提交",
|
||||
|
||||
@@ -144,6 +144,38 @@ func SendQuestionReportedMail(email, domain string, questionID uint, count int64
|
||||
return sendTemplateMail(email, "【狼的提问箱】你的内容被举报了", templates.FS, "mail/question-reported.html", params)
|
||||
}
|
||||
|
||||
// RestrictionNotice 处置通知邮件的内容。
|
||||
//
|
||||
// 全部文案由调用方按「箱主自己的界面语言」预先翻译好再传进来:
|
||||
// 邮件模板本身是单语言的,而箱主的语言存在 users.language 里,只能在调用侧解析。
|
||||
//
|
||||
// 刻意不包含 --reason 原文:那是运维的审计备注(可能写得很直白),
|
||||
// 对外只用「级别 + 到期时间 + 规则链接」,与公告模板的口径一致。
|
||||
type RestrictionNotice struct {
|
||||
To string
|
||||
Heading string
|
||||
Greeting string
|
||||
Body string
|
||||
Note string
|
||||
RulesText string
|
||||
RulesURL string
|
||||
Appeal string
|
||||
}
|
||||
|
||||
// SendRestrictionNoticeMail 告知箱主「你的提问箱已被限制」。
|
||||
func SendRestrictionNoticeMail(n RestrictionNotice) error {
|
||||
params := map[string]string{
|
||||
"heading": n.Heading,
|
||||
"greeting": n.Greeting,
|
||||
"body": n.Body,
|
||||
"note": n.Note,
|
||||
"rulesText": n.RulesText,
|
||||
"rulesURL": n.RulesURL,
|
||||
"appeal": n.Appeal,
|
||||
}
|
||||
return sendTemplateMail(n.To, n.Heading, templates.FS, "mail/restriction-notice.html", params)
|
||||
}
|
||||
|
||||
func sendTemplateMail(email, title string, templateFS embed.FS, templatePath string, params map[string]string) error {
|
||||
var content bytes.Buffer
|
||||
t, err := template.ParseFS(templateFS, templatePath)
|
||||
|
||||
Reference in New Issue
Block a user