diff --git a/apps/frontend/src/app/auth/layout.tsx b/apps/frontend/src/app/auth/layout.tsx index 0e8e4fc1..11c66d71 100644 --- a/apps/frontend/src/app/auth/layout.tsx +++ b/apps/frontend/src/app/auth/layout.tsx @@ -13,7 +13,7 @@ export default async function AuthLayout({ children: ReactNode; }) { return ( - <> +
@@ -75,6 +75,6 @@ export default async function AuthLayout({
- +
); } diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 7542bffa..d2572da5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -145,178 +145,240 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { }; } + private async createSingleMediaContent( + userId: string, + accessToken: string, + media: { url: string }, + message: string, + isCarouselItem = false, + replyToId?: string + ): Promise { + const mediaType = media.url.indexOf('.mp4') > -1 ? 'video_url' : 'image_url'; + const mediaParams = new URLSearchParams({ + ...(mediaType === 'video_url' ? { video_url: media.url } : {}), + ...(mediaType === 'image_url' ? { image_url: media.url } : {}), + ...(isCarouselItem ? { is_carousel_item: 'true' } : {}), + ...(replyToId ? { reply_to_id: replyToId } : {}), + media_type: mediaType === 'video_url' ? 'VIDEO' : 'IMAGE', + text: message, + access_token: accessToken, + }); + + console.log(mediaParams); + + const { id: mediaId } = await ( + await this.fetch( + `https://graph.threads.net/v1.0/${userId}/threads?${mediaParams.toString()}`, + { + method: 'POST', + } + ) + ).json(); + + return mediaId; + } + + private async createCarouselContent( + userId: string, + accessToken: string, + media: { url: string }[], + message: string, + replyToId?: string + ): Promise { + // Create each media item + const mediaIds = []; + for (const mediaItem of media) { + const mediaId = await this.createSingleMediaContent( + userId, + accessToken, + mediaItem, + message, + true + ); + mediaIds.push(mediaId); + } + + // Wait for all media to be loaded + await Promise.all( + mediaIds.map((id: string) => this.checkLoaded(id, accessToken)) + ); + + // Create carousel container + const params = new URLSearchParams({ + text: message, + media_type: 'CAROUSEL', + children: mediaIds.join(','), + ...(replyToId ? { reply_to_id: replyToId } : {}), + access_token: accessToken, + }); + + const { id: containerId } = await ( + await this.fetch( + `https://graph.threads.net/v1.0/${userId}/threads?${params.toString()}`, + { + method: 'POST', + } + ) + ).json(); + + return containerId; + } + + private async createTextContent( + userId: string, + accessToken: string, + message: string, + replyToId?: string + ): Promise { + const form = new FormData(); + form.append('media_type', 'TEXT'); + form.append('text', message); + form.append('access_token', accessToken); + + if (replyToId) { + form.append('reply_to_id', replyToId); + } + + const { id: contentId } = await ( + await this.fetch( + `https://graph.threads.net/v1.0/${userId}/threads`, + { + method: 'POST', + body: form, + } + ) + ).json(); + + return contentId; + } + + private async publishThread( + userId: string, + accessToken: string, + creationId: string + ): Promise<{ threadId: string; permalink: string }> { + await this.checkLoaded(creationId, accessToken); + + const { id: threadId } = await ( + await this.fetch( + `https://graph.threads.net/v1.0/${userId}/threads_publish?creation_id=${creationId}&access_token=${accessToken}`, + { + method: 'POST', + } + ) + ).json(); + + const { permalink } = await ( + await this.fetch( + `https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}` + ) + ).json(); + + return { threadId, permalink }; + } + + private async createThreadContent( + userId: string, + accessToken: string, + postDetails: PostDetails, + replyToId?: string + ): Promise { + // Handle content creation based on media type + if (!postDetails.media || postDetails.media.length === 0) { + // Text-only content + return await this.createTextContent( + userId, + accessToken, + postDetails.message, + replyToId + ); + } else if (postDetails.media.length === 1) { + // Single media content + return await this.createSingleMediaContent( + userId, + accessToken, + postDetails.media[0], + postDetails.message, + false, + replyToId + ); + } else { + // Carousel content + return await this.createCarouselContent( + userId, + accessToken, + postDetails.media, + postDetails.message, + replyToId + ); + } + } + async post( - id: string, + userId: string, accessToken: string, postDetails: PostDetails[] ): Promise { - const [firstPost, ...theRest] = postDetails; + if (!postDetails.length) { + return []; + } - let globalThread = ''; - let link = ''; - if (firstPost?.media?.length! <= 1) { - const type = !firstPost?.media?.[0]?.url - ? undefined - : firstPost?.media![0].url.indexOf('.mp4') > -1 - ? 'video_url' - : 'image_url'; - - const media = new URLSearchParams({ - ...(type === 'video_url' - ? { video_url: firstPost?.media![0].url } - : {}), - ...(type === 'image_url' - ? { image_url: firstPost?.media![0].url } - : {}), - media_type: - type === 'video_url' - ? 'VIDEO' - : type === 'image_url' - ? 'IMAGE' - : 'TEXT', - text: firstPost?.message, - access_token: accessToken, - }); - - const { id: containerId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`, - { - method: 'POST', - } - ) - ).json(); - - await this.checkLoaded(containerId, accessToken); - - const { id: threadId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`, - { - method: 'POST', - } - ) - ).json(); - - const { permalink, ...all } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}` - ) - ).json(); - - globalThread = threadId; - link = permalink; - } else { - const medias = []; - for (const mediaLoad of firstPost.media!) { - const type = - mediaLoad.url.indexOf('.mp4') > -1 ? 'video_url' : 'image_url'; - - const media = new URLSearchParams({ - ...(type === 'video_url' ? { video_url: mediaLoad.url } : {}), - ...(type === 'image_url' ? { image_url: mediaLoad.url } : {}), - is_carousel_item: 'true', - media_type: - type === 'video_url' - ? 'VIDEO' - : type === 'image_url' - ? 'IMAGE' - : 'TEXT', - text: firstPost?.message, - access_token: accessToken, - }); - - const { id: mediaId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`, - { - method: 'POST', - } - ) - ).json(); - - medias.push(mediaId); - } - - await Promise.all( - medias.map((p: string) => this.checkLoaded(p, accessToken)) + const [firstPost, ...replies] = postDetails; + + // Create the initial thread + const initialContentId = await this.createThreadContent( + userId, + accessToken, + firstPost + ); + + // Publish the thread + const { threadId, permalink } = await this.publishThread( + userId, + accessToken, + initialContentId + ); + + // Track the responses + const responses: PostResponse[] = [{ + id: firstPost.id, + postId: threadId, + status: 'success', + releaseURL: permalink, + }]; + + // Handle replies if any + let lastReplyId = threadId; + + for (const reply of replies) { + // Create reply content + const replyContentId = await this.createThreadContent( + userId, + accessToken, + reply, + lastReplyId ); - - const { id: containerId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads?text=${ - firstPost?.message - }&media_type=CAROUSEL&children=${medias.join( - ',' - )}&access_token=${accessToken}`, - { - method: 'POST', - } - ) - ).json(); - - await this.checkLoaded(containerId, accessToken); - - const { id: threadId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`, - { - method: 'POST', - } - ) - ).json(); - - const { permalink } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}` - ) - ).json(); - - globalThread = threadId; - link = permalink; - } - - let lastId = globalThread; - for (const post of theRest) { - const form = new FormData(); - form.append('media_type', 'TEXT'); - form.append('text', post.message); - form.append('reply_to_id', lastId); - form.append('access_token', accessToken); - - const { id: replyId } = await ( - await this.fetch('https://graph.threads.net/v1.0/me/threads', { - method: 'POST', - body: form, - }) - ).json(); - - const { id: threadMediaId } = await ( - await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${replyId}&access_token=${accessToken}`, - { - method: 'POST', - } - ) - ).json(); - - lastId = threadMediaId; - } - - return [ - { - id: firstPost.id, - postId: String(globalThread), + + // Publish the reply + const { threadId: replyThreadId } = await this.publishThread( + userId, + accessToken, + replyContentId + ); + + // Update the last reply ID for chaining + lastReplyId = replyThreadId; + + // Add to responses + responses.push({ + id: reply.id, + postId: threadId, // Main thread ID status: 'success', - releaseURL: link, - }, - ...theRest.map((p) => ({ - id: p.id, - postId: String(globalThread), - status: 'success', - releaseURL: link, - })), - ]; + releaseURL: permalink, // Main thread URL + }); + } + + return responses; } async analytics(