fix(social): complete shared listening request lifecycle

This commit is contained in:
mrleo1nid committed 2026-10-24 02:16:00 +03:00
1 parent cb22f15912
commit 59983ed1b3
14 files changed
+302 -18

No files matched your search

@@ -85,6 +85,18 @@ func TestActivityRoomsAndChatPrivacy(t *testing.T) {
require.NoError(t, s.publishActivity(ctx, owner, activity))
join := gen.ListeningAction{Id: uuid.New(), Action: "join", TargetId: owner}
require.Error(t, s.listeningAction(ctx, friend, join)) // default nobody
_, e = pool.Exec(ctx, `UPDATE users SET listening_requests='friends',listening_auto_accept='nobody' WHERE id=$1`, owner)
require.NoError(t, e)
require.NoError(t, s.listeningAction(ctx, friend, join))
pendingJoin, e := s.listeningState(ctx, friend)
require.NoError(t, e)
require.Nil(t, pendingJoin.Room, "sending a request is not admission")
require.Len(t, pendingJoin.Requests, 1)
pendingHost, e := s.listeningState(ctx, owner)
require.NoError(t, e)
require.Len(t, pendingHost.Room.Participants, 1)
require.NoError(t, s.listeningAction(ctx, friend, gen.ListeningAction{Id: uuid.New(), Action: "cancel", TargetId: join.Id}))
join.Id = uuid.New()
_, e = pool.Exec(ctx, `UPDATE users SET listening_requests='friends',listening_auto_accept='everyone' WHERE id=$1`, owner)
require.NoError(t, e)
require.Error(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "join", TargetId: owner}))
@@ -94,6 +106,12 @@ func TestActivityRoomsAndChatPrivacy(t *testing.T) {
require.NotNil(t, state.Room)
require.Equal(t, owner, state.Room.OwnerId)
require.Len(t, state.Room.Participants, 2)
// Reopening the dialog must not create another pending request for an
// already admitted listener, even with a new action ID.
require.NoError(t, s.listeningAction(ctx, friend, gen.ListeningAction{Id: uuid.New(), Action: "join", TargetId: owner}))
state, e = s.listeningState(ctx, friend)
require.NoError(t, e)
require.Empty(t, state.Requests)
hostctx := auth.WithPrincipal(ctx, auth.Principal{UserID: owner})
badctx := auth.WithPrincipal(ctx, auth.Principal{UserID: friend})
input := gen.ListeningRoomInput{RoomId: state.Room.Id, DeviceId: device, Sequence: 1, Playing: false, TrackId: &track, PositionMs: 3000}
@@ -113,11 +131,50 @@ func TestActivityRoomsAndChatPrivacy(t *testing.T) {
require.NoError(t, e)
require.Len(t, state.Requests, 1)
require.Nil(t, state.Room)
reserved, e := s.listeningState(ctx, owner)
require.NoError(t, e)
require.NotNil(t, reserved.Room)
require.Len(t, reserved.Room.Participants, 1, "pending invite must not admit the guest")
require.NoError(t, s.listeningAction(ctx, owner, invite), "network retry is idempotent")
duplicate := invite
duplicate.Id = uuid.New()
require.ErrorContains(t, s.listeningAction(ctx, owner, duplicate), "listening_pending")
require.Error(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "cancel", TargetId: invite.Id}))
require.NoError(t, s.listeningAction(ctx, owner, gen.ListeningAction{Id: uuid.New(), Action: "cancel", TargetId: invite.Id}))
state, e = s.listeningState(ctx, other)
require.NoError(t, e)
require.Empty(t, state.Requests)
require.Nil(t, state.Room)
invite.Id = uuid.New()
require.NoError(t, s.listeningAction(ctx, owner, invite))
_, e = pool.Exec(ctx, `UPDATE listening_requests SET expires_at=now()-interval '1 second' WHERE id=$1`, invite.Id)
require.NoError(t, e)
require.ErrorContains(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "accept", TargetId: invite.Id}), "listening_expired")
state, e = s.listeningState(ctx, other)
require.NoError(t, e)
require.Empty(t, state.Requests)
require.Nil(t, state.Room)
invite.Id = uuid.New()
require.NoError(t, s.listeningAction(ctx, owner, invite))
require.NoError(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "decline", TargetId: invite.Id}))
state, e = s.listeningState(ctx, other)
require.NoError(t, e)
require.Empty(t, state.Requests)
require.Nil(t, state.Room)
invite.Id = uuid.New()
require.NoError(t, s.listeningAction(ctx, owner, invite))
_, e = pool.Exec(ctx, `UPDATE users SET listening_requests='nobody' WHERE id=$1`, other)
require.NoError(t, e)
require.ErrorContains(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "accept", TargetId: invite.Id}), "listening_unavailable")
_, e = pool.Exec(ctx, `UPDATE users SET listening_requests='everyone' WHERE id=$1`, other)
require.NoError(t, e)
require.Error(t, s.listeningAction(ctx, friend, gen.ListeningAction{Id: uuid.New(), Action: "accept", TargetId: invite.Id}))
require.NoError(t, s.listeningAction(ctx, other, gen.ListeningAction{Id: uuid.New(), Action: "accept", TargetId: invite.Id}))
state, e = s.listeningState(ctx, other)
require.NoError(t, e)
require.Equal(t, owner, state.Room.OwnerId)
require.Len(t, state.Room.Participants, 2)
require.ErrorContains(t, s.listeningAction(ctx, friend, gen.ListeningAction{Id: uuid.New(), Action: "invite", TargetId: other, DeviceId: &device, TrackId: &track}), "listening_busy")
_, e = pool.Exec(ctx, `UPDATE listening_rooms SET updated_at=now()-interval '31 seconds' WHERE id=$1`, state.Room.Id)
require.NoError(t, e)
state, e = s.listeningState(ctx, other)
+16 -6
View File
@@ -17,6 +17,9 @@ func audienceAllows(policy string, friends bool) bool {
func validAudience(v gen.ActivityVisibility) bool {
return v == "nobody" || v == "friends" || v == "everyone"
}
func listeningDenied() error {
return apperr.Forbidden("social.listening_unavailable", "shared listening is not permitted")
}
func (h *Handler) SetListeningPreferences(ctx context.Context, r gen.SetListeningPreferencesRequestObject) (gen.SetListeningPreferencesResponseObject, error) {
id, e := userID(ctx)
if e != nil {
@@ -75,7 +78,7 @@ func (h *Handler) PublishListeningState(ctx context.Context, r gen.PublishListen
return nil, e
}
if !own {
return nil, denied()
return nil, listeningDenied()
}
}
return gen.PublishListeningState204Response{}, nil
@@ -147,7 +150,7 @@ func listeningPolicy(ctx context.Context, tx pgx.Tx, recipient, sender uuid.UUID
COALESCE(social_seen_at>now()-interval '45 seconds',false) AND EXISTS(SELECT 1 FROM users WHERE id=$2 AND social_seen_at>now()-interval '45 seconds')
FROM users WHERE id=$1`, recipient, sender).Scan(&policy, &automatic, &friends, &blocked, &online)
if errors.Is(e, pgx.ErrNoRows) {
return false, false, denied()
return false, false, listeningDenied()
}
if e != nil {
return false, false, e
@@ -164,7 +167,7 @@ func admitListening(ctx context.Context, tx pgx.Tx, request, room, guest, recipi
return e
}
if !allowed {
return denied()
return listeningDenied()
}
result, e := tx.Exec(ctx, `INSERT INTO listening_members(user_id,room_id) VALUES($1,$2) ON CONFLICT(user_id) DO UPDATE SET last_seen=now() WHERE listening_members.room_id=EXCLUDED.room_id`, guest, room)
if e != nil {
@@ -199,7 +202,7 @@ func (s *Store) listeningAction(ctx context.Context, id uuid.UUID, a gen.Listeni
return e
}
if a.Action == "end" && owner != id {
return denied()
return listeningDenied()
}
if owner == id {
_, e = tx.Exec(ctx, `DELETE FROM listening_rooms WHERE id=$1`, a.TargetId)
@@ -217,7 +220,7 @@ func (s *Store) listeningAction(ctx context.Context, id uuid.UUID, a gen.Listeni
return e
}
if (a.Action == "cancel" && sender != id) || (a.Action != "cancel" && recipient != id) {
return denied()
return listeningDenied()
}
if status != "pending" {
return nil
@@ -257,7 +260,7 @@ func (s *Store) listeningAction(ctx context.Context, id uuid.UUID, a gen.Listeni
return err
}
if !permitted {
return denied()
return listeningDenied()
}
var repeated bool
if e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM listening_requests WHERE id=$1 AND sender_id=$2 AND owner_id=$3 AND guest_id=$4 AND kind=$5)`, a.Id, id, owner, guest, a.Action).Scan(&repeated); e != nil {
@@ -273,6 +276,13 @@ func (s *Store) listeningAction(ctx context.Context, id uuid.UUID, a gen.Listeni
if busy {
return apperr.Conflict("social.listening_busy", "already in another room")
}
var alreadyJoined bool
if e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM listening_members m JOIN listening_rooms r ON r.id=m.room_id WHERE m.user_id=$1 AND r.owner_id=$2)`, guest, owner).Scan(&alreadyJoined); e != nil {
return e
}
if alreadyJoined {
return tx.Commit(ctx)
}
var recent int
if e = tx.QueryRow(ctx, `SELECT count(*) FROM listening_requests WHERE sender_id=$1 AND created_at>now()-interval '1 minute'`, id).Scan(&recent); e != nil {
return e