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/.
53 lines
2.9 KiB
TypeScript
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 }] };
|
|
}
|
|
}
|