87 lines
3.3 KiB
Go
87 lines
3.3 KiB
Go
// 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/flamego/recaptcha"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/tamakyi/TamaBox/internal/captcha"
|
|
"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 同内容重复举报**幂等成功**,且提示与首次完全一致,
|
|
// 否则「已举报过」会变成「这条内容是否存在」的探测接口;
|
|
// - **要验证码**:举报是匿名入口,不设成本等于给批量刷量开门(同 IP 唯一索引只挡单来源,
|
|
// 换 IP 即可绕过);这里与提问/私聊共用同一套验证码通道;
|
|
// - 举报只是信号:处置仍由箱主(后台删除/设私密)或运维(TamaBox restrict)完成,
|
|
// 本 handler 不改变任何内容可见性。
|
|
func Report(ctx context.Context, pageUser *db.User, question *db.Question, f form.ReportQuestion, recaptcha recaptcha.RecaptchaV3) {
|
|
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
|
|
}
|
|
|
|
// 验证码:与提问 / 私聊同源(recaptcha / turnstile / math 三种由配置决定)。
|
|
ok, err := captcha.Verify(f.Recaptcha, shared.ResolveClientIP(ctx.Request().Request), ctx.Session, recaptcha, ctx.Lang())
|
|
if err != nil {
|
|
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to check captcha for report")
|
|
ctx.SetErrorFlash(err.Error())
|
|
ctx.Redirect(back)
|
|
return
|
|
}
|
|
if !ok {
|
|
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error"))
|
|
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)
|
|
}
|