// Auto Publish Service - Unified posting interface // Path: src/modules/social-integration/services/auto-publish.service.ts import { Injectable, Logger } from '@nestjs/common'; import { OAuthService, SocialPlatform, ConnectedAccount } from './oauth.service'; import { TwitterApiService } from './twitter-api.service'; import { InstagramApiService } from './instagram-api.service'; import { LinkedInApiService } from './linkedin-api.service'; import { FacebookApiService } from './facebook-api.service'; import { TikTokApiService } from './tiktok-api.service'; import { YouTubeApiService } from './youtube-api.service'; export interface PublishRequest { userId: string; platforms: SocialPlatform[]; content: UnifiedContent; scheduledTime?: Date; queuePosition?: number; } export interface UnifiedContent { text: string; mediaUrls?: string[]; mediaType?: 'image' | 'video'; link?: string; thumbnail?: string; hashtags?: string[]; mentions?: string[]; title?: string; // For YouTube description?: string; // For YouTube } export interface PublishResult { platform: SocialPlatform; success: boolean; postId?: string; postUrl?: string; error?: string; publishedAt?: Date; } export interface QueuedPost { id: string; userId: string; platform: SocialPlatform; content: UnifiedContent; scheduledTime: Date; status: 'pending' | 'publishing' | 'published' | 'failed'; retryCount: number; result?: PublishResult; createdAt: Date; } export interface PublishStats { totalPosts: number; successfulPosts: number; failedPosts: number; platformBreakdown: Record; } @Injectable() export class AutoPublishService { private readonly logger = new Logger(AutoPublishService.name); private queue: Map = new Map(); constructor( private readonly oauthService: OAuthService, private readonly twitterApi: TwitterApiService, private readonly instagramApi: InstagramApiService, private readonly linkedinApi: LinkedInApiService, private readonly facebookApi: FacebookApiService, private readonly tiktokApi: TikTokApiService, private readonly youtubeApi: YouTubeApiService, ) { } /** * Publish to multiple platforms at once */ async publishToMultiplePlatforms(request: PublishRequest): Promise { const results: PublishResult[] = []; for (const platform of request.platforms) { try { const account = this.oauthService.getAccountByPlatform(request.userId, platform); if (!account) { results.push({ platform, success: false, error: `No connected ${platform} account found`, }); continue; } const tokens = await this.oauthService.ensureValidToken(account); const adaptedContent = this.adaptContentForPlatform(request.content, platform); const result = await this.publishToPlatform(platform, tokens.accessToken, adaptedContent, account); results.push(result); } catch (error) { results.push({ platform, success: false, error: error.message, }); } } return results; } /** * Schedule a post for later */ schedulePost(request: PublishRequest): QueuedPost[] { const scheduled: QueuedPost[] = []; for (const platform of request.platforms) { const post: QueuedPost = { id: `queue-${Date.now()}-${platform}`, userId: request.userId, platform, content: this.adaptContentForPlatform(request.content, platform), scheduledTime: request.scheduledTime || new Date(), status: 'pending', retryCount: 0, createdAt: new Date(), }; const userQueue = this.queue.get(request.userId) || []; userQueue.push(post); this.queue.set(request.userId, userQueue); scheduled.push(post); } return scheduled; } /** * Get user's queue */ getQueue(userId: string): QueuedPost[] { return this.queue.get(userId) || []; } /** * Cancel a scheduled post */ cancelScheduledPost(userId: string, postId: string): boolean { const userQueue = this.queue.get(userId) || []; const index = userQueue.findIndex((p) => p.id === postId); if (index !== -1 && userQueue[index].status === 'pending') { userQueue.splice(index, 1); this.queue.set(userId, userQueue); return true; } return false; } /** * Reschedule a post */ reschedulePost(userId: string, postId: string, newTime: Date): boolean { const userQueue = this.queue.get(userId) || []; const post = userQueue.find((p) => p.id === postId); if (post && post.status === 'pending') { post.scheduledTime = newTime; return true; } return false; } /** * Get publishing statistics */ getPublishingStats(userId: string): PublishStats { const userQueue = this.queue.get(userId) || []; const stats: PublishStats = { totalPosts: userQueue.length, successfulPosts: userQueue.filter((p) => p.status === 'published').length, failedPosts: userQueue.filter((p) => p.status === 'failed').length, platformBreakdown: {} as any, }; const platforms: SocialPlatform[] = ['twitter', 'instagram', 'linkedin', 'facebook', 'tiktok', 'youtube']; for (const platform of platforms) { const platformPosts = userQueue.filter((p) => p.platform === platform); stats.platformBreakdown[platform] = { total: platformPosts.length, successful: platformPosts.filter((p) => p.status === 'published').length, failed: platformPosts.filter((p) => p.status === 'failed').length, }; } return stats; } /** * Get optimal posting schedule */ getOptimalSchedule(platforms: SocialPlatform[]): Record { const schedule: any = {}; for (const platform of platforms) { switch (platform) { case 'twitter': schedule[platform] = { bestTimes: [ { day: 'Wednesday', time: '09:00', engagement: 'Peak' }, { day: 'Friday', time: '11:00', engagement: 'High' }, ], }; break; case 'instagram': schedule[platform] = { bestTimes: [ { day: 'Tuesday', time: '11:00', engagement: 'Peak' }, { day: 'Wednesday', time: '14:00', engagement: 'High' }, ], }; break; case 'linkedin': schedule[platform] = { bestTimes: [ { day: 'Tuesday', time: '10:00', engagement: 'Peak' }, { day: 'Wednesday', time: '12:00', engagement: 'High' }, ], }; break; case 'tiktok': schedule[platform] = { bestTimes: [ { day: 'Thursday', time: '19:00', engagement: 'Peak' }, { day: 'Saturday', time: '20:00', engagement: 'Peak' }, ], }; break; default: schedule[platform] = { bestTimes: [ { day: 'Wednesday', time: '12:00', engagement: 'Medium' }, ], }; } } return schedule; } /** * Preview how content will look on each platform */ previewContent(content: UnifiedContent, platforms: SocialPlatform[]): Record { const previews: any = {}; for (const platform of platforms) { const adapted = this.adaptContentForPlatform(content, platform); const limits = this.getCharacterLimit(platform); previews[platform] = { adaptedContent: adapted, truncated: content.text.length > limits, warnings: this.getContentWarnings(content, platform), }; } return previews; } // Private methods private async publishToPlatform( platform: SocialPlatform, accessToken: string, content: UnifiedContent, account: ConnectedAccount, ): Promise { try { let postId: string; let postUrl: string = ''; switch (platform) { case 'twitter': const tweet = await this.twitterApi.postTweet(accessToken, { text: content.text }); postId = tweet.id; postUrl = `https://twitter.com/i/status/${postId}`; break; case 'instagram': if (content.mediaUrls?.[0]) { const igPost = await this.instagramApi.createPost(accessToken, { mediaUrl: content.mediaUrls[0], mediaType: content.mediaType || 'image', caption: content.text, }); postId = igPost.id; } else { throw new Error('Instagram requires media'); } break; case 'linkedin': const liPost = await this.linkedinApi.createPost(accessToken, { text: content.text }); postId = liPost.id; break; case 'facebook': const fbPost = await this.facebookApi.createPost(accessToken, 'page-id', { message: content.text }); postId = fbPost.id; break; case 'tiktok': if (content.mediaUrls?.[0] && content.mediaType === 'video') { const tikTok = await this.tiktokApi.uploadVideo(accessToken, { videoUrl: content.mediaUrls[0], caption: content.text, }); postId = tikTok.id; } else { throw new Error('TikTok requires video'); } break; case 'youtube': if (content.mediaUrls?.[0] && content.mediaType === 'video') { const ytVideo = await this.youtubeApi.uploadVideo(accessToken, { videoUrl: content.mediaUrls[0], title: content.title || 'Untitled', description: content.description || content.text, }); postId = ytVideo.id; postUrl = `https://youtube.com/watch?v=${postId}`; } else { throw new Error('YouTube requires video'); } break; default: throw new Error(`Unsupported platform: ${platform}`); } return { platform, success: true, postId, postUrl: postUrl || `https://${platform}.com/post/${postId}`, publishedAt: new Date(), }; } catch (error) { return { platform, success: false, error: error.message, }; } } private adaptContentForPlatform(content: UnifiedContent, platform: SocialPlatform): UnifiedContent { const limit = this.getCharacterLimit(platform); let text = content.text; // Add hashtags if provided if (content.hashtags?.length) { const hashtagString = content.hashtags.map((h) => h.startsWith('#') ? h : `#${h}`).join(' '); if (text.length + hashtagString.length + 2 <= limit) { text = `${text}\n\n${hashtagString}`; } } // Truncate if needed if (text.length > limit) { text = text.substring(0, limit - 3) + '...'; } return { ...content, text, }; } private getCharacterLimit(platform: SocialPlatform): number { const limits: Record = { twitter: 280, instagram: 2200, linkedin: 3000, facebook: 63206, tiktok: 2200, youtube: 5000, threads: 500, pinterest: 500, }; return limits[platform] || 280; } private getContentWarnings(content: UnifiedContent, platform: SocialPlatform): string[] { const warnings: string[] = []; const limit = this.getCharacterLimit(platform); if (content.text.length > limit) { warnings.push(`Content exceeds ${platform} character limit (${limit})`); } if (['instagram', 'tiktok'].includes(platform) && !content.mediaUrls?.length) { warnings.push(`${platform} requires media (image or video)`); } if (platform === 'youtube' && (!content.title || !content.description)) { warnings.push('YouTube videos require a title and description'); } return warnings; } }