410 lines
12 KiB
TypeScript
410 lines
12 KiB
TypeScript
/**
|
|
* rSocials Automerge document schemas.
|
|
*
|
|
* Granularity: one Automerge document per space.
|
|
* DocId format: {space}:socials:data
|
|
*
|
|
* Images stay on filesystem, referenced by URL strings in the doc.
|
|
*/
|
|
|
|
import type { DocSchema } from '../../shared/local-first/document';
|
|
|
|
// ── Thread types ──
|
|
|
|
export interface ThreadData {
|
|
id: string;
|
|
name: string;
|
|
handle: string;
|
|
title: string;
|
|
tweets: string[];
|
|
imageUrl?: string | null;
|
|
tweetImages?: Record<string, string> | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
// ── Campaign types ──
|
|
|
|
export interface CampaignPost {
|
|
id: string;
|
|
platform: string;
|
|
postType: string;
|
|
stepNumber: number;
|
|
content: string;
|
|
scheduledAt: string;
|
|
status: string;
|
|
hashtags: string[];
|
|
phase: number;
|
|
phaseLabel: string;
|
|
}
|
|
|
|
export interface Campaign {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
duration: string;
|
|
platforms: string[];
|
|
phases: { name: string; label: string; days: string }[];
|
|
posts: CampaignPost[];
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
// ── Campaign planner (flow canvas) types ──
|
|
|
|
export type CampaignNodeType = 'post' | 'thread' | 'platform' | 'audience' | 'phase';
|
|
|
|
export interface PostNodeData {
|
|
label: string;
|
|
platform: string;
|
|
postType: string;
|
|
content: string;
|
|
scheduledAt: string;
|
|
status: 'draft' | 'scheduled' | 'published';
|
|
hashtags: string[];
|
|
}
|
|
|
|
export interface ThreadNodeData {
|
|
label: string;
|
|
threadId: string;
|
|
tweetCount: number;
|
|
status: 'draft' | 'ready' | 'published';
|
|
preview: string;
|
|
}
|
|
|
|
export interface PlatformNodeData {
|
|
label: string;
|
|
platform: string;
|
|
handle: string;
|
|
}
|
|
|
|
export interface AudienceNodeData {
|
|
label: string;
|
|
description: string;
|
|
sizeEstimate: string;
|
|
}
|
|
|
|
export interface PhaseNodeData {
|
|
label: string;
|
|
dateRange: string;
|
|
color: string;
|
|
progress?: number;
|
|
childNodeIds: string[];
|
|
size: { w: number; h: number };
|
|
}
|
|
|
|
export interface CampaignPlannerNode {
|
|
id: string;
|
|
type: CampaignNodeType;
|
|
position: { x: number; y: number };
|
|
data: PostNodeData | ThreadNodeData | PlatformNodeData | AudienceNodeData | PhaseNodeData;
|
|
}
|
|
|
|
export type CampaignEdgeType = 'publish' | 'sequence' | 'target';
|
|
|
|
export interface CampaignEdge {
|
|
id: string;
|
|
from: string;
|
|
to: string;
|
|
type: CampaignEdgeType;
|
|
waypoint?: { x: number; y: number };
|
|
}
|
|
|
|
export interface CampaignFlow {
|
|
id: string;
|
|
name: string;
|
|
nodes: CampaignPlannerNode[];
|
|
edges: CampaignEdge[];
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
createdBy: string | null;
|
|
}
|
|
|
|
// ── Campaign workflow (n8n-style) types ──
|
|
|
|
export type CampaignWorkflowNodeType =
|
|
// Triggers
|
|
| 'campaign-start'
|
|
| 'schedule-trigger'
|
|
| 'webhook-trigger'
|
|
// Delays
|
|
| 'wait-duration'
|
|
| 'wait-approval'
|
|
// Conditions
|
|
| 'engagement-check'
|
|
| 'time-window'
|
|
// Actions
|
|
| 'post-to-platform'
|
|
| 'cross-post'
|
|
| 'publish-thread'
|
|
| 'send-newsletter'
|
|
| 'post-webhook';
|
|
|
|
export type CampaignWorkflowNodeCategory = 'trigger' | 'delay' | 'condition' | 'action';
|
|
|
|
export interface CampaignWorkflowNodePort {
|
|
name: string;
|
|
type: 'trigger' | 'data' | 'boolean';
|
|
}
|
|
|
|
export interface CampaignWorkflowNode {
|
|
id: string;
|
|
type: CampaignWorkflowNodeType;
|
|
label: string;
|
|
position: { x: number; y: number };
|
|
config: Record<string, unknown>;
|
|
runtimeStatus?: 'idle' | 'running' | 'success' | 'error';
|
|
runtimeMessage?: string;
|
|
runtimeDurationMs?: number;
|
|
}
|
|
|
|
export interface CampaignWorkflowEdge {
|
|
id: string;
|
|
fromNode: string;
|
|
fromPort: string;
|
|
toNode: string;
|
|
toPort: string;
|
|
}
|
|
|
|
export interface CampaignWorkflow {
|
|
id: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
nodes: CampaignWorkflowNode[];
|
|
edges: CampaignWorkflowEdge[];
|
|
lastRunAt: number | null;
|
|
lastRunStatus: 'success' | 'error' | null;
|
|
runCount: number;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface CampaignWorkflowNodeDef {
|
|
type: CampaignWorkflowNodeType;
|
|
category: CampaignWorkflowNodeCategory;
|
|
label: string;
|
|
icon: string;
|
|
description: string;
|
|
inputs: CampaignWorkflowNodePort[];
|
|
outputs: CampaignWorkflowNodePort[];
|
|
configSchema: { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea' | 'cron'; options?: string[]; placeholder?: string }[];
|
|
}
|
|
|
|
export const CAMPAIGN_NODE_CATALOG: CampaignWorkflowNodeDef[] = [
|
|
// ── Triggers ──
|
|
{
|
|
type: 'campaign-start',
|
|
category: 'trigger',
|
|
label: 'Campaign Start',
|
|
icon: '\u25B6',
|
|
description: 'Manual kick-off for this campaign workflow',
|
|
inputs: [],
|
|
outputs: [{ name: 'trigger', type: 'trigger' }],
|
|
configSchema: [
|
|
{ key: 'description', label: 'Description', type: 'text', placeholder: 'Launch campaign' },
|
|
],
|
|
},
|
|
{
|
|
type: 'schedule-trigger',
|
|
category: 'trigger',
|
|
label: 'Schedule Trigger',
|
|
icon: '\u23F0',
|
|
description: 'Fire on a cron schedule',
|
|
inputs: [],
|
|
outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'timestamp', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'cronExpression', label: 'Cron Expression', type: 'cron', placeholder: '0 9 * * 1' },
|
|
{ key: 'timezone', label: 'Timezone', type: 'text', placeholder: 'America/Vancouver' },
|
|
],
|
|
},
|
|
{
|
|
type: 'webhook-trigger',
|
|
category: 'trigger',
|
|
label: 'Webhook Trigger',
|
|
icon: '\uD83D\uDD17',
|
|
description: 'Fire when an external webhook is received',
|
|
inputs: [],
|
|
outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'payload', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'hookId', label: 'Hook ID', type: 'text', placeholder: 'auto-generated' },
|
|
],
|
|
},
|
|
// ── Delays ──
|
|
{
|
|
type: 'wait-duration',
|
|
category: 'delay',
|
|
label: 'Wait Duration',
|
|
icon: '\u23F3',
|
|
description: 'Wait for a specified amount of time',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }],
|
|
configSchema: [
|
|
{ key: 'amount', label: 'Amount', type: 'number', placeholder: '2' },
|
|
{ key: 'unit', label: 'Unit', type: 'select', options: ['minutes', 'hours', 'days'] },
|
|
],
|
|
},
|
|
{
|
|
type: 'wait-approval',
|
|
category: 'delay',
|
|
label: 'Wait for Approval',
|
|
icon: '\u270B',
|
|
description: 'Pause until a team member approves',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }],
|
|
outputs: [{ name: 'approved', type: 'trigger' }, { name: 'rejected', type: 'trigger' }],
|
|
configSchema: [
|
|
{ key: 'approver', label: 'Approver', type: 'text', placeholder: 'Team lead' },
|
|
{ key: 'message', label: 'Approval Message', type: 'textarea', placeholder: 'Please review...' },
|
|
],
|
|
},
|
|
// ── Conditions ──
|
|
{
|
|
type: 'engagement-check',
|
|
category: 'condition',
|
|
label: 'Engagement Check',
|
|
icon: '\uD83D\uDCC8',
|
|
description: 'Check if engagement exceeds a threshold',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'metrics', type: 'data' }],
|
|
outputs: [{ name: 'above', type: 'trigger' }, { name: 'below', type: 'trigger' }],
|
|
configSchema: [
|
|
{ key: 'metric', label: 'Metric', type: 'select', options: ['likes', 'retweets', 'replies', 'impressions', 'clicks'] },
|
|
{ key: 'threshold', label: 'Threshold', type: 'number', placeholder: '100' },
|
|
],
|
|
},
|
|
{
|
|
type: 'time-window',
|
|
category: 'condition',
|
|
label: 'Time Window',
|
|
icon: '\uD83D\uDD50',
|
|
description: 'Check if current time is within posting hours',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }],
|
|
outputs: [{ name: 'in-window', type: 'trigger' }, { name: 'outside', type: 'trigger' }],
|
|
configSchema: [
|
|
{ key: 'startHour', label: 'Start Hour (0-23)', type: 'number', placeholder: '9' },
|
|
{ key: 'endHour', label: 'End Hour (0-23)', type: 'number', placeholder: '17' },
|
|
{ key: 'days', label: 'Days (1=Mon)', type: 'text', placeholder: '1,2,3,4,5' },
|
|
],
|
|
},
|
|
// ── Actions ──
|
|
{
|
|
type: 'post-to-platform',
|
|
category: 'action',
|
|
label: 'Post to Platform',
|
|
icon: '\uD83D\uDCE4',
|
|
description: 'Publish a post to a social platform',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'content', type: 'data' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }, { name: 'postId', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'platform', label: 'Platform', type: 'select', options: ['X', 'LinkedIn', 'Instagram', 'Threads', 'Bluesky', 'YouTube'] },
|
|
{ key: 'content', label: 'Post Content', type: 'textarea', placeholder: 'Your post text...' },
|
|
{ key: 'hashtags', label: 'Hashtags', type: 'text', placeholder: '#launch #campaign' },
|
|
],
|
|
},
|
|
{
|
|
type: 'cross-post',
|
|
category: 'action',
|
|
label: 'Cross-Post',
|
|
icon: '\uD83D\uDCE1',
|
|
description: 'Broadcast to multiple platforms at once',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'content', type: 'data' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }, { name: 'results', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'platforms', label: 'Platforms (comma-sep)', type: 'text', placeholder: 'X, LinkedIn, Threads' },
|
|
{ key: 'content', label: 'Post Content', type: 'textarea', placeholder: 'Shared content...' },
|
|
{ key: 'adaptPerPlatform', label: 'Adapt per platform', type: 'select', options: ['yes', 'no'] },
|
|
],
|
|
},
|
|
{
|
|
type: 'publish-thread',
|
|
category: 'action',
|
|
label: 'Publish Thread',
|
|
icon: '\uD83E\uDDF5',
|
|
description: 'Publish a multi-tweet thread',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }, { name: 'threadId', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'platform', label: 'Platform', type: 'select', options: ['X', 'Bluesky', 'Threads'] },
|
|
{ key: 'threadId', label: 'Thread ID (link existing)', type: 'text', placeholder: 'Leave empty to use inline content' },
|
|
{ key: 'threadContent', label: 'Thread (--- between tweets)', type: 'textarea', placeholder: 'Tweet 1\n---\nTweet 2\n---\nTweet 3' },
|
|
],
|
|
},
|
|
{
|
|
type: 'send-newsletter',
|
|
category: 'action',
|
|
label: 'Send Newsletter',
|
|
icon: '\uD83D\uDCE7',
|
|
description: 'Send an email campaign via Listmonk',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'content', type: 'data' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }, { name: 'campaignId', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'subject', label: 'Email Subject', type: 'text', placeholder: 'Newsletter: Campaign Update' },
|
|
{ key: 'listId', label: 'List ID', type: 'text', placeholder: '1' },
|
|
{ key: 'bodyTemplate', label: 'Body (HTML)', type: 'textarea', placeholder: '<p>Hello {{name}}</p>' },
|
|
],
|
|
},
|
|
{
|
|
type: 'post-webhook',
|
|
category: 'action',
|
|
label: 'POST Webhook',
|
|
icon: '\uD83C\uDF10',
|
|
description: 'Send an HTTP POST to an external URL',
|
|
inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }],
|
|
outputs: [{ name: 'done', type: 'trigger' }, { name: 'response', type: 'data' }],
|
|
configSchema: [
|
|
{ key: 'url', label: 'URL', type: 'text', placeholder: 'https://api.example.com/hook' },
|
|
{ key: 'bodyTemplate', label: 'Body Template', type: 'textarea', placeholder: '{"event": "campaign_step"}' },
|
|
],
|
|
},
|
|
];
|
|
|
|
// ── Document root ──
|
|
|
|
export interface SocialsDoc {
|
|
meta: {
|
|
module: string;
|
|
collection: string;
|
|
version: number;
|
|
spaceSlug: string;
|
|
createdAt: number;
|
|
};
|
|
threads: Record<string, ThreadData>;
|
|
campaigns: Record<string, Campaign>;
|
|
campaignFlows: Record<string, CampaignFlow>;
|
|
activeFlowId: string;
|
|
campaignWorkflows: Record<string, CampaignWorkflow>;
|
|
}
|
|
|
|
// ── Schema registration ──
|
|
|
|
export const socialsSchema: DocSchema<SocialsDoc> = {
|
|
module: 'socials',
|
|
collection: 'data',
|
|
version: 3,
|
|
init: (): SocialsDoc => ({
|
|
meta: {
|
|
module: 'socials',
|
|
collection: 'data',
|
|
version: 3,
|
|
spaceSlug: '',
|
|
createdAt: Date.now(),
|
|
},
|
|
threads: {},
|
|
campaigns: {},
|
|
campaignFlows: {},
|
|
activeFlowId: '',
|
|
campaignWorkflows: {},
|
|
}),
|
|
migrate: (doc: SocialsDoc, _fromVersion: number): SocialsDoc => {
|
|
if (!doc.campaignFlows) (doc as any).campaignFlows = {};
|
|
if (!doc.activeFlowId) (doc as any).activeFlowId = '';
|
|
if (!doc.campaignWorkflows) (doc as any).campaignWorkflows = {};
|
|
if (doc.meta) doc.meta.version = 3;
|
|
return doc;
|
|
},
|
|
};
|
|
|
|
// ── Helpers ──
|
|
|
|
export function socialsDocId(space: string) {
|
|
return `${space}:socials:data` as const;
|
|
}
|