T-HUB/packages/connector-mock-social/src/index.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

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(),
};
},
};
}