feat(guild-events): add webhook event envelope and message lifecycle emits

This commit is contained in:
nav
2026-05-12 11:24:43 +00:00
parent 8ca5d68ba4
commit 37ec670280
6 changed files with 114 additions and 2 deletions

View File

@@ -5,9 +5,16 @@ import { HealthController } from './common/health.controller';
import { GuildsModule } from './guilds/guilds.module';
import { ChannelsModule } from './channels/channels.module';
import { MessagingModule } from './messaging/messaging.module';
import { EventsModule } from './events/events.module';
@Module({
imports: [TypeOrmModule.forRoot(buildTypeOrmConfig()), GuildsModule, ChannelsModule, MessagingModule],
imports: [
TypeOrmModule.forRoot(buildTypeOrmConfig()),
EventsModule,
GuildsModule,
ChannelsModule,
MessagingModule,
],
controllers: [HealthController],
})
export class AppModule {}

View File

@@ -0,0 +1,9 @@
export type FabricEventEnvelope = {
event_id: string;
event_type: string;
occurred_at: string;
guild_id: string | null;
channel_id: string | null;
actor_id: string | null;
data: Record<string, unknown>;
};

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { EventsService } from './events.service';
@Global()
@Module({
providers: [EventsService],
exports: [EventsService],
})
export class EventsModule {}

View File

@@ -0,0 +1,57 @@
import { Injectable, Logger } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { FabricEventEnvelope } from './event-envelope';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
buildEnvelope(input: {
eventType: string;
guildId?: string | null;
channelId?: string | null;
actorId?: string | null;
data: Record<string, unknown>;
}): FabricEventEnvelope {
return {
event_id: randomUUID(),
event_type: input.eventType,
occurred_at: new Date().toISOString(),
guild_id: input.guildId ?? null,
channel_id: input.channelId ?? null,
actor_id: input.actorId ?? null,
data: input.data,
};
}
async emit(input: {
eventType: string;
guildId?: string | null;
channelId?: string | null;
actorId?: string | null;
data: Record<string, unknown>;
}): Promise<FabricEventEnvelope> {
const envelope = this.buildEnvelope(input);
const webhookUrl = process.env.FABRIC_WEBHOOK_URL;
if (!webhookUrl) {
this.logger.log(`event(no-webhook): ${JSON.stringify(envelope)}`);
return envelope;
}
try {
await fetch(webhookUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(envelope),
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(`event delivery failed: ${message}`);
}
return envelope;
}
}

View File

@@ -16,6 +16,7 @@ import { CreateMessageDto } from './dto.create-message.dto';
import { Channel } from '../entities/channel.entity';
import { Message } from '../entities/message.entity';
import { IdempotencyRecord } from '../entities/idempotency-record.entity';
import { EventsService } from '../events/events.service';
const EDIT_WINDOW_MS = 15 * 60 * 1000;
const DEFAULT_PAGE_LIMIT = 50;
@@ -31,6 +32,7 @@ export class MessagingController {
private readonly messageRepo: Repository<Message>,
@InjectRepository(IdempotencyRecord)
private readonly idemRepo: Repository<IdempotencyRecord>,
private readonly events: EventsService,
) {}
private async getIdempotentResponse(
@@ -115,6 +117,14 @@ export class MessagingController {
const responseBody = this.toView(message) as Record<string, unknown>;
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
await this.events.emit({
eventType: 'message.created',
channelId,
actorId: body.authorUserId ?? 'anonymous',
data: responseBody,
});
return responseBody;
}
@@ -143,6 +153,14 @@ export class MessagingController {
const saved = await this.messageRepo.save(item);
const responseBody = this.toView(saved) as Record<string, unknown>;
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
await this.events.emit({
eventType: 'message.updated',
channelId,
actorId: saved.authorUserId,
data: responseBody,
});
return responseBody;
}
@@ -172,6 +190,18 @@ export class MessagingController {
messageId,
} as Record<string, unknown>;
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
await this.events.emit({
eventType: 'message.deleted',
channelId,
actorId: item.authorUserId,
data: {
messageId,
seq: item.seq,
deletedAt: item.deletedAt?.toISOString() ?? null,
},
});
return responseBody;
}