Files
Fabric.Backend.Guild/src/channels/channels.controller.ts
hzhang b3fcefb5ec feat(channels): bypass-list for discuss/work rotation
- channel_turn_state.bypass_user_ids: order and bypass form a disjoint
  partition of the channel members; bypass excluded from rotation.
- initForChannel(channelId, members, bypass=[]) computes order = members
  − bypass; create() passes bypassUserIds (∩ members) for discuss/work.
- pushFrame() enforces mention nesting cap: max 4 sub-frames (5 levels
  incl. root); overflow evicts the bottom-most (root->A..D + E => root->B..E).
- mention sites use pushFrame so bypass members are only transiently
  pulled in via @-mention, then return to bypass on pop.
- moveToBypass(): move an order member to bypass mid-rotation; if current
  speaker, successor takes over. onMemberRemoved also strips bypass.
- POST /channels/:id/bypass; GET :id/members now returns {userId,bypass}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:26:18 +01:00

84 lines
3.0 KiB
TypeScript

import { Body, Controller, Get, Param, Post, Query, Req, UnauthorizedException } from '@nestjs/common';
import { ChannelsService } from './channels.service.js';
// ApiKeyGuard attaches the introspected Center user id onto the request.
type AuthedRequest = { userId?: string };
@Controller('channels')
export class ChannelsController {
constructor(private readonly channelsService: ChannelsService) {}
@Get()
list(@Req() req: AuthedRequest, @Query('guildId') guildId?: string) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.listForUser(String(guildId ?? ''), userId);
}
@Post()
create(@Req() req: AuthedRequest, @Body() body: Record<string, unknown>) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.create(
{
guildId: body.guildId as string | undefined,
name: body.name as string | undefined,
kind: body.kind as string | undefined,
xType: body.xType as string | undefined,
isPublic: Boolean(body.isPublic),
memberUserIds: Array.isArray(body.memberUserIds) ? (body.memberUserIds as string[]) : [],
onDuty: body.onDuty as string | undefined,
listeners: Array.isArray(body.listeners) ? (body.listeners as string[]) : [],
bypassUserIds: Array.isArray(body.bypassUserIds)
? (body.bypassUserIds as string[])
: [],
},
userId,
);
}
// Move an order member into the bypass list (discuss/work only).
@Post(':id/bypass')
bypass(
@Req() req: AuthedRequest,
@Param('id') channelId: string,
@Body() body: Record<string, unknown>,
) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.moveToBypass(
channelId,
userId,
String(body.userId ?? ''),
);
}
@Get(':id/members')
members(@Req() req: AuthedRequest, @Param('id') channelId: string) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.channelMembers(channelId);
}
@Post(':id/join')
join(@Req() req: AuthedRequest, @Param('id') channelId: string) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.joinChannel(channelId, userId);
}
@Post(':id/leave')
leave(@Req() req: AuthedRequest, @Param('id') channelId: string) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.leaveChannel(channelId, userId);
}
@Post(':id/close')
close(@Req() req: AuthedRequest, @Param('id') channelId: string) {
const userId = req.userId ?? '';
if (!userId) throw new UnauthorizedException('missing user');
return this.channelsService.closeChannel(channelId, userId);
}
}