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