feat: Add user metadata changing display name and description with sync
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
This commit is contained in:
@@ -2,13 +2,18 @@ import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach
|
||||
beforeEach,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import { connectedUsers } from './state';
|
||||
import { handleWebSocketMessage } from './handler';
|
||||
import { ConnectedUser } from './types';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
vi.mock('../services/server-access.service', () => ({
|
||||
authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const }))
|
||||
}));
|
||||
|
||||
/**
|
||||
* Minimal mock WebSocket that records sent messages.
|
||||
*/
|
||||
@@ -197,3 +202,94 @@ describe('server websocket handler - user_joined includes status', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('server websocket handler - profile metadata in presence messages', () => {
|
||||
beforeEach(() => {
|
||||
connectedUsers.clear();
|
||||
});
|
||||
|
||||
it('broadcasts updated profile metadata when an identified user changes it', async () => {
|
||||
const alice = createConnectedUser('conn-1', 'user-1', {
|
||||
displayName: 'Alice',
|
||||
viewedServerId: 'server-1'
|
||||
});
|
||||
const bob = createConnectedUser('conn-2', 'user-2', {
|
||||
viewedServerId: 'server-1'
|
||||
});
|
||||
|
||||
alice.serverIds.add('server-1');
|
||||
bob.serverIds.add('server-1');
|
||||
getSentMessagesStore(bob).sentMessages.length = 0;
|
||||
|
||||
await handleWebSocketMessage('conn-1', {
|
||||
type: 'identify',
|
||||
oderId: 'user-1',
|
||||
displayName: 'Alice Updated',
|
||||
description: 'Updated bio',
|
||||
profileUpdatedAt: 789
|
||||
});
|
||||
|
||||
const messages = getSentMessagesStore(bob).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const joinMsg = messages.find((message: { type: string }) => message.type === 'user_joined');
|
||||
|
||||
expect(joinMsg?.displayName).toBe('Alice Updated');
|
||||
expect(joinMsg?.description).toBe('Updated bio');
|
||||
expect(joinMsg?.profileUpdatedAt).toBe(789);
|
||||
expect(joinMsg?.serverId).toBe('server-1');
|
||||
});
|
||||
|
||||
it('includes description and profileUpdatedAt in server_users responses', async () => {
|
||||
const alice = createConnectedUser('conn-1', 'user-1');
|
||||
const bob = createConnectedUser('conn-2', 'user-2');
|
||||
|
||||
alice.serverIds.add('server-1');
|
||||
bob.serverIds.add('server-1');
|
||||
|
||||
await handleWebSocketMessage('conn-1', {
|
||||
type: 'identify',
|
||||
oderId: 'user-1',
|
||||
displayName: 'Alice',
|
||||
description: 'Alice bio',
|
||||
profileUpdatedAt: 123
|
||||
});
|
||||
|
||||
getSentMessagesStore(bob).sentMessages.length = 0;
|
||||
|
||||
await handleWebSocketMessage('conn-2', {
|
||||
type: 'view_server',
|
||||
serverId: 'server-1'
|
||||
});
|
||||
|
||||
const messages = getSentMessagesStore(bob).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const serverUsersMsg = messages.find((message: { type: string }) => message.type === 'server_users');
|
||||
const aliceInList = serverUsersMsg?.users?.find((userEntry: { oderId: string }) => userEntry.oderId === 'user-1');
|
||||
|
||||
expect(aliceInList?.description).toBe('Alice bio');
|
||||
expect(aliceInList?.profileUpdatedAt).toBe(123);
|
||||
});
|
||||
|
||||
it('includes description and profileUpdatedAt in user_joined broadcasts', async () => {
|
||||
const bob = createConnectedUser('conn-2', 'user-2');
|
||||
|
||||
bob.serverIds.add('server-1');
|
||||
bob.viewedServerId = 'server-1';
|
||||
|
||||
createConnectedUser('conn-1', 'user-1', {
|
||||
displayName: 'Alice',
|
||||
description: 'Alice bio',
|
||||
profileUpdatedAt: 456,
|
||||
viewedServerId: 'server-1'
|
||||
});
|
||||
|
||||
await handleWebSocketMessage('conn-1', {
|
||||
type: 'join_server',
|
||||
serverId: 'server-1'
|
||||
});
|
||||
|
||||
const messages = getSentMessagesStore(bob).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const joinMsg = messages.find((message: { type: string }) => message.type === 'user_joined');
|
||||
|
||||
expect(joinMsg?.description).toBe('Alice bio');
|
||||
expect(joinMsg?.profileUpdatedAt).toBe(456);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,22 @@ function normalizeDisplayName(value: unknown, fallback = 'User'): string {
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function normalizeDescription(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = value.trim();
|
||||
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function normalizeProfileUpdatedAt(value: unknown): number | undefined {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readMessageId(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
@@ -37,7 +53,13 @@ function readMessageId(value: unknown): string | undefined {
|
||||
/** Sends the current user list for a given server to a single connected user. */
|
||||
function sendServerUsers(user: ConnectedUser, serverId: string): void {
|
||||
const users = getUniqueUsersInServer(serverId, user.oderId)
|
||||
.map(cu => ({ oderId: cu.oderId, displayName: normalizeDisplayName(cu.displayName), status: cu.status ?? 'online' }));
|
||||
.map(cu => ({
|
||||
oderId: cu.oderId,
|
||||
displayName: normalizeDisplayName(cu.displayName),
|
||||
description: cu.description,
|
||||
profileUpdatedAt: cu.profileUpdatedAt,
|
||||
status: cu.status ?? 'online'
|
||||
}));
|
||||
|
||||
user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
|
||||
}
|
||||
@@ -45,6 +67,9 @@ function sendServerUsers(user: ConnectedUser, serverId: string): void {
|
||||
function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void {
|
||||
const newOderId = readMessageId(message['oderId']) ?? connectionId;
|
||||
const newScope = typeof message['connectionScope'] === 'string' ? message['connectionScope'] : undefined;
|
||||
const previousDisplayName = normalizeDisplayName(user.displayName);
|
||||
const previousDescription = user.description;
|
||||
const previousProfileUpdatedAt = user.profileUpdatedAt;
|
||||
|
||||
// Close stale connections from the same identity AND the same connection
|
||||
// scope so offer routing always targets the freshest socket (e.g. after
|
||||
@@ -67,9 +92,38 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s
|
||||
|
||||
user.oderId = newOderId;
|
||||
user.displayName = normalizeDisplayName(message['displayName'], normalizeDisplayName(user.displayName));
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(message, 'description')) {
|
||||
user.description = normalizeDescription(message['description']);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(message, 'profileUpdatedAt')) {
|
||||
user.profileUpdatedAt = normalizeProfileUpdatedAt(message['profileUpdatedAt']);
|
||||
}
|
||||
|
||||
user.connectionScope = newScope;
|
||||
connectedUsers.set(connectionId, user);
|
||||
console.log(`User identified: ${user.displayName} (${user.oderId})`);
|
||||
|
||||
if (
|
||||
user.displayName === previousDisplayName
|
||||
&& user.description === previousDescription
|
||||
&& user.profileUpdatedAt === previousProfileUpdatedAt
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const serverId of user.serverIds) {
|
||||
broadcastToServer(serverId, {
|
||||
type: 'user_joined',
|
||||
oderId: user.oderId,
|
||||
displayName: normalizeDisplayName(user.displayName),
|
||||
description: user.description,
|
||||
profileUpdatedAt: user.profileUpdatedAt,
|
||||
status: user.status ?? 'online',
|
||||
serverId
|
||||
}, user.oderId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJoinServer(user: ConnectedUser, message: WsMessage, connectionId: string): Promise<void> {
|
||||
@@ -108,6 +162,8 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect
|
||||
type: 'user_joined',
|
||||
oderId: user.oderId,
|
||||
displayName: normalizeDisplayName(user.displayName),
|
||||
description: user.description,
|
||||
profileUpdatedAt: user.profileUpdatedAt,
|
||||
status: user.status ?? 'online',
|
||||
serverId: sid
|
||||
}, user.oderId);
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface ConnectedUser {
|
||||
serverIds: Set<string>;
|
||||
viewedServerId?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
profileUpdatedAt?: number;
|
||||
/**
|
||||
* Opaque scope string sent by the client (typically the signal URL it
|
||||
* connected through). Stale-connection eviction only targets connections
|
||||
|
||||
Reference in New Issue
Block a user