T-HUB/apps/api/src/live-chat/live-chat.module.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

53 lines
2.9 KiB
TypeScript

import { Module, type DynamicModule } from "@nestjs/common";
import { withTenant } from "../db/client.js";
import { LIVE_CHAT_RUNTIME, LiveChatRuntime, type LiveChatRuntimeOptions } from "./runtime.js";
import { ChatRealtimeBridge } from "./bridge.js";
import { LiveChatGuard } from "./live-chat.guard.js";
import { DeviceService } from "./devices.js";
import { LiveChatController } from "./live-chat.controller.js";
import { LiveChatHubService } from "./live-chat.service.js";
import { ChatRingBuffer } from "./ring-buffer.js";
import { RedisChatGovernors, type ChatGovernors } from "./redis-governors.js";
import { SendGovernors } from "./governors.js";
import type { ChatTarget } from "./types.js";
/** Central review imports this module with credential-bound transports,
* capability targets and ingestion drivers via LiveChatModule.register(). */
function defaultTargets(): (workspaceId: string, channelId: string) => Promise<ChatTarget[]> {
return async (workspaceId, channelId) => {
const result = await withTenant(workspaceId, client => client.query<{ connection_id: string; provider: string; live_remote_id: string | null }>(
`SELECT vc.id AS connection_id, vc.provider, vcs.remote_ref AS live_remote_id
FROM virtual_channel_sources vcs
JOIN connections vc ON vc.id = vcs.connection_id AND vc.workspace_id = vcs.workspace_id
WHERE vcs.workspace_id = $1 AND vcs.virtual_channel_id = $2`, [workspaceId, channelId]));
return result.rows.map(row => ({
connectionId: row.connection_id,
provider: row.provider,
liveRemoteId: row.live_remote_id ?? row.connection_id,
read: false, reply: false, moderate: false, state: "degraded" as const,
reason: "chat adapter not configured",
}));
};
}
@Module({
controllers: [LiveChatController],
providers: [
{ provide: ChatRingBuffer, useValue: new ChatRingBuffer(500) },
{ provide: SendGovernors, useFactory: () => new RedisChatGovernors({ buckets: { twitch: { capacity: 100, refillPerSecond: 100 / 30 } }, quotas: { youtube: { budget: 10000, costPerCall: 50 } } }) },
{ provide: ChatRealtimeBridge, useFactory: () => new ChatRealtimeBridge() },
{
provide: LiveChatHubService,
useFactory: (buffer: ChatRingBuffer, governors: ChatGovernors, bridge: ChatRealtimeBridge, runtime?: LiveChatRuntimeOptions) =>
new LiveChatHubService({ buffer, governors, publish: bridge.publish, targets: defaultTargets(), ...runtime }),
inject: [ChatRingBuffer, SendGovernors, ChatRealtimeBridge, { token: LIVE_CHAT_RUNTIME, optional: true }],
},
DeviceService, LiveChatGuard, LiveChatRuntime,
],
exports: [LiveChatHubService, DeviceService, ChatRealtimeBridge],
})
export class LiveChatModule {
static register(options: LiveChatRuntimeOptions): DynamicModule {
return { module: LiveChatModule, providers: [{ provide: LIVE_CHAT_RUNTIME, useValue: options }] };
}
}