/** * 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 | 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; 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: '

Hello {{name}}

' }, ], }, { 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; campaigns: Record; campaignFlows: Record; activeFlowId: string; campaignWorkflows: Record; } // ── Schema registration ── export const socialsSchema: DocSchema = { 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; }