Normalize audio waveform peaks for Flutter
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
121
docs/artist-dashboard-api.md
Normal file
121
docs/artist-dashboard-api.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Artist Dashboard API
|
||||
|
||||
This endpoint powers the Flutter artist profile dashboard for the currently authenticated user.
|
||||
|
||||
## Endpoint
|
||||
|
||||
`GET /api/v1/users/me/dashboard`
|
||||
|
||||
## Headers
|
||||
|
||||
```http
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This endpoint is private to the authenticated user.
|
||||
- It does not accept a `userId` parameter.
|
||||
- It does not expose another user's private analytics.
|
||||
- It keeps the existing `GET /api/v1/users/:id/profile-overview` endpoint unchanged.
|
||||
- Earnings, audience non-followers, and chart data are not faked. They return safe empty/default values until dedicated analytics storage exists.
|
||||
|
||||
## Success Response Example
|
||||
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"_id": "665f00000000000000000001",
|
||||
"coverImage": "",
|
||||
"avatar": "",
|
||||
"name": "Artist Name",
|
||||
"stageName": "Stage Name",
|
||||
"bio": "Short artist bio",
|
||||
"isVerified": false
|
||||
},
|
||||
"stats": {
|
||||
"followersCount": 120,
|
||||
"followingCount": 45,
|
||||
"postsCount": 12,
|
||||
"collaborationsCount": 2,
|
||||
"viewCount": 3000,
|
||||
"playCount": 980,
|
||||
"listenCount": 980,
|
||||
"likesCount": 220,
|
||||
"commentsCount": 40,
|
||||
"savesCount": 18,
|
||||
"sharesCount": 11,
|
||||
"engagementRate": 7.26,
|
||||
"scorePercentage": 0,
|
||||
"earnings": 0
|
||||
},
|
||||
"audience": {
|
||||
"followers": 120,
|
||||
"nonFollowers": 0,
|
||||
"newFollowersThisWeek": 4,
|
||||
"newFollowersThisMonth": 19
|
||||
},
|
||||
"engagementChart": [],
|
||||
"topContent": [
|
||||
{
|
||||
"_id": "665f00000000000000000002",
|
||||
"postType": "audio",
|
||||
"thumbnailUrl": "",
|
||||
"displayUrl": "",
|
||||
"content": "New track",
|
||||
"viewCount": 1000,
|
||||
"playCount": 600,
|
||||
"likesCount": 90,
|
||||
"commentsCount": 10,
|
||||
"savesCount": 8,
|
||||
"engagementScore": 113,
|
||||
"createdAt": "2026-06-08T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"recentActivity": [],
|
||||
"meta": {
|
||||
"generatedAt": "2026-06-08T00:00:00.000Z",
|
||||
"chartAvailable": false,
|
||||
"earningsAvailable": false,
|
||||
"audienceAnalyticsAvailable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Flutter Model Suggestion
|
||||
|
||||
- `ArtistDashboard`
|
||||
- `ArtistDashboardProfile`
|
||||
- `ArtistDashboardStats`
|
||||
- `ArtistDashboardAudience`
|
||||
- `ArtistDashboardTopContent`
|
||||
- `ArtistDashboardRecentActivity`
|
||||
- `ArtistDashboardMeta`
|
||||
|
||||
## UI Sections
|
||||
|
||||
1. Profile header: use `profile.coverImage`, `profile.avatar`, `profile.name`, `profile.stageName`, `profile.bio`, `profile.isVerified`.
|
||||
2. Stats cards: use `stats.followersCount`, `stats.followingCount`, `stats.viewCount`, `stats.listenCount`, `stats.engagementRate`.
|
||||
3. Engagement chart: render only when `meta.chartAvailable === true`; otherwise show the empty state.
|
||||
4. Top content list: use `topContent`.
|
||||
5. Audience summary: use `audience.followers`, `audience.nonFollowers`, `audience.newFollowersThisWeek`, `audience.newFollowersThisMonth`.
|
||||
6. Recent activity: use `recentActivity`.
|
||||
7. Empty states: show clear UI for no posts, no analytics, chart unavailable, and earnings unavailable.
|
||||
|
||||
## Empty States
|
||||
|
||||
- No posts yet: `stats.postsCount === 0`.
|
||||
- No analytics yet: `stats.viewCount === 0 && stats.playCount === 0`.
|
||||
- Chart not available: `meta.chartAvailable === false`.
|
||||
- Earnings not available: `meta.earningsAvailable === false`.
|
||||
|
||||
## Future Analytics Upgrade
|
||||
|
||||
Daily chart data requires event-level or snapshot storage. Recommended future collections:
|
||||
|
||||
- `post_view_events`
|
||||
- `post_play_events`
|
||||
- `post_engagement_events`
|
||||
- `daily_artist_analytics`
|
||||
|
||||
Audience split requires viewer identity tracking per view/play event. Earnings requires a monetization/revenue module. Until those exist, the backend intentionally returns safe defaults instead of fake values.
|
||||
@@ -5599,6 +5599,382 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Realtime Socket.IO",
|
||||
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||
"item": [
|
||||
{
|
||||
"name": "Connection - Chat Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Connection - Notifications Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit join_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit leave_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit send_message",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit typing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_delivered",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_seen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen joined_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen left_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen new_message",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_delivered",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_seen",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen presence",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen socket_error",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:new",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:unread_counts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Legacy Notification Events",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feed",
|
||||
"item": [
|
||||
@@ -9782,6 +10158,22 @@
|
||||
{
|
||||
"key": "notificationCategory",
|
||||
"value": "interactions"
|
||||
},
|
||||
{
|
||||
"key": "socketBaseUrl",
|
||||
"value": "http://localhost:4000"
|
||||
},
|
||||
{
|
||||
"key": "chatSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/chat"
|
||||
},
|
||||
{
|
||||
"key": "notificationsSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/notifications"
|
||||
},
|
||||
{
|
||||
"key": "socketAccessToken",
|
||||
"value": "{{accessToken}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4684,6 +4684,382 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Realtime Socket.IO",
|
||||
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||
"item": [
|
||||
{
|
||||
"name": "Connection - Chat Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Connection - Notifications Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit join_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit leave_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit send_message",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit typing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_delivered",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_seen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen joined_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen left_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen new_message",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_delivered",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_seen",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen presence",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen socket_error",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:new",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:unread_counts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Legacy Notification Events",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Support Tickets",
|
||||
"item": [
|
||||
@@ -5003,6 +5379,382 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Realtime Socket.IO",
|
||||
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||
"item": [
|
||||
{
|
||||
"name": "Connection - Chat Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Connection - Notifications Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit join_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit leave_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit send_message",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit typing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_delivered",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_seen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen joined_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen left_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen new_message",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_delivered",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_seen",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen presence",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen socket_error",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:new",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:unread_counts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Legacy Notification Events",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
@@ -5576,6 +6328,22 @@
|
||||
{
|
||||
"key": "notificationCategory",
|
||||
"value": "interactions"
|
||||
},
|
||||
{
|
||||
"key": "socketBaseUrl",
|
||||
"value": "http://localhost:4000"
|
||||
},
|
||||
{
|
||||
"key": "chatSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/chat"
|
||||
},
|
||||
{
|
||||
"key": "notificationsSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/notifications"
|
||||
},
|
||||
{
|
||||
"key": "socketAccessToken",
|
||||
"value": "{{accessToken}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5025,6 +5025,382 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Realtime Socket.IO",
|
||||
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||
"item": [
|
||||
{
|
||||
"name": "Connection - Chat Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Connection - Notifications Namespace",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit join_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit leave_conversation",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit send_message",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit typing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_delivered",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Emit mark_seen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen joined_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen left_conversation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen new_message",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_delivered",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen message_seen",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen presence",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen socket_error",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{chatSocketUrl}}",
|
||||
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:new",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen notification:unread_counts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Legacy Notification Events",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Socket.IO",
|
||||
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||
},
|
||||
{
|
||||
"key": "Auth",
|
||||
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||
}
|
||||
],
|
||||
"url": "{{notificationsSocketUrl}}",
|
||||
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feed",
|
||||
"item": [
|
||||
@@ -6998,6 +7374,22 @@
|
||||
{
|
||||
"key": "notificationCategory",
|
||||
"value": "interactions"
|
||||
},
|
||||
{
|
||||
"key": "socketBaseUrl",
|
||||
"value": "http://localhost:4000"
|
||||
},
|
||||
{
|
||||
"key": "chatSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/chat"
|
||||
},
|
||||
{
|
||||
"key": "notificationsSocketUrl",
|
||||
"value": "{{socketBaseUrl}}/notifications"
|
||||
},
|
||||
{
|
||||
"key": "socketAccessToken",
|
||||
"value": "{{accessToken}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
76
src/modules/users/dto/artist-dashboard-response.dto.ts
Normal file
76
src/modules/users/dto/artist-dashboard-response.dto.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export type ArtistDashboardProfile = {
|
||||
_id: string;
|
||||
coverImage: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
stageName: string;
|
||||
bio: string;
|
||||
isVerified: boolean;
|
||||
};
|
||||
|
||||
export type ArtistDashboardStats = {
|
||||
followersCount: number;
|
||||
followingCount: number;
|
||||
postsCount: number;
|
||||
collaborationsCount: number;
|
||||
viewCount: number;
|
||||
playCount: number;
|
||||
listenCount: number;
|
||||
likesCount: number;
|
||||
commentsCount: number;
|
||||
savesCount: number;
|
||||
sharesCount: number;
|
||||
engagementRate: number;
|
||||
scorePercentage: number;
|
||||
earnings: number;
|
||||
};
|
||||
|
||||
export type ArtistDashboardAudience = {
|
||||
followers: number;
|
||||
nonFollowers: number;
|
||||
newFollowersThisWeek: number;
|
||||
newFollowersThisMonth: number;
|
||||
};
|
||||
|
||||
export type ArtistDashboardTopContentItem = {
|
||||
_id: string;
|
||||
postType: string;
|
||||
thumbnailUrl: string;
|
||||
displayUrl: string;
|
||||
content: string;
|
||||
viewCount: number;
|
||||
playCount: number;
|
||||
likesCount: number;
|
||||
commentsCount: number;
|
||||
savesCount: number;
|
||||
engagementScore: number;
|
||||
createdAt: string | Date;
|
||||
};
|
||||
|
||||
export type ArtistDashboardRecentActivityItem = {
|
||||
_id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
previewText: string;
|
||||
resourceType: string;
|
||||
referenceId: string | null;
|
||||
deepLink: string;
|
||||
createdAt: string | Date;
|
||||
};
|
||||
|
||||
export type ArtistDashboardMeta = {
|
||||
generatedAt: string;
|
||||
chartAvailable: boolean;
|
||||
earningsAvailable: boolean;
|
||||
audienceAnalyticsAvailable: boolean;
|
||||
};
|
||||
|
||||
export type ArtistDashboardResponse = {
|
||||
profile: ArtistDashboardProfile;
|
||||
stats: ArtistDashboardStats;
|
||||
audience: ArtistDashboardAudience;
|
||||
engagementChart: unknown[];
|
||||
topContent: ArtistDashboardTopContentItem[];
|
||||
recentActivity: ArtistDashboardRecentActivityItem[];
|
||||
meta: ArtistDashboardMeta;
|
||||
};
|
||||
15
src/modules/users/users.controller.spec.ts
Normal file
15
src/modules/users/users.controller.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
describe('UsersController artist dashboard', () => {
|
||||
it('uses the authenticated user id for the private dashboard endpoint', async () => {
|
||||
const usersService = {
|
||||
getMyDashboard: jest.fn().mockResolvedValue({ profile: { _id: 'user-1' } }),
|
||||
};
|
||||
const controller = new UsersController(usersService as any);
|
||||
|
||||
const result = await controller.getMyDashboard({ sub: 'user-1' } as any);
|
||||
|
||||
expect(usersService.getMyDashboard).toHaveBeenCalledWith('user-1');
|
||||
expect(result).toEqual({ profile: { _id: 'user-1' } });
|
||||
});
|
||||
});
|
||||
@@ -281,6 +281,13 @@ export class UsersController {
|
||||
return this.usersService.discoverTalents(query);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('me/dashboard')
|
||||
async getMyDashboard(@CurrentUser() user: JwtPayload) {
|
||||
return this.usersService.getMyDashboard(user.sub);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/profile-overview')
|
||||
|
||||
185
src/modules/users/users.service.spec.ts
Normal file
185
src/modules/users/users.service.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Types } from 'mongoose';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
const chain = <T>(value: T) => ({
|
||||
sort: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
project: jest.fn().mockReturnThis(),
|
||||
toArray: jest.fn().mockResolvedValue(value),
|
||||
});
|
||||
|
||||
const createService = (options: {
|
||||
user?: Record<string, any> | null;
|
||||
postStats?: Record<string, number>;
|
||||
topContent?: Record<string, any>[];
|
||||
collaborationsCount?: number;
|
||||
newFollowersThisWeek?: number;
|
||||
newFollowersThisMonth?: number;
|
||||
recentActivity?: Record<string, any>[];
|
||||
} = {}) => {
|
||||
const userId = new Types.ObjectId().toString();
|
||||
const user = options.user ?? {
|
||||
_id: new Types.ObjectId(userId),
|
||||
name: 'Artist',
|
||||
stageName: 'Stage',
|
||||
bio: 'Bio',
|
||||
avatar: '',
|
||||
coverImage: '',
|
||||
followersCount: 10,
|
||||
followingCount: 4,
|
||||
isVerified: true,
|
||||
isDisabled: false,
|
||||
toObject() {
|
||||
return {
|
||||
_id: userId,
|
||||
name: this.name,
|
||||
stageName: this.stageName,
|
||||
bio: this.bio,
|
||||
avatar: this.avatar,
|
||||
coverImage: this.coverImage,
|
||||
followersCount: this.followersCount,
|
||||
followingCount: this.followingCount,
|
||||
isVerified: this.isVerified,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const postsAggregate = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
toArray: jest.fn().mockResolvedValue(options.postStats ? [options.postStats] : []),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
toArray: jest.fn().mockResolvedValue(options.topContent ?? []),
|
||||
});
|
||||
|
||||
const postsCollection = {
|
||||
aggregate: postsAggregate,
|
||||
countDocuments: jest.fn().mockResolvedValue(options.collaborationsCount ?? 0),
|
||||
};
|
||||
const followsCollection = {
|
||||
countDocuments: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(options.newFollowersThisWeek ?? 0)
|
||||
.mockResolvedValueOnce(options.newFollowersThisMonth ?? 0),
|
||||
};
|
||||
const notificationsCollection = {
|
||||
find: jest.fn().mockReturnValue(chain(options.recentActivity ?? [])),
|
||||
};
|
||||
const connection = {
|
||||
collection: jest.fn((name: string) => {
|
||||
if (name === 'posts') {
|
||||
return postsCollection;
|
||||
}
|
||||
if (name === 'follows') {
|
||||
return followsCollection;
|
||||
}
|
||||
if (name === 'notifications') {
|
||||
return notificationsCollection;
|
||||
}
|
||||
throw new Error(`Unexpected collection ${name}`);
|
||||
}),
|
||||
};
|
||||
const usersRepository = {
|
||||
findById: jest.fn().mockResolvedValue(user),
|
||||
};
|
||||
const service = new UsersService(
|
||||
connection as any,
|
||||
usersRepository as any,
|
||||
{ logSuperAdminAction: jest.fn() } as any,
|
||||
{ get: jest.fn() } as any,
|
||||
{ saveFile: jest.fn(), deleteFile: jest.fn() } as any,
|
||||
);
|
||||
|
||||
return { service, userId, usersRepository, postsCollection, followsCollection };
|
||||
};
|
||||
|
||||
describe('UsersService artist dashboard', () => {
|
||||
it('returns zeros and safe empty arrays for an authenticated user with no posts', async () => {
|
||||
const { service, userId } = createService();
|
||||
|
||||
const result = await service.getMyDashboard(userId);
|
||||
|
||||
expect(result.profile._id).toBe(userId);
|
||||
expect(result.stats.postsCount).toBe(0);
|
||||
expect(result.stats.viewCount).toBe(0);
|
||||
expect(result.stats.playCount).toBe(0);
|
||||
expect(result.stats.listenCount).toBe(0);
|
||||
expect(result.stats.engagementRate).toBe(0);
|
||||
expect(result.stats.earnings).toBe(0);
|
||||
expect(result.stats.scorePercentage).toBe(0);
|
||||
expect(result.audience.followers).toBe(10);
|
||||
expect(result.audience.nonFollowers).toBe(0);
|
||||
expect(result.engagementChart).toEqual([]);
|
||||
expect(result.topContent).toEqual([]);
|
||||
expect(result.meta.chartAvailable).toBe(false);
|
||||
expect(result.meta.earningsAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it('aggregates owned posts and calculates engagement rate safely', async () => {
|
||||
const postId = new Types.ObjectId();
|
||||
const { service, userId } = createService({
|
||||
postStats: {
|
||||
postsCount: 2,
|
||||
viewCount: 100,
|
||||
playCount: 50,
|
||||
likesCount: 12,
|
||||
commentsCount: 3,
|
||||
savesCount: 2,
|
||||
sharesCount: 1,
|
||||
},
|
||||
topContent: [
|
||||
{
|
||||
_id: postId,
|
||||
postType: 'audio',
|
||||
content: 'Track',
|
||||
thumbnailUrl: '',
|
||||
audioUrl: '/uploads/posts/audios/audio.mp3',
|
||||
viewCount: 100,
|
||||
playCount: 50,
|
||||
likesCount: 12,
|
||||
commentsCount: 3,
|
||||
savesCount: 2,
|
||||
engagementScore: 18,
|
||||
createdAt: new Date('2026-06-08T00:00:00.000Z'),
|
||||
},
|
||||
],
|
||||
collaborationsCount: 2,
|
||||
newFollowersThisWeek: 1,
|
||||
newFollowersThisMonth: 3,
|
||||
});
|
||||
|
||||
const result = await service.getMyDashboard(userId);
|
||||
|
||||
expect(result.stats.postsCount).toBe(2);
|
||||
expect(result.stats.viewCount).toBe(100);
|
||||
expect(result.stats.playCount).toBe(50);
|
||||
expect(result.stats.listenCount).toBe(50);
|
||||
expect(result.stats.likesCount).toBe(12);
|
||||
expect(result.stats.commentsCount).toBe(3);
|
||||
expect(result.stats.savesCount).toBe(2);
|
||||
expect(result.stats.sharesCount).toBe(1);
|
||||
expect(result.stats.collaborationsCount).toBe(2);
|
||||
expect(result.stats.engagementRate).toBe(12);
|
||||
expect(result.audience.newFollowersThisWeek).toBe(1);
|
||||
expect(result.audience.newFollowersThisMonth).toBe(3);
|
||||
expect(result.topContent).toHaveLength(1);
|
||||
expect(result.topContent[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
_id: postId.toString(),
|
||||
postType: 'audio',
|
||||
content: 'Track',
|
||||
engagementScore: 18,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid current user ids', async () => {
|
||||
const { service } = createService();
|
||||
|
||||
await expect(service.getMyDashboard('not-object-id')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,12 +13,18 @@ import { ExperienceLevel } from '../../common/enums/experience-level.enum';
|
||||
import { MusicRole } from '../../common/enums/music-role.enum';
|
||||
import { hashValue } from '../../common/utils/hash.util';
|
||||
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
||||
import { resolveManagedFileUrl } from '../../common/utils/public-url.util';
|
||||
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
||||
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { UserRole } from '../../common/enums/user-role.enum';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { AdminDisableUserDto } from './dto/admin-disable-user.dto';
|
||||
import {
|
||||
ArtistDashboardRecentActivityItem,
|
||||
ArtistDashboardResponse,
|
||||
ArtistDashboardTopContentItem,
|
||||
} from './dto/artist-dashboard-response.dto';
|
||||
import { CreateAdminDto } from './dto/create-admin.dto';
|
||||
import { MusicSetupDto } from './dto/music-setup.dto';
|
||||
import { ProfileSetupDto } from './dto/profile-setup.dto';
|
||||
@@ -36,6 +42,16 @@ type UploadedImageFile = {
|
||||
originalname?: string;
|
||||
};
|
||||
|
||||
type UserPostStats = {
|
||||
postsCount: number;
|
||||
viewCount: number;
|
||||
playCount: number;
|
||||
likesCount: number;
|
||||
commentsCount: number;
|
||||
savesCount: number;
|
||||
sharesCount: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@@ -790,6 +806,94 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async getMyDashboard(currentUserId: string): Promise<ArtistDashboardResponse> {
|
||||
if (!Types.ObjectId.isValid(currentUserId)) {
|
||||
throw new BadRequestException('Invalid user id');
|
||||
}
|
||||
|
||||
const user = await this.findByIdOrFail(currentUserId);
|
||||
if (user.isDisabled) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const objectUserId = new Types.ObjectId(currentUserId);
|
||||
const now = new Date();
|
||||
const weekAgo = new Date(now);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
const monthAgo = new Date(now);
|
||||
monthAgo.setDate(monthAgo.getDate() - 30);
|
||||
|
||||
const [postStats, collaborationsCount, topContent, recentActivity, newFollowersThisWeek, newFollowersThisMonth] =
|
||||
await Promise.all([
|
||||
this.aggregateUserPostStats(objectUserId),
|
||||
this.countUserCollaborations(objectUserId),
|
||||
this.findTopDashboardContent(objectUserId, 5),
|
||||
this.findArtistRecentActivity(objectUserId, 10),
|
||||
this.connection.collection('follows').countDocuments({
|
||||
followingId: objectUserId,
|
||||
createdAt: { $gte: weekAgo },
|
||||
}),
|
||||
this.connection.collection('follows').countDocuments({
|
||||
followingId: objectUserId,
|
||||
createdAt: { $gte: monthAgo },
|
||||
}),
|
||||
]);
|
||||
|
||||
const followersCount = this.toSafeNumber(user.followersCount);
|
||||
const followingCount = this.toSafeNumber(user.followingCount);
|
||||
const totalEngagement =
|
||||
postStats.likesCount + postStats.commentsCount + postStats.savesCount + postStats.sharesCount;
|
||||
const totalReach = postStats.viewCount + postStats.playCount;
|
||||
const engagementRate = this.roundPercent((totalEngagement / Math.max(totalReach, 1)) * 100);
|
||||
const userObject = typeof user.toObject === 'function' ? (user.toObject() as Record<string, any>) : (user as any);
|
||||
|
||||
return {
|
||||
profile: {
|
||||
_id: currentUserId,
|
||||
coverImage: userObject.coverImage ?? '',
|
||||
avatar: userObject.avatar ?? '',
|
||||
name: userObject.name ?? '',
|
||||
stageName: userObject.stageName ?? '',
|
||||
bio: userObject.bio ?? '',
|
||||
isVerified: userObject.isVerified ?? false,
|
||||
},
|
||||
stats: {
|
||||
followersCount,
|
||||
followingCount,
|
||||
postsCount: postStats.postsCount,
|
||||
collaborationsCount,
|
||||
viewCount: postStats.viewCount,
|
||||
playCount: postStats.playCount,
|
||||
listenCount: postStats.playCount,
|
||||
likesCount: postStats.likesCount,
|
||||
commentsCount: postStats.commentsCount,
|
||||
savesCount: postStats.savesCount,
|
||||
sharesCount: postStats.sharesCount,
|
||||
engagementRate,
|
||||
// TODO: Replace with real artist score logic when product scoring rules exist.
|
||||
scorePercentage: 0,
|
||||
// TODO: Replace with real monetization calculation when earnings module exists.
|
||||
earnings: 0,
|
||||
},
|
||||
audience: {
|
||||
followers: followersCount,
|
||||
// TODO: Requires viewer-level analytics/events to calculate non-followers.
|
||||
nonFollowers: 0,
|
||||
newFollowersThisWeek,
|
||||
newFollowersThisMonth,
|
||||
},
|
||||
engagementChart: [],
|
||||
topContent,
|
||||
recentActivity,
|
||||
meta: {
|
||||
generatedAt: now.toISOString(),
|
||||
chartAvailable: false,
|
||||
earningsAvailable: false,
|
||||
audienceAnalyticsAvailable: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{
|
||||
items: UserDocument[];
|
||||
page: number;
|
||||
@@ -819,6 +923,193 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
private async aggregateUserPostStats(authorId: Types.ObjectId): Promise<UserPostStats> {
|
||||
const [row] = await this.connection
|
||||
.collection('posts')
|
||||
.aggregate<UserPostStats>([
|
||||
{
|
||||
$match: {
|
||||
authorId,
|
||||
isDeleted: { $ne: true },
|
||||
isArchived: { $ne: true },
|
||||
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
postsCount: { $sum: 1 },
|
||||
viewCount: { $sum: { $ifNull: ['$viewCount', 0] } },
|
||||
playCount: { $sum: { $ifNull: ['$playCount', 0] } },
|
||||
likesCount: { $sum: { $ifNull: ['$likesCount', 0] } },
|
||||
commentsCount: { $sum: { $ifNull: ['$commentsCount', 0] } },
|
||||
savesCount: { $sum: { $ifNull: ['$savesCount', 0] } },
|
||||
sharesCount: { $sum: { $ifNull: ['$shareCount', 0] } },
|
||||
},
|
||||
},
|
||||
{ $project: { _id: 0 } },
|
||||
])
|
||||
.toArray();
|
||||
|
||||
return {
|
||||
postsCount: this.toSafeNumber(row?.postsCount),
|
||||
viewCount: this.toSafeNumber(row?.viewCount),
|
||||
playCount: this.toSafeNumber(row?.playCount),
|
||||
likesCount: this.toSafeNumber(row?.likesCount),
|
||||
commentsCount: this.toSafeNumber(row?.commentsCount),
|
||||
savesCount: this.toSafeNumber(row?.savesCount),
|
||||
sharesCount: this.toSafeNumber(row?.sharesCount),
|
||||
};
|
||||
}
|
||||
|
||||
private async countUserCollaborations(userId: Types.ObjectId): Promise<number> {
|
||||
return this.connection.collection('posts').countDocuments({
|
||||
isDeleted: { $ne: true },
|
||||
isArchived: { $ne: true },
|
||||
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||
$or: [
|
||||
{ authorId: userId, 'taggedUserIds.0': { $exists: true } },
|
||||
{ authorId: { $ne: userId }, taggedUserIds: userId },
|
||||
{ collaboratorIds: userId },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async findTopDashboardContent(
|
||||
authorId: Types.ObjectId,
|
||||
limit: number,
|
||||
): Promise<ArtistDashboardTopContentItem[]> {
|
||||
const rows = await this.connection
|
||||
.collection('posts')
|
||||
.aggregate<Record<string, any>>([
|
||||
{
|
||||
$match: {
|
||||
authorId,
|
||||
isDeleted: { $ne: true },
|
||||
isArchived: { $ne: true },
|
||||
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
engagementScore: {
|
||||
$add: [
|
||||
{ $ifNull: ['$likesCount', 0] },
|
||||
{ $ifNull: ['$commentsCount', 0] },
|
||||
{ $ifNull: ['$savesCount', 0] },
|
||||
{ $ifNull: ['$shareCount', 0] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $sort: { engagementScore: -1, viewCount: -1, playCount: -1, createdAt: -1 } },
|
||||
{ $limit: Math.max(1, limit) },
|
||||
{
|
||||
$project: {
|
||||
postType: 1,
|
||||
thumbnailUrl: 1,
|
||||
imageUrls: 1,
|
||||
imageItems: 1,
|
||||
videoUrl: 1,
|
||||
audioUrl: 1,
|
||||
content: 1,
|
||||
viewCount: 1,
|
||||
playCount: 1,
|
||||
likesCount: 1,
|
||||
commentsCount: 1,
|
||||
savesCount: 1,
|
||||
engagementScore: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray();
|
||||
|
||||
return rows.map((post) => {
|
||||
const thumbnailUrl = this.resolveManagedStringUrl(post.thumbnailUrl);
|
||||
const fallbackDisplayUrl = this.resolveTopContentDisplayUrl(post);
|
||||
return {
|
||||
_id: post._id?.toString?.() ?? '',
|
||||
postType: post.postType ?? '',
|
||||
thumbnailUrl,
|
||||
displayUrl: thumbnailUrl || fallbackDisplayUrl,
|
||||
content: post.content ?? '',
|
||||
viewCount: this.toSafeNumber(post.viewCount),
|
||||
playCount: this.toSafeNumber(post.playCount),
|
||||
likesCount: this.toSafeNumber(post.likesCount),
|
||||
commentsCount: this.toSafeNumber(post.commentsCount),
|
||||
savesCount: this.toSafeNumber(post.savesCount),
|
||||
engagementScore: this.toSafeNumber(post.engagementScore),
|
||||
createdAt: post.createdAt ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async findArtistRecentActivity(
|
||||
userId: Types.ObjectId,
|
||||
limit: number,
|
||||
): Promise<ArtistDashboardRecentActivityItem[]> {
|
||||
const rows = await this.connection
|
||||
.collection('notifications')
|
||||
.find({
|
||||
$or: [{ recipientId: userId }, { actorId: userId }],
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(Math.max(1, limit))
|
||||
.project({
|
||||
type: 1,
|
||||
title: 1,
|
||||
previewText: 1,
|
||||
resourceType: 1,
|
||||
referenceId: 1,
|
||||
deepLink: 1,
|
||||
createdAt: 1,
|
||||
})
|
||||
.toArray();
|
||||
|
||||
return rows.map((activity) => ({
|
||||
_id: activity._id?.toString?.() ?? '',
|
||||
type: activity.type ?? '',
|
||||
title: activity.title ?? '',
|
||||
previewText: activity.previewText ?? '',
|
||||
resourceType: activity.resourceType ?? '',
|
||||
referenceId: activity.referenceId?.toString?.() ?? null,
|
||||
deepLink: activity.deepLink ?? '',
|
||||
createdAt: activity.createdAt ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
private resolveTopContentDisplayUrl(post: Record<string, any>): string {
|
||||
const imageItems = Array.isArray(post.imageItems) ? post.imageItems : [];
|
||||
const imageUrls = Array.isArray(post.imageUrls) ? post.imageUrls : [];
|
||||
const candidate =
|
||||
imageItems.find((item) => typeof item?.url === 'string' && item.url)?.url ??
|
||||
imageUrls.find((url) => typeof url === 'string' && url) ??
|
||||
post.videoUrl ??
|
||||
post.audioUrl ??
|
||||
'';
|
||||
|
||||
return this.resolveManagedStringUrl(candidate);
|
||||
}
|
||||
|
||||
private resolveManagedStringUrl(value: unknown): string {
|
||||
const resolved = resolveManagedFileUrl(typeof value === 'string' ? value : '');
|
||||
return typeof resolved === 'string' ? resolved : '';
|
||||
}
|
||||
|
||||
private toSafeNumber(value: unknown): number {
|
||||
const numeric = Number(value ?? 0);
|
||||
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
||||
}
|
||||
|
||||
private roundPercent(value: number): number {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
private async assertTargetIsAdmin(userId: string): Promise<UserDocument> {
|
||||
const user = await this.findByIdOrFail(userId);
|
||||
if (user.role !== UserRole.ADMIN) {
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم