package logic import ( "context" "strings" "sync" "sync/atomic" friendentity "spark-server/internal/module/friend/entity" "spark-server/internal/module/square/entity" userentity "spark-server/internal/module/user/entity" "spark-server/internal/pkg/media" "go.uber.org/zap" ) // Repository persists square data. Model implements it. type Repository interface { CreatePost(ctx context.Context, post *entity.Post, audience []int64) error CreateBotPost(ctx context.Context, post *entity.Post) error GetPost(ctx context.Context, id int64) (*entity.Post, error) ListPublicFeed(ctx context.Context, channel entity.Channel, q entity.FeedListQuery) ([]*entity.Post, error) ListByAuthor(ctx context.Context, channel entity.Channel, authorID, cursor int64, limit int) ([]*entity.Post, error) SoftDeletePost(ctx context.Context, userID, postID int64) (*entity.Post, error) SoftDeleteExpiredBotPosts(ctx context.Context, nowMS int64, limit int) ([]*entity.Post, error) ListAudience(ctx context.Context, postIDs []int64) (map[int64][]int64, error) ListLikes(ctx context.Context, postIDs []int64) (map[int64][]*entity.Like, error) AddLike(ctx context.Context, like *entity.Like) (created bool, err error) RemoveLike(ctx context.Context, postID, userID int64) error ListComments(ctx context.Context, postIDs []int64) (map[int64][]*entity.Comment, error) GetComment(ctx context.Context, id int64) (*entity.Comment, error) AddComment(ctx context.Context, c *entity.Comment) error SoftDeleteComment(ctx context.Context, id int64) error CreateNotices(ctx context.Context, rows []*entity.Notice) error ListNotices(ctx context.Context, userID int64, channel entity.Channel, cursor int64, limit int) ([]*entity.Notice, error) MarkNoticesRead(ctx context.Context, userID int64, channel entity.Channel, cursor int64) error GetNoticeState(ctx context.Context, userID int64, channel entity.Channel) (*entity.NoticeState, error) LatestNoticeID(ctx context.Context, userID int64, channel entity.Channel) (int64, error) AdvanceNoticeRead(ctx context.Context, userID int64, channel entity.Channel, noticeID int64) error ListAllByAuthor(ctx context.Context, authorID int64) ([]*entity.Post, error) PurgeUser(ctx context.Context, userID int64) error } // Friends is the friend graph used for audience and block checks. type Friends interface { AreFriends(ctx context.Context, a, b int64) (bool, error) ListBlockedPeerIDs(ctx context.Context, userID int64) ([]int64, error) Relation(ctx context.Context, userID, peerID int64) (*friendentity.Relation, error) BlockedEither(ctx context.Context, a, b int64) (bool, error) } // Users loads profile cards. type Users interface { Get(ctx context.Context, id int64) (*userentity.User, error) GetByEmail(ctx context.Context, email string) (*userentity.User, error) } // Objects deletes square media. Nil skips GC. type Objects interface { Delete(ctx context.Context, key string) error } // IDGen issues Snowflake ids. type IDGen interface { NextID() (int64, error) } // Service is the square use-case layer. type Service struct { repo Repository friends Friends users Users objects Objects ids IDGen burst *burst media media.Policy log *zap.Logger discoverMu sync.Mutex discoverReady atomic.Bool gate TextGate } func NewService(repo Repository, friends Friends, users Users, objects Objects, ids IDGen) *Service { return &Service{repo: repo, friends: friends, users: users, objects: objects, ids: ids, burst: newBurst(), log: zap.NewNop()} } // ConfigureMedia sets user-path media detection (bots compress via squarebot MediaCompressor). func (s *Service) ConfigureMedia(p media.Policy, log *zap.Logger) { if s == nil { return } s.media = p if log != nil { s.log = log } } func (s *Service) ready() bool { return s != nil && s.repo != nil && s.friends != nil && s.users != nil && s.ids != nil } // TextGate scans user-authored text. Nil skips the check. type TextGate interface { GateText(ctx context.Context, userID int64, text string) (string, error) } // BindModeration wires the shared sensitive-word gate. func (s *Service) BindModeration(gate TextGate) { if s == nil { return } s.gate = gate } func (s *Service) gateText(ctx context.Context, userID int64, text string) (string, error) { if s == nil || s.gate == nil || strings.TrimSpace(text) == "" { return text, nil } return s.gate.GateText(ctx, userID, text) } func normalizeChannel(ch entity.Channel) entity.Channel { if ch == "" { return entity.ChannelSquare } return ch }