- 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.
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
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}`;
|
|
}
|