aliyun sensor server
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/tamakyi/TamaBox/internal/form"
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/mail"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||||
"github.com/tamakyi/TamaBox/internal/shared"
|
||||
)
|
||||
|
||||
@@ -100,17 +100,10 @@ func PublishFollowUp(ctx context.Context, pageUser *db.User, question *db.Questi
|
||||
return
|
||||
}
|
||||
|
||||
// 内容安全检测
|
||||
censorResponse, err := censor.Text(ctx.Request().Context(), f.Content)
|
||||
if err != nil {
|
||||
// 审核源全部不可用:fail-closed,拒绝发布而非免审放行。
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to censor follow up text")
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "censor.unavailable"))
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/%d?t=%s", pageUser.Domain, question.ID, question.Token))
|
||||
return
|
||||
}
|
||||
if !censorResponse.Pass {
|
||||
ctx.SetErrorFlash(censorResponse.ErrorMessage())
|
||||
// 内容安全检测:统一守卫,按箱主「审核严格度」裁决是否放行
|
||||
// (提问 / 投票 / 私聊共用同一份判定,见 internal/security/moderation)。
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), f.Content); err != nil {
|
||||
ctx.SetErrorFlash(err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/%d?t=%s", pageUser.Domain, question.ID, question.Token))
|
||||
return
|
||||
}
|
||||
@@ -167,17 +160,9 @@ func AnswerFollowUp(ctx context.Context, pageUser *db.User, question *db.Questio
|
||||
|
||||
followUpID := uint(ctx.ParamInt("followUpID"))
|
||||
|
||||
// 内容安全检测
|
||||
censorResponse, err := censor.Text(ctx.Request().Context(), f.Answer)
|
||||
if err != nil {
|
||||
// 审核源全部不可用:fail-closed,拒绝发布而非免审放行。
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to censor follow up answer")
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "censor.unavailable"))
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
if !censorResponse.Pass {
|
||||
ctx.SetErrorFlash(censorResponse.ErrorMessage())
|
||||
// 内容安全检测:统一守卫,箱主本人回复同样按自己的审核严格度裁决。
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), f.Answer); err != nil {
|
||||
ctx.SetErrorFlash(err.Error())
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
|
||||
+14
-23
@@ -36,7 +36,7 @@ import (
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/imagecheck"
|
||||
"github.com/tamakyi/TamaBox/internal/mail"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||||
"github.com/tamakyi/TamaBox/internal/shared"
|
||||
"github.com/tamakyi/TamaBox/internal/storage"
|
||||
templatepkg "github.com/tamakyi/TamaBox/internal/template"
|
||||
@@ -343,6 +343,11 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
|
||||
return
|
||||
}
|
||||
|
||||
// 内容审核严格度:提问 / 私聊 / 投票三条链路共用同一份档位(users.moderation_strictness),
|
||||
// 取值解释与裁决逻辑见 internal/security/censor/policy.go。
|
||||
// 在此解析一次,供下方私聊分支与普通提问分支复用,保证同一请求的判定口径一致。
|
||||
strictness := moderation.StrictnessFor(pageUser.ModerationStrictness)
|
||||
|
||||
// ---------- 私聊处理 ----------
|
||||
if f.StartChat == "true" && ctx.IsLogged {
|
||||
|
||||
@@ -431,17 +436,10 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
|
||||
return
|
||||
}
|
||||
|
||||
// 内容安全检测
|
||||
censorResponse, err := censor.Text(ctx.Request().Context(), f.Content)
|
||||
if err != nil {
|
||||
// 审核源全部不可用:fail-closed,拒绝发布而非免审放行。
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to censor text")
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "censor.unavailable")), f)
|
||||
ctx.Success("question/list")
|
||||
return
|
||||
}
|
||||
if !censorResponse.Pass {
|
||||
ctx.SetError(errors.New(censorResponse.ErrorMessage()), f)
|
||||
// 内容安全检测:统一守卫,按箱主「审核严格度」裁决是否放行
|
||||
// (审核源不可用时 moderation.Check 内部仍为 fail-closed)。
|
||||
if _, err := moderation.Check(ctx.Request().Context(), strictness, ctx.Lang(), f.Content); err != nil {
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/list")
|
||||
return
|
||||
}
|
||||
@@ -571,18 +569,11 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
|
||||
return
|
||||
}
|
||||
|
||||
censorResponse, err := censor.Text(ctx.Request().Context(), content)
|
||||
// 内容安全检测:统一守卫,按箱主「审核严格度」裁决是否放行。
|
||||
// 返回的 censorResponse 供下方落 content_censor_metadata(审计留痕)。
|
||||
censorResponse, err := moderation.Check(ctx.Request().Context(), strictness, ctx.Lang(), content)
|
||||
if err != nil {
|
||||
// 审核源全部不可用:fail-closed,拒绝发布而非免审放行,
|
||||
// 避免审核服务宕机期间提问内容免审直接公开。
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to censor text")
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "censor.unavailable")), f)
|
||||
ctx.Success("question/list")
|
||||
return
|
||||
}
|
||||
if !censorResponse.Pass {
|
||||
errorMessage := censorResponse.ErrorMessage()
|
||||
ctx.SetError(errors.New(errorMessage), f)
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/list")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/imagecheck"
|
||||
"github.com/tamakyi/TamaBox/internal/mail"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||||
"github.com/tamakyi/TamaBox/internal/shared"
|
||||
)
|
||||
|
||||
@@ -165,17 +165,11 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
||||
answer := f.Answer
|
||||
|
||||
// 🚨 Content security check.
|
||||
censorResponse, err := censor.Text(ctx.Request().Context(), answer)
|
||||
// 统一守卫:按箱主「审核严格度」裁决是否放行(审核源不可用仍为 fail-closed,
|
||||
// 拒绝发布而非免审放行)。返回的 censorResponse 用于下方落审计字段。
|
||||
censorResponse, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), answer)
|
||||
if err != nil {
|
||||
// 审核源全部不可用:fail-closed,拒绝发布而非免审放行。
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to censor text")
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "censor.unavailable")), f)
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
if !censorResponse.Pass {
|
||||
errorMessage := censorResponse.ErrorMessage()
|
||||
ctx.SetError(errors.New(errorMessage), f)
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2022 E99p1ant. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"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/security/censor"
|
||||
)
|
||||
|
||||
// UpdateModerationStrictness 保存当前账号的「内容审核严格度」档位。
|
||||
//
|
||||
// 该档位同时作用于提问、投票、私聊三条链路(由 internal/security/moderation.Check
|
||||
// 统一裁决),因此只需在账号侧存一份。
|
||||
func UpdateModerationStrictness(ctx context.Context, f form.UpdateModerationStrictness) {
|
||||
// 反序列化失败(如请求体超限)时零值 Strictness="" 会被当成「跟随站点默认」写入,
|
||||
// 把账号已选择的档位静默改回默认档 —— 先中止,不触碰现有设置。
|
||||
if abortOnFormError(ctx, returnTab(ctx)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非法取值一律拒绝保存:宁可保留原档位,也不要写入一个解释不了的中间状态。
|
||||
if !censor.IsValidStrictness(f.Strictness) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "admin.flash.moderationStrictnessInvalid"))
|
||||
ctx.Redirect(returnTab(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Users.UpdateModerationStrictness(ctx.Request().Context(), ctx.User.ID, f.Strictness); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update moderation strictness")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(returnTab(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.moderationStrictnessUpdated"))
|
||||
ctx.Redirect(returnTab(ctx))
|
||||
}
|
||||
+39
-25
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||||
votepkg "github.com/tamakyi/TamaBox/route/vote"
|
||||
)
|
||||
|
||||
@@ -58,17 +59,13 @@ type voteForm struct {
|
||||
}
|
||||
|
||||
// censorCheck 对单段文本做内容审核。
|
||||
// fail-closed:审核服务异常时拒绝发布(与提问/回答流程一致);
|
||||
// 未开启 EnableTextCensor 时 censor.Text 直接放行,不影响开发环境。
|
||||
//
|
||||
// 与提问 / 私聊共用同一个守卫(internal/security/moderation.Check):是否拦截由
|
||||
// 投票发起人本人的「审核严格度」档位决定。
|
||||
// fail-closed:审核服务异常时拒绝发布;未开启 EnableTextCensor 时底层直接放行。
|
||||
func censorCheck(ctx context.Context, text string) error {
|
||||
resp, err := censor.Text(ctx.Request().Context(), text)
|
||||
if err != nil {
|
||||
return errors.New(i18n.T(ctx.Lang(), "censor.unavailable"))
|
||||
}
|
||||
if !resp.Pass {
|
||||
return errors.New(resp.ErrorMessage())
|
||||
}
|
||||
return nil
|
||||
_, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), text)
|
||||
return err
|
||||
}
|
||||
|
||||
// parseTimeInput 解析 "YYYY-MM-DDTHH:MM"(HTML datetime-local 格式),空字符串返回 nil。
|
||||
@@ -159,12 +156,9 @@ func parseVoteMeta(ctx context.Context, draft bool) (*voteForm, error) {
|
||||
// 内容审核(fail-closed):非草稿执行;草稿跳过(见上说明)
|
||||
var censorMeta datatypes.JSON
|
||||
if !draft {
|
||||
titleCensor, err := censor.Text(ctx.Request().Context(), title)
|
||||
titleCensor, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), title)
|
||||
if err != nil {
|
||||
return nil, errors.New("内容审核服务暂时不可用,请稍后再试")
|
||||
}
|
||||
if !titleCensor.Pass {
|
||||
return nil, errors.New(titleCensor.ErrorMessage())
|
||||
return nil, err
|
||||
}
|
||||
censorMeta = datatypes.JSON(titleCensor.ToJSON())
|
||||
if description != "" {
|
||||
@@ -996,32 +990,52 @@ func validateForPublish(ctx context.Context, vote *db.Vote) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// voteOwnerStrictness 读取投票发起人本人的「审核严格度」档位。
|
||||
//
|
||||
// 这里不用 ctx.User 的原因:发布动作未必由发起人本人触发(后台管理、补发布等场景),
|
||||
// ctx.User 有可能是操作者而非发起人,直接沿用会导致「按别人的档位审核别人箱子的内容」。
|
||||
// 取不到发起人时回退最严档位 —— 宁可沿用严格判定,也不要因为查不到就把判定放宽。
|
||||
func voteOwnerStrictness(ctx context.Context, ownerID uint) censor.Strictness {
|
||||
owner, err := db.Users.GetByID(ctx.Request().Context(), ownerID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).WithField("owner_id", ownerID).
|
||||
Error("Failed to get vote owner for moderation strictness")
|
||||
return censor.StrictnessStrict
|
||||
}
|
||||
return moderation.StrictnessFor(owner.ModerationStrictness)
|
||||
}
|
||||
|
||||
// censorVoteForPublish 发布前对投票全部可见文本做内容审核(fail-closed)。
|
||||
// 草稿保存阶段跳过了审核,发布(对外公开)时必须补齐,确保公开内容受管控。
|
||||
func censorVoteForPublish(ctx context.Context, vote *db.Vote) error {
|
||||
// 同一次发布的全部可见文本必须用「同一个」档位裁决:这里只解析一次发起人的档位并复用。
|
||||
// 否则标题走发起人档位、描述/选项走 ctx.User 档位(censorCheck 内部读 ctx.User),
|
||||
// 一旦发布动作不是发起人本人触发,同一条投票的两段文本就会落到两个账号的松紧档上。
|
||||
strictness := voteOwnerStrictness(ctx, vote.UserID)
|
||||
check := func(text string) error {
|
||||
_, err := moderation.Check(ctx.Request().Context(), strictness, ctx.Lang(), text)
|
||||
return err
|
||||
}
|
||||
|
||||
if vote.Title != "" {
|
||||
resp, err := censor.Text(ctx.Request().Context(), vote.Title)
|
||||
if err != nil {
|
||||
return errors.New(i18n.T(ctx.Lang(), "censor.unavailable"))
|
||||
}
|
||||
if !resp.Pass {
|
||||
return errors.New(resp.ErrorMessage())
|
||||
if err := check(vote.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if vote.Description != "" {
|
||||
if err := censorCheck(ctx, vote.Description); err != nil {
|
||||
if err := check(vote.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
options, _ := db.VoteOptions.ListByVoteID(ctx.Request().Context(), vote.ID)
|
||||
for _, o := range options {
|
||||
if err := censorCheck(ctx, o.Content); err != nil {
|
||||
if err := check(o.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fields, _ := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), vote.ID)
|
||||
for _, f := range fields {
|
||||
if err := censorCheck(ctx, f.Label); err != nil {
|
||||
if err := check(f.Label); err != nil {
|
||||
return err
|
||||
}
|
||||
// 预置选项(select/radio/checkbox/rank)也纳入审核
|
||||
@@ -1029,7 +1043,7 @@ func censorVoteForPublish(ctx context.Context, vote *db.Vote) error {
|
||||
if fo == "" {
|
||||
continue
|
||||
}
|
||||
if err := censorCheck(ctx, fo); err != nil {
|
||||
if err := check(fo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+17
-13
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/mail"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
"github.com/tamakyi/TamaBox/internal/security/moderation"
|
||||
"github.com/tamakyi/TamaBox/internal/shared"
|
||||
)
|
||||
|
||||
@@ -57,16 +58,19 @@ func InvalidateResultsCache(voteID uint) {
|
||||
resultsCache.Delete(strconv.FormatUint(uint64(voteID), 10))
|
||||
}
|
||||
|
||||
// censorCheck 对开放文本做内容审核(fail-closed;EnableTextCensor=false 时自动放行)。
|
||||
func censorCheck(ctx context.Context, lang, text string) error {
|
||||
resp, err := censor.Text(ctx.Request().Context(), text)
|
||||
if err != nil {
|
||||
return errors.New(i18n.T(lang, "censor.unavailable"))
|
||||
// censorCheck 对开放文本做内容审核。
|
||||
//
|
||||
// 与提问 / 私聊共用同一个守卫(internal/security/moderation.Check):是否拦截由
|
||||
// 投票发起人的「审核严格度」档位决定,而不是无条件采信审核服务的判定。
|
||||
// 审核服务异常仍为 fail-closed 拒绝提交;EnableTextCensor=false 时底层自动放行。
|
||||
// owner 为 nil 时回退到最严档位,避免发起人为空就把或从宽放行。
|
||||
func censorCheck(ctx context.Context, owner *db.User, lang, text string) error {
|
||||
strictness := censor.StrictnessStrict
|
||||
if owner != nil {
|
||||
strictness = moderation.StrictnessFor(owner.ModerationStrictness)
|
||||
}
|
||||
if !resp.Pass {
|
||||
return errors.New(resp.ErrorMessage())
|
||||
}
|
||||
return nil
|
||||
_, err := moderation.Check(ctx.Request().Context(), strictness, lang, text)
|
||||
return err
|
||||
}
|
||||
|
||||
// blockWordCheck 校验开放文本是否命中投票发起人的全局屏蔽词表
|
||||
@@ -968,7 +972,7 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
return nil, err
|
||||
}
|
||||
// 开放文本内容审核(fail-closed)
|
||||
if err := censorCheck(ctx, lang, custom); err != nil {
|
||||
if err := censorCheck(ctx, owner, lang, custom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formData[f.Label] = custom
|
||||
@@ -1004,7 +1008,7 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
return nil, err
|
||||
}
|
||||
// 开放文本内容审核(fail-closed)
|
||||
if err := censorCheck(ctx, lang, custom); err != nil {
|
||||
if err := censorCheck(ctx, owner, lang, custom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chosen = append(chosen, custom)
|
||||
@@ -1088,7 +1092,7 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
return nil, err
|
||||
}
|
||||
// 开放文本内容审核(fail-closed)
|
||||
if err := censorCheck(ctx, lang, val); err != nil {
|
||||
if err := censorCheck(ctx, owner, lang, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formData[f.Label] = val
|
||||
@@ -1110,7 +1114,7 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
return nil, err
|
||||
}
|
||||
// 开放文本内容审核(fail-closed)
|
||||
if err := censorCheck(ctx, lang, custom); err != nil {
|
||||
if err := censorCheck(ctx, owner, lang, custom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.CustomText = custom
|
||||
|
||||
Reference in New Issue
Block a user