127 lines
4.7 KiB
Go
127 lines
4.7 KiB
Go
// 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 语义:响应未写即自动续链),零值结构体会进入业务动作,把用户既有配置静默清空:
|
||
// 防骚扰被改回 none、屏蔽词/标签被清空、访问与提问密码被解除。
|
||
// 错误信息已由 Bind 写入 Data["Error"],此处转入 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(见 abortOnFormError)。
|
||
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、
|
||
// 跳过空词与重复词;单词长度超限(conf.Limits.MaxBlockWordLen)或总数超限
|
||
// (conf.Limits.MaxBlockWords)返回对应提示。成功时 errKey 为空,errWord 为超长的具体词。
|
||
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」不符。
|
||
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, "", ""
|
||
}
|