Files
tamabox/route/user/harassment.go
T
2026-09-08 10:10:44 +08:00

140 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"strings"
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/context"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/form"
"github.com/tamakyi/TamaBox/internal/i18n"
)
// returnTab 表单提交后回跳的账号设置面板:屏蔽词设置已分散展示在
// 提问/私聊/投票三个面板,action 携带 ?tab= 来源参数,保存后停留原面板。
func returnTab(ctx context.Context) string {
return profileURLWithTab(ProfileTab(ctx.Query("tab")))
}
// abortOnFormError 处理表单校验/反序列化失败:返回 true 表示已中止本次写入。
//
// 背景:form.Bind 在反序列化失败(如请求体超限、body 畸形)时只置位错误并继续执行
// handler 链(flamego 语义:响应未写即自动续链),零值结构体会照常进入业务动作——
// 对不检查 ctx.HasError() 的 handler,会把用户既有配置静默清空/关闭,例如:
// - UpdateHarassment:RegisterOnly="" → 防骚扰被改回 none(关闭"仅允许注册用户");
// - UpdateBlockWords:BlockWords="" → 全部屏蔽词被清空;
// - UpdateTags:Tags="" → 标签被清空、ShowInHome 被关;
// - UpdatePagePassword / UpdateAskPassword:Password="" → 访问/提问密码被解除。
//
// 错误信息已由 Bind 经 context.SetError 写入 Data["Error"](govalid 校验错误已按界面
// 语言翻译,解析错误为原文),此处转入 flash,保证 PRG 重定向后仍能向用户展示。
func abortOnFormError(ctx context.Context, redirectTo string) bool {
if !ctx.HasError() {
return false
}
if msg, ok := ctx.Data["Error"].(string); ok && msg != "" {
ctx.SetErrorFlash(msg)
}
ctx.Redirect(redirectTo)
return true
}
// UpdateHarassment 仅更新「防骚扰类型」(none / register_only,即"仅允许注册用户向我提问")。
// 只写 harassment_setting 列;全局屏蔽词已拆分为独立入口 UpdateBlockWords,
// 二者互不覆盖(此前同一个表单一次写两列,面板拆分后单侧保存会误清空另一侧)。
func UpdateHarassment(ctx context.Context, f form.UpdateHarassment) {
// 反序列化失败(如请求体超限)时零值 RegisterOnly="" 会把防骚扰静默改回 none
// (若此前为 register_only)——先中止,不触碰现有设置。
if abortOnFormError(ctx, returnTab(ctx)) {
return
}
harassmentSettingType := db.HarassmentSettingNone
if f.RegisterOnly != "" {
harassmentSettingType = db.HarassmentSettingTypeRegisterOnly
}
if err := db.Users.UpdateHarassmentType(ctx.Request().Context(), ctx.User.ID, harassmentSettingType); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update harassment setting")
ctx.SetInternalErrorFlash()
ctx.Redirect(returnTab(ctx))
return
}
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.harassmentUpdated"))
ctx.Redirect(returnTab(ctx))
}
// UpdateBlockWords 更新全局屏蔽词表:提问、私聊(对方发给我的消息)与投票
// 提交的开放文本共用同一份词表(users.block_words)。
func UpdateBlockWords(ctx context.Context, f form.UpdateBlockWords) {
// 反序列化失败(如请求体超限)时零值 BlockWords="" 会经 sanitizeBlockWords 得到
// 空词表,静默清空用户全部屏蔽词——先中止,不触碰现有词表。
if abortOnFormError(ctx, returnTab(ctx)) {
return
}
words, errKey, errWord := sanitizeBlockWords(f.BlockWords)
if errKey != "" {
if errKey == "admin.flash.blockWordTooLong" {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), errKey, errWord))
} else {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), errKey))
}
ctx.Redirect(returnTab(ctx))
return
}
if err := db.Users.UpdateBlockWords(ctx.Request().Context(), ctx.User.ID, strings.Join(words, ",")); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update block words")
ctx.SetInternalErrorFlash()
ctx.Redirect(returnTab(ctx))
return
}
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.blockWordsUpdated"))
ctx.Redirect(returnTab(ctx))
}
// sanitizeBlockWords 把输入的屏蔽词串规整为去重后的词表:
// - 全角逗号转半角;逐词 TrimSpace;跳过空词与重复词。
// - 单词长度按字符(rune)计数,超限(conf.Limits.MaxBlockWordLen)返回超长提示;
// - 词总数超限(conf.Limits.MaxBlockWords)返回数量提示。
//
// 成功时 errKey 为空,errWord 为触发超长的具体词(供 flash 展示)。
func sanitizeBlockWords(raw string) (words []string, errKey string, errWord string) {
blockWords := strings.ReplaceAll(raw, ",", ",")
blockWords = strings.TrimSpace(blockWords)
words = make([]string, 0)
wordSet := make(map[string]struct{})
for _, word := range strings.Split(blockWords, ",") {
word := strings.TrimSpace(word)
if word == "" {
continue
}
if _, ok := wordSet[word]; ok {
continue
}
wordSet[word] = struct{}{}
// 按字符(rune)计数:早期用 len(word) 按字节计数,中文一词 3 字节,
// 导致中文屏蔽词实际只能输 3 个字,与界面文案「最大长度为 10」不符。
// 上限取自 conf.Limits.MaxBlockWordLen([limits] 配置段)。
if len([]rune(word)) > conf.Limits.MaxBlockWordLen {
return nil, "admin.flash.blockWordTooLong", word
}
words = append(words, word)
}
if len(words) > conf.Limits.MaxBlockWords {
return nil, "admin.flash.blockWordsTooMany", ""
}
return words, "", ""
}