feat(guild): wake_mapping, per-recipient wakeup, discuss/work turn engine, channel join/leave
- wake_mapping table; triage onDuty (auto-added member) / custom listeners - per-recipient wakeup metadata on message.created (one message-id; added only at push). Rules: author=false; triage/custom=wake_mapping only; general=all; report=none - discuss/work rotation: channel_turn_state (order/currentSpeaker/round events/cross-round no-reply streak); null activation, queue-jump, /no-reply pass, all-/no-reply pause, end-of-round shuffle (trailing no-reply run to tail, head shuffled, first != last normal speaker) - slash-command registry (/no-reply, /force-proceed); registered commands intercepted and never delivered; guild-authored /ack persisted - POST /channels/:id/join|leave; leave cleans channel_members, wake_mapping and turn-state order Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,34 @@ import { Logger } from '@nestjs/common';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { introspectGuildToken } from '../common/center-auth';
|
||||
|
||||
type XType = 'general' | 'work' | 'report' | 'discuss' | 'triage' | 'custom';
|
||||
|
||||
// Wakeup for non-rotating channels only (general/report/triage/custom).
|
||||
// discuss/work go through TurnService + emitMessageTargeted, never here.
|
||||
// Precedence:
|
||||
// 1. the author never gets woken by their own message
|
||||
// 2. triage/custom: only wake users in the channel's wake_mapping
|
||||
// 3. general: wake everyone
|
||||
// 4. report (and anything else): wake nobody
|
||||
export function computeWakeup(args: {
|
||||
xType: XType;
|
||||
recipientUserId: string;
|
||||
authorUserId: string;
|
||||
wakeUserIds: Set<string>;
|
||||
}): boolean {
|
||||
const { xType, recipientUserId, authorUserId, wakeUserIds } = args;
|
||||
if (recipientUserId === authorUserId) return false;
|
||||
switch (xType) {
|
||||
case 'general':
|
||||
return true;
|
||||
case 'triage':
|
||||
case 'custom':
|
||||
return wakeUserIds.has(recipientUserId);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@WebSocketGateway({
|
||||
namespace: '/realtime',
|
||||
cors: {
|
||||
@@ -133,4 +161,38 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
|
||||
emitChannelEvent(channelId: string, event: string, data: Record<string, unknown>): void {
|
||||
this.server.to(`channel:${channelId}`).emit(event, data);
|
||||
}
|
||||
|
||||
// Emits message.created per-recipient so each carries its own `wakeup` flag.
|
||||
async emitMessageCreated(
|
||||
channelId: string,
|
||||
data: Record<string, unknown>,
|
||||
ctx: { xType: XType; authorUserId: string; wakeUserIds: Set<string> },
|
||||
): Promise<void> {
|
||||
const sockets = await this.server.in(`channel:${channelId}`).fetchSockets();
|
||||
for (const s of sockets) {
|
||||
const recipientUserId = typeof s.data.userId === 'string' ? s.data.userId : `anon:${s.id}`;
|
||||
const wakeup = computeWakeup({
|
||||
xType: ctx.xType,
|
||||
recipientUserId,
|
||||
authorUserId: ctx.authorUserId,
|
||||
wakeUserIds: ctx.wakeUserIds,
|
||||
});
|
||||
s.emit('message.created', { ...data, wakeup });
|
||||
}
|
||||
}
|
||||
|
||||
// discuss/work + /ack: exactly one recipient (the new current speaker) gets
|
||||
// wakeup=true; everyone else false. One message-id; metadata at push only.
|
||||
async emitMessageTargeted(
|
||||
channelId: string,
|
||||
data: Record<string, unknown>,
|
||||
wakeupUserId: string | null,
|
||||
): Promise<void> {
|
||||
const sockets = await this.server.in(`channel:${channelId}`).fetchSockets();
|
||||
for (const s of sockets) {
|
||||
const recipientUserId = typeof s.data.userId === 'string' ? s.data.userId : `anon:${s.id}`;
|
||||
const wakeup = wakeupUserId !== null && recipientUserId === wakeupUserId;
|
||||
s.emit('message.created', { ...data, wakeup });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user