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:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
199
packages/connector-mock-social/src/index.ts
Normal file
199
packages/connector-mock-social/src/index.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* Deterministic social mock: publish (text/images/carousel), feed, comments,
|
||||
* replies, webhooks. In-memory state — CI runs need no network.
|
||||
*/
|
||||
import type {
|
||||
Comment,
|
||||
ConnectorCapabilities,
|
||||
ConnectorConnection,
|
||||
FeedItem,
|
||||
NormalizedIdentity,
|
||||
NormalizedPost,
|
||||
NormalizedWebhookEvent,
|
||||
Page,
|
||||
Publication,
|
||||
} from "@tactical-media-broadcast-hub/core";
|
||||
import {
|
||||
assertValidManifest,
|
||||
pageOf,
|
||||
type ConnectorManifest,
|
||||
type PlatformConnector,
|
||||
} from "@tactical-media-broadcast-hub/connector-sdk";
|
||||
|
||||
const CAPABILITIES: ConnectorCapabilities = {
|
||||
publishText: true,
|
||||
publishImages: true,
|
||||
publishCarousel: true,
|
||||
feed: true,
|
||||
mentions: true,
|
||||
comments: true,
|
||||
replies: true,
|
||||
amplifyRepost: true,
|
||||
amplifyQuote: true,
|
||||
webhooks: true,
|
||||
deletePublication: true,
|
||||
};
|
||||
|
||||
let seq = 0;
|
||||
const nextId = (prefix: string): string => `${prefix}-${(seq += 1)}`;
|
||||
|
||||
export interface MockSocialState {
|
||||
publications: Map<string, Publication>;
|
||||
feed: FeedItem[];
|
||||
comments: Map<string, Comment[]>;
|
||||
}
|
||||
|
||||
export function createMockSocialState(): MockSocialState {
|
||||
return { publications: new Map(), feed: [], comments: new Map() };
|
||||
}
|
||||
|
||||
export function createMockSocialConnector(
|
||||
state: MockSocialState = createMockSocialState(),
|
||||
): PlatformConnector {
|
||||
const manifest: ConnectorManifest = {
|
||||
id: "mock-social",
|
||||
displayName: "Mock Social",
|
||||
capabilities: CAPABILITIES,
|
||||
connectionFields: [{ name: "apiToken", label: "API token", required: true, secret: true }],
|
||||
webhookRoute: "/webhooks/mock-social",
|
||||
};
|
||||
|
||||
return {
|
||||
manifest: () => {
|
||||
assertValidManifest(manifest);
|
||||
return manifest;
|
||||
},
|
||||
|
||||
async authorize() {
|
||||
throw new Error("mock-social uses token auth in tests only");
|
||||
},
|
||||
|
||||
async getIdentity(connection: ConnectorConnection): Promise<NormalizedIdentity> {
|
||||
return {
|
||||
provider: manifest.id,
|
||||
remoteId: connection.channelId,
|
||||
handle: "mock@social",
|
||||
displayName: "Mock Social Account",
|
||||
};
|
||||
},
|
||||
|
||||
async publish(connection: ConnectorConnection, post: NormalizedPost): Promise<Publication> {
|
||||
const publication: Publication = {
|
||||
id: nextId("pub"),
|
||||
workspaceId: connection.workspaceId,
|
||||
draftId: "direct",
|
||||
connectionId: connection.id,
|
||||
remoteId: nextId("remote"),
|
||||
publishedAt: new Date(),
|
||||
providerMetadata: post.providerMetadata,
|
||||
};
|
||||
state.publications.set(publication.remoteId, publication);
|
||||
state.feed.unshift({
|
||||
id: publication.remoteId,
|
||||
provider: manifest.id,
|
||||
remoteId: publication.remoteId,
|
||||
authorHandle: "mock@social",
|
||||
text: post.text,
|
||||
mediaUrls: [],
|
||||
createdAt: publication.publishedAt,
|
||||
metrics: {},
|
||||
});
|
||||
return publication;
|
||||
},
|
||||
|
||||
async deletePublication(_connection: ConnectorConnection, remoteId: string): Promise<void> {
|
||||
state.publications.delete(remoteId);
|
||||
state.feed = state.feed.filter((item) => item.remoteId !== remoteId);
|
||||
},
|
||||
|
||||
async getFeed(
|
||||
_connection: ConnectorConnection,
|
||||
request: { kind: "home" | "mentions" | "search"; query?: string; cursor?: string; limit: number },
|
||||
): Promise<Page<FeedItem>> {
|
||||
const filtered =
|
||||
request.kind === "search" && request.query
|
||||
? state.feed.filter((item) => item.text.includes(request.query ?? ""))
|
||||
: state.feed;
|
||||
const start = request.cursor ? Number(request.cursor) : 0;
|
||||
const items = filtered.slice(start, start + request.limit);
|
||||
const next = start + request.limit;
|
||||
return next < filtered.length ? pageOf(items, String(next)) : pageOf(items);
|
||||
},
|
||||
|
||||
async getComments(
|
||||
_connection: ConnectorConnection,
|
||||
request: { onRemoteId: string; cursor?: string; limit: number },
|
||||
): Promise<Page<Comment>> {
|
||||
const all = state.comments.get(request.onRemoteId) ?? [];
|
||||
return pageOf(all.slice(0, request.limit));
|
||||
},
|
||||
|
||||
async reply(
|
||||
connection: ConnectorConnection,
|
||||
request: { onRemoteId: string; text: string },
|
||||
): Promise<Publication> {
|
||||
const existing = state.comments.get(request.onRemoteId) ?? [];
|
||||
const comment: Comment = {
|
||||
id: nextId("comment"),
|
||||
provider: manifest.id,
|
||||
remoteId: nextId("cremote"),
|
||||
onRemoteId: request.onRemoteId,
|
||||
authorHandle: "mock@social",
|
||||
text: request.text,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
existing.push(comment);
|
||||
state.comments.set(request.onRemoteId, existing);
|
||||
return {
|
||||
id: comment.id,
|
||||
workspaceId: connection.workspaceId,
|
||||
draftId: "reply",
|
||||
connectionId: connection.id,
|
||||
remoteId: comment.remoteId,
|
||||
publishedAt: comment.createdAt,
|
||||
providerMetadata: {},
|
||||
};
|
||||
},
|
||||
|
||||
async amplify(
|
||||
connection: ConnectorConnection,
|
||||
request: { kind: "repost" | "quote" | "share"; targetRemoteId: string; commentary?: string },
|
||||
): Promise<Publication> {
|
||||
const target = state.feed.find((item) => item.remoteId === request.targetRemoteId);
|
||||
if (target === undefined) throw new Error(`no item ${request.targetRemoteId} to amplify`);
|
||||
const amplified: Publication = {
|
||||
id: nextId("amp"),
|
||||
workspaceId: connection.workspaceId,
|
||||
draftId: `amplify:${request.kind}`,
|
||||
connectionId: connection.id,
|
||||
remoteId: nextId("remote"),
|
||||
publishedAt: new Date(),
|
||||
providerMetadata: { amplifies: request.targetRemoteId, kind: request.kind },
|
||||
};
|
||||
state.feed.unshift({
|
||||
id: amplified.remoteId,
|
||||
provider: manifest.id,
|
||||
remoteId: amplified.remoteId,
|
||||
authorHandle: "mock@social",
|
||||
text: request.kind === "quote" ? `${request.commentary ?? ""} ↻ ${target.text}` : `↻ ${target.text}`,
|
||||
mediaUrls: [],
|
||||
createdAt: amplified.publishedAt,
|
||||
metrics: {},
|
||||
});
|
||||
return amplified;
|
||||
},
|
||||
|
||||
async subscribeWebhooks() {
|
||||
return { subscriptionId: nextId("hook") };
|
||||
},
|
||||
|
||||
async handleWebhook(request: { headers: Record<string, string>; body: unknown }): Promise<NormalizedWebhookEvent> {
|
||||
return {
|
||||
provider: manifest.id,
|
||||
eventType: "mock.event",
|
||||
payload: (request.body as Record<string, unknown>) ?? {},
|
||||
receivedAt: new Date(),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue