package social import ( "context" "errors" "strings" "shared-album/internal/auth" "shared-album/internal/httpx" "shared-album/internal/models" "shared-album/internal/roles" "shared-album/internal/sse" ) type AlbumAccess interface { Access(ctx context.Context, albumID, userID string) (*models.Album, string, error) } type PhotoLookup interface { GetByID(ctx context.Context, id, viewerID string) (*models.Photo, error) } type Notifier interface { Create(ctx context.Context, userID, typ string, payload map[string]any) error } type Service struct { store *Store albums AlbumAccess photos PhotoLookup hub *sse.Hub notify Notifier } func NewService(store *Store, albums AlbumAccess, photos PhotoLookup, hub *sse.Hub, notify Notifier) *Service { return &Service{store: store, albums: albums, photos: photos, hub: hub, notify: notify} } func (s *Service) requireComment(ctx context.Context, photo *models.Photo, userID string) (string, error) { _, role, err := s.albums.Access(ctx, photo.AlbumID, userID) if err != nil { return "", err } if !roles.CanComment(role) { return "", httpx.Forbidden("forbidden", "you do not have permission to interact with photos") } return role, nil } func (s *Service) AddLike(ctx context.Context, photoID, userID string) error { photo, err := s.photos.GetByID(ctx, photoID, userID) if err != nil { if errors.Is(err, ErrNotFound) { return httpx.NotFound("photo_not_found", "photo not found") } return err } if _, err := s.requireComment(ctx, photo, userID); err != nil { return err } if err := s.store.Like(ctx, photoID, userID); err != nil { return err } s.hub.Publish(photo.AlbumID, "like.added", map[string]string{ "photo_id": photoID, "album_id": photo.AlbumID, "user_id": userID, }) if s.notify != nil && photo.UploaderID != userID { _ = s.notify.Create(ctx, photo.UploaderID, "like.added", map[string]any{ "photo_id": photoID, "album_id": photo.AlbumID, "user_id": userID, }) } return nil } func (s *Service) RemoveLike(ctx context.Context, photoID, userID string) error { photo, err := s.photos.GetByID(ctx, photoID, userID) if err != nil { if errors.Is(err, ErrNotFound) { return httpx.NotFound("photo_not_found", "photo not found") } return err } if _, err := s.requireComment(ctx, photo, userID); err != nil { return err } if err := s.store.Unlike(ctx, photoID, userID); err != nil { return err } s.hub.Publish(photo.AlbumID, "like.removed", map[string]string{ "photo_id": photoID, "album_id": photo.AlbumID, "user_id": userID, }) return nil } func (s *Service) AddComment(ctx context.Context, photoID, userID, text string) (*models.Comment, error) { text = strings.TrimSpace(text) if text == "" { return nil, httpx.BadRequest("invalid_text", "comment text is required") } photo, err := s.photos.GetByID(ctx, photoID, userID) if err != nil { if errors.Is(err, ErrNotFound) { return nil, httpx.NotFound("photo_not_found", "photo not found") } return nil, err } if _, err := s.requireComment(ctx, photo, userID); err != nil { return nil, err } c := &models.Comment{ ID: auth.NewID(), PhotoID: photoID, UserID: userID, Text: text, } if err := s.store.CreateComment(ctx, c); err != nil { return nil, err } list, err := s.store.ListComments(ctx, photoID) if err == nil { for _, item := range list { if item.ID == c.ID { c.User = item.User break } } } s.hub.Publish(photo.AlbumID, "comment.created", c) if s.notify != nil && photo.UploaderID != userID { _ = s.notify.Create(ctx, photo.UploaderID, "comment.created", map[string]any{ "photo_id": photoID, "album_id": photo.AlbumID, "user_id": userID, "comment_id": c.ID, }) } return c, nil } func (s *Service) ListComments(ctx context.Context, photoID, userID string) ([]models.Comment, error) { photo, err := s.photos.GetByID(ctx, photoID, userID) if err != nil { if errors.Is(err, ErrNotFound) { return nil, httpx.NotFound("photo_not_found", "photo not found") } return nil, err } if _, _, err := s.albums.Access(ctx, photo.AlbumID, userID); err != nil { return nil, err } return s.store.ListComments(ctx, photoID) } func (s *Service) DeleteComment(ctx context.Context, commentID, userID string) error { c, err := s.store.GetComment(ctx, commentID) if err != nil { if errors.Is(err, ErrNotFound) { return httpx.NotFound("comment_not_found", "comment not found") } return err } photo, err := s.photos.GetByID(ctx, c.PhotoID, userID) if err != nil { return err } _, role, err := s.albums.Access(ctx, photo.AlbumID, userID) if err != nil { return err } if c.UserID != userID && !roles.CanDeleteAnyPhoto(role) { return httpx.Forbidden("forbidden", "you cannot delete this comment") } return s.store.DeleteComment(ctx, commentID) }