Files
tamabox/internal/cmd/hide_public.go
T
2026-10-19 01:58:00 +08:00

159 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"gorm.io/gorm"
"github.com/tamakyi/TamaBox/internal/db"
)
// HidePublic 把某箱主已公开的回答批量转为私密。
//
// 存在意义:处置一个「组织挂人」的箱主时,L1 只停分发、L2 只影响新内容,
// 已经挂在上面的几十上百条旧内容仍然公开可访问。逐条点后台不现实,
// 而直接封号又过重(这类账号往往同时有大量正常内容)。
//
// 默认**只动「公开且已回答」的题**:未回答的本来就不公开,已私密的不必再动,
// 不会误伤草稿态数据。加上 --only-reported 可进一步只处理「有未处理举报」的条,
// 这是最精准的用法 —— 只收掉被投诉的部分,其余正常内容保留。
var HidePublic = &cli.Command{
Name: "hide-public",
Usage: "Hide all public answers of a user (set is_private = true)",
Flags: []cli.Flag{
&cli.StringFlag{Name: "email", Usage: "Email of the user"},
&cli.StringFlag{Name: "domain", Usage: "Custom domain of the user"},
&cli.BoolFlag{Name: "only-reported", Usage: "Only hide questions that have pending reports"},
&cli.IntFlag{Name: "since", Usage: "Only hide questions created within the last N days (0 = all)"},
&cli.BoolFlag{Name: "yes", Usage: "Skip confirmation"},
},
Action: runHidePublic,
}
func runHidePublic(ctx *cli.Context) error {
database, err := openDatabase()
if err != nil {
return err
}
user, err := findTargetUser(database, ctx.String("email"), ctx.String("domain"))
if err != nil {
return err
}
onlyReported := ctx.Bool("only-reported")
sinceDays := ctx.Int("since")
if sinceDays < 0 {
return errors.New("--since must not be negative")
}
// ⚠️ 每次都重新构建 query:GORM 的链式对象在 Count/Find 之后会带上 Statement 状态,
// 复用同一个实例做「统计 → 预览 → 更新」会导致条件互相污染。
build := func() *gorm.DB {
q := database.Model(&db.Question{}).
Where("user_id = ? AND answer != '' AND is_private = false", user.ID)
if onlyReported {
q = q.Where("id IN (?)", database.Model(&db.ContentReport{}).
Select("question_id").
Where("status = ?", db.ReportStatusPending))
}
if sinceDays > 0 {
q = q.Where("created_at >= ?", time.Now().AddDate(0, 0, -sinceDays))
}
return q
}
var total int64
if err := build().Count(&total).Error; err != nil {
return errors.Wrap(err, "count public questions")
}
fmt.Printf("箱主: %s (@%s) id=%d\n", user.Name, user.Domain, user.ID)
if onlyReported {
fmt.Println("范围: 仅「有未处理举报」的公开回答")
} else {
fmt.Println("范围: 全部公开回答")
}
if sinceDays > 0 {
fmt.Printf("时间: 仅最近 %d 天\n", sinceDays)
}
if total == 0 {
fmt.Println("没有需要隐藏的内容")
return nil
}
var preview []*db.Question
if err := build().Order("id DESC").Limit(10).Find(&preview).Error; err != nil {
return errors.Wrap(err, "preview questions")
}
fmt.Printf("将把 %d 条回答转为私密(最新 10 条预览):\n", total)
for _, q := range preview {
fmt.Printf(" #%-6d %s %s\n", q.ID, q.CreatedAt.Format("2006-01-02 15:04"), truncateForCLI(q.Content, 44))
}
if total > int64(len(preview)) {
fmt.Printf(" ...(其余 %d 条略)\n", total-int64(len(preview)))
}
if !ctx.Bool("yes") {
logrus.Warn("输入大写 YES 确认执行(可逆:箱主可在后台逐条重新设为公开):")
var answer string
if _, err := fmtScan(&answer); err != nil {
return errors.Wrap(err, "read confirmation")
}
if answer != "YES" {
return errors.New("已取消")
}
}
// ⚠️ 必须先快照目标 ID 再更新:build() 的条件里有 is_private = false,
// 一旦执行了 Update,再拿 build() 当子查询就恒为空集,举报闭环会静默失效
// (实测:隐藏成功但 content_reports 仍是 pending)。
var targetIDs []uint
if err := build().Pluck("id", &targetIDs).Error; err != nil {
return errors.Wrap(err, "collect target question ids")
}
if len(targetIDs) == 0 {
fmt.Println("没有需要隐藏的内容")
return nil
}
result := database.Model(&db.Question{}).Where("id IN ?", targetIDs).Update("is_private", true)
if result.Error != nil {
return errors.Wrap(result.Error, "hide public questions")
}
logrus.WithFields(logrus.Fields{
"user_id": user.ID,
"affected": result.RowsAffected,
"only_reported": onlyReported,
}).Warn("公开回答已批量转为私密")
// 举报闭环:被隐藏的内容等同于已处理,对应的待处理举报一并标记为成立,
// 否则这些举报会继续计入告警阈值(详见 internal/db/report.go 的说明)。
accepted := database.WithContext(ctx.Context).
Model(&db.ContentReport{}).
Where("question_id IN ?", targetIDs).
Where("status = ?", db.ReportStatusPending).
Update("status", db.ReportStatusAccepted)
if accepted.Error != nil {
logrus.WithError(accepted.Error).Warn("标记举报为已处理时异常(内容隐藏已生效,可稍后手动处理)")
} else if accepted.RowsAffected > 0 {
fmt.Printf("同时把 %d 条待处理举报标记为已处理\n", accepted.RowsAffected)
}
fmt.Printf("完成:%d 条已转为私密\n", result.RowsAffected)
fmt.Println(strings.TrimSpace(`
注意:这只改变可见性,不改变回答内容;箱主仍可在自己的后台逐条改回公开。
如果目的是「停止分发」,还需要配合:TamaBox restrict --domain <domain> --level L1 --days N`))
return nil
}