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/.
This commit is contained in:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { createMockVideoConnector, createMockVideoState } from "./index.js";
import { connectorContractTests, videoConnectorContractTests } from "@tactical-media-broadcast-hub/connector-sdk/testing";
import type { ConnectorConnection } from "@tactical-media-broadcast-hub/core";
const connection: ConnectorConnection = {
id: "conn-video",
workspaceId: "ws-1",
provider: "mock-video",
channelId: "ch-1",
credentialId: { kind: "credential-ref", id: "cred-1" },
fields: {},
createdAt: new Date(),
};
connectorContractTests(createMockVideoConnector(createMockVideoState()), { connection });
videoConnectorContractTests(createMockVideoConnector(createMockVideoState()), { connection });
describe("mock-video processing machine", () => {
it("reaches ready after enough status polls", async () => {
const connector = createMockVideoConnector(createMockVideoState());
const upload = await connector.uploadVideo?.(connection, {
videoAssetId: "asset-1",
visibility: "provider_default",
formatIntent: "long_form",
providerMetadata: {},
});
expect(upload?.remoteId).toBeTruthy();
let status = await connector.getVideoStatus?.(connection, upload!.remoteId);
expect(status?.state).toBe("processing");
status = await connector.getVideoStatus?.(connection, upload!.remoteId);
expect(status?.state).toBe("ready");
});
});

View file

@ -0,0 +1,116 @@
/**
* Deterministic video mock: upload → processing → ready state machine,
* library pagination, thumbnails/captions capability.
*/
import type {
ConnectorCapabilities,
ConnectorConnection,
NormalizedIdentity,
NormalizedVideoPublication,
Page,
VideoProcessingStatus,
} from "@tactical-media-broadcast-hub/core";
import {
assertValidManifest,
pageOf,
type ConnectorManifest,
type PlatformConnector,
} from "@tactical-media-broadcast-hub/connector-sdk";
const CAPABILITIES: ConnectorCapabilities = {
publishVideo: true,
longFormVideo: true,
videoThumbnails: true,
videoCaptions: true,
playlists: true,
videoAnalytics: true,
};
let seq = 0;
const nextId = (prefix: string): string => `${prefix}-${(seq += 1)}`;
interface MockVideo {
remoteId: string;
title?: string;
createdAt: Date;
processingAdvance: number;
}
export interface MockVideoState {
videos: MockVideo[];
}
export function createMockVideoState(): MockVideoState {
return { videos: [] };
}
export function createMockVideoConnector(
state: MockVideoState = createMockVideoState(),
): PlatformConnector {
const manifest: ConnectorManifest = {
id: "mock-video",
displayName: "Mock Video",
capabilities: CAPABILITIES,
connectionFields: [{ name: "apiToken", label: "API token", required: true, secret: true }],
};
return {
manifest: () => {
assertValidManifest(manifest);
return manifest;
},
async authorize() {
throw new Error("mock-video uses token auth in tests only");
},
async getIdentity(connection: ConnectorConnection): Promise<NormalizedIdentity> {
return {
provider: manifest.id,
remoteId: connection.channelId,
handle: "mock@video",
displayName: "Mock Video Channel",
};
},
async uploadVideo(
_connection: ConnectorConnection,
video: NormalizedVideoPublication,
): Promise<{ remoteId: string }> {
const remoteId = nextId("vid");
state.videos.push({
remoteId,
title: video.title,
createdAt: new Date(),
processingAdvance: 0,
});
return { remoteId };
},
async getVideoStatus(
_connection: ConnectorConnection,
remoteId: string,
): Promise<VideoProcessingStatus> {
const video = state.videos.find((v) => v.remoteId === remoteId);
if (video === undefined) {
return { remoteId, state: "failed", error: "unknown remote id" };
}
video.processingAdvance = Math.min(100, video.processingAdvance + 50);
const state_: VideoProcessingStatus["state"] =
video.processingAdvance >= 100 ? "ready" : "processing";
return { remoteId, state: state_, progressPercent: video.processingAdvance };
},
async getVideos(
_connection: ConnectorConnection,
request: { cursor?: string; limit: number },
): Promise<Page<{ remoteId: string; title?: string; createdAt?: Date }>> {
const start = request.cursor ? Number(request.cursor) : 0;
const items = state.videos
.slice(start, start + request.limit)
.map((v) => ({ remoteId: v.remoteId, title: v.title, createdAt: v.createdAt }));
const next = start + request.limit;
return next < state.videos.length ? pageOf(items, String(next)) : pageOf(items);
},
};
}