{
+ 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.';
+ }
+
+ if (
+ posts.some((p) =>
+ p.some((a) => !a.thumbnail && a.path.indexOf('mp4') > -1)
+ )
+ ) {
+ return 'You must attach a thumbnail to your video post.';
+ }
+
+ return true;
+ },
maximumCharacters: 10000,
});
diff --git a/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx b/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx
index f53db361..9e002e1f 100644
--- a/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx
+++ b/apps/frontend/src/components/new-launch/providers/reddit/subreddit.tsx
@@ -233,24 +233,6 @@ export const Subreddit: FC<{
onChange={setURL}
/>
)}
- {value.type === 'media' && (
-
- )}
>
) : (
diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts
index d92d169e..754c833d 100644
--- a/apps/frontend/src/components/new-launch/store.ts
+++ b/apps/frontend/src/components/new-launch/store.ts
@@ -11,7 +11,7 @@ import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
interface Values {
id: string;
content: string;
- media: { id: string; path: string }[];
+ media: { id: string; path: string, thumbnail?: string }[];
}
interface Internal {
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
index 1eeef00f..8785ddbe 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
@@ -9,7 +9,6 @@ import {
ValidateIf,
ValidateNested,
} from 'class-validator';
-import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
import { Type } from 'class-transformer';
export class RedditFlairDto {
@@ -57,12 +56,6 @@ export class RedditSettingsDtoInner {
@IsDefined()
@ValidateNested()
flair: RedditFlairDto;
-
- @ValidateIf((e) => e.type === 'media')
- @ValidateNested({ each: true })
- @Type(() => MediaDto)
- @ArrayMinSize(1)
- media: MediaDto[];
}
export class RedditSettingsValueDto {
diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
index 2676706a..c29194c8 100644
--- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
@@ -9,6 +9,12 @@ import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provider
import { timer } from '@gitroom/helpers/utils/timer';
import { groupBy } from 'lodash';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
+import { lookup } from 'mime-types';
+import axios from 'axios';
+import WebSocket from 'ws';
+
+// @ts-ignore
+global.WebSocket = WebSocket;
export class RedditProvider extends SocialAbstract implements SocialProvider {
override maxConcurrentJob = 1; // Reddit has strict rate limits (1 request per second)
@@ -117,6 +123,55 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
};
}
+ private async uploadFileToReddit(accessToken: string, path: 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 (
+ await this.fetch(
+ 'https://oauth.reddit.com/api/media/asset',
+ {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: formData,
+ },
+ 'reddit',
+ 0,
+ true
+ )
+ ).json();
+
+ const { data } = await axios.get(path, {
+ responseType: 'arraybuffer',
+ });
+
+ const upload = (fields as { name: string; value: string }[]).reduce(
+ (acc, value) => {
+ acc.append(value.name, value.value);
+ return acc;
+ },
+ new FormData()
+ );
+
+ upload.append(
+ 'file',
+ new Blob([Buffer.from(data)], { type: mimeType as string })
+ );
+
+ const d = await fetch('https:' + action, {
+ method: 'POST',
+ body: upload,
+ });
+
+ return [...(await d.text()).matchAll(/(.*?)<\/Location>/g)][0][1];
+ }
+
async post(
id: string,
accessToken: string,
@@ -131,7 +186,9 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
title: firstPostSettings.value.title || '',
kind:
firstPostSettings.value.type === 'media'
- ? 'image'
+ ? post.media[0].path.indexOf('mp4') > -1
+ ? 'video'
+ : 'image'
: firstPostSettings.value.type,
...(firstPostSettings.value.flair
? { flair_id: firstPostSettings.value.flair.id }
@@ -143,22 +200,25 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
: {}),
...(firstPostSettings.value.type === 'media'
? {
- url: `${
- firstPostSettings.value.media[0].path.indexOf('http') === -1
- ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/uploads`
- : ``
- }${firstPostSettings.value.media[0].path}`,
+ 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 {
- json: {
- data: { id, name, url },
- },
- } = await (
+ const all = await (
await this.fetch('https://oauth.reddit.com/api/submit', {
method: 'POST',
headers: {
@@ -169,6 +229,38 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
})
).json();
+ const { id, name, url } = await new Promise<{
+ id: string;
+ name: string;
+ url: string;
+ }>((res) => {
+ if (all?.json?.data?.id) {
+ res(all.json.data);
+ }
+
+ const ws = new WebSocket(all.json.data.websocket_url);
+ ws.on('message', (data: any) => {
+ setTimeout(() => {
+ res({ id: '', name: '', url: '' });
+ ws.close();
+ }, 30_000);
+ try {
+ const parsedData = JSON.parse(data.toString());
+ if (parsedData?.payload?.redirect) {
+ const onlyId = parsedData?.payload?.redirect.replace(
+ /https:\/\/www\.reddit\.com\/r\/.*?\/comments\/(.*?)\/.*/g,
+ '$1'
+ );
+ res({
+ id: onlyId,
+ name: `t3_${onlyId}`,
+ url: parsedData?.payload?.redirect,
+ });
+ }
+ } catch (err) {}
+ });
+ });
+
valueArray.push({
postId: id,
releaseURL: url,
@@ -202,8 +294,6 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
})
).json();
- // console.log(JSON.stringify(allTop, null, 2), JSON.stringify(allJson, null, 2), JSON.stringify(allData, null, 2));
-
valueArray.push({
postId: commentId,
releaseURL: 'https://www.reddit.com' + permalink,
@@ -233,7 +323,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
const {
data: { children },
} = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`,
{
method: 'GET',
@@ -241,7 +331,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
- }
+ },
+ 'reddit',
+ 0,
+ false
)
).json();
@@ -267,28 +360,34 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
permissions.push('link');
}
- // if (submissionType === 'any' || allow_images) {
- // permissions.push('media');
- // }
+ if (allow_images) {
+ permissions.push('media');
+ }
return permissions;
}
async restrictions(accessToken: string, data: { subreddit: string }) {
const {
- data: { submission_type, allow_images },
+ data: { submission_type, allow_images, ...all2 },
} = await (
- await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, {
- method: 'GET',
- headers: {
- Authorization: `Bearer ${accessToken}`,
- 'Content-Type': 'application/x-www-form-urlencoded',
+ await this.fetch(
+ `https://oauth.reddit.com/${data.subreddit}/about`,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
},
- })
+ 'reddit',
+ 0,
+ false
+ )
).json();
const { is_flair_required, ...all } = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/api/v1/${
data.subreddit.split('/r/')[1]
}/post_requirements`,
@@ -298,7 +397,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
- }
+ },
+ 'reddit',
+ 0,
+ false
)
).json();
@@ -307,7 +409,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
async (res) => {
try {
const flair = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
{
method: 'GET',
@@ -315,7 +417,10 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
- }
+ },
+ 'reddit',
+ 0,
+ false
)
).json();