diff --git a/Fabric.Backend.Guild/src/realtime/realtime.gateway.ts b/Fabric.Backend.Guild/src/realtime/realtime.gateway.ts index d71a3d9..b231636 100644 --- a/Fabric.Backend.Guild/src/realtime/realtime.gateway.ts +++ b/Fabric.Backend.Guild/src/realtime/realtime.gateway.ts @@ -21,6 +21,14 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect server!: Server; private readonly logger = new Logger(RealtimeGateway.name); + private readonly onlineUsers = new Set(); + + private userIdFromClient(client: Socket): string { + const authUser = client.handshake.auth?.userId; + const headerUser = client.handshake.headers['x-user-id']; + const userId = typeof authUser === 'string' ? authUser : Array.isArray(headerUser) ? headerUser[0] : headerUser; + return userId && typeof userId === 'string' && userId.trim() !== '' ? userId : `anon:${client.id}`; + } handleConnection(client: Socket): void { const expected = process.env.FABRIC_API_KEY; @@ -40,10 +48,27 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect } this.logger.log(`socket connected: ${client.id}`); + + const userId = this.userIdFromClient(client); + client.data.userId = userId; + this.onlineUsers.add(userId); + this.server.emit('presence.online', { + userId, + onlineCount: this.onlineUsers.size, + occurredAt: new Date().toISOString(), + }); } handleDisconnect(client: Socket): void { this.logger.log(`socket disconnected: ${client.id}`); + + const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`; + this.onlineUsers.delete(userId); + this.server.emit('presence.offline', { + userId, + onlineCount: this.onlineUsers.size, + occurredAt: new Date().toISOString(), + }); } @SubscribeMessage('join_channel') @@ -66,6 +91,38 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect return { ok: true }; } + @SubscribeMessage('typing.start') + typingStart( + @ConnectedSocket() client: Socket, + @MessageBody() body: { channelId?: string }, + ): { ok: boolean } { + if (!body?.channelId) return { ok: false }; + + const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`; + this.server.to(`channel:${body.channelId}`).emit('typing.start', { + channelId: body.channelId, + userId, + occurredAt: new Date().toISOString(), + }); + return { ok: true }; + } + + @SubscribeMessage('typing.stop') + typingStop( + @ConnectedSocket() client: Socket, + @MessageBody() body: { channelId?: string }, + ): { ok: boolean } { + if (!body?.channelId) return { ok: false }; + + const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`; + this.server.to(`channel:${body.channelId}`).emit('typing.stop', { + channelId: body.channelId, + userId, + occurredAt: new Date().toISOString(), + }); + return { ok: true }; + } + emitChannelEvent(channelId: string, event: string, data: Record): void { this.server.to(`channel:${channelId}`).emit(event, data); } diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index ecbb82c..cfcf3dc 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -58,7 +58,7 @@ ### 2.4 实时通信(MVP 后半) - [x] WebSocket 网关接入 - [x] message.created/updated/deleted 事件广播 -- [ ] 在线状态 + typing 事件 +- [x] 在线状态 + typing 事件 ---