@devchitchat/chat 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -0
- package/index.js +148 -0
- package/migrate/001-drop-channel-invites.js +3 -0
- package/migrate/002-invite-initial-roles.js +5 -0
- package/migrate/003-dm-channels.js +35 -0
- package/migrate/004-notifications.js +15 -0
- package/migrate/005-uploads.js +21 -0
- package/migrate/006-mention-priority.js +3 -0
- package/migrate/007-push-subscriptions.js +14 -0
- package/migrate/008-messages-channel-seq-index.js +3 -0
- package/migrate/009-message-reactions.js +15 -0
- package/migrate/010-edit-messages.js +7 -0
- package/package.json +51 -0
- package/pages/_error.html +12 -0
- package/pages/_layout.html +31 -0
- package/pages/_layout.js +13 -0
- package/pages/admin/_layout.html +52 -0
- package/pages/admin/_layout.js +8 -0
- package/pages/admin/bots/[userId].js +88 -0
- package/pages/admin/bots/[userId].phtml +89 -0
- package/pages/admin/bots/index.js +41 -0
- package/pages/admin/bots/index.phtml +58 -0
- package/pages/admin/index.js +8 -0
- package/pages/admin/invites/index.js +72 -0
- package/pages/admin/invites/index.phtml +88 -0
- package/pages/admin/users/[userId].js +60 -0
- package/pages/admin/users/[userId].phtml +57 -0
- package/pages/admin/users/index.js +20 -0
- package/pages/admin/users/index.phtml +37 -0
- package/pages/api/uploads/index.js +66 -0
- package/pages/api/user/settings.js +26 -0
- package/pages/auth/signout.js +14 -0
- package/pages/channels/[channelId].js +99 -0
- package/pages/channels/[channelId].phtml +173 -0
- package/pages/index.js +33 -0
- package/pages/invite/[token].js +10 -0
- package/pages/login/index.js +57 -0
- package/pages/login/index.phtml +29 -0
- package/pages/public/client/action-sheet.js +77 -0
- package/pages/public/client/app.js +38 -0
- package/pages/public/client/auth-tabs.js +13 -0
- package/pages/public/client/emoji-data.js +197 -0
- package/pages/public/client/islands/call.js +1770 -0
- package/pages/public/client/islands/sidebar.js +1197 -0
- package/pages/public/client/long-press.js +59 -0
- package/pages/public/client/modal.js +50 -0
- package/pages/public/client/router.js +87 -0
- package/pages/public/client/rtc-peer-manager.js +344 -0
- package/pages/public/client/settings-sync.js +76 -0
- package/pages/public/client/shared/messages.js +147 -0
- package/pages/public/client/swipe-nav.js +98 -0
- package/pages/public/client/theme.js +27 -0
- package/pages/public/client/ws.js +71 -0
- package/pages/public/favicon.ico +0 -0
- package/pages/public/favicon.png +0 -0
- package/pages/public/icon.png +0 -0
- package/pages/public/manifest.json +11 -0
- package/pages/public/sw.js +38 -0
- package/pages/public/themes/base.css +1786 -0
- package/pages/public/themes/dark.css +22 -0
- package/pages/public/themes/forest.css +22 -0
- package/pages/public/themes/light.css +23 -0
- package/pages/public/themes/ocean.css +22 -0
- package/pages/public/themes/rose.css +22 -0
- package/pages/registration/index.js +35 -0
- package/pages/registration/index.phtml +38 -0
- package/pages/uploads/[uploadId]/[filename].js +45 -0
- package/src/adapters/InMemoryAuthRepository.js +74 -0
- package/src/adapters/InMemoryChannelRepository.js +138 -0
- package/src/adapters/InMemoryDeliveryRepository.js +52 -0
- package/src/adapters/InMemoryFileStore.js +53 -0
- package/src/adapters/InMemoryHubRepository.js +85 -0
- package/src/adapters/InMemoryMessageRepository.js +35 -0
- package/src/adapters/InMemoryReactionRepository.js +45 -0
- package/src/adapters/InMemorySearchRepository.js +37 -0
- package/src/adapters/InMemorySignalingRepository.js +35 -0
- package/src/adapters/InMemoryUploadRepository.js +36 -0
- package/src/adapters/InMemoryUserSettingsRepository.js +15 -0
- package/src/adapters/LocalFileStore.js +40 -0
- package/src/adapters/SqliteAuthRepository.js +184 -0
- package/src/adapters/SqliteChannelRepository.js +149 -0
- package/src/adapters/SqliteDeliveryRepository.js +53 -0
- package/src/adapters/SqliteHubRepository.js +99 -0
- package/src/adapters/SqliteMessageRepository.js +90 -0
- package/src/adapters/SqlitePushRepository.js +39 -0
- package/src/adapters/SqliteReactionRepository.js +50 -0
- package/src/adapters/SqliteSearchRepository.js +42 -0
- package/src/adapters/SqliteSignalingRepository.js +50 -0
- package/src/adapters/SqliteUploadRepository.js +34 -0
- package/src/adapters/SqliteUserSettingsRepository.js +23 -0
- package/src/adminAuth.js +25 -0
- package/src/config.js +11 -0
- package/src/context.js +77 -0
- package/src/core/dm.js +10 -0
- package/src/core/mentions.js +27 -0
- package/src/core/messages.js +21 -0
- package/src/core/reactions.js +6 -0
- package/src/core/roles.js +5 -0
- package/src/core/uploads.js +107 -0
- package/src/db/initDb.js +225 -0
- package/src/db/openDb.js +18 -0
- package/src/db/runMigrations.js +45 -0
- package/src/db/transaction.js +11 -0
- package/src/ports/IFileStore.js +34 -0
- package/src/services/AuthService.js +208 -0
- package/src/services/BotService.js +148 -0
- package/src/services/ChannelService.js +176 -0
- package/src/services/DeliveryService.js +28 -0
- package/src/services/HubService.js +133 -0
- package/src/services/MessageService.js +122 -0
- package/src/services/NotificationService.js +45 -0
- package/src/services/PresenceService.js +55 -0
- package/src/services/ReactionService.js +57 -0
- package/src/services/SearchService.js +21 -0
- package/src/services/SignalingService.js +177 -0
- package/src/services/UploadService.js +111 -0
- package/src/services/UserSettingsService.js +30 -0
- package/src/services/WebPushService.js +217 -0
- package/src/util/crypto.js +21 -0
- package/src/util/errors.js +14 -0
- package/src/util/ids.js +3 -0
- package/src/util/logger.js +21 -0
- package/src/ws/ChatServer.js +478 -0
- package/src/ws/handlers/authHandlers.js +152 -0
- package/src/ws/handlers/channelHandlers.js +166 -0
- package/src/ws/handlers/hubHandlers.js +82 -0
- package/src/ws/handlers/messageHandlers.js +88 -0
- package/src/ws/handlers/pushHandlers.js +25 -0
- package/src/ws/handlers/reactionHandlers.js +27 -0
- package/src/ws/handlers/rtcHandlers.js +126 -0
- package/styles.css +22 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth & admin WS handlers.
|
|
3
|
+
*
|
|
4
|
+
* Each function receives (ws, msg, ctx) where ctx is the shared context
|
|
5
|
+
* object built in ChatServer — services, state maps, and bound helpers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function handleHello(ws, msg, ctx) {
|
|
9
|
+
const { auth, botService, sendWs, sendDigest, attachUser } = ctx
|
|
10
|
+
const { session_token, bot_token } = msg.body?.resume ?? {}
|
|
11
|
+
let user = null
|
|
12
|
+
let lastSeenAt = null
|
|
13
|
+
|
|
14
|
+
if (bot_token) {
|
|
15
|
+
const bot = botService.authenticateToken(bot_token)
|
|
16
|
+
user = { user_id: bot.userId, handle: bot.handle, display_name: bot.displayName, roles: bot.roles }
|
|
17
|
+
attachUser(ws, user, null)
|
|
18
|
+
} else if (session_token) {
|
|
19
|
+
const session = auth.validateSession(session_token)
|
|
20
|
+
if (session) {
|
|
21
|
+
user = session.user
|
|
22
|
+
lastSeenAt = session.last_seen_at
|
|
23
|
+
attachUser(ws, user, session.session_id)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
sendWs(ws, {
|
|
28
|
+
t: 'hello_ack', reply_to: msg.id, ok: true,
|
|
29
|
+
body: {
|
|
30
|
+
server: { name: 'devchitchat', ver: '2.0.0' },
|
|
31
|
+
session: { authenticated: !!user, user: user ?? null, session_token: session_token ?? null },
|
|
32
|
+
limits: { max_channels: 200, max_group_members: 20, max_message_bytes: 8000, max_signaling_bytes: 64000 }
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (user) sendDigest(ws, user.user_id, lastSeenAt)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handleInviteRedeem(ws, msg, ctx) {
|
|
40
|
+
const { auth, sendWs, sendDigest, attachUser } = ctx
|
|
41
|
+
const { invite_token, profile, password } = msg.body || {}
|
|
42
|
+
const result = await auth.redeemInvite({ inviteToken: invite_token, profile, password })
|
|
43
|
+
const session = auth.validateSession(result.sessionToken)
|
|
44
|
+
attachUser(ws, session.user, session.session_id)
|
|
45
|
+
sendWs(ws, { t: 'auth.session', reply_to: msg.id, ok: true, body: { session_token: result.sessionToken, user: result.user } })
|
|
46
|
+
sendDigest(ws, session.user.user_id, null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleSignIn(ws, msg, ctx) {
|
|
50
|
+
const { auth, sendWs, sendDigest, attachUser } = ctx
|
|
51
|
+
const { handle, password } = msg.body || {}
|
|
52
|
+
const result = await auth.signInWithPassword({ handle, password })
|
|
53
|
+
const session = auth.validateSession(result.sessionToken)
|
|
54
|
+
attachUser(ws, session.user, session.session_id)
|
|
55
|
+
sendWs(ws, { t: 'auth.session', reply_to: msg.id, ok: true, body: { session_token: result.sessionToken, user: result.user } })
|
|
56
|
+
sendDigest(ws, session.user.user_id, session.last_seen_at)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function handleSignOut(ws, msg, ctx) {
|
|
60
|
+
const { auth, sendWs } = ctx
|
|
61
|
+
if (ws.data.sessionId) auth.revokeSession(ws.data.sessionId)
|
|
62
|
+
ws.data.userId = null
|
|
63
|
+
ws.data.sessionId = null
|
|
64
|
+
sendWs(ws, { t: 'auth.signed_out', reply_to: msg.id, ok: true, body: {} })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Admin — invites ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export function handleAdminInviteCreate(ws, msg, ctx) {
|
|
70
|
+
const { auth, sendWs } = ctx
|
|
71
|
+
const { ttl_ms, max_uses, note, roles } = msg.body || {}
|
|
72
|
+
const invite = auth.createInvite({ createdByUserId: ws.data.userId, ttlMs: ttl_ms, maxUses: max_uses, note, roles })
|
|
73
|
+
sendWs(ws, { t: 'admin.invite', reply_to: msg.id, ok: true, body: { invite_token: invite.inviteToken, expires_at: invite.expiresAt, max_uses: invite.maxUses, roles: invite.roles } })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function handleAdminInviteList(ws, msg, ctx) {
|
|
77
|
+
const { auth, sendWs } = ctx
|
|
78
|
+
const invites = auth.listInvites({ requestingUserId: ws.data.userId })
|
|
79
|
+
sendWs(ws, { t: 'admin.invite_list_result', reply_to: msg.id, ok: true, body: { invites } })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function handleAdminInviteRevoke(ws, msg, ctx) {
|
|
83
|
+
const { auth, sendWs } = ctx
|
|
84
|
+
const { invite_id } = msg.body || {}
|
|
85
|
+
auth.revokeInvite({ inviteId: invite_id, requestingUserId: ws.data.userId })
|
|
86
|
+
sendWs(ws, { t: 'admin.invite_revoked', reply_to: msg.id, ok: true, body: { invite_id } })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Admin — users ──────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function handleAdminUserList(ws, msg, ctx) {
|
|
92
|
+
const { auth, sendWs } = ctx
|
|
93
|
+
const users = auth.listUsers({ requestingUserId: ws.data.userId })
|
|
94
|
+
sendWs(ws, { t: 'admin.user_list_result', reply_to: msg.id, ok: true, body: { users } })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function handleAdminUserSetRoles(ws, msg, ctx) {
|
|
98
|
+
const { auth, sendWs } = ctx
|
|
99
|
+
const { user_id, roles } = msg.body || {}
|
|
100
|
+
auth.setUserRoles({ targetUserId: user_id, roles, requestingUserId: ws.data.userId })
|
|
101
|
+
sendWs(ws, { t: 'admin.user_updated', reply_to: msg.id, ok: true, body: { user_id } })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function handleAdminUserSetPassword(ws, msg, ctx) {
|
|
105
|
+
const { auth, sendWs } = ctx
|
|
106
|
+
const { user_id, new_password } = msg.body || {}
|
|
107
|
+
await auth.adminSetPassword({ targetUserId: user_id, newPassword: new_password, requestingUserId: ws.data.userId })
|
|
108
|
+
sendWs(ws, { t: 'admin.user_updated', reply_to: msg.id, ok: true, body: { user_id } })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function handleAdminUserSetDisplayName(ws, msg, ctx) {
|
|
112
|
+
const { auth, sendWs } = ctx
|
|
113
|
+
const { user_id, display_name } = msg.body || {}
|
|
114
|
+
auth.adminUpdateDisplayName({ targetUserId: user_id, displayName: display_name, requestingUserId: ws.data.userId })
|
|
115
|
+
sendWs(ws, { t: 'admin.user_updated', reply_to: msg.id, ok: true, body: { user_id } })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Admin — bots ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export function handleAdminBotCreate(ws, msg, ctx) {
|
|
121
|
+
const { botService, sendWs } = ctx
|
|
122
|
+
const { handle, display_name, token_label } = msg.body || {}
|
|
123
|
+
const result = botService.createBot({ handle, displayName: display_name, tokenLabel: token_label, requestingUserId: ws.data.userId })
|
|
124
|
+
sendWs(ws, { t: 'admin.bot_created', reply_to: msg.id, ok: true, body: result })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function handleAdminBotList(ws, msg, ctx) {
|
|
128
|
+
const { botService, sendWs } = ctx
|
|
129
|
+
const bots = botService.listBots({ requestingUserId: ws.data.userId })
|
|
130
|
+
sendWs(ws, { t: 'admin.bot_list_result', reply_to: msg.id, ok: true, body: { bots } })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function handleAdminBotTokenCreate(ws, msg, ctx) {
|
|
134
|
+
const { botService, sendWs } = ctx
|
|
135
|
+
const { user_id, label } = msg.body || {}
|
|
136
|
+
const result = botService.createToken({ userId: user_id, label, requestingUserId: ws.data.userId })
|
|
137
|
+
sendWs(ws, { t: 'admin.bot_token_created', reply_to: msg.id, ok: true, body: result })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function handleAdminBotTokenRevoke(ws, msg, ctx) {
|
|
141
|
+
const { botService, sendWs } = ctx
|
|
142
|
+
const { token_id } = msg.body || {}
|
|
143
|
+
botService.revokeToken({ tokenId: token_id, requestingUserId: ws.data.userId })
|
|
144
|
+
sendWs(ws, { t: 'admin.bot_token_revoked', reply_to: msg.id, ok: true, body: { token_id } })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function handleAdminBotSetChannels(ws, msg, ctx) {
|
|
148
|
+
const { botService, sendWs } = ctx
|
|
149
|
+
const { user_id, channel_ids } = msg.body || {}
|
|
150
|
+
botService.setBotChannels({ userId: user_id, channelIds: channel_ids, requestingUserId: ws.data.userId })
|
|
151
|
+
sendWs(ws, { t: 'admin.bot_updated', reply_to: msg.id, ok: true, body: { user_id } })
|
|
152
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel, user, and DM WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function handleChannelList(ws, msg, ctx) {
|
|
6
|
+
const { auth, channelService, sendWs } = ctx
|
|
7
|
+
const user = auth.getUser(ws.data.userId)
|
|
8
|
+
const channels = channelService.listChannels(ws.data.userId, user?.roles || [], msg.body?.hub_id)
|
|
9
|
+
sendWs(ws, { t: 'channel.list_result', reply_to: msg.id, ok: true, body: { channels, hub_id: msg.body?.hub_id } })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleChannelCreate(ws, msg, ctx) {
|
|
13
|
+
const { auth, hubService, channelService, sendWs, broadcastToChannelAudience } = ctx
|
|
14
|
+
const user = auth.getUser(ws.data.userId)
|
|
15
|
+
let { hub_id, kind, name, topic, visibility } = msg.body || {}
|
|
16
|
+
if (!hub_id || hub_id === 'default') {
|
|
17
|
+
hub_id = hubService.ensureDefaultHub(ws.data.userId).hub_id
|
|
18
|
+
}
|
|
19
|
+
const channel = channelService.createChannel({ hubId: hub_id, kind, name, topic, visibility, createdByUserId: ws.data.userId, userRoles: user?.roles || [] })
|
|
20
|
+
sendWs(ws, { t: 'channel.created', reply_to: msg.id, ok: true, body: { channel } })
|
|
21
|
+
broadcastToChannelAudience(channel.channel_id, { t: 'channel.created', ok: true, body: { channel } }, ws)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function handleChannelUpdate(ws, msg, ctx) {
|
|
25
|
+
const { auth, channelService, sendWs, publishChannel } = ctx
|
|
26
|
+
const user = auth.getUser(ws.data.userId)
|
|
27
|
+
const { channel_id, name, topic, visibility } = msg.body || {}
|
|
28
|
+
const channel = channelService.updateChannel({ channelId: channel_id, userId: ws.data.userId, roles: user?.roles || [], name, topic, visibility })
|
|
29
|
+
sendWs(ws, { t: 'channel.updated', reply_to: msg.id, ok: true, body: { channel } })
|
|
30
|
+
publishChannel(channel_id, { t: 'channel.updated', ok: true, body: { channel } })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handleChannelDelete(ws, msg, ctx) {
|
|
34
|
+
const { auth, channelService, sendWs, collectChannelAudience } = ctx
|
|
35
|
+
const user = auth.getUser(ws.data.userId)
|
|
36
|
+
const { channel_id } = msg.body || {}
|
|
37
|
+
// Collect audience before deletion — access checks fail once deleted_at is set,
|
|
38
|
+
// and publishChannel only reaches pub/sub subscribers (not sidebar connections)
|
|
39
|
+
const audience = collectChannelAudience(channel_id, ws)
|
|
40
|
+
const result = channelService.deleteChannel({ channelId: channel_id, userId: ws.data.userId, roles: user?.roles || [] })
|
|
41
|
+
sendWs(ws, { t: 'channel.deleted', reply_to: msg.id, ok: true, body: result })
|
|
42
|
+
audience.forEach(conn => sendWs(conn, { t: 'channel.deleted', ok: true, body: result }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function handleChannelJoin(ws, msg, ctx) {
|
|
46
|
+
const { auth, channelService, deliveryService, presenceService, signalingService, sendWs } = ctx
|
|
47
|
+
const user = auth.getUser(ws.data.userId)
|
|
48
|
+
const { channel_id } = msg.body || {}
|
|
49
|
+
const result = channelService.joinChannel({ channelId: channel_id, userId: ws.data.userId, userRoles: user?.roles || [] })
|
|
50
|
+
ws.subscribe(`channel:${channel_id}`)
|
|
51
|
+
presenceService.joinChannel(ws.data.connectionId, channel_id)
|
|
52
|
+
deliveryService.getOrCreate({ channelId: channel_id, userId: ws.data.userId })
|
|
53
|
+
sendWs(ws, { t: 'channel.joined', reply_to: msg.id, ok: true, body: result })
|
|
54
|
+
|
|
55
|
+
// Push current call state so the joining client immediately sees "N in call" or nothing
|
|
56
|
+
const activeCall = signalingService.getActiveCallForChannel(channel_id)
|
|
57
|
+
const peers = activeCall ? Array.from(activeCall.peers.values()) : []
|
|
58
|
+
sendWs(ws, {
|
|
59
|
+
t: 'rtc.call_state', ok: true,
|
|
60
|
+
body: { channel_id, call_id: activeCall?.call_id ?? null, count: peers.length, users: peers.map(p => ({ user_id: p.user_id })) }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handleChannelLeave(ws, msg, ctx) {
|
|
65
|
+
const { channelService, presenceService, sendWs } = ctx
|
|
66
|
+
const { channel_id } = msg.body || {}
|
|
67
|
+
channelService.leaveChannel({ channelId: channel_id, userId: ws.data.userId })
|
|
68
|
+
ws.unsubscribe(`channel:${channel_id}`)
|
|
69
|
+
presenceService.leaveChannel(ws.data.connectionId, channel_id)
|
|
70
|
+
sendWs(ws, { t: 'channel.left', reply_to: msg.id, ok: true, body: { channel_id } })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function handleChannelReorder(ws, msg, ctx) {
|
|
74
|
+
const { auth, channelService, broadcastToHubAudience } = ctx
|
|
75
|
+
const user = auth.getUser(ws.data.userId)
|
|
76
|
+
const { hub_id, channel_ids } = msg.body || {}
|
|
77
|
+
const channels = channelService.reorderChannels({ hubId: hub_id, channelIds: channel_ids, userId: ws.data.userId, userRoles: user?.roles || [] })
|
|
78
|
+
broadcastToHubAudience(hub_id, { t: 'channel.reordered', ok: true, body: { hub_id, channels } }, null)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function handleChannelAddMember(ws, msg, ctx) {
|
|
82
|
+
const { channelService, sendWs } = ctx
|
|
83
|
+
const { channel_id, user_id } = msg.body || {}
|
|
84
|
+
const result = channelService.addMember({ channelId: channel_id, createdByUserId: ws.data.userId, targetUserId: user_id })
|
|
85
|
+
sendWs(ws, { t: 'channel.member_added', reply_to: msg.id, ok: true, body: result })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleChannelRemoveMember(ws, msg, ctx) {
|
|
89
|
+
const { channelService, sendWs, publishChannel } = ctx
|
|
90
|
+
const { channel_id, user_id } = msg.body || {}
|
|
91
|
+
const result = channelService.removeMember({ channelId: channel_id, removedByUserId: ws.data.userId, targetUserId: user_id })
|
|
92
|
+
sendWs(ws, { t: 'channel.member_removed', reply_to: msg.id, ok: true, body: result })
|
|
93
|
+
publishChannel(channel_id, { t: 'channel.member_removed', ok: true, body: result })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function handleChannelListMembers(ws, msg, ctx) {
|
|
97
|
+
const { auth, channelService, sendWs } = ctx
|
|
98
|
+
const { channel_id } = msg.body || {}
|
|
99
|
+
const user = auth.getUser(ws.data.userId)
|
|
100
|
+
const roles = user?.roles || []
|
|
101
|
+
if (!channelService.canAccessChannel(channel_id, ws.data.userId, roles)) {
|
|
102
|
+
return sendWs(ws, { t: 'error', reply_to: msg.id, ok: false, body: { code: 'FORBIDDEN', message: 'Access denied' } })
|
|
103
|
+
}
|
|
104
|
+
const members = channelService.listChannelMembers(channel_id)
|
|
105
|
+
const enriched = members.map(m => {
|
|
106
|
+
const u = auth.getUser(m.user_id)
|
|
107
|
+
return { user_id: m.user_id, handle: u?.handle ?? null, display_name: u?.display_name ?? null, role: m.role }
|
|
108
|
+
})
|
|
109
|
+
sendWs(ws, { t: 'channel.list_members_result', reply_to: msg.id, ok: true, body: { channel_id, members: enriched } })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Users ──────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function handleUserList(ws, msg, ctx) {
|
|
115
|
+
const { auth, sendWs } = ctx
|
|
116
|
+
const users = auth.listUsersBasic().filter(u => !u.roles.includes('bot'))
|
|
117
|
+
sendWs(ws, { t: 'user.list_result', reply_to: msg.id, ok: true, body: { users } })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function handleBotList(ws, msg, ctx) {
|
|
121
|
+
const { auth, sendWs } = ctx
|
|
122
|
+
const bots = auth.listUsersBasic().filter(u => u.roles.includes('bot'))
|
|
123
|
+
sendWs(ws, { t: 'bot.list_result', reply_to: msg.id, ok: true, body: { bots } })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Direct messages ────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export function handleDmOpen(ws, msg, ctx) {
|
|
129
|
+
const { auth, channelService, sendWs, subscribeUserToChannel } = ctx
|
|
130
|
+
const { target_user_id } = msg.body || {}
|
|
131
|
+
const result = channelService.findOrCreateDm({ userId: ws.data.userId, targetUserId: target_user_id })
|
|
132
|
+
const targetUser = auth.getUser(target_user_id)
|
|
133
|
+
|
|
134
|
+
// Subscribe all active connections for both users to the DM channel topic.
|
|
135
|
+
subscribeUserToChannel(ws.data.userId, result.channel_id)
|
|
136
|
+
subscribeUserToChannel(target_user_id, result.channel_id)
|
|
137
|
+
|
|
138
|
+
// Tell the initiating connection to navigate (no notify_only)
|
|
139
|
+
sendWs(ws, {
|
|
140
|
+
t: 'dm.opened', reply_to: msg.id, ok: true,
|
|
141
|
+
body: { channel_id: result.channel_id, is_new: result.is_new, with_user: { user_id: target_user_id, display_name: targetUser?.display_name ?? null } }
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Notify the initiating user's OTHER connections (sidebar) so the DM appears in the list
|
|
145
|
+
ctx.server?.publish(`user:${ws.data.userId}`, JSON.stringify({
|
|
146
|
+
v: 1, server_ts: Date.now(), t: 'dm.opened', ok: true,
|
|
147
|
+
body: { channel_id: result.channel_id, is_new: result.is_new, notify_only: true, with_user: { user_id: target_user_id, display_name: targetUser?.display_name ?? null } }
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
// Notify the target user's connections — add to DM list, show unread dot, don't navigate
|
|
151
|
+
ctx.server?.publish(`user:${target_user_id}`, JSON.stringify({
|
|
152
|
+
v: 1, server_ts: Date.now(), t: 'dm.opened', ok: true,
|
|
153
|
+
body: { channel_id: result.channel_id, is_new: result.is_new, notify_only: true, with_user: { user_id: ws.data.userId, display_name: ws.data.displayName } }
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function handleDmList(ws, msg, ctx) {
|
|
158
|
+
const { auth, channelService, sendWs } = ctx
|
|
159
|
+
const dms = channelService.listDms({ userId: ws.data.userId })
|
|
160
|
+
for (const dm of dms) ws.subscribe(`channel:${dm.channel_id}`)
|
|
161
|
+
const enriched = dms.map(dm => {
|
|
162
|
+
const other = auth.getUser(dm.other_user_id)
|
|
163
|
+
return { channel_id: dm.channel_id, with_user: { user_id: dm.other_user_id, display_name: other?.display_name ?? dm.other_user_id } }
|
|
164
|
+
})
|
|
165
|
+
sendWs(ws, { t: 'dm.list_result', reply_to: msg.id, ok: true, body: { dms: enriched } })
|
|
166
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function handleHubList(ws, msg, ctx) {
|
|
6
|
+
const { auth, hubService, sendWs } = ctx
|
|
7
|
+
const user = auth.getUser(ws.data.userId)
|
|
8
|
+
const hubs = hubService.listHubs(ws.data.userId, user?.roles || [])
|
|
9
|
+
sendWs(ws, { t: 'hub.list_result', reply_to: msg.id, ok: true, body: { hubs } })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleHubCreate(ws, msg, ctx) {
|
|
13
|
+
const { hubService, sendWs, broadcastToHubAudience } = ctx
|
|
14
|
+
const { name, description, visibility } = msg.body || {}
|
|
15
|
+
const hub = hubService.createHub({ name, description, visibility, createdByUserId: ws.data.userId })
|
|
16
|
+
sendWs(ws, { t: 'hub.created', reply_to: msg.id, ok: true, body: { hub } })
|
|
17
|
+
broadcastToHubAudience(hub.hub_id, { t: 'hub.created', ok: true, body: { hub } }, ws)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function handleHubUpdate(ws, msg, ctx) {
|
|
21
|
+
const { auth, hubService, sendWs, broadcastToHubAudience } = ctx
|
|
22
|
+
const user = auth.getUser(ws.data.userId)
|
|
23
|
+
const { hub_id, name, description, visibility } = msg.body || {}
|
|
24
|
+
const hub = hubService.updateHub({ hubId: hub_id, userId: ws.data.userId, roles: user?.roles || [], name, description, visibility })
|
|
25
|
+
sendWs(ws, { t: 'hub.updated', reply_to: msg.id, ok: true, body: { hub } })
|
|
26
|
+
broadcastToHubAudience(hub.hub_id, { t: 'hub.updated', ok: true, body: { hub } }, ws)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function handleHubDelete(ws, msg, ctx) {
|
|
30
|
+
const { auth, hubService, sendWs, collectHubAudience } = ctx
|
|
31
|
+
const user = auth.getUser(ws.data.userId)
|
|
32
|
+
const { hub_id } = msg.body || {}
|
|
33
|
+
// Collect audience before deletion — access checks fail once deleted_at is set
|
|
34
|
+
const audience = collectHubAudience(hub_id, ws)
|
|
35
|
+
const result = hubService.deleteHub({ hubId: hub_id, userId: ws.data.userId, roles: user?.roles || [] })
|
|
36
|
+
sendWs(ws, { t: 'hub.deleted', reply_to: msg.id, ok: true, body: result })
|
|
37
|
+
audience.forEach(conn => sendWs(conn, { t: 'hub.deleted', ok: true, body: result }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function handleHubAddMember(ws, msg, ctx) {
|
|
41
|
+
const { auth, hubService, sendWs, broadcastToHubAudience } = ctx
|
|
42
|
+
const user = auth.getUser(ws.data.userId)
|
|
43
|
+
const { hub_id, user_id } = msg.body || {}
|
|
44
|
+
const result = hubService.addHubMember({ hubId: hub_id, targetUserId: user_id, requestingUserId: ws.data.userId, requestingRoles: user?.roles || [] })
|
|
45
|
+
sendWs(ws, { t: 'hub.member_added', reply_to: msg.id, ok: true, body: result })
|
|
46
|
+
ctx.server?.publish(`user:${user_id}`, JSON.stringify({ v: 1, server_ts: Date.now(), t: 'hub.member_added', ok: true, body: result }))
|
|
47
|
+
broadcastToHubAudience(hub_id, { t: 'hub.member_added', ok: true, body: result }, ws)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function handleHubRemoveMember(ws, msg, ctx) {
|
|
51
|
+
const { auth, hubService, sendWs, broadcastToHubAudience } = ctx
|
|
52
|
+
const user = auth.getUser(ws.data.userId)
|
|
53
|
+
const { hub_id, user_id } = msg.body || {}
|
|
54
|
+
const result = hubService.removeHubMember({ hubId: hub_id, targetUserId: user_id, requestingUserId: ws.data.userId, requestingRoles: user?.roles || [] })
|
|
55
|
+
sendWs(ws, { t: 'hub.member_removed', reply_to: msg.id, ok: true, body: result })
|
|
56
|
+
ctx.server?.publish(`user:${user_id}`, JSON.stringify({ v: 1, server_ts: Date.now(), t: 'hub.member_removed', ok: true, body: result }))
|
|
57
|
+
broadcastToHubAudience(hub_id, { t: 'hub.member_removed', ok: true, body: result }, ws)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function handleHubReorder(ws, msg, ctx) {
|
|
61
|
+
const { auth, hubService, sendWs, connections } = ctx
|
|
62
|
+
const user = auth.getUser(ws.data.userId)
|
|
63
|
+
const { hub_ids } = msg.body || {}
|
|
64
|
+
const hubs = hubService.reorderHubs({ hubIds: hub_ids, userId: ws.data.userId, userRoles: user?.roles || [] })
|
|
65
|
+
// Broadcast to all authenticated connections — every sidebar needs to update
|
|
66
|
+
for (const [, conn] of connections) {
|
|
67
|
+
if (conn.data.userId) sendWs(conn, { t: 'hub.reordered', ok: true, body: { hubs } })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function handleHubListMembers(ws, msg, ctx) {
|
|
72
|
+
const { auth, hubService, sendWs } = ctx
|
|
73
|
+
const user = auth.getUser(ws.data.userId)
|
|
74
|
+
const { hub_id } = msg.body || {}
|
|
75
|
+
const members = hubService.listHubMembers({ hubId: hub_id, requestingUserId: ws.data.userId, requestingRoles: user?.roles || [] })
|
|
76
|
+
const enriched = members.map(m => {
|
|
77
|
+
if (m.handle) return m // SqliteHubRepository joins users — already enriched
|
|
78
|
+
const u = auth.getUser(m.user_id)
|
|
79
|
+
return { user_id: m.user_id, handle: u?.handle ?? null, display_name: u?.display_name ?? null, joined_at: m.joined_at }
|
|
80
|
+
})
|
|
81
|
+
sendWs(ws, { t: 'hub.list_members_result', reply_to: msg.id, ok: true, body: { hub_id, members: enriched } })
|
|
82
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message, search, and presence WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { renderMarkdown } from '@devchitchat/index97/markdown'
|
|
5
|
+
|
|
6
|
+
export function handleMsgSend(ws, msg, ctx) {
|
|
7
|
+
const { messageService, deliveryService, sendWs, publishChannel, dispatchMentions } = ctx
|
|
8
|
+
const { channel_id, text, client_msg_id, priority, attachments } = msg.body || {}
|
|
9
|
+
const result = messageService.sendMessage({
|
|
10
|
+
channelId: channel_id, userId: ws.data.userId, text, clientMsgId: client_msg_id, priority,
|
|
11
|
+
attachments: Array.isArray(attachments) ? attachments : []
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
sendWs(ws, { t: 'msg.ack', reply_to: msg.id, ok: true, body: { msg_id: result.msg_id, seq: result.seq, client_msg_id, priority: result.priority } })
|
|
15
|
+
|
|
16
|
+
publishChannel(channel_id, {
|
|
17
|
+
t: 'msg.event', ok: true,
|
|
18
|
+
body: {
|
|
19
|
+
msg_id: result.msg_id, channel_id, seq: result.seq,
|
|
20
|
+
user_id: ws.data.userId, user_display_name: ws.data.displayName,
|
|
21
|
+
ts: result.ts, text, rendered_text: renderMarkdown(text).html,
|
|
22
|
+
priority: result.priority, attachments: result.attachments ?? []
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
deliveryService.advance({ channelId: channel_id, userId: ws.data.userId, afterSeq: result.seq })
|
|
27
|
+
dispatchMentions({ channelId: channel_id, senderId: ws.data.userId, text, seq: result.seq, priority: result.priority })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function handleMsgEdit(ws, msg, ctx) {
|
|
31
|
+
const { messageService, publishChannel } = ctx
|
|
32
|
+
const { msg_id, channel_id, text } = msg.body ?? {}
|
|
33
|
+
const result = messageService.editMessage({ msgId: msg_id, channelId: channel_id, userId: ws.data.userId, newText: text })
|
|
34
|
+
const renderedText = renderMarkdown(result.text).html
|
|
35
|
+
publishChannel(channel_id, {
|
|
36
|
+
t: 'msg.edited', ok: true,
|
|
37
|
+
body: { msg_id: result.msgId, channel_id: result.channelId, text: result.text, edited_at: result.editedAt, rendered_text: renderedText },
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function handleMsgList(ws, msg, ctx) {
|
|
42
|
+
const { messageService, sendWs } = ctx
|
|
43
|
+
const { channel_id, after_seq, before_seq, limit } = msg.body || {}
|
|
44
|
+
|
|
45
|
+
if (before_seq != null) {
|
|
46
|
+
const result = messageService.listMessagesBefore({
|
|
47
|
+
channelId: channel_id,
|
|
48
|
+
userId: ws.data.userId,
|
|
49
|
+
beforeSeq: before_seq,
|
|
50
|
+
limit: limit ?? 50,
|
|
51
|
+
})
|
|
52
|
+
sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, channel_id, direction: 'before' } })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = messageService.listMessages({ channelId: channel_id, userId: ws.data.userId, afterSeq: after_seq ?? 0, limit: limit ?? 50 })
|
|
57
|
+
sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, channel_id, direction: 'after' } })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function handleMsgDelete(ws, msg, ctx) {
|
|
61
|
+
const { messageService, publishChannel } = ctx
|
|
62
|
+
const { msg_id, channel_id } = msg.body ?? {}
|
|
63
|
+
const result = messageService.deleteMessage({ msgId: msg_id, channelId: channel_id, userId: ws.data.userId })
|
|
64
|
+
publishChannel(channel_id, {
|
|
65
|
+
t: 'msg.deleted', ok: true,
|
|
66
|
+
body: { msg_id: result.msgId, channel_id: result.channelId, seq: result.seq },
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function handleSearchQuery(ws, msg, ctx) {
|
|
71
|
+
const { auth, channelService, searchService, sendWs } = ctx
|
|
72
|
+
const { channel_id, q, limit } = msg.body || {}
|
|
73
|
+
const roles = auth.getUser(ws.data.userId)?.roles || []
|
|
74
|
+
if (!channelService.canAccessChannel(channel_id, ws.data.userId, roles)) {
|
|
75
|
+
return sendWs(ws, { t: 'error', reply_to: msg.id, ok: false, body: { code: 'FORBIDDEN', message: 'Access denied' } })
|
|
76
|
+
}
|
|
77
|
+
const hits = searchService.searchMessages({ channelId: channel_id, query: q, limit })
|
|
78
|
+
sendWs(ws, { t: 'search.result', reply_to: msg.id, ok: true, body: { hits } })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function handlePresenceSubscribe(ws, msg, ctx) {
|
|
82
|
+
const { auth, channelService, presenceService, sendWs } = ctx
|
|
83
|
+
const roles = auth.getUser(ws.data.userId)?.roles || []
|
|
84
|
+
const accessibleChannels = channelService.listChannels(ws.data.userId, roles)
|
|
85
|
+
const channelIds = accessibleChannels.map(c => c.channel_id)
|
|
86
|
+
const users = presenceService.listOnlineUsersInChannels(channelIds)
|
|
87
|
+
sendWs(ws, { t: 'presence.snapshot', reply_to: msg.id, ok: true, body: { users } })
|
|
88
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Push subscription WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function handlePushSubscribe(ws, msg, ctx) {
|
|
6
|
+
const { pushRepo, sendWs } = ctx
|
|
7
|
+
const { subscription } = msg.body ?? {}
|
|
8
|
+
const endpoint = subscription?.endpoint
|
|
9
|
+
const p256dh = subscription?.keys?.p256dh
|
|
10
|
+
const auth = subscription?.keys?.auth
|
|
11
|
+
|
|
12
|
+
if (!endpoint || !p256dh || !auth) {
|
|
13
|
+
return sendWs(ws, { t: 'error', reply_to: msg.id, ok: false, body: { code: 'BAD_REQUEST', message: 'Invalid push subscription' } })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pushRepo.upsertSubscription({ userId: ws.data.userId, endpoint, p256dh, auth })
|
|
17
|
+
sendWs(ws, { t: 'push.subscribed', reply_to: msg.id, ok: true, body: {} })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function handlePushUnsubscribe(ws, msg, ctx) {
|
|
21
|
+
const { pushRepo, sendWs } = ctx
|
|
22
|
+
const { endpoint } = msg.body ?? {}
|
|
23
|
+
if (endpoint) pushRepo.removeSubscription(endpoint)
|
|
24
|
+
sendWs(ws, { t: 'push.unsubscribed', reply_to: msg.id, ok: true, body: {} })
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaction WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function handleReactionAdd(ws, msg, ctx) {
|
|
6
|
+
const { reactionService, publishChannel } = ctx
|
|
7
|
+
const { msg_id, channel_id, emoji } = msg.body ?? {}
|
|
8
|
+
const reactions = reactionService.addReaction({
|
|
9
|
+
msgId: msg_id, channelId: channel_id, userId: ws.data.userId, emoji,
|
|
10
|
+
})
|
|
11
|
+
publishChannel(channel_id, {
|
|
12
|
+
t: 'reaction.event', ok: true,
|
|
13
|
+
body: { msg_id, channel_id, emoji, user_id: ws.data.userId, action: 'add', reactions },
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function handleReactionRemove(ws, msg, ctx) {
|
|
18
|
+
const { reactionService, publishChannel } = ctx
|
|
19
|
+
const { msg_id, channel_id, emoji } = msg.body ?? {}
|
|
20
|
+
const reactions = reactionService.removeReaction({
|
|
21
|
+
msgId: msg_id, channelId: channel_id, userId: ws.data.userId, emoji,
|
|
22
|
+
})
|
|
23
|
+
publishChannel(channel_id, {
|
|
24
|
+
t: 'reaction.event', ok: true,
|
|
25
|
+
body: { msg_id, channel_id, emoji, user_id: ws.data.userId, action: 'remove', reactions },
|
|
26
|
+
})
|
|
27
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC signaling WS handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function handleRtcCallCreate(ws, msg, ctx) {
|
|
6
|
+
const { auth, channelService, signalingService, sendWs, getIceServers } = ctx
|
|
7
|
+
const { channel_id, kind = 'mesh' } = msg.body || {}
|
|
8
|
+
const roles = auth.getUser(ws.data.userId)?.roles || []
|
|
9
|
+
if (!channelService.canAccessChannel(channel_id, ws.data.userId, roles)) {
|
|
10
|
+
return sendWs(ws, { t: 'error', reply_to: msg.id, ok: false, body: { code: 'FORBIDDEN', message: 'Access denied' } })
|
|
11
|
+
}
|
|
12
|
+
const result = signalingService.createCall({ roomId: channel_id, createdByUserId: ws.data.userId, topology: kind })
|
|
13
|
+
sendWs(ws, { t: 'rtc.call', reply_to: msg.id, ok: true, body: { ...result, channel_id, ice_servers: getIceServers() } })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function handleRtcJoin(ws, msg, ctx) {
|
|
17
|
+
const { auth, channelService, signalingService, sendWs, publishCall, publishChannel, publishCallState, getIceServers, peerConnections } = ctx
|
|
18
|
+
const { call_id } = msg.body || {}
|
|
19
|
+
|
|
20
|
+
// Guard: verify user can access the channel this call belongs to
|
|
21
|
+
const call = signalingService.getCall(call_id)
|
|
22
|
+
if (call) {
|
|
23
|
+
const roles = auth.getUser(ws.data.userId)?.roles || []
|
|
24
|
+
if (!channelService.canAccessChannel(call.room_id, ws.data.userId, roles)) {
|
|
25
|
+
return sendWs(ws, { t: 'error', reply_to: msg.id, ok: false, body: { code: 'FORBIDDEN', message: 'Access denied' } })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = signalingService.joinOrSwitch({
|
|
30
|
+
callId: call_id,
|
|
31
|
+
userId: ws.data.userId,
|
|
32
|
+
displayName: ws.data.displayName,
|
|
33
|
+
currentPeerId: ws.data.peerId,
|
|
34
|
+
currentCallId: ws.data.callId,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (result.status === 'already_in_call') {
|
|
38
|
+
sendWs(ws, { t: 'rtc.joined', reply_to: msg.id, ok: true, body: { call_id, peer_id: result.peerId, peers: result.peers, ice_servers: getIceServers() } })
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Publish transport side effects for previous call departure (status === 'switched')
|
|
43
|
+
if (result.previousLeft) {
|
|
44
|
+
const { call_id: prevCallId, room_id, peerId: prevPeerId, removed, ended } = result.previousLeft
|
|
45
|
+
peerConnections.delete(prevPeerId)
|
|
46
|
+
ws.unsubscribe(`call:${prevCallId}`)
|
|
47
|
+
if (removed) {
|
|
48
|
+
publishCall(prevCallId, { t: 'rtc.peer_event', ok: true, body: { call_id: prevCallId, kind: 'leave', peer: { peer_id: prevPeerId, user_id: ws.data.userId } } })
|
|
49
|
+
}
|
|
50
|
+
if (ended) {
|
|
51
|
+
publishChannel(room_id, { t: 'rtc.call_end', ok: true, body: { call_id: prevCallId, channel_id: room_id } })
|
|
52
|
+
publishCallState(room_id, null, [])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ws.data.peerId = result.peerId
|
|
57
|
+
ws.data.callId = call_id
|
|
58
|
+
peerConnections.set(result.peerId, ws.data.connectionId)
|
|
59
|
+
ws.subscribe(`call:${call_id}`)
|
|
60
|
+
sendWs(ws, { t: 'rtc.joined', reply_to: msg.id, ok: true, body: { call_id, peer_id: result.peerId, peers: result.peers, ice_servers: getIceServers() } })
|
|
61
|
+
publishCall(call_id, { t: 'rtc.peer_event', ok: true, body: { call_id, kind: 'join', peer: { peer_id: result.peerId, user_id: ws.data.userId, display_name: ws.data.displayName } } })
|
|
62
|
+
|
|
63
|
+
const updatedCall = signalingService.getCall(call_id)
|
|
64
|
+
if (updatedCall) publishCallState(updatedCall.room_id, call_id, Array.from(updatedCall.peers.values()))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function handleRtcOffer(ws, msg, ctx) {
|
|
68
|
+
const { signalingService } = ctx
|
|
69
|
+
const { call_id, to_peer_id, sdp } = msg.body || {}
|
|
70
|
+
if (!ws.data.peerId || ws.data.callId !== call_id) return
|
|
71
|
+
signalingService.routeOffer({ callId: call_id, fromPeerId: ws.data.peerId, toPeerId: to_peer_id, sdp })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function handleRtcAnswer(ws, msg, ctx) {
|
|
75
|
+
const { signalingService } = ctx
|
|
76
|
+
const { call_id, to_peer_id, sdp } = msg.body || {}
|
|
77
|
+
if (!ws.data.peerId || ws.data.callId !== call_id) return
|
|
78
|
+
signalingService.routeAnswer({ callId: call_id, fromPeerId: ws.data.peerId, toPeerId: to_peer_id, sdp })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function handleRtcIce(ws, msg, ctx) {
|
|
82
|
+
const { signalingService } = ctx
|
|
83
|
+
const { call_id, to_peer_id, candidate } = msg.body || {}
|
|
84
|
+
if (!ws.data.peerId || ws.data.callId !== call_id) return
|
|
85
|
+
signalingService.routeIce({ callId: call_id, fromPeerId: ws.data.peerId, toPeerId: to_peer_id, candidate })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleRtcStreamPublish(ws, msg, ctx) {
|
|
89
|
+
const { publishCall } = ctx
|
|
90
|
+
const { call_id, stream } = msg.body || {}
|
|
91
|
+
publishCall(call_id, { t: 'rtc.stream_event', ok: true, body: { call_id, peer_id: ws.data.peerId, stream } })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function handleRtcLeave(ws, msg, ctx) {
|
|
95
|
+
const { signalingService, sendWs, publishCall, publishChannel, publishCallState, peerConnections } = ctx
|
|
96
|
+
const { call_id } = msg.body || {}
|
|
97
|
+
if (!ws.data.peerId) return
|
|
98
|
+
const leavingPeerId = ws.data.peerId
|
|
99
|
+
const result = signalingService.leaveCall({ callId: call_id, peerId: leavingPeerId })
|
|
100
|
+
ws.unsubscribe(`call:${call_id}`)
|
|
101
|
+
peerConnections.delete(leavingPeerId)
|
|
102
|
+
ws.data.peerId = null
|
|
103
|
+
ws.data.callId = null
|
|
104
|
+
sendWs(ws, { t: 'rtc.left', reply_to: msg.id, ok: true, body: { call_id } })
|
|
105
|
+
if (result.removed) {
|
|
106
|
+
publishCall(call_id, { t: 'rtc.peer_event', ok: true, body: { call_id, kind: 'leave', peer: { peer_id: leavingPeerId, user_id: ws.data.userId } } })
|
|
107
|
+
}
|
|
108
|
+
if (result.ended && result.room_id) {
|
|
109
|
+
publishChannel(result.room_id, { t: 'rtc.call_end', ok: true, body: { call_id, channel_id: result.room_id } })
|
|
110
|
+
publishCallState(result.room_id, null, [])
|
|
111
|
+
} else if (result.removed && result.room_id) {
|
|
112
|
+
const call = signalingService.getCall(call_id)
|
|
113
|
+
publishCallState(result.room_id, call_id, call ? Array.from(call.peers.values()) : [])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function handleRtcEndCall(ws, msg, ctx) {
|
|
118
|
+
const { signalingService, sendWs, publishCall, publishChannel, publishCallState } = ctx
|
|
119
|
+
const { call_id } = msg.body || {}
|
|
120
|
+
const result = signalingService.endCall({ callId: call_id })
|
|
121
|
+
if (!result) return
|
|
122
|
+
publishCall(call_id, { t: 'rtc.call_end', ok: true, body: { call_id, channel_id: result.room_id } })
|
|
123
|
+
publishChannel(result.room_id, { t: 'rtc.call_end', ok: true, body: { call_id, channel_id: result.room_id } })
|
|
124
|
+
publishCallState(result.room_id, null, [])
|
|
125
|
+
sendWs(ws, { t: 'rtc.call_ended', reply_to: msg.id, ok: true, body: { call_id } })
|
|
126
|
+
}
|