feat(social): add listening activity, shared rooms and modern chat
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

This commit is contained in:
mrleo1nid committed 2026-10-23 10:45:40 +03:00
1 parent 0a05bfe2cd
commit 11a16cd023
59 files changed
+5022 -542

No files matched your search

+296 -2
View File
@@ -22,6 +22,118 @@ tags:
security:
- bearerAuth: []
paths:
/api/v1/me/social/recommendations:
put:
operationId: setFriendsRecommendations
tags: [me]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled: {type: boolean}
responses:
'204': {description: Saved}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/listening:
get:
operationId: getListeningState
tags: [me]
responses:
'200':
description: Listening preferences, current room and pending requests
content:
application/json:
schema: {$ref: '#/components/schemas/ListeningState'}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/listening/preferences:
put:
operationId: setListeningPreferences
tags: [me]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ListeningPreferences'}
responses:
'204': {description: Saved}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/listening/actions:
post:
operationId: listeningAction
tags: [me]
description: Join requests target the host; invites target a guest and use the sender's own stream. Accept/decline/cancel target a request UUID. Leave/end target the room UUID. Automatic acceptance always respects the receiving user's admission policy. No control transfer.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ListeningAction'}
responses:
'204': {description: Applied}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/listening/state:
put:
operationId: publishListeningState
tags: [me]
description: Only the room owner's source device may publish. Monotonic sequence prevents stale updates; room expires after 30 seconds without host heartbeat.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ListeningRoomInput'}
responses:
'204': {description: Applied}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/activity:
put:
operationId: publishListeningActivity
tags: [me]
description: Ephemeral playback lease, expires after 90 seconds. Ignored when visibility is nobody. Sequence is monotonic per device; paused updates clear only that device. No offline replay.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ListeningActivityInput'}
responses:
'204': {description: Applied}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/activity/visibility:
put:
operationId: setActivityVisibility
tags: [me]
description: Applies to all devices. Nobody clears all current activity immediately.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [visibility]
properties:
visibility: {$ref: '#/components/schemas/ActivityVisibility'}
responses:
'204': {description: Saved}
default: {$ref: '#/components/responses/Problem'}
/api/v1/social/activity/{userId}/playlist:
get:
operationId: getActivityPlaylist
tags: [me]
description: Read-only current playlist subject to live visibility, friendship, blocking and expiry. Does not create or reveal a permanent share token.
parameters:
- in: path
name: userId
required: true
schema: {type: string, format: uuid}
responses:
'200':
description: Read-only playlist
content:
application/json:
schema: {$ref: '#/components/schemas/Playlist'}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social:
get:
operationId: getSocial
@@ -79,6 +191,9 @@ paths:
- in: query
name: before
schema: {type: integer, format: int64, minimum: 1}
- in: query
name: q
schema: {type: string, maxLength: 100}
responses:
'200':
description: Message page
@@ -104,6 +219,69 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/DirectMessage'}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/chats/{peerId}/meta:
parameters:
- in: path
name: peerId
required: true
schema: {type: string, format: uuid}
get:
operationId: getChatMeta
tags: [me]
responses:
'200':
description: Ephemeral typing indicator, friends only
content:
application/json:
schema:
type: object
required: [typing]
properties:
typing: {type: boolean}
default: {$ref: '#/components/responses/Problem'}
put:
operationId: setChatTyping
tags: [me]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [typing]
properties:
typing: {type: boolean}
responses:
'204': {description: Applied, lease expires after 6 seconds}
default: {$ref: '#/components/responses/Problem'}
/api/v1/me/social/chats/{peerId}/messages/{messageId}:
patch:
operationId: updateChatMessage
tags: [me]
description: Friends only. Edit/delete are sender-only. Delete removes content for both participants; react toggles one allowed emoji. Reply targets must belong to the same conversation.
parameters:
- in: path
name: peerId
required: true
schema: {type: string, format: uuid}
- in: path
name: messageId
required: true
schema: {type: string, format: uuid}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [action]
properties:
action: {type: string, enum: [edit, delete, react]}
text: {type: string, maxLength: 4000}
emoji: {type: string, enum: ['❤️', '👍', '🔥', '😂', '🎵', '👏']}
responses:
'204': {description: Applied}
default: {$ref: '#/components/responses/Problem'}
/api/v1/admin/server/updates:
get:
operationId: getServerUpdates
@@ -2076,6 +2254,103 @@ components:
schema:
$ref: "#/components/schemas/Problem"
schemas:
ListeningPreferences:
type: object
required: [requests, auto_accept, autoplay]
properties:
requests: {$ref: '#/components/schemas/ActivityVisibility'}
auto_accept: {$ref: '#/components/schemas/ActivityVisibility'}
autoplay: {type: boolean, default: true}
ListeningAction:
type: object
required: [id, action, target_id]
properties:
id: {type: string, format: uuid}
action: {type: string, enum: [join, invite, accept, decline, cancel, leave, end]}
target_id: {type: string, format: uuid}
device_id: {type: string, format: uuid}
track_id: {type: string, format: uuid}
position_ms: {type: integer, minimum: 0}
playing: {type: boolean}
ListeningRoomInput:
type: object
required: [room_id, device_id, sequence, playing, position_ms]
properties:
room_id: {type: string, format: uuid}
device_id: {type: string, format: uuid}
sequence: {type: integer, format: int64, minimum: 1}
track_id: {type: string, format: uuid}
playing: {type: boolean}
position_ms: {type: integer, minimum: 0}
ListeningRoom:
type: object
required: [id, owner_id, owner_name, device_id, track_id, position_ms, playing, updated_at, participants]
properties:
id: {type: string, format: uuid}
owner_id: {type: string, format: uuid}
owner_name: {type: string}
device_id: {type: string, format: uuid}
track_id: {type: string, format: uuid, nullable: true}
position_ms: {type: integer}
playing: {type: boolean}
updated_at: {type: string, format: date-time}
participants:
type: array
items:
type: object
required: [id, name]
properties:
id: {type: string, format: uuid}
name: {type: string}
ListeningRequest:
type: object
required: [id, sender_id, sender_name, recipient_id, owner_id, kind, expires_at]
properties:
id: {type: string, format: uuid}
sender_id: {type: string, format: uuid}
sender_name: {type: string}
recipient_id: {type: string, format: uuid}
owner_id: {type: string, format: uuid}
kind: {type: string, enum: [join, invite]}
expires_at: {type: string, format: date-time}
ListeningState:
type: object
required: [preferences, room, requests, server_time]
properties:
preferences: {$ref: '#/components/schemas/ListeningPreferences'}
room:
nullable: true
allOf:
- {$ref: '#/components/schemas/ListeningRoom'}
requests:
type: array
items: {$ref: '#/components/schemas/ListeningRequest'}
server_time: {type: string, format: date-time}
ActivityVisibility:
type: string
enum: [nobody, friends, everyone]
default: nobody
ListeningActivityInput:
type: object
required: [device_id, sequence, playing, context]
properties:
device_id: {type: string, format: uuid}
sequence: {type: integer, format: int64, minimum: 1}
playing: {type: boolean}
position_ms: {type: integer, minimum: 0, default: 0}
track_id: {type: string, format: uuid}
context: {type: string, enum: [other, wave, playlist]}
playlist_id: {type: string, format: uuid}
ListeningActivity:
type: object
required: [track_id, track_title, artist, context]
properties:
track_id: {type: string, format: uuid}
track_title: {type: string}
artist: {type: string}
context: {type: string, enum: [other, wave, playlist]}
playlist_id: {type: string, format: uuid}
playlist_name: {type: string}
SocialPerson:
type: object
required: [id, username, display_name, relationship, unread]
@@ -2085,6 +2360,7 @@ components:
display_name: {type: string}
relationship: {type: string, enum: [none, incoming, outgoing, friend, blocked]}
unread: {type: integer}
activity: {$ref: '#/components/schemas/ListeningActivity'}
SocialNotification:
type: object
required: [id, actor_id, actor_name, kind, created_at, read]
@@ -2092,13 +2368,15 @@ components:
id: {type: string, format: uuid}
actor_id: {type: string, format: uuid}
actor_name: {type: string}
kind: {type: string, enum: [friend_request, friend_accepted, message, music]}
kind: {type: string, enum: [friend_request, friend_accepted, message, music, listening_request, listening_invite, listening_accepted]}
created_at: {type: string, format: date-time}
read: {type: boolean}
SocialInbox:
type: object
required: [people, notifications, unread]
required: [people, notifications, unread, activity_visibility, friends_recommendations]
properties:
friends_recommendations: {type: boolean, default: true}
activity_visibility: {$ref: '#/components/schemas/ActivityVisibility'}
people:
type: array
items: {$ref: '#/components/schemas/SocialPerson'}
@@ -2121,6 +2399,7 @@ components:
text: {type: string, maxLength: 4000}
attachment_kind: {type: string, enum: [none, track, playlist]}
attachment_id: {type: string, format: uuid}
reply_to: {type: string, format: uuid}
DirectMessage:
type: object
required: [id, seq, sender_id, text, attachment_kind, created_at, read]
@@ -2133,6 +2412,21 @@ components:
attachment_id: {type: string, format: uuid}
created_at: {type: string, format: date-time}
read: {type: boolean}
reply_to: {type: string, format: uuid}
reply_preview: {type: string}
edited_at: {type: string, format: date-time}
deleted: {type: boolean}
reactions:
type: array
items:
$ref: '#/components/schemas/MessageReaction'
MessageReaction:
type: object
required: [emoji, count, mine]
properties:
emoji: {type: string}
count: {type: integer}
mine: {type: boolean}
ServerSource:
type: object
required: [id, name, url]