feat: images and videos to reddit
This commit is contained in:
parent
27f1db769a
commit
caf99d38c5
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export const withProvider = function <T extends object>(params: {
|
|||
value: Array<
|
||||
Array<{
|
||||
path: string;
|
||||
thumbnail?: string;
|
||||
}>
|
||||
>,
|
||||
settings: T,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue