89 lines
2.6 KiB
Go
89 lines
2.6 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).
|
|
// Images must already be compressed and stored under perm/square/image/.
|
|
type BotPublishInput struct {
|
|
Text string
|
|
SourceURL string
|
|
TTL time.Duration
|
|
Images []*entity.Image
|
|
}
|
|
|
|
// PublishBotMedia publishes a public 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{Images: in.Images}
|
|
if err := content.Validate(); err != nil {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil))
|
|
}
|
|
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: content.Kind(), 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
|
|
}
|