import { randomUUID } from 'node:crypto'; import { BadRequestException, Body, Controller, Get, Headers, HttpCode, Param, Post, Query, } from '@nestjs/common'; import { ThreadsService } from './threads.service'; import { MessageService } from '../messaging/message.service'; import { SessionVerifier } from '../platform/session.verifier'; import { SendMessageDto } from './send-message.dto'; @Controller('v1/threads') export class ThreadsController { constructor( private readonly threads: ThreadsService, private readonly messages: MessageService, private readonly session: SessionVerifier, ) {} /** * Generic "my threads" — every thread the caller participates in (last message + unread). * `?metadata[key]=value` narrows to threads whose opaque attribute bag matches (equality on each key). */ @Get() async listThreads(@Headers('authorization') auth?: string, @Query('metadata') metadata?: Record) { const metaFilter = metadata && typeof metadata === 'object' ? metadata : undefined; return this.messages.listThreads(this.principal(auth), metaFilter ? { metadata: metaFilter } : undefined); } /** Generic "my annotated messages" (e.g. ?type=save for a personal bookmarks list). */ @Get('my-annotations') async myAnnotations(@Headers('authorization') auth?: string, @Query('type') type = 'save') { return this.messages.listMyAnnotated(this.principal(auth), type); } /** Create a thread; `membership`/`creatorRole`/`subject`/`metadata` are opaque, app-supplied attributes the kernel stores but never interprets. */ @Post() @HttpCode(201) async createThread(@Body() body: { membership?: string; creatorRole?: string; subject?: string; metadata?: Record }, @Headers('authorization') auth?: string) { return this.messages.openThread(null, this.principal(auth), { membership: body?.membership, creatorRole: body?.creatorRole, subject: body?.subject, metadata: body?.metadata }); } /** Governed membership: add a user (by userId) to a thread — policy enforces DM cap / roles. */ @Post(':id/participants') @HttpCode(201) async addParticipant(@Param('id') id: string, @Body() body: { userId: string; role?: string }, @Headers('authorization') auth?: string) { return this.messages.addParticipant(id, this.principal(auth), body.userId, body.role); } @Get(':id/messages') async listMessages(@Param('id') id: string) { return this.threads.getMessages(id); } /** Mute / unmute notifications from this thread for the caller. */ @Post(':id/mute') @HttpCode(200) async mute(@Param('id') id: string, @Headers('authorization') auth?: string) { return this.messages.muteThread(id, this.principal(auth), true); } @Post(':id/unmute') @HttpCode(200) async unmute(@Param('id') id: string, @Headers('authorization') auth?: string) { return this.messages.muteThread(id, this.principal(auth), false); } /** REST/polling fallback for native send (same write as the socket path). */ @Post(':id/messages') @HttpCode(201) async send( @Param('id') id: string, @Body() body: SendMessageDto, @Headers('authorization') authorization?: string, @Headers('idempotency-key') idempotencyKey?: string, ) { return this.messages.send( id, this.principal(authorization), { content: body.content, contentRef: body.contentRef, mimeType: body.mimeType, sizeBytes: body.sizeBytes, checksumSha256: body.checksumSha256 }, idempotencyKey ?? randomUUID(), undefined, body.parentInteractionId, ); } private principal(authorization?: string) { const token = (authorization ?? '').replace(/^Bearer\s+/i, ''); if (!token) throw new BadRequestException('Authorization bearer token is required'); return this.session.verify(token); } }