package logic import ( "context" "errors" "time" "spark-server/internal/module/friend/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/i18n" "spark-server/internal/pkg/reqctx" ) // FollowView is the viewer's follow state toward one user. type FollowView struct { UID int64 `json:"uid"` Following bool `json:"following"` FollowedBy bool `json:"followed_by"` Mutual bool `json:"mutual"` Friend bool `json:"friend"` } func (s *Service) Follow(ctx context.Context, userID, targetID int64) (*FollowView, error) { if err := s.followReady(ctx, userID, targetID); err != nil { return nil, err } if _, err := s.activeUser(ctx, targetID); err != nil { return nil, err } if err := s.followBlocks(ctx, userID, targetID); err != nil { return nil, err } if err := s.repo.PutFollow(ctx, userID, targetID); err != nil { return nil, err } if err := s.friendIfMutual(ctx, userID, targetID); err != nil { return nil, err } return s.followView(ctx, userID, targetID) } func (s *Service) Unfollow(ctx context.Context, userID, targetID int64) (*FollowView, error) { if err := s.followReady(ctx, userID, targetID); err != nil { return nil, err } mutual, err := s.mutualFollow(ctx, userID, targetID) if err != nil { return nil, err } if err := s.repo.DeleteFollow(ctx, userID, targetID); err != nil { return nil, err } if mutual { // DeleteFriend removes both directed edges. Messages stay. if err := s.repo.DeleteFriend(ctx, userID, targetID); err != nil && !errors.Is(err, errs.ErrNotFound) { return nil, err } } return s.followView(ctx, userID, targetID) } func (s *Service) GetFollow(ctx context.Context, userID, targetID int64) (*FollowView, error) { if err := s.followReady(ctx, userID, targetID); err != nil { return nil, err } if _, err := s.activeUser(ctx, targetID); err != nil { return nil, err } return s.followView(ctx, userID, targetID) } func (s *Service) followReady(ctx context.Context, userID, targetID int64) error { if !s.ready() || userID <= 0 { return errs.ErrUnauthorized } if targetID <= 0 { return errs.ErrInvalidArgument } if entity.SameUser(userID, targetID) { return errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyFriendSelf, nil)) } return nil } func (s *Service) followBlocks(ctx context.Context, from, to int64) error { lang := reqctx.Lang(ctx) blockedMe, err := s.repo.IsBlocked(ctx, from, to) if err != nil { return err } if blockedMe { return errs.ErrForbidden.WithMsg(i18n.T(lang, i18n.KeyFriendBlocked, nil)) } blockedBy, err := s.repo.IsBlocked(ctx, to, from) if err != nil { return err } if blockedBy { return errs.ErrNotFound } return nil } func (s *Service) friendIfMutual(ctx context.Context, userID, targetID int64) error { mutual, err := s.mutualFollow(ctx, userID, targetID) if err != nil || !mutual { return err } ok, err := s.AreFriends(ctx, userID, targetID) if err != nil || ok { return err } now := time.Now() if err := s.repo.AcceptFriend(ctx, entity.AcceptParams{Edges: []*entity.Relation{ entity.NewFriendEdge(userID, targetID, "", entity.SourceFollow, now), entity.NewFriendEdge(targetID, userID, "", entity.SourceFollow, now), }}); err != nil { return err } if s.greeter != nil { _ = s.greeter.BecameFriends(ctx, userID, targetID) } return nil } func (s *Service) mutualFollow(ctx context.Context, userID, targetID int64) (bool, error) { following, err := s.repo.HasFollow(ctx, userID, targetID) if err != nil || !following { return false, err } return s.repo.HasFollow(ctx, targetID, userID) } func (s *Service) followView(ctx context.Context, userID, targetID int64) (*FollowView, error) { following, err := s.repo.HasFollow(ctx, userID, targetID) if err != nil { return nil, err } followedBy, err := s.repo.HasFollow(ctx, targetID, userID) if err != nil { return nil, err } friend, err := s.AreFriends(ctx, userID, targetID) if err != nil { return nil, err } return &FollowView{ UID: targetID, Following: following, FollowedBy: followedBy, Mutual: following && followedBy, Friend: friend, }, nil }