feat: images and videos to reddit

This commit is contained in:
Nevo David 2025-08-09 18:21:07 +07:00
parent 27f1db769a
commit caf99d38c5
8 changed files with 169 additions and 69 deletions

View File

@ -505,7 +505,7 @@ export const MainBillingComponent: FC<{
{t(
'your_subscription_will_be_canceled_at',
'Your subscription will be canceled at'
)}
)}{' '}
{newDayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
<br />
{t(

View File

@ -106,16 +106,12 @@ export const CreateThumbnail: FC<{
const [isCapturing, setIsCapturing] = useState(false);
const handleLoadedMetadata = useCallback(() => {
if (videoRef.current) {
setDuration(videoRef.current.duration);
setIsLoaded(true);
}
setDuration(videoRef?.current?.duration);
setIsLoaded(true);
}, []);
const handleTimeUpdate = useCallback(() => {
if (videoRef.current) {
setCurrentTime(videoRef.current.currentTime);
}
setCurrentTime(videoRef?.current?.currentTime);
}, []);
const handleSeek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@ -127,8 +123,6 @@ export const CreateThumbnail: FC<{
}, []);
const captureFrame = useCallback(async () => {
if (!videoRef.current || !canvasRef.current) return;
setIsCapturing(true);
try {
@ -299,7 +293,14 @@ export const MediaComponentInner: FC<{
alt: string;
}) => void;
media:
| { id: string; name: string; path: string; thumbnail: string; alt: string, thumbnailTimestamp?: number }
| {
id: string;
name: string;
path: string;
thumbnail: string;
alt: string;
thumbnailTimestamp?: number;
}
| undefined;
}> = (props) => {
const { onClose, onSelect, media } = props;

View File

@ -54,6 +54,7 @@ export const withProvider = function <T extends object>(params: {
value: Array<
Array<{
path: string;
thumbnail?: string;
}>
>,
settings: T,

View File

@ -111,7 +111,6 @@ const RedditPreview: FC = (props) => {
<div className="font-[600] text-[24px] mb-[16px]">
{value.title}
</div>
<RenderRedditComponent type={value.type} images={value.media} />
<div
className={clsx(
restOfPosts.length && 'mt-[40px] flex flex-col gap-[20px]'
@ -213,8 +212,27 @@ export default withProvider({
postComment: PostComment.POST,
minimumCharacters: [],
SettingsComponent: RedditSettings,
CustomPreviewComponent: RedditPreview,
CustomPreviewComponent: undefined,
dto: RedditSettingsDto,
checkValidity: undefined,
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.';
}
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,
});

View File

@ -233,24 +233,6 @@ export const Subreddit: FC<{
onChange={setURL}
/>
)}
{value.type === 'media' && (
<div className="flex flex-col">
<div className="w-full h-[10px] bg-input rounded-tr-[8px] rounded-tl-[8px]" />
<div className="flex flex-col text-nowrap">
<MultiMediaComponent
allData={[]}
dummy={dummy}
text=""
description=""
name="media"
label="Media"
value={value.media}
onChange={setMedia}
error={errors?.media?.message}
/>
</div>
</div>
)}
</>
) : (
<div className="relative">

View File

@ -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 {

View File

@ -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 {

View File

@ -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>(.*?)<\/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();