diff --git a/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx b/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx index 18ce5cae1..ce2586dc4 100644 --- a/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/reddit/reddit.provider.tsx @@ -215,21 +215,44 @@ export default withProvider({ CustomPreviewComponent: undefined, dto: RedditSettingsDto, checkValidity: async (posts, settings: any) => { - if ( - settings?.subreddit?.some( - (p: any, index: number) => - p?.value?.type === 'media' && posts[0].length !== 1 - ) - ) { - return 'When posting a media post, you must attached exactly one media file.'; - } + // Check if type is media in any subreddit setting + const hasMediaType = settings?.subreddit?.some( + (p: any) => p?.value?.type === 'media' + ); - if ( - posts.some((p) => - p.some((a) => !a.thumbnail && a.path.indexOf('mp4') > -1) - ) - ) { - return 'You must attach a thumbnail to your video post.'; + if (hasMediaType && posts[0]) { + const firstPost = posts[0]; + + // Detect video and image media + const hasVideo = firstPost.some((m: any) => m.path.indexOf('mp4') > -1); + const hasImage = firstPost.some((m: any) => m.path.indexOf('mp4') === -1); + + // Disallow mixed media (video + images) + if (hasVideo && hasImage) { + return 'You cannot mix videos and images in a Reddit post. Please use either a video or images, not both.'; + } + + // Video validation + if (hasVideo) { + if (firstPost.length !== 1) { + return 'When posting a video, you must attach exactly one media file.'; + } + + // Check for thumbnail + const videoMedia = firstPost.find((m: any) => m.path.indexOf('mp4') > -1); + if (!videoMedia?.thumbnail) { + return 'You must attach a thumbnail to your video post.'; + } + } else { + // Image validation (no video) + if (firstPost.length === 0) { + return 'When posting a media post, you must attach at least one image.'; + } + + if (firstPost.length > 20) { + return 'Reddit allows a maximum of 20 images per post.'; + } + } } return true; diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index f249c023d..dfebf17a9 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -123,15 +123,16 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { }; } - private async uploadFileToReddit(accessToken: string, path: string) { + private async uploadFileToReddit( + accessToken: string, + path: string + ): Promise<{ url: string; assetId?: string }> { const mimeType = lookup(path); const formData = new FormData(); formData.append('filepath', path.split('/').pop()); formData.append('mimetype', mimeType || 'application/octet-stream'); - const { - args: { action, fields }, - } = await ( + const responseData = await ( await this.fetch( 'https://oauth.reddit.com/api/media/asset', { @@ -147,6 +148,13 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { ) ).json(); + const { + args: { action, fields }, + asset, + } = responseData; + + const assetId = asset?.asset_id || asset?.id; + const { data } = await axios.get(path, { responseType: 'arraybuffer', }); @@ -169,7 +177,11 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { body: upload, }); - return [...(await d.text()).matchAll(/(.*?)<\/Location>/g)][0][1]; + const url = [ + ...(await d.text()).matchAll(/(.*?)<\/Location>/g), + ][0][1]; + + return { url, assetId }; } async post( @@ -181,53 +193,159 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { const valueArray: PostResponse[] = []; for (const firstPostSettings of post.settings.subreddit) { - const postData = { + const hasVideo = + post.media?.some((m) => m.path.indexOf('mp4') > -1) === true; + const images = (post.media || []).filter( + (m) => m.path.indexOf('mp4') === -1 + ); + + // Determine post kind based on actual content + let useGalleryEndpoint = false; + let kind: string; + + if (hasVideo) { + kind = 'video'; + } else if (images.length > 1) { + useGalleryEndpoint = true; + kind = 'gallery'; + } else if (images.length === 1) { + kind = 'image'; + } else { + kind = firstPostSettings.value.type; + } + + let postData: any = { api_type: 'json', title: firstPostSettings.value.title || '', - kind: - firstPostSettings.value.type === 'media' - ? post.media[0].path.indexOf('mp4') > -1 - ? 'video' - : 'image' - : firstPostSettings.value.type, + kind, + text: post.message, + sr: firstPostSettings.value.subreddit, ...(firstPostSettings.value.flair ? { flair_id: firstPostSettings.value.flair.id } : {}), - ...(firstPostSettings.value.type === 'link' - ? { - url: firstPostSettings.value.url, - } - : {}), - ...(firstPostSettings.value.type === 'media' - ? { - url: await this.uploadFileToReddit( - accessToken, - post.media[0].path - ), - ...(post.media[0].path.indexOf('mp4') > -1 - ? { - video_poster_url: await this.uploadFileToReddit( - accessToken, - post.media[0].thumbnail - ), - } - : {}), - } - : {}), - text: post.message, - sr: firstPostSettings.value.subreddit, }; - const all = await ( - await this.fetch('https://oauth.reddit.com/api/submit', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(postData), - }) - ).json(); + // Handle different post types + if (firstPostSettings.value.type === 'link') { + postData.url = firstPostSettings.value.url; + } else if (firstPostSettings.value.type === 'media') { + if (kind === 'video') { + const videoUpload = await this.uploadFileToReddit( + accessToken, + post.media[0].path + ); + postData.url = videoUpload.url; + + if (post.media[0].thumbnail) { + const thumbnailUpload = await this.uploadFileToReddit( + accessToken, + post.media[0].thumbnail + ); + postData.video_poster_url = thumbnailUpload.url; + } + } else if (!useGalleryEndpoint) { + const imageUpload = await this.uploadFileToReddit( + accessToken, + post.media[0].path + ); + postData.url = imageUpload.url; + } + } + + let all: any; + + if (useGalleryEndpoint && images.length > 1) { + // Upload all images for gallery + const uploads: { url: string; assetId?: string }[] = []; + for (let i = 0; i < images.length; i++) { + const uploaded = await this.uploadFileToReddit( + accessToken, + images[i].path + ); + uploads.push(uploaded); + + if (i < images.length - 1) { + await timer(1000); + } + } + + const items = uploads + .filter((u) => u.assetId) + .map((u) => ({ + media_id: u.assetId, + caption: '', + outbound_url: '', + })); + + if (items.length < 2) { + // Fallback to single image if insufficient asset IDs + const firstImageUpload = await this.uploadFileToReddit( + accessToken, + images[0].path + ); + postData = { + api_type: 'json', + kind: 'image', + sr: firstPostSettings.value.subreddit, + title: firstPostSettings.value.title || '', + url: firstImageUpload.url, + ...(firstPostSettings.value.flair + ? { flair_id: firstPostSettings.value.flair.id } + : {}), + }; + + all = await ( + await this.fetch('https://oauth.reddit.com/api/submit', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(postData), + }) + ).json(); + } else { + // Submit gallery post + const galleryBody = { + sr: firstPostSettings.value.subreddit, + title: firstPostSettings.value.title || '', + items, + api_type: 'json', + resubmit: true, + sendreplies: true, + nsfw: false, + spoiler: false, + ...(firstPostSettings.value.flair + ? { flair_id: firstPostSettings.value.flair.id } + : {}), + }; + + all = await ( + await this.fetch( + 'https://oauth.reddit.com/api/submit_gallery_post.json?raw_json=1', + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(galleryBody), + } + ) + ).json(); + } + } else { + all = await ( + await this.fetch('https://oauth.reddit.com/api/submit', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(postData), + }) + ).json(); + } const { id, name, url } = await new Promise<{ id: string; @@ -238,6 +356,11 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { res(all.json.data); } + if (!all?.json?.data?.websocket_url) { + res({ id: '', name: '', url: '' }); + return; + } + const ws = new WebSocket(all.json.data.websocket_url); ws.on('message', (data: any) => { setTimeout(() => {