restrict v6

This commit is contained in:
tamakyi
2026-10-18 22:18:21 +08:00
parent d14d26df17
commit bd81398a06
18 changed files with 621 additions and 10 deletions
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package question
import (
stdctx "context"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/mail"
"github.com/tamakyi/TamaBox/internal/shared"
)
// 举报阈值:达到任一条件就发一封汇总邮件给运维。
//
// 为什么是 8 / 20 而不是更低:一次粉圈摩擦里,围观者顺手举报同一内容很容易攒到三五条,
// 阈值太低会天天误报,最后这个告警会被无视;而组织化挂人往往短时间内几十条。
const (
abuseAlertThreshold24h = 8
abuseAlertThreshold7d = 20
abuseAlertCooldown = 24 * time.Hour
)
// abuseAlertCooldowns 每个箱主上次发送阈值告警的时间。
//
// 用进程内 map 而不是加表:告警是「一次性提醒」,重启后丢失最坏结果是多发一封,
// 而为此新增一张表和一套清理逻辑不划算。并发下用互斥锁保证同一箱主不会连发。
var (
abuseAlertMu sync.Mutex
abuseAlertCooldowns = make(map[uint]time.Time)
)
func allowAbuseAlert(userID uint) bool {
abuseAlertMu.Lock()
defer abuseAlertMu.Unlock()
if last, ok := abuseAlertCooldowns[userID]; ok && time.Since(last) < abuseAlertCooldown {
return false
}
abuseAlertCooldowns[userID] = time.Now()
return true
}
// notifyAbuseIfNeeded 在举报写入后判断是否达到告警阈值,异步发送汇总邮件。
//
// 只发「该看一眼」的信号,不做任何自动处置:举报数达标不等于内容违规,
// 尤其这类互殴场景,自动处置会直接变成站队。
func notifyAbuseIfNeeded(pageUser *db.User) {
if conf.Security.SecurityAlertEmail == "" {
return
}
shared.SafeGo(func() {
ctx := stdctx.Background()
count24, err := db.Reports.CountByUserSince(ctx, pageUser.ID, time.Now().Add(-24*time.Hour))
if err != nil {
logrus.WithError(err).Error("Failed to count reports within 24h")
return
}
count7d, err := db.Reports.CountByUserSince(ctx, pageUser.ID, time.Now().AddDate(0, 0, -7))
if err != nil {
logrus.WithError(err).Error("Failed to count reports within 7d")
return
}
if count24 < abuseAlertThreshold24h && count7d < abuseAlertThreshold7d {
return
}
if !allowAbuseAlert(pageUser.ID) {
return
}
if err := mail.SendAbuseAlertMail(pageUser.Domain, pageUser.Name, count24, count7d); err != nil {
logrus.WithError(err).Error("Failed to send abuse alert mail")
return
}
logrus.WithFields(logrus.Fields{
"user_id": pageUser.ID,
"domain": pageUser.Domain,
"count24": count24,
"count7d": count7d,
}).Warn("Abuse alert mail sent")
})
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package question
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/context"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/form"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/shared"
)
// Report 接收访客对单条已公开回答的举报。
//
// 设计取舍(都是有意为之,改动前请先想清楚):
// - **不要求登录**:这类内容的主要目击者是匿名访客,要求登录等于把举报率降到零;
// - 理由只接受固定枚举,不做自由文本 —— 否则举报框本身成了新的辱骂输入口;
// - 同 IP 同内容重复举报**幂等成功**,且提示与首次完全一致,
// 否则「已举报过」会变成「这条内容是否存在」的探测接口;
// - 举报只是信号:处置仍由箱主(后台删除/设私密)或运维(TamaBox restrict)完成,
// 本 handler 不改变任何内容可见性。
func Report(ctx context.Context, pageUser *db.User, question *db.Question, f form.ReportQuestion) {
back := fmt.Sprintf("/_/%s/%d", pageUser.Domain, question.ID)
if ctx.HasError() || !db.IsValidReportReason(f.Reason) {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "report.errInvalidReason"))
ctx.Redirect(back)
return
}
// 兜底:只有公开且已回答的内容可被举报(Questioner 中间件已挡住私密/未回答)。
if question.Answer == "" || question.IsPrivate {
ctx.Redirect(back)
return
}
ip := shared.ResolveClientIP(ctx.Request().Request)
created, err := db.Reports.Create(ctx.Request().Context(), db.CreateReportOptions{
QuestionID: question.ID,
ReporterIP: ip,
Reason: f.Reason,
})
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create content report")
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "report.errFailed"))
ctx.Redirect(back)
return
}
if created {
logrus.WithContext(ctx.Request().Context()).WithFields(logrus.Fields{
"question_id": question.ID,
"user_id": pageUser.ID,
"reason": f.Reason,
}).Warn("Content reported")
notifyAbuseIfNeeded(pageUser)
}
// 首次与重复使用同一句提示(见上方设计取舍)。
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "report.flash.received"))
ctx.Redirect(back)
}