Files
Content-Hunter_BE/src/modules/social-integration/services/auto-publish.service.ts
Harun CAN fc88faddb9
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s
main
2026-02-10 12:27:14 +03:00

425 lines
14 KiB
TypeScript

// 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<SocialPlatform, {
total: number;
successful: number;
failed: number;
}>;
}
@Injectable()
export class AutoPublishService {
private readonly logger = new Logger(AutoPublishService.name);
private queue: Map<string, QueuedPost[]> = 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<PublishResult[]> {
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<SocialPlatform, {
bestTimes: { day: string; time: string; engagement: string }[];
}> {
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<SocialPlatform, {
adaptedContent: UnifiedContent;
truncated: boolean;
warnings: string[];
}> {
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<PublishResult> {
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<SocialPlatform, number> = {
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;
}
}