This commit is contained in:
tamakyi
2026-09-08 10:10:44 +08:00
commit 2ed27dd55e
288 changed files with 67061 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
// 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, "", ""
}