0904
This commit is contained in:
@@ -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, "", ""
|
||||
}
|
||||
Reference in New Issue
Block a user