// Package social owns friendships, private conversations and the persistent inbox. package social import ( "context" "strings" "unicode/utf8" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "pipu/api/internal/api/gen" "pipu/api/internal/auth" "pipu/api/internal/platform/apperr" ) type ActivityPlaylistReader interface { ReadSocialPlaylist(context.Context, uuid.UUID) (gen.Playlist, error) } type Handler struct { store *Store playlists ActivityPlaylistReader } func NewHandler(pool *pgxpool.Pool, playlists ActivityPlaylistReader) *Handler { return &Handler{store: &Store{pool: pool}, playlists: playlists} } func userID(ctx context.Context) (uuid.UUID, error) { p, ok := auth.PrincipalFrom(ctx) if !ok { return uuid.Nil, apperr.Unauthorized("auth.unauthorized", "sign in required") } return p.UserID, nil } func invalid() error { return apperr.Validation("social", "social.invalid", "invalid social operation", nil) } func denied() error { return apperr.Forbidden("social.unavailable", "friendship required or person unavailable") } func (h *Handler) GetSocial(ctx context.Context, _ gen.GetSocialRequestObject) (gen.GetSocialResponseObject, error) { id, e := userID(ctx) if e != nil { return nil, e } v, e := h.store.inbox(ctx, id) return gen.GetSocial200JSONResponse(v), e } func (h *Handler) SearchPeople(ctx context.Context, r gen.SearchPeopleRequestObject) (gen.SearchPeopleResponseObject, error) { id, e := userID(ctx) if e != nil { return nil, e } q := strings.TrimSpace(r.Params.Q) if utf8.RuneCountInString(q) < 2 || utf8.RuneCountInString(q) > 80 { return nil, invalid() } v, e := h.store.people(ctx, id, q) return gen.SearchPeople200JSONResponse(v), e } func (h *Handler) SocialAction(ctx context.Context, r gen.SocialActionRequestObject) (gen.SocialActionResponseObject, error) { id, e := userID(ctx) if e != nil { return nil, e } if r.Body == nil || r.Body.TargetId == uuid.Nil || r.Body.TargetId == id { return nil, invalid() } if e = h.store.action(ctx, id, *r.Body); e != nil { return nil, e } return gen.SocialAction204Response{}, nil } func (h *Handler) GetDirectMessages(ctx context.Context, r gen.GetDirectMessagesRequestObject) (gen.GetDirectMessagesResponseObject, error) { id, e := userID(ctx) if e != nil { return nil, e } if r.PeerId == uuid.Nil || r.PeerId == id || (r.Params.Before != nil && *r.Params.Before < 1) { return nil, invalid() } q := "" if r.Params.Q != nil { q = strings.TrimSpace(*r.Params.Q) } if utf8.RuneCountInString(q) > 100 { return nil, invalid() } v, e := h.store.messagePage(ctx, id, r.PeerId, r.Params.Before, q) return gen.GetDirectMessages200JSONResponse(v), e } func validateMessage(m gen.SendDirectMessage) error { if m.ReplyTo != nil && *m.ReplyTo == uuid.Nil { return invalid() } if m.Id == uuid.Nil || utf8.RuneCountInString(m.Text) > 4000 { return invalid() } switch m.AttachmentKind { case "none": if strings.TrimSpace(m.Text) == "" || m.AttachmentId != nil { return invalid() } case "track", "playlist": if m.AttachmentId == nil || *m.AttachmentId == uuid.Nil { return invalid() } default: return invalid() } return nil } func (h *Handler) SendDirectMessage(ctx context.Context, r gen.SendDirectMessageRequestObject) (gen.SendDirectMessageResponseObject, error) { id, e := userID(ctx) if e != nil { return nil, e } if r.Body == nil || r.PeerId == id || r.PeerId == uuid.Nil { return nil, invalid() } if e = validateMessage(*r.Body); e != nil { return nil, e } v, e := h.store.send(ctx, id, r.PeerId, *r.Body) return gen.SendDirectMessage200JSONResponse(v), e }