mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-12 01:36:54 +02:00
Ajoute la gestion multi-comptes TikTok et permet d'envoyer la même vidéo sur plusieurs pages Facebook ET plusieurs comptes TikTok en une seule publication. Un compte TikTok vit dans social_pages comme une page Facebook : il hérite donc des permissions par utilisateur, de la planification et du calendrier. Trois particularités de l'API TikTok structurent l'implémentation : - pas de jeton saisi à la main : chaque compte passe par OAuth, et l'access token (24h) est renouvelé via le refresh token (1 an) avant chaque publication ; - la publication est asynchrone : l'upload rend un publish_id, et un poller récupère l'identifiant définitif du post ; - creator_info doit être interrogé avant chaque envoi pour ne demander qu'un niveau de confidentialité réellement autorisé sur le compte. Serveur : - service TikTok (OAuth, creator_info, upload FILE_UPLOAD par chunks, suivi de statut) et routes de connexion/configuration - schéma : platform 'tiktok', refresh token et scopes sur social_pages, publish_id/publish_status sur scheduled_posts, table tiktok_config - routage par plateforme dans les deux flux de reels et le planificateur - cron de suivi des publications, token manager et analytics adaptés Client : - connexion et reconnexion des comptes TikTok depuis « Pages gérées » - sélection combinée pages Facebook / comptes TikTok à la publication - configuration de l'application TikTok dans les paramètres (admin) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const UPLOADS_BASE = path.resolve(process.cwd(), 'uploads');
|
|
|
|
function ensureDir(dir: string): void {
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the public URL for a locally stored file.
|
|
* Returns a relative path so it works on any domain/protocol.
|
|
* objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg").
|
|
*/
|
|
export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string {
|
|
return `/uploads/${objectKey}`;
|
|
}
|
|
|
|
/**
|
|
* Resolves a possibly-relative URL to an absolute URL for use by external
|
|
* services (Facebook API, Instagram API) that must download the file.
|
|
* Uses APP_URL env var (public-facing domain).
|
|
*/
|
|
export function resolvePublicUrl(url: string): string {
|
|
if (!url || !url.startsWith('/')) return url;
|
|
const base = (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
|
|
return `${base}${url}`;
|
|
}
|
|
|
|
/**
|
|
* Resolves a possibly-relative URL to an absolute URL for use by internal
|
|
* Docker services (FFmpeg, etc.) that communicate via internal Docker network.
|
|
* Uses INTERNAL_APP_URL env var.
|
|
*/
|
|
export function resolveInternalUrl(url: string): string {
|
|
if (!url || !url.startsWith('/')) return url;
|
|
const base = (process.env.INTERNAL_APP_URL || process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
|
|
return `${base}${url}`;
|
|
}
|
|
|
|
/**
|
|
* Charge un média en mémoire, que son URL soit locale (/uploads/...) ou distante.
|
|
* Nécessaire pour les APIs qui veulent recevoir le fichier plutôt qu'une URL
|
|
* (upload TikTok, Reels Facebook).
|
|
*/
|
|
export async function readMediaBuffer(url: string): Promise<Buffer> {
|
|
if (url.startsWith('/')) {
|
|
const localPath = path.join(process.cwd(), url);
|
|
if (fs.existsSync(localPath)) {
|
|
return await fs.promises.readFile(localPath);
|
|
}
|
|
}
|
|
|
|
const absoluteUrl = resolvePublicUrl(url);
|
|
const response = await fetch(absoluteUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Impossible de récupérer le média depuis ${absoluteUrl} (${response.status})`);
|
|
}
|
|
return Buffer.from(await response.arrayBuffer());
|
|
}
|
|
|
|
class LocalStorageService {
|
|
async uploadMedia(
|
|
file: Buffer | string,
|
|
fileName: string,
|
|
_userId: string,
|
|
_mimeType: string
|
|
): Promise<{
|
|
publicId: string;
|
|
originalUrl: string;
|
|
facebookFeedUrl: string | null;
|
|
instagramFeedUrl: string | null;
|
|
instagramStoryUrl: string | null;
|
|
}> {
|
|
const dir = path.join(UPLOADS_BASE, 'media');
|
|
ensureDir(dir);
|
|
|
|
const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
const objectKey = `media/${Date.now()}-${safeName}`;
|
|
const filePath = path.join(UPLOADS_BASE, objectKey);
|
|
|
|
const body: Buffer = typeof file === 'string' ? fs.readFileSync(file) : file;
|
|
fs.writeFileSync(filePath, body);
|
|
|
|
const url = buildMinioUrl('', objectKey);
|
|
return { publicId: objectKey, originalUrl: url, facebookFeedUrl: url, instagramFeedUrl: url, instagramStoryUrl: url };
|
|
}
|
|
|
|
async deleteMedia(publicId: string, _userId: string, _mediaType: 'image' | 'video'): Promise<void> {
|
|
const filePath = path.join(UPLOADS_BASE, publicId);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
|
|
async uploadStoryImageWithText(buffer: Buffer, originalFileName: string): Promise<string> {
|
|
const dir = path.join(UPLOADS_BASE, 'stories');
|
|
ensureDir(dir);
|
|
|
|
const safeName = path.basename(originalFileName).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
const objectKey = `stories/story-${Date.now()}-${safeName}`;
|
|
|
|
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
|
|
return buildMinioUrl('', objectKey);
|
|
}
|
|
|
|
async uploadLogo(buffer: Buffer, fileName: string): Promise<{ publicId: string; url: string }> {
|
|
const dir = path.join(UPLOADS_BASE, 'logos');
|
|
ensureDir(dir);
|
|
|
|
const ext = path.extname(fileName) || '.png';
|
|
const objectKey = `logos/logo-${Date.now()}${ext}`;
|
|
|
|
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
|
|
return { publicId: objectKey, url: buildMinioUrl('', objectKey) };
|
|
}
|
|
|
|
async deleteLogo(publicId: string): Promise<void> {
|
|
const filePath = path.join(UPLOADS_BASE, publicId);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const minioService = new LocalStorageService();
|