From b1f746716100977b6ab1564a09a8f4494a11de14 Mon Sep 17 00:00:00 2001 From: hzhang Date: Mon, 18 May 2026 09:18:19 +0100 Subject: [PATCH] feat(guild): add 'dm' x-type (private 1:1, always-wake) channel enum + X_TYPES + realtime XType gain 'dm'. dm channels are forced private (never public) and non-unique (no dedup; create() always makes a fresh one). computeWakeup: dm wakes every non-author participant unconditionally (no rotation / no wake_mapping). The message.created realtime payload now carries xType so the plugin can treat dm specially. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/channels/channels.service.ts | 11 ++++++++--- src/entities/channel.entity.ts | 4 ++-- src/realtime/realtime.gateway.ts | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/channels/channels.service.ts b/src/channels/channels.service.ts index ab61067..e7575bc 100644 --- a/src/channels/channels.service.ts +++ b/src/channels/channels.service.ts @@ -6,7 +6,7 @@ import { ChannelMember } from '../entities/channel-member.entity.js'; import { WakeMapping } from '../entities/wake-mapping.entity.js'; import { TurnService } from './turn.service.js'; -const X_TYPES = ['general', 'work', 'report', 'discuss', 'triage', 'custom'] as const; +const X_TYPES = ['general', 'work', 'report', 'discuss', 'triage', 'custom', 'dm'] as const; type XType = (typeof X_TYPES)[number]; type CreateChannelInput = { @@ -130,14 +130,19 @@ export class ChannelsService { .map((x) => String(x ?? '').trim()) .filter(Boolean); + // dm channels are always private (a 1:1 conversation); never public. + // dm is not unique — multiple dm channels between the same users are + // allowed (create() always makes a fresh one, no dedup). + const isPublic = xType === 'dm' ? false : Boolean(input.isPublic); + const channel = await this.channelRepo.save( this.channelRepo.create({ guildId, name, xType, kind: input.kind === 'announcement' ? 'announcement' : 'text', - isPrivate: !input.isPublic, - isPublic: Boolean(input.isPublic), + isPrivate: !isPublic, + isPublic, lastSeq: 0, }), ); diff --git a/src/entities/channel.entity.ts b/src/entities/channel.entity.ts index c21ee6b..aef082a 100644 --- a/src/entities/channel.entity.ts +++ b/src/entities/channel.entity.ts @@ -16,9 +16,9 @@ export class Channel { @Column({ name: 'x_type', type: 'enum', - enum: ['general', 'work', 'report', 'discuss', 'triage', 'custom'], + enum: ['general', 'work', 'report', 'discuss', 'triage', 'custom', 'dm'], }) - xType!: 'general' | 'work' | 'report' | 'discuss' | 'triage' | 'custom'; + xType!: 'general' | 'work' | 'report' | 'discuss' | 'triage' | 'custom' | 'dm'; @Column({ type: 'varchar', length: 16, default: 'text' }) kind!: 'text' | 'announcement'; diff --git a/src/realtime/realtime.gateway.ts b/src/realtime/realtime.gateway.ts index f575e43..ce680cf 100644 --- a/src/realtime/realtime.gateway.ts +++ b/src/realtime/realtime.gateway.ts @@ -11,7 +11,7 @@ import { Logger } from '@nestjs/common'; import { Server, Socket } from 'socket.io'; import { introspectGuildToken } from '../common/center-auth.js'; -type XType = 'general' | 'work' | 'report' | 'discuss' | 'triage' | 'custom'; +type XType = 'general' | 'work' | 'report' | 'discuss' | 'triage' | 'custom' | 'dm'; // Wakeup for non-rotating channels only (general/report/triage/custom). // discuss/work go through TurnService + emitMessageTargeted, never here. @@ -40,6 +40,9 @@ export function computeWakeup(args: { case 'triage': case 'custom': return wakeUserIds.has(recipientUserId); + case 'dm': + // 1:1 conversation: every non-author participant is always woken. + return true; default: return false; } @@ -189,7 +192,7 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect wakeUserIds: ctx.wakeUserIds, mentionUserIds: ctx.mentionUserIds, }); - s.emit('message.created', { ...data, channelId, wakeup }); + s.emit('message.created', { ...data, channelId, wakeup, xType: ctx.xType }); } }