// 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, "", "" }