Merge branch 'main' into sentry-ai/mcp

This commit is contained in:
Enno Gelhaus 2025-08-12 18:13:03 +02:00 committed by GitHub
commit 500acd90bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 524 additions and 243 deletions

View File

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

View File

@ -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);

View File

@ -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);

View File

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

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

@ -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]"

View File

@ -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}`);

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

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

View File

@ -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;
};

View File

@ -16,7 +16,7 @@ export const getTimezone = () => {
};
export const newDayjs = (config?: ConfigType) => {
return dayjs.tz(config, getTimezone());
return dayjs(config);
};
const SetTimezone: FC = () => {

View File

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

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

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

View File

@ -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>
);
};

View File

@ -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;
};

View File

@ -232,7 +232,7 @@ export const stripHtmlValidation = (
convertMentionFunction
);
return striptags(processedHtml, ['h1', 'h2', 'h3']);
return striptags(processedHtml);
}
// Strip all other tags

View File

@ -34,7 +34,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy {
},
{
maxStalledCount: 10,
concurrency: 5,
concurrency: 300,
connection: ioRedis,
removeOnComplete: {
count: 0,

View File

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

View File

@ -73,6 +73,10 @@ export class IntegrationService {
);
}
checkPreviousConnections(org: string, id: string) {
return this._integrationRepository.checkPreviousConnections(org, id);
}
async createOrUpdateIntegration(
additionalSettings:
| {

View File

@ -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(),

View File

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

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

@ -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 };
}
}

View File

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

View File

@ -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,
}
})),
};
}

View File

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

View File

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

View File

@ -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,
};
}

View File

@ -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: '',
};
}

View File

@ -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,
};
}

View File

@ -91,7 +91,7 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider {
accessToken: body.apiKey,
id,
name,
picture: profilePicture,
picture: profilePicture || '',
username,
};
} catch (err) {

View File

@ -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;
}

View File

@ -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,
};
}

View File

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

View File

@ -80,7 +80,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
refreshToken,
expiresIn: expires_in,
name,
picture,
picture: picture || '',
username: vanityName,
};
}

View File

@ -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,
};
}

View File

@ -72,7 +72,7 @@ export class MediumProvider extends SocialAbstract implements SocialProvider {
accessToken: body.apiKey,
id,
name,
picture: imageUrl,
picture: imageUrl || '',
username,
};
} catch (err) {

View File

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

View File

@ -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);

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)
@ -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();

View File

@ -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,
};
}

View File

@ -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!,
};
}

View File

@ -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,
};
}

View File

@ -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',
}),
}),

View File

@ -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(),
};
}

View File

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

View File

@ -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: '',
};
}

View File

@ -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(/&amp;/g, '&')
.replace(/&quest;/g, '?')
.replace(/&num;/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));