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/.
199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
/**
|
|
* 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(),
|
|
};
|
|
},
|
|
};
|
|
}
|