Merge branch 'main' into sentry-ai/mcp
This commit is contained in:
commit
500acd90bd
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
|
|
@ -261,7 +262,7 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid integration');
|
||||
}
|
||||
|
||||
let newList: any[] | {none: true} = [];
|
||||
let newList: any[] | { none: true } = [];
|
||||
try {
|
||||
newList = (await this.functionIntegration(org, body)) || [];
|
||||
} catch (err) {
|
||||
|
|
@ -298,7 +299,7 @@ export class IntegrationsController {
|
|||
image: p.image,
|
||||
label: p.name,
|
||||
})),
|
||||
...newList as any[],
|
||||
...(newList as any[]),
|
||||
],
|
||||
(p) => p.id
|
||||
).filter((f) => f.label && f.id);
|
||||
|
|
@ -487,6 +488,18 @@ export class IntegrationsController {
|
|||
validName = `Channel_${String(id).slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
org.isTrailing &&
|
||||
(await this._integrationService.checkPreviousConnections(
|
||||
org.id,
|
||||
String(id)
|
||||
))
|
||||
) {
|
||||
throw new HttpException('', 412);
|
||||
}
|
||||
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ export class CheckMissingQueues {
|
|||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
(await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)) === 'delayed',
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ export class PostNowPendingQueues {
|
|||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
(await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)) === 'delayed',
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.
|
|||
import { headers } from 'next/headers';
|
||||
import { headerName } from '@gitroom/react/translation/i18n.config';
|
||||
import { HtmlComponent } from '@gitroom/frontend/components/layout/html.component';
|
||||
import dynamicLoad from 'next/dynamic';
|
||||
const SetTimezone = dynamicLoad(
|
||||
() => import('@gitroom/frontend/components/layout/set.timezone'),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
// import dynamicLoad from 'next/dynamic';
|
||||
// const SetTimezone = dynamicLoad(
|
||||
// () => import('@gitroom/frontend/components/layout/set.timezone'),
|
||||
// {
|
||||
// ssr: false,
|
||||
// }
|
||||
// );
|
||||
|
||||
const jakartaSans = Plus_Jakarta_Sans({
|
||||
weight: ['600', '500'],
|
||||
|
|
@ -79,7 +79,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
}
|
||||
>
|
||||
<SentryComponent>
|
||||
<SetTimezone />
|
||||
{/*<SetTimezone />*/}
|
||||
<HtmlComponent />
|
||||
<ToltScript />
|
||||
<FacebookComponent />
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { useModals } from '@mantine/modals';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
|
@ -42,9 +42,16 @@ export const AddProviderButton: FC<{
|
|||
update?: () => void;
|
||||
}> = (props) => {
|
||||
const { update } = props;
|
||||
const query = useSearchParams();
|
||||
const add = useAddProvider(update);
|
||||
const t = useT();
|
||||
|
||||
useEffect(() => {
|
||||
if (query.get('onboarding')) {
|
||||
add();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="text-btnText bg-btnSimple h-[44px] pt-[12px] pb-[14px] ps-[16px] pe-[20px] justify-center items-center flex rounded-[8px] gap-[8px]"
|
||||
|
|
|
|||
|
|
@ -40,9 +40,14 @@ export const ContinueIntegration: FC<{
|
|||
|
||||
const data = await fetch(`/integrations/social/${provider}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({...modifiedParams, timezone}),
|
||||
body: JSON.stringify({ ...modifiedParams, timezone }),
|
||||
});
|
||||
|
||||
if (data.status === HttpStatusCode.PreconditionFailed) {
|
||||
push(`/launches?precondition=true`);
|
||||
return ;
|
||||
}
|
||||
|
||||
if (data.status === HttpStatusCode.NotAcceptable) {
|
||||
const { msg } = await data.json();
|
||||
push(`/launches?msg=${msg}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<NewSubscription />
|
||||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-6 text-textColor flex flex-col">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ModalWrapperComponent } from '@gitroom/frontend/components/new-launch/modal.wrapper.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
||||
export const PreConditionComponentModal: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-[16px]">
|
||||
<div className="whitespace-pre-line">
|
||||
This social channel was connected previously to another Postiz account.{'\n'}
|
||||
To continue, please fast-track your trial for an immediate charge.{'\n'}{'\n'}
|
||||
** Please be advised that the account will not eligible for a refund, and the charge is final.
|
||||
</div>
|
||||
<div className="flex gap-[2px] justify-center">
|
||||
<Button onClick={() => window.location.href='/billing?finishTrial=true'}>Fast track - Charge me now</Button>
|
||||
<Button secondary={true}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const PreConditionComponent: FC = () => {
|
||||
const modal = useModals();
|
||||
const query = useSearchParams();
|
||||
useEffect(() => {
|
||||
if (query.get('precondition')) {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'text-textColor',
|
||||
},
|
||||
size: 'auto',
|
||||
children: (
|
||||
<ModalWrapperComponent title="Suspicious activity detected">
|
||||
<PreConditionComponentModal />
|
||||
</ModalWrapperComponent>
|
||||
),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ export const getTimezone = () => {
|
|||
};
|
||||
|
||||
export const newDayjs = (config?: ConfigType) => {
|
||||
return dayjs.tz(config, getTimezone());
|
||||
return dayjs(config);
|
||||
};
|
||||
|
||||
const SetTimezone: FC = () => {
|
||||
|
|
|
|||
|
|
@ -759,66 +759,84 @@ export const OnlyEditor = forwardRef<
|
|||
InterceptUnderlineShortcut,
|
||||
BulletList,
|
||||
ListItem,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
protocols: ['http', 'https'],
|
||||
isAllowedUri: (url, ctx) => {
|
||||
try {
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`${ctx.defaultProtocol}://${url}`);
|
||||
...(editorType === 'html' || editorType === 'markdown'
|
||||
? [
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
protocols: ['http', 'https'],
|
||||
isAllowedUri: (url, ctx) => {
|
||||
try {
|
||||
// prevent transforming plain emails like foo@bar.com into links
|
||||
const trimmed = String(url).trim();
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailPattern.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// use default validation
|
||||
if (!ctx.defaultValidate(parsedUrl.href)) {
|
||||
return false;
|
||||
}
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`${ctx.defaultProtocol}://${url}`);
|
||||
|
||||
// disallowed protocols
|
||||
const disallowedProtocols = ['ftp', 'file', 'mailto'];
|
||||
const protocol = parsedUrl.protocol.replace(':', '');
|
||||
// use default validation
|
||||
if (!ctx.defaultValidate(parsedUrl.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disallowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
// disallowed protocols
|
||||
const disallowedProtocols = ['ftp', 'file', 'mailto'];
|
||||
const protocol = parsedUrl.protocol.replace(':', '');
|
||||
|
||||
// only allow protocols specified in ctx.protocols
|
||||
const allowedProtocols = ctx.protocols.map((p) =>
|
||||
typeof p === 'string' ? p : p.scheme
|
||||
);
|
||||
if (disallowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!allowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
// only allow protocols specified in ctx.protocols
|
||||
const allowedProtocols = ctx.protocols.map((p) =>
|
||||
typeof p === 'string' ? p : p.scheme
|
||||
);
|
||||
|
||||
// all checks have passed
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
shouldAutoLink: (url) => {
|
||||
try {
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`https://${url}`);
|
||||
if (!allowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only auto-link if the domain is not in the disallowed list
|
||||
const disallowedDomains = [
|
||||
'example-no-autolink.com',
|
||||
'another-no-autolink.com',
|
||||
];
|
||||
const domain = parsedUrl.hostname;
|
||||
// all checks have passed
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
shouldAutoLink: (url) => {
|
||||
try {
|
||||
// prevent auto-linking of plain emails like foo@bar.com
|
||||
const trimmed = String(url).trim();
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailPattern.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !disallowedDomains.includes(domain);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`https://${url}`);
|
||||
|
||||
// only auto-link if the domain is not in the disallowed list
|
||||
const disallowedDomains = [
|
||||
'example-no-autolink.com',
|
||||
'another-no-autolink.com',
|
||||
];
|
||||
const domain = parsedUrl.hostname;
|
||||
|
||||
return !disallowedDomains.includes(domain);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(internal?.integration?.id
|
||||
? [
|
||||
Mention.configure({
|
||||
|
|
@ -839,9 +857,13 @@ export const OnlyEditor = forwardRef<
|
|||
}),
|
||||
]
|
||||
: []),
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
...(editorType === 'html' || editorType === 'markdown'
|
||||
? [
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
History.configure({
|
||||
depth: 100, // default is 100
|
||||
newGroupDelay: 100, // default is 500ms
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { ChromeExtensionComponent } from '@gitroom/frontend/components/layout/ch
|
|||
import NotificationComponent from '@gitroom/frontend/components/notifications/notification.component';
|
||||
import { BillingAfter } from '@gitroom/frontend/components/new-layout/billing.after';
|
||||
import { OrganizationSelector } from '@gitroom/frontend/components/layout/organization.selector';
|
||||
import { PreConditionComponent } from '@gitroom/frontend/components/layout/pre-condition.component';
|
||||
|
||||
const jakartaSans = Plus_Jakarta_Sans({
|
||||
weight: ['600', '500'],
|
||||
|
|
@ -79,8 +80,8 @@ export const LayoutComponent = ({ children }: { children: ReactNode }) => {
|
|||
<MediaSettingsLayout />
|
||||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<PreConditionComponent />
|
||||
<NewSubscription />
|
||||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -46,23 +46,23 @@ const MetricComponent = () => {
|
|||
))}
|
||||
</Select>
|
||||
|
||||
<div className="mt-[4px]">Current Timezone</div>
|
||||
<Select
|
||||
name="timezone"
|
||||
disableForm={true}
|
||||
label=""
|
||||
onChange={changeTimezone}
|
||||
>
|
||||
{timezones.map((metric) => (
|
||||
<option
|
||||
key={metric.name}
|
||||
value={metric.tzCode}
|
||||
selected={metric.tzCode === timezone}
|
||||
>
|
||||
{metric.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{/*<div className="mt-[4px]">Current Timezone</div>*/}
|
||||
{/*<Select*/}
|
||||
{/* name="timezone"*/}
|
||||
{/* disableForm={true}*/}
|
||||
{/* label=""*/}
|
||||
{/* onChange={changeTimezone}*/}
|
||||
{/*>*/}
|
||||
{/* {timezones.map((metric) => (*/}
|
||||
{/* <option*/}
|
||||
{/* key={metric.name}*/}
|
||||
{/* value={metric.tzCode}*/}
|
||||
{/* selected={metric.tzCode === timezone}*/}
|
||||
{/* >*/}
|
||||
{/* {metric.label}*/}
|
||||
{/* </option>*/}
|
||||
{/* ))}*/}
|
||||
{/*</Select>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import Bottleneck from 'bottleneck';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { BadBody } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
|
||||
const connection = new Bottleneck.IORedisConnection({
|
||||
client: ioRedis,
|
||||
|
|
@ -11,7 +12,8 @@ const mapper = {} as Record<string, Bottleneck>;
|
|||
export const concurrency = async <T>(
|
||||
identifier: string,
|
||||
maxConcurrent = 1,
|
||||
func: (...args: any[]) => Promise<T>
|
||||
func: (...args: any[]) => Promise<T>,
|
||||
ignoreConcurrency = false
|
||||
) => {
|
||||
const strippedIdentifier = identifier.toLowerCase().split('-')[0];
|
||||
mapper[strippedIdentifier] ??= new Bottleneck({
|
||||
|
|
@ -22,16 +24,29 @@ export const concurrency = async <T>(
|
|||
minTime: 1000,
|
||||
});
|
||||
let load: T;
|
||||
|
||||
if (ignoreConcurrency) {
|
||||
return await func();
|
||||
}
|
||||
|
||||
try {
|
||||
load = await mapper[strippedIdentifier].schedule<T>(
|
||||
{ expiration: 600000 },
|
||||
{ expiration: 60000 },
|
||||
async () => {
|
||||
try {
|
||||
return await func();
|
||||
} catch (err) {}
|
||||
}
|
||||
);
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new BadBody(
|
||||
identifier,
|
||||
JSON.stringify({}),
|
||||
{} as any,
|
||||
`Something is wrong with ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return load;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export const stripHtmlValidation = (
|
|||
convertMentionFunction
|
||||
);
|
||||
|
||||
return striptags(processedHtml, ['h1', 'h2', 'h3']);
|
||||
return striptags(processedHtml);
|
||||
}
|
||||
|
||||
// Strip all other tags
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy {
|
|||
},
|
||||
{
|
||||
maxStalledCount: 10,
|
||||
concurrency: 5,
|
||||
concurrency: 300,
|
||||
connection: ioRedis,
|
||||
removeOnComplete: {
|
||||
count: 0,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class IntegrationRepository {
|
|||
mentions: { name: string; username: string; image: string }[]
|
||||
) {
|
||||
if (mentions.length === 0) {
|
||||
return [];
|
||||
return [] as any[];
|
||||
}
|
||||
return this._mentions.model.mentions.createMany({
|
||||
data: mentions.map((mention) => ({
|
||||
|
|
@ -68,6 +68,24 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async checkPreviousConnections(org: string, id: string) {
|
||||
const findIt = await this._integration.model.integration.findMany({
|
||||
where: {
|
||||
rootInternalId: id.split('_').pop(),
|
||||
},
|
||||
select: {
|
||||
organizationId: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (findIt.some((f) => f.organizationId === org)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return findIt.length > 0;
|
||||
}
|
||||
|
||||
updateProviderSettings(org: string, id: string, settings: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ export class IntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
checkPreviousConnections(org: string, id: string) {
|
||||
return this._integrationRepository.checkPreviousConnections(org, id);
|
||||
}
|
||||
|
||||
async createOrUpdateIntegration(
|
||||
additionalSettings:
|
||||
| {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ export class PostsRepository {
|
|||
searchForMissingThreeHoursPosts() {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
integration: {
|
||||
refreshNeeded: false,
|
||||
inBetweenSteps: false,
|
||||
disabled: false,
|
||||
},
|
||||
publishDate: {
|
||||
gte: dayjs.utc().toDate(),
|
||||
lt: dayjs.utc().add(3, 'hour').toDate(),
|
||||
|
|
@ -66,6 +71,11 @@ export class PostsRepository {
|
|||
getOldPosts(orgId: string, date: string) {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
integration: {
|
||||
refreshNeeded: false,
|
||||
inBetweenSteps: false,
|
||||
disabled: false,
|
||||
},
|
||||
organizationId: orgId,
|
||||
publishDate: {
|
||||
lte: dayjs(date).toDate(),
|
||||
|
|
|
|||
|
|
@ -338,9 +338,14 @@ model Integration {
|
|||
@@unique([organizationId, internalId])
|
||||
@@index([rootInternalId])
|
||||
@@index([organizationId])
|
||||
@@index([providerIdentifier])
|
||||
@@index([updatedAt])
|
||||
@@index([createdAt])
|
||||
@@index([deletedAt])
|
||||
@@index([customerId])
|
||||
@@index([inBetweenSteps])
|
||||
@@index([refreshNeeded])
|
||||
@@index([disabled])
|
||||
}
|
||||
|
||||
model Signatures {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -14,14 +14,20 @@ export class ResendProvider implements EmailInterface {
|
|||
emailFromAddress: string,
|
||||
replyTo?: string
|
||||
) {
|
||||
const sends = await resend.emails.send({
|
||||
from: `${emailFromName} <${emailFromAddress}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
...(replyTo && { reply_to: replyTo }),
|
||||
});
|
||||
try {
|
||||
const sends = await resend.emails.send({
|
||||
from: `${emailFromName} <${emailFromAddress}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
...(replyTo && { reply_to: replyTo }),
|
||||
});
|
||||
|
||||
return sends;
|
||||
return sends;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
return { sent: false };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,12 +68,14 @@ export abstract class SocialAbstract {
|
|||
url: string,
|
||||
options: RequestInit = {},
|
||||
identifier = '',
|
||||
totalRetries = 0
|
||||
totalRetries = 0,
|
||||
ignoreConcurrency = false
|
||||
): Promise<Response> {
|
||||
const request = await concurrency(
|
||||
this.identifier,
|
||||
this.maxConcurrentJob,
|
||||
() => fetch(url, options)
|
||||
() => fetch(url, options),
|
||||
ignoreConcurrency
|
||||
);
|
||||
|
||||
if (request.status === 200 || request.status === 201) {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ async function reduceImageBySize(url: string, maxSizeKB = 976) {
|
|||
if (width < 10 || height < 10) break; // Prevent overly small dimensions
|
||||
}
|
||||
|
||||
return imageBuffer;
|
||||
return { width, height, buffer: imageBuffer };
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
throw error;
|
||||
|
|
@ -213,7 +213,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: accessJwt,
|
||||
id: did,
|
||||
name: profile.data.displayName!,
|
||||
picture: profile.data.avatar!,
|
||||
picture: profile?.data?.avatar || '',
|
||||
username: profile.data.handle!,
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
@ -259,9 +259,12 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
// Upload images
|
||||
const images = await Promise.all(
|
||||
imageMedia.map(async (p) => {
|
||||
return await agent.uploadBlob(
|
||||
new Blob([await reduceImageBySize(p.path)])
|
||||
);
|
||||
const { buffer, width, height } = await reduceImageBySize(p.path);
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
buffer: await agent.uploadBlob(new Blob([buffer])),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -288,7 +291,11 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
$type: 'app.bsky.embed.images',
|
||||
images: images.map((p, index) => ({
|
||||
alt: imageMedia?.[index]?.alt || '',
|
||||
image: p.data.blob,
|
||||
image: p.buffer.data.blob,
|
||||
aspectRatio: {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
}
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export class DevToProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
picture: profile_image,
|
||||
picture: profile_image || '',
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
.filter((role: any) =>
|
||||
role.name.toLowerCase().includes(data.query.toLowerCase())
|
||||
)
|
||||
.filter((f) => f.name !== '@everyone' && f.name !== '@here');
|
||||
.filter((f: any) => f.name !== '@everyone' && f.name !== '@here');
|
||||
|
||||
const list = await (
|
||||
await fetch(
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: refreshToken,
|
||||
expiresIn: expires_in,
|
||||
picture: profile_image,
|
||||
picture: profile_image || '',
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,9 +236,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
picture
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
|
|
@ -251,7 +249,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: url,
|
||||
picture: picture?.data?.url || '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export class FarcasterProvider
|
|||
accessToken: data.signer_uuid,
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: data.pfp_url,
|
||||
picture: data?.pfp_url || '',
|
||||
username: data.username,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
picture: profilePicture,
|
||||
picture: profilePicture || '',
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -364,9 +364,7 @@ export class InstagramProvider
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
picture
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
|
|
@ -379,7 +377,7 @@ export class InstagramProvider
|
|||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: url,
|
||||
picture: picture?.data?.url || '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -498,10 +496,14 @@ export class InstagramProvider
|
|||
while (status === 'IN_PROGRESS') {
|
||||
const { status_code } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${photoId}?access_token=${accessToken}&fields=status_code`
|
||||
`https://${type}/v20.0/${photoId}?access_token=${accessToken}&fields=status_code`,
|
||||
undefined,
|
||||
'',
|
||||
0,
|
||||
true,
|
||||
)
|
||||
).json();
|
||||
await timer(10000);
|
||||
await timer(30000);
|
||||
status = status_code;
|
||||
}
|
||||
console.log('in progress3', id);
|
||||
|
|
@ -558,10 +560,14 @@ export class InstagramProvider
|
|||
while (status === 'IN_PROGRESS') {
|
||||
const { status_code } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${containerId}?fields=status_code&access_token=${accessToken}`
|
||||
`https://${type}/v20.0/${containerId}?fields=status_code&access_token=${accessToken}`,
|
||||
undefined,
|
||||
'',
|
||||
0,
|
||||
true
|
||||
)
|
||||
).json();
|
||||
await timer(10000);
|
||||
await timer(30000);
|
||||
status = status_code;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class InstagramStandaloneProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
const { user_id, name, username, profile_picture_url } = await (
|
||||
const { user_id, name, username, profile_picture_url = '' } = await (
|
||||
await fetch(
|
||||
`https://graph.instagram.com/v21.0/me?fields=user_id,username,name,profile_picture_url&access_token=${access_token}`
|
||||
)
|
||||
|
|
@ -53,7 +53,7 @@ export class InstagramStandaloneProvider
|
|||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: profile_picture_url,
|
||||
picture: profile_picture_url || '',
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
|||
user.person_view.person.display_name ||
|
||||
user.person_view.person.name ||
|
||||
'',
|
||||
picture: user.person_view.person.avatar || '',
|
||||
picture: user?.person_view?.person?.avatar || '',
|
||||
username: body.identifier || '',
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
refreshToken,
|
||||
expiresIn: expires_in,
|
||||
name,
|
||||
picture,
|
||||
picture: picture || '',
|
||||
username: vanityName,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: tokenInformation.access_token,
|
||||
refreshToken: 'null',
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
picture: personalInformation.avatar,
|
||||
picture: personalInformation?.avatar || '',
|
||||
username: personalInformation.username,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class MediumProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
picture: imageUrl,
|
||||
picture: imageUrl || '',
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export class NostrProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: AuthService.signJWT({ password: body.password }),
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: user.picture,
|
||||
picture: user?.picture || '',
|
||||
username: user.name || 'nousername',
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export class PinterestProvider
|
|||
value: string;
|
||||
}
|
||||
| undefined {
|
||||
|
||||
if (body.indexOf('cover_image_url or cover_image_content_type') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
|
|
@ -83,7 +82,7 @@ export class PinterestProvider
|
|||
accessToken: access_token,
|
||||
refreshToken: refreshToken,
|
||||
expiresIn: expires_in,
|
||||
picture: profile_image,
|
||||
picture: profile_image || '',
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
|
@ -212,12 +211,18 @@ export class PinterestProvider
|
|||
let statusCode = '';
|
||||
while (statusCode !== 'succeeded') {
|
||||
const mediafile = await (
|
||||
await this.fetch('https://api.pinterest.com/v5/media/' + media_id, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
await this.fetch(
|
||||
'https://api.pinterest.com/v5/media/' + media_id,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
'',
|
||||
0,
|
||||
true
|
||||
)
|
||||
).json();
|
||||
|
||||
await timer(30000);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -53,7 +59,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0],
|
||||
picture: icon_img?.split?.('?')?.[0] || '',
|
||||
username: name,
|
||||
};
|
||||
}
|
||||
|
|
@ -112,11 +118,60 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0],
|
||||
picture: icon_img?.split?.('?')?.[0] || '',
|
||||
username: name,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export class SlackProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: 'null',
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
picture: user.profile.image_original,
|
||||
picture: user?.profile?.image_original || '',
|
||||
username: user.name,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: String(chat.id),
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: photo,
|
||||
picture: photo || '',
|
||||
username: chat.username!,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { capitalize, chunk } from 'lodash';
|
|||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { TwitterApi } from 'twitter-api-v2';
|
||||
|
||||
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'threads';
|
||||
|
|
@ -41,9 +40,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
id,
|
||||
name,
|
||||
username,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
picture
|
||||
} = await this.fetchPageInformation(access_token);
|
||||
|
||||
return {
|
||||
|
|
@ -52,7 +49,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: url,
|
||||
picture: picture?.data?.url || '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -112,9 +109,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
id,
|
||||
name,
|
||||
username,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
picture,
|
||||
} = await this.fetchPageInformation(access_token);
|
||||
|
||||
return {
|
||||
|
|
@ -123,7 +118,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: url,
|
||||
picture: picture?.data?.url || '',
|
||||
username: username,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
id: open_id.replace(/-/g, ''),
|
||||
name: display_name,
|
||||
picture: avatar_url,
|
||||
picture: avatar_url || '',
|
||||
username: username,
|
||||
};
|
||||
}
|
||||
|
|
@ -375,7 +375,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
body: JSON.stringify({
|
||||
publish_id: publishId,
|
||||
}),
|
||||
}
|
||||
},
|
||||
'',
|
||||
0,
|
||||
true
|
||||
)
|
||||
).json();
|
||||
|
||||
|
|
@ -399,11 +402,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
'titok-error-upload',
|
||||
JSON.stringify(post),
|
||||
Buffer.from(JSON.stringify(post)),
|
||||
handleError?.value || '',
|
||||
handleError?.value || ''
|
||||
);
|
||||
}
|
||||
|
||||
await timer(3000);
|
||||
await timer(10000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -496,7 +499,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
photo_cover_index: 0,
|
||||
photo_images: firstPost.media?.map((p) => p.path),
|
||||
},
|
||||
post_mode: firstPost?.settings?.content_posting_method === 'DIRECT_POST' ? 'DIRECT_POST' : 'MEDIA_UPLOAD',
|
||||
post_mode:
|
||||
firstPost?.settings?.content_posting_method ===
|
||||
'DIRECT_POST'
|
||||
? 'DIRECT_POST'
|
||||
: 'MEDIA_UPLOAD',
|
||||
media_type: 'PHOTO',
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: refresh_token + '&&&&' + device_id,
|
||||
expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(),
|
||||
picture: avatar,
|
||||
picture: avatar || '',
|
||||
username: first_name.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: access_token,
|
||||
refreshToken: refresh_token + '&&&&' + device_id,
|
||||
expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(),
|
||||
picture: avatar,
|
||||
picture: avatar || '',
|
||||
username: first_name.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
name,
|
||||
refreshToken: '',
|
||||
expiresIn: 999999999,
|
||||
picture: profile_image_url,
|
||||
picture: profile_image_url || '',
|
||||
username,
|
||||
additionalSettings: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
|||
refreshToken: credentials.refresh_token!,
|
||||
id: data.id!,
|
||||
name: data.name!,
|
||||
picture: data.picture!,
|
||||
picture: data?.picture || '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
|||
refreshToken: tokens.refresh_token!,
|
||||
id: data.id!,
|
||||
name: data.name!,
|
||||
picture: data.picture!,
|
||||
picture: data?.picture || '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ export class ShortLinkService {
|
|||
}
|
||||
|
||||
const mergeMessages = messages.join(' ');
|
||||
const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm;
|
||||
const urlRegex =
|
||||
/(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm;
|
||||
const urls = mergeMessages.match(urlRegex);
|
||||
if (!urls) {
|
||||
// No URLs found, return the original text
|
||||
|
|
@ -49,12 +50,20 @@ export class ShortLinkService {
|
|||
);
|
||||
}
|
||||
|
||||
async convertTextToShortLinks(id: string, messages: string[]) {
|
||||
async convertTextToShortLinks(id: string, messagesList: string[]) {
|
||||
if (ShortLinkService.provider.shortLinkDomain === 'empty') {
|
||||
return messages;
|
||||
return messagesList;
|
||||
}
|
||||
|
||||
const urlRegex = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm;
|
||||
const messages = messagesList.map((text) => {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/?/g, '?')
|
||||
.replace(/#/g, '#');
|
||||
});
|
||||
|
||||
const urlRegex =
|
||||
/(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm;
|
||||
return Promise.all(
|
||||
messages.map(async (text) => {
|
||||
const urls = uniq(text.match(urlRegex));
|
||||
|
|
|
|||
Loading…
Reference in New Issue