// Platform Adapters Service - Transform content for different platforms // Path: src/modules/content/services/platform-adapters.service.ts import { Injectable, Logger } from '@nestjs/common'; import { SocialPlatform } from '@prisma/client'; export interface PlatformConfig { name: string; maxLength: number; supportsMedia: boolean; supportsLinks: boolean; supportsHashtags: boolean; supportsMentions: boolean; supportsEmoji: boolean; mediaFormats?: string[]; optimalLength?: number; bestPostingTimes?: string[]; engagementTips?: string[]; } export const PLATFORM_CONFIGS: Record = { TWITTER: { name: 'X (Twitter)', maxLength: 280, optimalLength: 240, supportsMedia: true, supportsLinks: true, supportsHashtags: true, supportsMentions: true, supportsEmoji: true, mediaFormats: ['image', 'video', 'gif'], bestPostingTimes: ['9:00', '12:00', '17:00', '21:00'], engagementTips: [ 'Use threads for longer content', 'Ask questions to boost engagement', 'Quote tweet with commentary', ], }, LINKEDIN: { name: 'LinkedIn', maxLength: 3000, optimalLength: 1300, supportsMedia: true, supportsLinks: true, supportsHashtags: true, supportsMentions: true, supportsEmoji: true, mediaFormats: ['image', 'video', 'document', 'carousel'], bestPostingTimes: ['7:30', '12:00', '17:00'], engagementTips: [ 'Hook in first line (before "see more")', 'Use line breaks for readability', 'End with a question', ], }, INSTAGRAM: { name: 'Instagram', maxLength: 2200, optimalLength: 1500, supportsMedia: true, supportsLinks: false, supportsHashtags: true, supportsMentions: true, supportsEmoji: true, mediaFormats: ['image', 'video', 'reels', 'carousel', 'stories'], bestPostingTimes: ['6:00', '11:00', '19:00'], engagementTips: [ 'Use up to 30 hashtags', 'Put hashtags in first comment', 'Include CTA in caption', ], }, FACEBOOK: { name: 'Facebook', maxLength: 63206, optimalLength: 250, supportsMedia: true, supportsLinks: true, supportsHashtags: true, supportsMentions: true, supportsEmoji: true, mediaFormats: ['image', 'video', 'reels', 'stories'], bestPostingTimes: ['9:00', '13:00', '19:00'], engagementTips: [ 'Shorter posts perform better', 'Native video preferred', 'Use Facebook Live for engagement', ], }, TIKTOK: { name: 'TikTok', maxLength: 300, optimalLength: 150, supportsMedia: true, supportsLinks: false, supportsHashtags: true, supportsMentions: true, supportsEmoji: true, mediaFormats: ['video'], bestPostingTimes: ['7:00', '10:00', '19:00', '23:00'], engagementTips: [ 'Hook in first 3 seconds', 'Use trending sounds', 'Keep videos under 60 seconds', ], }, YOUTUBE: { name: 'YouTube', maxLength: 5000, optimalLength: 500, supportsMedia: true, supportsLinks: true, supportsHashtags: true, supportsMentions: false, supportsEmoji: true, mediaFormats: ['video', 'shorts'], bestPostingTimes: ['12:00', '15:00', '19:00'], engagementTips: [ 'Use timestamps in description', 'Include relevant keywords', 'Add cards and end screens', ], }, PINTEREST: { name: 'Pinterest', maxLength: 500, optimalLength: 300, supportsMedia: true, supportsLinks: true, supportsHashtags: true, supportsMentions: false, supportsEmoji: true, mediaFormats: ['image', 'video', 'idea_pin'], bestPostingTimes: ['20:00', '21:00', '22:00'], engagementTips: [ 'Vertical images perform best', 'Use keyword-rich descriptions', 'Create multiple pins per post', ], }, THREADS: { name: 'Threads', maxLength: 500, optimalLength: 280, supportsMedia: true, supportsLinks: false, supportsHashtags: false, supportsMentions: true, supportsEmoji: true, mediaFormats: ['image', 'video'], bestPostingTimes: ['12:00', '17:00', '21:00'], engagementTips: [ 'Conversational tone works best', 'Reply to trending topics', 'Cross-post from Instagram', ], }, }; @Injectable() export class PlatformAdaptersService { private readonly logger = new Logger(PlatformAdaptersService.name); /** * Get platform configuration */ getConfig(platform: SocialPlatform): PlatformConfig { return PLATFORM_CONFIGS[platform]; } /** * Get all platform configs */ getAllConfigs(): Record { return PLATFORM_CONFIGS; } /** * Adapt content for specific platform */ adapt(content: string, sourcePlatform: SocialPlatform, targetPlatform: SocialPlatform): string { const sourceConfig = PLATFORM_CONFIGS[sourcePlatform]; const targetConfig = PLATFORM_CONFIGS[targetPlatform]; let adapted = content; // Handle length constraints if (adapted.length > targetConfig.maxLength) { adapted = this.truncate(adapted, targetConfig.maxLength); } // Handle link support if (!targetConfig.supportsLinks && sourceConfig.supportsLinks) { adapted = this.removeLinks(adapted); } // Handle hashtag support if (!targetConfig.supportsHashtags && sourceConfig.supportsHashtags) { adapted = this.removeHashtags(adapted); } return adapted; } /** * Format content for platform */ format(content: string, platform: SocialPlatform): string { const config = PLATFORM_CONFIGS[platform]; switch (platform) { case 'TWITTER': return this.formatForTwitter(content, config); case 'LINKEDIN': return this.formatForLinkedIn(content, config); case 'INSTAGRAM': return this.formatForInstagram(content, config); default: return this.truncate(content, config.maxLength); } } /** * Check if content is valid for platform */ validate(content: string, platform: SocialPlatform): { valid: boolean; issues: string[] } { const config = PLATFORM_CONFIGS[platform]; const issues: string[] = []; if (content.length > config.maxLength) { issues.push(`Content exceeds ${config.maxLength} character limit`); } if (!config.supportsLinks && this.containsLinks(content)) { issues.push('Platform does not support links in captions'); } return { valid: issues.length === 0, issues }; } // Private helpers private truncate(content: string, maxLength: number): string { if (content.length <= maxLength) return content; return content.substring(0, maxLength - 3) + '...'; } private removeLinks(content: string): string { return content.replace(/https?:\/\/[^\s]+/g, '[link in bio]'); } private removeHashtags(content: string): string { return content.replace(/#\w+/g, '').replace(/\s+/g, ' ').trim(); } private containsLinks(content: string): boolean { return /https?:\/\/[^\s]+/.test(content); } private formatForTwitter(content: string, config: PlatformConfig): string { let formatted = content; // Ensure hashtags at end const hashtags = formatted.match(/#\w+/g) || []; formatted = formatted.replace(/#\w+/g, '').trim(); formatted = `${formatted}\n\n${hashtags.slice(0, 3).join(' ')}`; return this.truncate(formatted, config.maxLength); } private formatForLinkedIn(content: string, config: PlatformConfig): string { // Add line breaks for readability const lines = content.split(/(?<=[.!?])\s+/); return lines.slice(0, 20).join('\n\n'); } private formatForInstagram(content: string, config: PlatformConfig): string { let formatted = content; // Add dots before hashtags (common Instagram style) if (formatted.includes('#')) { const mainContent = formatted.split('#')[0].trim(); const hashtags = formatted.match(/#\w+/g) || []; formatted = `${mainContent}\n.\n.\n.\n${hashtags.join(' ')}`; } return formatted; } }