Stage 5: Add Prisma integration and enhance mission management
- Introduced Prisma as a dependency in package.json and updated pnpm-lock.yaml. - Created Prisma module and service for database interactions. - Added initial Prisma schema and migration for user, survivor, mission, and related entities. - Implemented throttling in the API using @nestjs/throttler for rate limiting. - Enhanced mission management logic to utilize Prisma for database transactions. - Updated missions controller and service to handle mission state and participant management. - Added Twitch PubSub service for real-time updates on mission states.
This commit is contained in:
72
apps/api/src/app/tick-engine/twitch-pubsub.service.ts
Normal file
72
apps/api/src/app/tick-engine/twitch-pubsub.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { createHmac } from 'crypto';
|
||||
import type { MissionStateResponse } from '@fog-explorer/api-interfaces';
|
||||
|
||||
const PUBSUB_URL = 'https://api.twitch.tv/extensions/message';
|
||||
const TOKEN_TTL_SECONDS = 60;
|
||||
|
||||
@Injectable()
|
||||
export class TwitchPubSubService {
|
||||
private readonly logger = new Logger(TwitchPubSubService.name);
|
||||
|
||||
async broadcast(
|
||||
channelId: string | null,
|
||||
state: NonNullable<MissionStateResponse>
|
||||
): Promise<void> {
|
||||
if (!channelId) return;
|
||||
|
||||
const clientId = process.env['TWITCH_CLIENT_ID'];
|
||||
const secret = process.env['TWITCH_EXTENSION_SECRET'];
|
||||
if (!clientId || !secret) return;
|
||||
|
||||
const token = buildServerToken(clientId, secret);
|
||||
const payload = JSON.stringify({
|
||||
content_type: 'application/json',
|
||||
message: JSON.stringify(state),
|
||||
targets: ['broadcast'],
|
||||
});
|
||||
|
||||
if (Buffer.byteLength(payload, 'utf8') > 5000) {
|
||||
this.logger.warn({ message: 'pubsub payload too large, skipping', channelId });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${PUBSUB_URL}/${channelId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Client-Id': clientId,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: payload,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
this.logger.warn({
|
||||
message: 'pubsub broadcast failed',
|
||||
channelId,
|
||||
status: res.status,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ message: 'pubsub broadcast error', channelId, err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildServerToken(clientId: string, secretB64: string): string {
|
||||
const secretBytes = Buffer.from(secretB64, 'base64');
|
||||
const exp = Math.floor(Date.now() / 1000) + TOKEN_TTL_SECONDS;
|
||||
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ exp, user_id: clientId, role: 'external' })
|
||||
).toString('base64url');
|
||||
|
||||
const sig = createHmac('sha256', secretBytes)
|
||||
.update(`${header}.${payload}`)
|
||||
.digest('base64url');
|
||||
|
||||
return `${header}.${payload}.${sig}`;
|
||||
}
|
||||
Reference in New Issue
Block a user