spark-server/internal/module/square/logic/bot_publish.go
DESKTOP-L8OUL9F\gfl0034 715ace7ac6 feat(square): 广场 Bot/RSS 发布与媒体校验
支持广场机器人发帖与媒体类型检查,并补充相关迁移与 i18n。
2026-09-24 23:07:06 +08:00

84 lines
2.3 KiB
Go

package logic
import (
"context"
"strings"
"time"
"unicode/utf8"
"spark-server/internal/module/square/entity"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/i18n"
"spark-server/internal/pkg/reqctx"
)
// BotPublishInput is an internal publish from the media bot worker (no rate limit).
type BotPublishInput struct {
Text string
SourceURL string
TTL time.Duration
}
// PublishBotMedia publishes a public text post as a bot user with TTL and URL dedupe.
func (s *Service) PublishBotMedia(ctx context.Context, userID int64, in *BotPublishInput) (*View, error) {
if !s.ready() || userID <= 0 {
return nil, errs.ErrUnauthorized
}
if in == nil {
return nil, errs.ErrInvalidArgument
}
text := strings.TrimSpace(in.Text)
url := strings.TrimSpace(in.SourceURL)
if text == "" || url == "" {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareEmpty, nil))
}
if utf8.RuneCountInString(text) > entity.MaxTextRunes {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareTextInvalid, nil))
}
ttl := in.TTL
if ttl <= 0 {
ttl = 15 * 24 * time.Hour
}
content := &entity.Content{}
raw, err := content.Marshal()
if err != nil {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil))
}
id, err := s.ids.NextID()
if err != nil {
return nil, err
}
now := time.Now()
post := &entity.Post{
ID: id, UserID: userID, Kind: entity.KindText, Text: text, Content: raw,
Visibility: entity.VisibilityPublic, AuditStatus: entity.AuditApproved,
Status: entity.StatusNormal, Source: entity.SourceBotRSS, SourceURL: url,
ExpiresAtMS: now.Add(ttl).UnixMilli(), CreatedAtMS: now.UnixMilli(),
}
if err := s.repo.CreateBotPost(ctx, post); err != nil {
return nil, err
}
return s.viewOf(ctx, userID, post, true)
}
// ExpireBotPosts soft-deletes expired bot posts and best-effort deletes media.
func (s *Service) ExpireBotPosts(ctx context.Context, now time.Time, limit int) (int, error) {
if !s.ready() {
return 0, nil
}
if now.IsZero() {
now = time.Now()
}
if limit <= 0 {
limit = 200
}
posts, err := s.repo.SoftDeleteExpiredBotPosts(ctx, now.UnixMilli(), limit)
if err != nil {
return 0, err
}
for _, post := range posts {
s.deleteMedia(ctx, post)
}
return len(posts), nil
}