feat(guild-events): add webhook event envelope and message lifecycle emits
This commit is contained in:
@@ -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 {}
|
||||
|
||||
9
Fabric.Backend.Guild/src/events/event-envelope.ts
Normal file
9
Fabric.Backend.Guild/src/events/event-envelope.ts
Normal 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>;
|
||||
};
|
||||
9
Fabric.Backend.Guild/src/events/events.module.ts
Normal file
9
Fabric.Backend.Guild/src/events/events.module.ts
Normal 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 {}
|
||||
57
Fabric.Backend.Guild/src/events/events.service.ts
Normal file
57
Fabric.Backend.Guild/src/events/events.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user