ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
211 lines
7.0 KiB
Go
211 lines
7.0 KiB
Go
package social
|
|
|
|
import (
|
|
"context"
|
|
"github.com/google/uuid"
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/platform/apperr"
|
|
)
|
|
|
|
func validateActivity(a gen.ListeningActivityInput) error {
|
|
if a.DeviceId == uuid.Nil || a.Sequence < 1 {
|
|
return invalid()
|
|
}
|
|
if a.PositionMs != nil && (*a.PositionMs < 0 || *a.PositionMs > 86400000) {
|
|
return invalid()
|
|
}
|
|
if a.Context != "other" && a.Context != "wave" && a.Context != "playlist" {
|
|
return invalid()
|
|
}
|
|
if a.Playing && (a.TrackId == nil || *a.TrackId == uuid.Nil) {
|
|
return invalid()
|
|
}
|
|
if a.Context == "playlist" && (a.PlaylistId == nil || *a.PlaylistId == uuid.Nil) {
|
|
return invalid()
|
|
}
|
|
if a.Context != "playlist" && a.PlaylistId != nil {
|
|
return invalid()
|
|
}
|
|
return nil
|
|
}
|
|
func (h *Handler) SetFriendsRecommendations(ctx context.Context, r gen.SetFriendsRecommendationsRequestObject) (gen.SetFriendsRecommendationsResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body == nil {
|
|
return nil, invalid()
|
|
}
|
|
_, e = h.store.pool.Exec(ctx, `UPDATE users SET friends_recommendations=$2 WHERE id=$1`, id, r.Body.Enabled)
|
|
return gen.SetFriendsRecommendations204Response{}, e
|
|
}
|
|
func (h *Handler) PublishListeningActivity(ctx context.Context, r gen.PublishListeningActivityRequestObject) (gen.PublishListeningActivityResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body == nil {
|
|
return nil, invalid()
|
|
}
|
|
if e = validateActivity(*r.Body); e != nil {
|
|
return nil, e
|
|
}
|
|
if e = h.store.publishActivity(ctx, id, *r.Body); e != nil {
|
|
return nil, e
|
|
}
|
|
return gen.PublishListeningActivity204Response{}, nil
|
|
}
|
|
func (h *Handler) SetActivityVisibility(ctx context.Context, r gen.SetActivityVisibilityRequestObject) (gen.SetActivityVisibilityResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if r.Body == nil {
|
|
return nil, invalid()
|
|
}
|
|
v := r.Body.Visibility
|
|
if v != "nobody" && v != "friends" && v != "everyone" {
|
|
return nil, invalid()
|
|
}
|
|
tx, e := h.store.pool.Begin(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if _, e = tx.Exec(ctx, `UPDATE users SET activity_visibility=$2 WHERE id=$1`, id, v); e != nil {
|
|
return nil, e
|
|
}
|
|
if v == "nobody" {
|
|
if _, e = tx.Exec(ctx, `DELETE FROM listening_activity WHERE user_id=$1`, id); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
if e = tx.Commit(ctx); e != nil {
|
|
return nil, e
|
|
}
|
|
return gen.SetActivityVisibility204Response{}, nil
|
|
}
|
|
func (h *Handler) GetActivityPlaylist(ctx context.Context, r gen.GetActivityPlaylistRequestObject) (gen.GetActivityPlaylistResponseObject, error) {
|
|
id, e := userID(ctx)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
activity, e := h.store.activities(ctx, id, []uuid.UUID{r.UserId})
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
a := activity[r.UserId]
|
|
if a == nil || a.PlaylistId == nil {
|
|
return nil, apperr.NotFound("social.activity_unavailable", "activity unavailable")
|
|
}
|
|
p, e := h.playlists.ReadSocialPlaylist(ctx, *a.PlaylistId)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
// Recheck after projecting the playlist: a concurrent privacy change must close the view.
|
|
current, e := h.store.activities(ctx, id, []uuid.UUID{r.UserId})
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
now := current[r.UserId]
|
|
if now == nil || now.PlaylistId == nil || *now.PlaylistId != *a.PlaylistId {
|
|
return nil, apperr.NotFound("social.activity_unavailable", "activity unavailable")
|
|
}
|
|
return gen.GetActivityPlaylist200JSONResponse(p), nil
|
|
}
|
|
|
|
func (s *Store) publishActivity(ctx context.Context, id uuid.UUID, a gen.ListeningActivityInput) error {
|
|
tx, e := s.pool.Begin(ctx)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
var visibility string
|
|
if e = tx.QueryRow(ctx, `SELECT activity_visibility FROM users WHERE id=$1 FOR UPDATE`, id).Scan(&visibility); e != nil {
|
|
return e
|
|
}
|
|
if visibility == "nobody" {
|
|
return tx.Commit(ctx)
|
|
}
|
|
var track, playlist *uuid.UUID
|
|
kind := "other"
|
|
if a.Playing {
|
|
var exists bool
|
|
if e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM tracks WHERE id=$1)`, a.TrackId).Scan(&exists); e != nil {
|
|
return e
|
|
}
|
|
if !exists {
|
|
return invalid()
|
|
}
|
|
track = a.TrackId
|
|
kind = string(a.Context)
|
|
if kind == "playlist" {
|
|
if e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM playlists WHERE id=$2 AND (user_id=$1 OR share_id IS NOT NULL))`, id, a.PlaylistId).Scan(&exists); e != nil {
|
|
return e
|
|
}
|
|
if exists {
|
|
playlist = a.PlaylistId
|
|
} else {
|
|
kind = "other"
|
|
}
|
|
}
|
|
}
|
|
position := 0
|
|
if a.PositionMs != nil {
|
|
position = *a.PositionMs
|
|
}
|
|
_, e = tx.Exec(ctx, `INSERT INTO listening_activity(user_id,device_id,sequence,track_id,context,playlist_id,position_ms) VALUES($1,$2,$3,$4,$5,$6,$7)
|
|
ON CONFLICT(user_id,device_id) DO UPDATE SET sequence=EXCLUDED.sequence,track_id=EXCLUDED.track_id,context=EXCLUDED.context,playlist_id=EXCLUDED.playlist_id,position_ms=EXCLUDED.position_ms,updated_at=now()
|
|
WHERE EXCLUDED.sequence>listening_activity.sequence`, id, a.DeviceId, a.Sequence, track, kind, playlist, position)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if _, e = tx.Exec(ctx, `DELETE FROM listening_activity WHERE user_id=$1 AND updated_at<now()-interval '1 day'`, id); e != nil {
|
|
return e
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
func (s *Store) activities(ctx context.Context, viewer uuid.UUID, ids []uuid.UUID) (map[uuid.UUID]*gen.ListeningActivity, error) {
|
|
out := map[uuid.UUID]*gen.ListeningActivity{}
|
|
if len(ids) == 0 {
|
|
return out, nil
|
|
}
|
|
rows, e := s.pool.Query(ctx, `SELECT u.id,t.id,t.title,
|
|
COALESCE((SELECT string_agg(ar.name,', ' ORDER BY ta.position) FROM track_artists ta JOIN artists ar ON ar.id=ta.artist_id WHERE ta.track_id=t.id AND ta.role='main'),''),
|
|
CASE WHEN a.context='playlist' AND p.id IS NULL THEN 'other' ELSE a.context END,p.id,p.name
|
|
FROM users u JOIN LATERAL(SELECT * FROM listening_activity la WHERE la.user_id=u.id AND la.track_id IS NOT NULL AND la.updated_at>now()-interval '90 seconds' ORDER BY la.updated_at DESC,la.device_id LIMIT 1) a ON true
|
|
JOIN tracks t ON t.id=a.track_id
|
|
LEFT JOIN playlists p ON p.id=a.playlist_id AND (p.user_id=u.id OR p.share_id IS NOT NULL)
|
|
WHERE u.id=ANY($2::uuid[]) AND NOT u.is_blocked
|
|
AND (u.id=$1 OR u.activity_visibility='everyone' OR (u.activity_visibility='friends' AND EXISTS(SELECT 1 FROM friendships f WHERE f.low_id=LEAST($1::uuid,u.id) AND f.high_id=GREATEST($1::uuid,u.id) AND f.accepted)))
|
|
AND NOT EXISTS(SELECT 1 FROM social_blocks b WHERE (b.user_id=$1 AND b.peer_id=u.id) OR (b.user_id=u.id AND b.peer_id=$1))`, viewer, ids)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id uuid.UUID
|
|
var a gen.ListeningActivity
|
|
if e = rows.Scan(&id, &a.TrackId, &a.TrackTitle, &a.Artist, &a.Context, &a.PlaylistId, &a.PlaylistName); e != nil {
|
|
return nil, e
|
|
}
|
|
out[id] = &a
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
func (s *Store) attachActivity(ctx context.Context, viewer uuid.UUID, people []gen.SocialPerson) error {
|
|
ids := make([]uuid.UUID, 0, len(people))
|
|
for _, p := range people {
|
|
ids = append(ids, p.Id)
|
|
}
|
|
activities, e := s.activities(ctx, viewer, ids)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for i := range people {
|
|
people[i].Activity = activities[people[i].Id]
|
|
}
|
|
return nil
|
|
}
|