- rename Go module, binary, systemd unit, PG_DATABASE default, frontend package and docs from shared-album to galo - add theme-aware halo emblem and favicon (new ui-branding capability) - add make build-release (versioned, stripped, static binary) and run the binary from the repo root
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package social
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"galo/internal/auth"
|
|
"galo/internal/httpx"
|
|
"galo/internal/models"
|
|
"galo/internal/roles"
|
|
"galo/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 Repository
|
|
albums AlbumAccess
|
|
photos PhotoLookup
|
|
hub *sse.Hub
|
|
notify Notifier
|
|
}
|
|
|
|
func NewService(store Repository, 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)
|
|
}
|