feat: more social media

This commit is contained in:
Nevo David 2024-05-26 16:42:23 +07:00
parent e4eff984f8
commit e51906d485
56 changed files with 1374 additions and 286 deletions

View File

@ -24,4 +24,11 @@ CLOUDFLARE_SECRET_ACCESS_KEY=
CLOUDFLARE_BUCKETNAME=
CLOUDFLARE_BUCKET_URL=
CLOUDFLARE_REGION=
FEE_AMOUNT=
FEE_AMOUNT=
OPENAI_API_KEY=""
FACEBOOK_APP_ID=""
FACEBOOK_APP_SECRET=""
YOUTUBE_CLIENT_ID=""
YOUTUBE_CLIENT_SECRET=""
TIKTOK_CLIENT_ID=""
TIKTOK_CLIENT_SECRET=""

View File

@ -49,6 +49,7 @@ export class IntegrationsController {
picture: p.picture,
identifier: p.providerIdentifier,
inBetweenSteps: p.inBetweenSteps,
refreshNeeded: p.refreshNeeded,
type: p.type,
})),
};
@ -71,7 +72,10 @@ export class IntegrationsController {
@Get('/social/:integration')
@CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL])
async getIntegrationUrl(@Param('integration') integration: string) {
async getIntegrationUrl(
@Param('integration') integration: string,
@Query('refresh') refresh: string
) {
if (
!this._integrationManager
.getAllowedSocialsIntegrations()
@ -83,7 +87,7 @@ export class IntegrationsController {
const integrationProvider =
this._integrationManager.getSocialIntegration(integration);
const { codeVerifier, state, url } =
await integrationProvider.generateAuthUrl();
await integrationProvider.generateAuthUrl(refresh);
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300);
return { url };
@ -170,7 +174,8 @@ export class IntegrationsController {
token,
'',
undefined,
username
username,
false
);
}
@ -207,6 +212,7 @@ export class IntegrationsController {
} = await integrationProvider.authenticate({
code: body.code,
codeVerifier: getCodeVerifier,
refresh: body.refresh,
});
if (!id) {
@ -224,7 +230,8 @@ export class IntegrationsController {
refreshToken,
expiresIn,
username,
integrationProvider.isBetweenSteps
integrationProvider.isBetweenSteps,
body.refresh
);
}
@ -239,7 +246,7 @@ export class IntegrationsController {
@Post('/instagram/:id')
async saveInstagram(
@Param('id') id: string,
@Body() body: { pageId: string, id: string },
@Body() body: { pageId: string; id: string },
@GetOrgFromRequest() org: Organization
) {
return this._integrationService.saveInstagram(org.id, id, body);

View File

@ -82,7 +82,7 @@ export class PermissionsService {
if (section === Sections.CHANNEL) {
const totalChannels = (
await this._integrationService.getIntegrationsList(orgId)
).length;
).filter(f => !f.refreshNeeded).length;
if (
(options.channel && options.channel > totalChannels) ||

View File

@ -1,6 +1,5 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { RefreshTokens } from '@gitroom/cron/tasks/refresh.tokens';
import { CheckStars } from '@gitroom/cron/tasks/check.stars';
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module';
@ -16,6 +15,6 @@ import { SyncTrending } from '@gitroom/cron/tasks/sync.trending';
}),
],
controllers: [],
providers: [RefreshTokens, CheckStars, SyncTrending],
providers: [CheckStars, SyncTrending],
})
export class CronModule {}

View File

@ -1,15 +0,0 @@
import { Injectable } from '@nestjs/common';
import {Cron} from '@nestjs/schedule';
import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service";
@Injectable()
export class RefreshTokens {
constructor(
private _integrationService: IntegrationService,
) {
}
@Cron('0 * * * *')
async refresh() {
await this._integrationService.refreshTokens();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import {AnalyticsComponent} from "@gitroom/frontend/components/analytics/analytics.component";
import {Metadata} from "next";
export const metadata: Metadata = {
title: 'Gitroom Analytics',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Analytics`,
description: '',
}

View File

@ -3,9 +3,10 @@ import { LifetimeDeal } from '@gitroom/frontend/components/billing/lifetime.deal
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const metadata: Metadata = {
title: 'Gitroom Lifetime deal',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Lifetime deal`,
description: '',
};

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import { BillingComponent } from '@gitroom/frontend/components/billing/billing.component';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Gitroom Billing',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Billing`,
description: '',
};

View File

@ -15,6 +15,7 @@ export default async function Page({
...searchParams,
state: searchParams.oauth_token || '',
code: searchParams.oauth_verifier || '',
refresh: searchParams.refresh || '',
};
}
@ -25,7 +26,7 @@ export default async function Page({
})
).json();
if (inBetweenSteps) {
if (inBetweenSteps && !searchParams.refresh) {
return redirect(`/launches?added=${provider}&continue=${id}`);
}

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches.component";
import {Metadata} from "next";
export const metadata: Metadata = {
title: 'Gitroom Launches',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Launches`,
description: '',
}

View File

@ -2,9 +2,10 @@ import { Buyer } from '@gitroom/frontend/components/marketplace/buyer';
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const metadata: Metadata = {
title: 'Gitroom Marketplace',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Marketplace`,
description: '',
};
export default async function Index({

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export const metadata: Metadata = {
title: 'Gitroom Marketplace',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Marketplace`,
description: '',
};
export default async function Index({

View File

@ -2,9 +2,10 @@ import { Seller } from '@gitroom/frontend/components/marketplace/seller';
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const metadata: Metadata = {
title: 'Gitroom Marketplace',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Marketplace`,
description: '',
};
export default async function Index({

View File

@ -3,9 +3,10 @@ import { Messages } from '@gitroom/frontend/components/messages/messages';
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const metadata: Metadata = {
title: 'Gitroom Messages',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Messages`,
description: '',
};

View File

@ -1,9 +1,11 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import {Metadata} from "next";
export const metadata: Metadata = {
title: 'Gitroom Messages',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Messages`,
description: '',
}

View File

@ -1,3 +1,5 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import { SettingsComponent } from '@gitroom/frontend/components/settings/settings.component';
@ -7,7 +9,7 @@ import { RedirectType } from 'next/dist/client/components/redirect';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Gitroom Settings',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Settings`,
description: '',
};
export default async function Index({

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import { ForgotReturn } from '@gitroom/frontend/components/auth/forgot-return';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Gitroom Forgot Password',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Forgot Password`,
description: '',
};
export default async function Auth(params: { params: { token: string } }) {

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import {Forgot} from "@gitroom/frontend/components/auth/forgot";
import {Metadata} from "next";
export const metadata: Metadata = {
title: 'Gitroom Forgot Password',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Forgot Password`,
description: '',
};

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import {Login} from "@gitroom/frontend/components/auth/login";
import {Metadata} from "next";
export const metadata: Metadata = {
title: 'Gitroom Login',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Login`,
description: '',
};

View File

@ -1,10 +1,12 @@
import { isGeneral } from '@gitroom/react/helpers/is.general';
export const dynamic = 'force-dynamic';
import { Register } from '@gitroom/frontend/components/auth/register';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Gitroom Register',
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Register`,
description: '',
};

View File

@ -1,11 +1,12 @@
import { FC, useCallback, useState } from 'react';
import clsx from 'clsx';
import interClass from '@gitroom/react/helpers/inter.font';
import { isGeneral } from '@gitroom/react/helpers/is.general';
const list = [
{
title: 'What are channels?',
description: `Gitroom allows you to schedule your posts between different channels.
description: `${isGeneral() ? 'Postiz' : 'Gitroom'} allows you to schedule your posts between different channels.
A channel is a publishing platform where you can schedule your posts.
For example, you can schedule your posts on Twitter, Linkedin, DEV and Hashnode`,
},
@ -13,31 +14,9 @@ For example, you can schedule your posts on Twitter, Linkedin, DEV and Hashnode`
title: 'What are team members?',
description: `If you have a team with multiple members, you can invite them to your workspace to collaborate on your posts and add their personal channels`,
},
{
title: 'What do I need to import content from channels?',
description: `Gitroom can help you schedule your launch, but you might write your content on other platforms such as Notion, Google Docs, etc.
You may experience problems copy your content with different formats or uploaded images.
That's why we have a feature to import your content from different platforms.
`,
},
{
title: 'What can I find in the community features?',
description: `Gitroom is all about the community, You can enjoy features such as: exchanging posts with other members,
exchanging links as part of the "Gitroom Friends" and buy social media services from other members`,
},
{
title: 'What is AI auto-complete?',
description: `We automate ChatGPT to help you write your social posts based on the articles you schedule`,
},
{
title: 'Why would I want to become featured by Gitroom?',
description: `Gitroom will feature your posts on our social media platforms and our website to help you get more exposure and followers`,
},
{
title: 'Can I get everything for free?',
description: `Gitroom is 100% open-source, you can deploy it on your own server and use it for free.
However, you might not be able to enjoy the community features Click <a class="underline font-bold" target="_blank" href="https://github.com/gitroomhq/gitroom">here for the open-source</a>
`,
description: `We automate ChatGPT to help you write your social posts and articles`,
},
];

View File

@ -54,7 +54,7 @@ export const LifetimeDeal = () => {
const currentPricing = user?.tier;
const channelsOr = currentPricing.channel;
const list = [];
list.push(`${channelsOr} ${channelsOr === 1 ? 'channel' : 'channels'}`);
list.push(`${user.totalChannels} ${user.totalChannels === 1 ? 'channel' : 'channels'}`);
list.push(
`${
currentPricing.posts_per_month > 10000
@ -66,20 +66,8 @@ export const LifetimeDeal = () => {
list.push(`Unlimited team members`);
}
if (currentPricing.import_from_channels) {
list.push(`Import content from channels (coming soon)`);
}
if (currentPricing.community_features) {
list.push(`Community features (coming soon)`);
}
if (currentPricing.ai) {
list.push(`AI auto-complete (coming soon)`);
}
if (currentPricing.featured_by_gitroom) {
list.push(`Become featured by Gitroom (coming soon)`);
list.push(`AI auto-complete`);
}
return list;
@ -104,22 +92,10 @@ export const LifetimeDeal = () => {
list.push(`Unlimited team members`);
}
if (currentPricing.import_from_channels) {
list.push(`Import content from channels (coming soon)`);
}
if (currentPricing.community_features) {
list.push(`Community features (coming soon)`);
}
if (currentPricing.ai) {
list.push(`AI auto-complete`);
}
if (currentPricing.featured_by_gitroom) {
list.push(`Become featured by Gitroom (coming soon)`);
}
return list;
}, [user, nextPackage]);
@ -136,7 +112,7 @@ export const LifetimeDeal = () => {
<div className="flex gap-[30px]">
<div className="border border-[#172034] bg-[#0B101B] p-[24px] flex flex-col gap-[20px] flex-1 rounded-[4px]">
<div className="text-[30px]">
Current Package: {user?.tier?.current}
Current Package: {user?.totalChannels > 8 ? 'EXTRA' : user?.tier?.current}
</div>
<div className="flex flex-col gap-[10px] justify-center text-[16px] text-[#AAA]">
@ -162,51 +138,55 @@ export const LifetimeDeal = () => {
</div>
</div>
{user?.tier?.current !== 'PRO' && (
<div className="border border-[#172034] bg-[#0B101B] p-[24px] flex flex-col gap-[20px] flex-1 rounded-[4px]">
<div className="text-[30px]">Next Package: {nextPackage}</div>
<div className="border border-[#172034] bg-[#0B101B] p-[24px] flex flex-col gap-[20px] flex-1 rounded-[4px]">
<div className="text-[30px]">
Next Package:{' '}
{user?.tier?.current === 'PRO' ? 'EXTRA' : !user?.tier?.current ? 'FREE' : user?.tier?.current === 'STANDARD' ? 'PRO' : 'STANDARD'}
</div>
<div className="flex flex-col gap-[10px] justify-center text-[16px] text-[#AAA]">
{nextFeature.map((feature) => (
<div key={feature} className="flex gap-[20px]">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z"
fill="#06ff00"
/>
</svg>
</div>
<div>{feature}</div>
</div>
))}
<div className="mt-[20px] flex items-center gap-[10px]">
<div className="flex-1">
<Input
label="Code"
placeholder="Enter your code"
disableForm={true}
name="code"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
</div>
<div className="flex flex-col gap-[10px] justify-center text-[16px] text-[#AAA]">
{(user?.tier?.current === 'PRO'
? [`${(user?.totalChannels || 0) + 5} channels`]
: nextFeature
).map((feature) => (
<div key={feature} className="flex gap-[20px]">
<div>
<Button disabled={code.length < 4} onClick={claim}>
Claim
</Button>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z"
fill="#06ff00"
/>
</svg>
</div>
<div>{feature}</div>
</div>
))}
<div className="mt-[20px] flex items-center gap-[10px]">
<div className="flex-1">
<Input
label="Code"
placeholder="Enter your code"
disableForm={true}
name="code"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
</div>
<div>
<Button disabled={code.length < 4} onClick={claim}>
Claim
</Button>
</div>
</div>
</div>
)}
</div>
</div>
);
};

View File

@ -104,20 +104,8 @@ export const Features: FC<{
list.push(`Unlimited team members`);
}
if (currentPricing.import_from_channels) {
list.push(`Import content from channels (coming soon)`);
}
if (currentPricing.community_features) {
list.push(`Community features (coming soon)`);
}
if (currentPricing.ai) {
list.push(`AI auto-complete (coming soon)`);
}
if (currentPricing.featured_by_gitroom) {
list.push(`Become featured by Gitroom (coming soon)`);
list.push(`AI auto-complete`);
}
return list;

View File

@ -274,7 +274,6 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
}));
const getIntegration = useCallback(async (post: Post & { integration: Integration }) => {
console.log('hello');
return (
await fetch(
`/integrations/${post.integration.id}?order=${post.submittedForOrderId}`,
@ -345,7 +344,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
title: ``,
});
},
[]
[integrations]
);
const addModal = useCallback(() => {
@ -366,7 +365,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
size: '80%',
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
});
}, []);
}, [integrations]);
const addProvider = useAddProvider();

View File

@ -7,16 +7,20 @@ export const useCustomProviderFunction = () => {
const fetch = useFetch();
const get = useCallback(
async (funcName: string, customData?: any) => {
return (
await fetch('/integrations/function', {
method: 'POST',
body: JSON.stringify({
name: funcName,
id: integration?.id!,
data: customData,
}),
})
).json();
const load = await fetch('/integrations/function', {
method: 'POST',
body: JSON.stringify({
name: funcName,
id: integration?.id!,
data: customData,
}),
});
if (load.status !== 200 && load.status !== 201) {
throw new Error('Failed to fetch');
}
return load.json();
},
[integration]
);

View File

@ -15,6 +15,7 @@ import { useUser } from '../layout/user.context';
import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator';
import { useRouter } from 'next/navigation';
import { Integration } from '@prisma/client';
export const LaunchesComponent = () => {
const fetch = useFetch();
@ -60,9 +61,25 @@ export const LaunchesComponent = () => {
}
}, []);
const continueIntegration = useCallback((integration: any) => async () => {
router.push(`/launches?added=${integration.identifier}&continue=${integration.id}`);
}, []);
const continueIntegration = useCallback(
(integration: any) => async () => {
router.push(
`/launches?added=${integration.identifier}&continue=${integration.id}`
);
},
[]
);
const refreshChannel = useCallback(
(integration: Integration & {identifier: string}) => async () => {
const {url} = await (await fetch(`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`, {
method: 'GET',
})).json();
window.location.href = url;
},
[]
);
useEffect(() => {
if (typeof window !== 'undefined' && window.opener) {
@ -87,6 +104,11 @@ export const LaunchesComponent = () => {
)}
{sortedIntegrations.map((integration) => (
<div
{...(integration.refreshNeeded && {
'data-tooltip-id': 'tooltip',
'data-tooltip-content':
'Channel disconnected, click to reconnect.',
})}
key={integration.id}
className="flex gap-[8px] items-center"
>
@ -96,8 +118,16 @@ export const LaunchesComponent = () => {
integration.disabled && 'opacity-50'
)}
>
{integration.inBetweenSteps && (
<div className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer" onClick={continueIntegration(integration)}>
{(integration.inBetweenSteps ||
integration.refreshNeeded) && (
<div
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
onClick={
integration.refreshNeeded
? refreshChannel(integration)
: continueIntegration(integration)
}
>
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
!
</div>

View File

@ -6,15 +6,23 @@ import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const FacebookContinue: FC<{closeModal: () => void, existingId: string[]}> = (props) => {
export const FacebookContinue: FC<{
closeModal: () => void;
existingId: string[];
}> = (props) => {
const { closeModal, existingId } = props;
const call = useCustomProviderFunction();
const { integration } = useIntegration();
const [page, setSelectedPage] = useState<null | string>(null);
const fetch = useFetch();
const loadPages = useCallback(() => {
return call.get('pages');
const loadPages = useCallback(async () => {
try {
const pages = await call.get('pages');
return pages;
} catch (e) {
closeModal();
}
}, []);
const setPage = useCallback(
@ -44,7 +52,9 @@ export const FacebookContinue: FC<{closeModal: () => void, existingId: string[]}
}, [integration, page]);
const filteredData = useMemo(() => {
return data?.filter((p: { id: string }) => !existingId.includes(p.id)) || [];
return (
data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
);
}, [data]);
return (

View File

@ -13,15 +13,23 @@ export const InstagramContinue: FC<{
const { closeModal, existingId } = props;
const call = useCustomProviderFunction();
const { integration } = useIntegration();
const [page, setSelectedPage] = useState<null | {id: string, pageId: string}>(null);
const [page, setSelectedPage] = useState<null | {
id: string;
pageId: string;
}>(null);
const fetch = useFetch();
const loadPages = useCallback(() => {
return call.get('pages');
const loadPages = useCallback(async () => {
try {
const pages = await call.get('pages');
return pages;
} catch (e) {
closeModal();
}
}, []);
const setPage = useCallback(
(param: {id: string, pageId: string}) => () => {
(param: { id: string; pageId: string }) => () => {
setSelectedPage(param);
},
[]

View File

@ -3,5 +3,5 @@ import { FacebookContinue } from '@gitroom/frontend/components/launches/provider
export const continueProviderList = {
instagram: InstagramContinue,
facebook: FacebookContinue,
facebook: FacebookContinue
}

View File

@ -8,6 +8,8 @@ import MediumProvider from "@gitroom/frontend/components/launches/providers/medi
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.provider';
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
export const Providers = [
{identifier: 'devto', component: DevtoProvider},
@ -18,10 +20,11 @@ export const Providers = [
{identifier: 'hashnode', component: HashnodeProvider},
{identifier: 'facebook', component: FacebookProvider},
{identifier: 'instagram', component: InstagramProvider},
{identifier: 'youtube', component: YoutubeProvider},
{identifier: 'tiktok', component: TiktokProvider},
];
export const ShowAllProviders: FC<{integrations: Integrations[], value: Array<{content: string, id?: string}>, selectedProvider?: Integrations}> = (props) => {
const {integrations, value, selectedProvider} = props;
return (

View File

@ -0,0 +1,113 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import {
afterLinkedinCompanyPreventRemove,
linkedinCompanyPreventRemove,
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
const TikTokPreview: FC = (props) => {
const { value: topValue, integration } = useIntegration();
const mediaDir = useMediaDirectory();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
saveBreaklines: true,
beforeSpecialFunc: (text: string) => {
return linkedinCompanyPreventRemove(text);
},
specialFunc: (text: string) => {
return afterLinkedinCompanyPreventRemove(text.slice(0, 280));
},
});
const [firstPost, ...morePosts] = newValues;
if (!firstPost) {
return null;
}
return (
<div className="rounded-[8px] flex flex-col gap-[8px] border border-black/90 w-[555px] pt-[12px] pl-[16px] pb-[12px] pr-[40px] bg-white text-black font-['helvetica']">
<div className="flex gap-[8px]">
<div className="w-[48px] h-[48px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex flex-col leading-[16px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[12px] font-[400] text-black/60">1m</div>
</div>
</div>
<div>
<pre
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
/>
{!!firstPost?.images?.length && (
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
{firstPost.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
className="flex-1"
target="_blank"
>
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
</a>
))}
</div>
)}
</div>
{morePosts.map((p, index) => (
<div className="flex gap-[8px]" key={index}>
<div className="w-[40px] h-[40px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex-1 flex flex-col leading-[16px] bg-[#F2F2F2] w-full pt-[8px] pr-[64px] pl-[12px] pb-[8px] rounded-[8px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[14px] mt-[8px] font-[400] text-black/90">
{p.text}
</div>
{!!p?.images?.length && (
<div className="w-full h-[120px] flex overflow-hidden mt-[12px] gap-[2px]">
{p.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
target="_blank"
>
<div className="w-[120px] h-full">
<VideoOrImage
autoplay={true}
src={mediaDir.set(image.path)}
/>
</div>
</a>
))}
</div>
)}
</div>
</div>
))}
</div>
);
};
export default withProvider(null, TikTokPreview);

View File

@ -0,0 +1,138 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import {
afterLinkedinCompanyPreventRemove,
linkedinCompanyPreventRemove,
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
const YoutubeSettings: FC = () => {
const { register, control } = useSettings();
return (
<div className="flex flex-col">
<Input label="Title" {...register('title')} />
<MediumTags label="Tags" {...register('tags')} />
<div className="mt-[20px]">
<MediaComponent
label="Thumbnail"
description="Thumbnail picture (optional)"
{...register('thumbnail')}
/>
</div>
</div>
);
};
const YoutubePreview: FC = (props) => {
const { value: topValue, integration } = useIntegration();
const mediaDir = useMediaDirectory();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
saveBreaklines: true,
beforeSpecialFunc: (text: string) => {
return linkedinCompanyPreventRemove(text);
},
specialFunc: (text: string) => {
return afterLinkedinCompanyPreventRemove(text.slice(0, 280));
},
});
const [firstPost, ...morePosts] = newValues;
if (!firstPost) {
return null;
}
return (
<div className="rounded-[8px] flex flex-col gap-[8px] border border-black/90 w-[555px] pt-[12px] pl-[16px] pb-[12px] pr-[40px] bg-white text-black font-['helvetica']">
<div className="flex gap-[8px]">
<div className="w-[48px] h-[48px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex flex-col leading-[16px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[12px] font-[400] text-black/60">1m</div>
</div>
</div>
<div>
<pre
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
/>
{!!firstPost?.images?.length && (
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
{firstPost.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
className="flex-1"
target="_blank"
>
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
</a>
))}
</div>
)}
</div>
{morePosts.map((p, index) => (
<div className="flex gap-[8px]" key={index}>
<div className="w-[40px] h-[40px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex-1 flex flex-col leading-[16px] bg-[#F2F2F2] w-full pt-[8px] pr-[64px] pl-[12px] pb-[8px] rounded-[8px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[14px] mt-[8px] font-[400] text-black/90">
{p.text}
</div>
{!!p?.images?.length && (
<div className="w-full h-[120px] flex overflow-hidden mt-[12px] gap-[2px]">
{p.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
target="_blank"
>
<div className="w-[120px] h-full">
<VideoOrImage
autoplay={true}
src={mediaDir.set(image.path)}
/>
</div>
</a>
))}
</div>
)}
</div>
</div>
))}
</div>
);
};
export default withProvider(
YoutubeSettings,
YoutubePreview,
YoutubeSettingsDto
);

View File

@ -1,4 +1,4 @@
import React, { FC, useCallback, useMemo } from 'react';
import React, { FC, useCallback, useEffect, useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { continueProviderList } from '@gitroom/frontend/components/launches/providers/continue-provider/list';
@ -38,7 +38,7 @@ export const ContinueProvider: FC = () => {
if (!added) {
return Null;
}
return continueProviderList[added as keyof typeof continueProviderList];
return continueProviderList[added as keyof typeof continueProviderList] || Null;
}, [added]);
if (!added || !continueId || !integrations) {

View File

@ -189,7 +189,7 @@ export const TeamsComponent = () => {
<h2 className="text-[24px] mb-[24px]">Team Members</h2>
<h3 className="text-[20px]">Account Managers</h3>
<div className="text-[#AAA] mt-[4px]">
Invite your assistant or team member to manage your Gitroom account
Invite your assistant or team member to manage your account
</div>
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[24px]">
<div className="flex flex-col gap-[16px]">

View File

@ -34,7 +34,8 @@ export class IntegrationRepository {
refreshToken = '',
expiresIn = 999999999,
username?: string,
isBetweenSteps = false
isBetweenSteps = false,
refresh?: string
) {
return this._integration.model.integration.upsert({
where: {
@ -57,15 +58,20 @@ export class IntegrationRepository {
: {}),
internalId,
organizationId: org,
refreshNeeded: false,
},
update: {
type: type as any,
...(!refresh
? {
inBetweenSteps: isBetweenSteps,
}
: {}),
name,
providerIdentifier: provider,
inBetweenSteps: isBetweenSteps,
token,
picture,
profile: username,
providerIdentifier: provider,
token,
refreshToken,
...(expiresIn
? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
@ -73,6 +79,7 @@ export class IntegrationRepository {
internalId,
organizationId: org,
deletedAt: null,
refreshNeeded: false,
},
});
}
@ -85,6 +92,19 @@ export class IntegrationRepository {
},
inBetweenSteps: false,
deletedAt: null,
refreshNeeded: false,
},
});
}
refreshNeeded(org: string, id: string) {
return this._integration.model.integration.update({
where: {
id,
organizationId: org,
},
data: {
refreshNeeded: true,
},
});
}
@ -104,7 +124,6 @@ export class IntegrationRepository {
user: string,
org: string
) {
console.log(id, order, user, org);
const integration = await this._posts.model.post.findFirst({
where: {
integrationId: id,
@ -204,7 +223,7 @@ export class IntegrationRepository {
},
data: {
internalId: makeId(10),
}
},
});
}

View File

@ -3,12 +3,17 @@ import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
import { SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { Integration, Organization } from '@prisma/client';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider';
@Injectable()
export class IntegrationService {
constructor(
private _integrationRepository: IntegrationRepository,
private _integrationManager: IntegrationManager
private _integrationManager: IntegrationManager,
private _notificationService: NotificationService
) {}
createOrUpdateIntegration(
org: string,
@ -21,7 +26,8 @@ export class IntegrationService {
refreshToken = '',
expiresIn?: number,
username?: string,
isBetweenSteps = false
isBetweenSteps = false,
refresh?: string
) {
return this._integrationRepository.createOrUpdateIntegration(
org,
@ -34,7 +40,8 @@ export class IntegrationService {
refreshToken,
expiresIn,
username,
isBetweenSteps
isBetweenSteps,
refresh
);
}
@ -55,6 +62,30 @@ export class IntegrationService {
return this._integrationRepository.getIntegrationById(org, id);
}
async refreshToken(provider: SocialProvider, refresh: string) {
try {
const { refreshToken, accessToken, expiresIn } =
await provider.refreshToken(refresh);
if (!refreshToken || !accessToken || !expiresIn) {
return false;
}
return { refreshToken, accessToken, expiresIn };
} catch (e) {
return false;
}
}
async informAboutRefreshError(orgId: string, integration: Integration) {
await this._notificationService.inAppNotification(
orgId,
`Could not refresh your ${integration.providerIdentifier} channel`,
`Could not refresh your ${integration.providerIdentifier} channel. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`,
true
);
}
async refreshTokens() {
const integrations = await this._integrationRepository.needsToBeRefreshed();
for (const integration of integrations) {
@ -62,8 +93,21 @@ export class IntegrationService {
integration.providerIdentifier
);
const { refreshToken, accessToken, expiresIn } =
await provider.refreshToken(integration.refreshToken!);
const data = await this.refreshToken(provider, integration.refreshToken!);
if (!data) {
await this.informAboutRefreshError(
integration.organizationId,
integration
);
await this._integrationRepository.refreshNeeded(
integration.organizationId,
integration.id
);
return;
}
const { refreshToken, accessToken, expiresIn } = data;
await this.createOrUpdateIntegration(
integration.organizationId,
@ -117,7 +161,11 @@ export class IntegrationService {
return this._integrationRepository.checkForDeletedOnceAndUpdate(org, page);
}
async saveInstagram(org: string, id: string, data: { pageId: string, id: string }) {
async saveInstagram(
org: string,
id: string,
data: { pageId: string; id: string }
) {
const getIntegration = await this._integrationRepository.getIntegrationById(
org,
id
@ -141,6 +189,7 @@ export class IntegrationService {
name: getIntegrationInformation.name,
inBetweenSteps: false,
token: getIntegrationInformation.access_token,
profile: getIntegrationInformation.username,
});
return { success: true };
@ -170,6 +219,7 @@ export class IntegrationService {
name: getIntegrationInformation.name,
inBetweenSteps: false,
token: getIntegrationInformation.access_token,
profile: getIntegrationInformation.username,
});
return { success: true };

View File

@ -1,7 +1,7 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { APPROVED_SUBMIT_FOR_ORDER, Post } from '@prisma/client';
import { APPROVED_SUBMIT_FOR_ORDER, Post, State } from '@prisma/client';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
@ -75,7 +75,7 @@ export class PostsRepository {
},
{
submittedForOrganizationId: orgId,
}
},
],
publishDate: {
gte: startDate,
@ -163,6 +163,17 @@ export class PostsRepository {
});
}
changeState(id: string, state: State) {
return this._post.model.post.update({
where: {
id,
},
data: {
state,
},
});
}
async changeDate(orgId: string, id: string, date: string) {
return this._post.model.post.update({
where: {

View File

@ -89,6 +89,16 @@ export class PostsService {
return;
}
if (firstPost.integration?.refreshNeeded) {
await this._notificationService.inAppNotification(
firstPost.organizationId,
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
true
);
return;
}
if (firstPost.integration?.disabled) {
await this._notificationService.inAppNotification(
firstPost.organizationId,
@ -112,6 +122,13 @@ export class PostsService {
]);
if (!finalPost?.postId || !finalPost?.releaseURL) {
await this._postRepository.changeState(firstPost.id, 'ERROR');
await this._notificationService.inAppNotification(
firstPost.organizationId,
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
`An error occurred while posting on ${firstPost.integration?.providerIdentifier}`,
true
);
return;
}
@ -124,10 +141,11 @@ export class PostsService {
});
}
} catch (err: any) {
await this._postRepository.changeState(firstPost.id, 'ERROR');
await this._notificationService.inAppNotification(
firstPost.organizationId,
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
`An error occurred while posting on ${firstPost.integration?.providerIdentifier}: ${err.message}`,
`An error occurred while posting on ${firstPost.integration?.providerIdentifier}`,
true
);
}
@ -159,10 +177,30 @@ export class PostsService {
const getIntegration = this._integrationManager.getSocialIntegration(
integration.providerIdentifier
);
if (!getIntegration) {
return;
}
if (dayjs(integration?.tokenExpiration).isBefore(dayjs())) {
const { accessToken, expiresIn, refreshToken } =
await getIntegration.refreshToken(integration.refreshToken!);
await this._integrationService.createOrUpdateIntegration(
integration.organizationId,
integration.name,
integration.picture!,
'social',
integration.internalId,
integration.providerIdentifier,
accessToken,
refreshToken,
expiresIn
);
integration.token = accessToken;
}
const newPosts = await this.updateTags(integration.organizationId, posts);
const publishedPosts = await getIntegration.post(

View File

@ -27,6 +27,7 @@ model Organization {
Comments Comments[]
notifications Notifications[]
buyerOrganization MessagesGroup[]
usedCodes UsedCodes[]
}
model User {
@ -70,6 +71,8 @@ model User {
model UsedCodes {
id String @id @default(uuid())
code String
orgId String
organization Organization @relation(fields: [orgId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -201,6 +204,7 @@ model Integration {
updatedAt DateTime? @updatedAt
orderItems OrderItems[]
inBetweenSteps Boolean @default(false)
refreshNeeded Boolean @default(false)
@@index([updatedAt])
@@index([deletedAt])

View File

@ -159,6 +159,7 @@ export class SubscriptionRepository {
await this._usedCodes.model.usedCodes.create({
data: {
code,
orgId: findOrg.id,
},
});
}

View File

@ -1,4 +1,4 @@
import {IsDefined, IsString} from "class-validator";
import { IsDefined, IsOptional, IsString } from 'class-validator';
export class ConnectIntegrationDto {
@IsString()
@ -8,4 +8,8 @@ export class ConnectIntegrationDto {
@IsString()
@IsDefined()
code: string;
@IsString()
@IsOptional()
refresh?: string;
}

View File

@ -8,6 +8,7 @@ import {AllProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/provide
import {MediumSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto";
import {HashnodeSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto";
import {RedditSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto";
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
export class EmptySettings {}
export class Integration {
@ -60,6 +61,7 @@ export class Post {
{ value: MediumSettingsDto, name: 'medium' },
{ value: HashnodeSettingsDto, name: 'hashnode' },
{ value: RedditSettingsDto, name: 'reddit' },
{ value: YoutubeSettingsDto, name: 'youtube' },
],
},
})

View File

@ -0,0 +1,35 @@
import {
ArrayMaxSize,
IsArray,
IsDefined,
IsOptional,
IsString,
MinLength,
ValidateNested,
} from 'class-validator';
import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
import { Type } from 'class-transformer';
export class YoutubeTagsSettings {
@IsString()
value: string;
@IsString()
label: string;
}
export class YoutubeSettingsDto {
@IsString()
@MinLength(2)
@IsDefined()
title: string;
@IsOptional()
@ValidateNested()
@Type(() => MediaDto)
thumbnail?: MediaDto;
@IsArray()
@IsOptional()
tags: YoutubeTagsSettings[];
}

View File

@ -9,6 +9,8 @@ import { MediumProvider } from '@gitroom/nestjs-libraries/integrations/article/m
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider';
import { TiktokProvider } from '@gitroom/nestjs-libraries/integrations/social/tiktok.provider';
const socialIntegrationList = [
new XProvider(),
@ -16,6 +18,8 @@ const socialIntegrationList = [
new RedditProvider(),
new FacebookProvider(),
new InstagramProvider(),
new YoutubeProvider(),
new TiktokProvider(),
];
const articleIntegrationList = [

View File

@ -5,6 +5,7 @@ import {
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
export class FacebookProvider implements SocialProvider {
identifier = 'facebook';
@ -12,47 +13,25 @@ export class FacebookProvider implements SocialProvider {
isBetweenSteps = true;
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
const { access_token, expires_in, ...all } = await (
await fetch(
'https://graph.facebook.com/v19.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&fb_exchange_token=${refresh_token}`
)
).json();
const {
id,
name,
picture: {
data: { url },
},
} = await (
await fetch(
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
return {
id,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: expires_in,
picture: url,
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl() {
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
return {
url:
'https://www.facebook.com/v19.0/dialog/oauth' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/facebook`
`${process.env.FRONTEND_URL}/integrations/social/facebook${refresh ? `?refresh=${refresh}` : ''}`
)}` +
`&state=${state}` +
'&scope=pages_show_list,business_management,pages_manage_posts,publish_video,pages_manage_engagement,pages_read_engagement',
@ -61,29 +40,51 @@ export class FacebookProvider implements SocialProvider {
};
}
async authenticate(params: { code: string; codeVerifier: string }) {
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const getAccessToken = await (
await fetch(
'https://graph.facebook.com/v19.0/oauth/access_token' +
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/facebook`
`${process.env.FRONTEND_URL}/integrations/social/facebook${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
)}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&code=${params.code}`
)
).json();
const { access_token, expires_in, ...all } = await (
const { access_token } = await (
await fetch(
'https://graph.facebook.com/v19.0/oauth/access_token' +
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&fb_exchange_token=${getAccessToken.access_token}`
`&fb_exchange_token=${getAccessToken.access_token}&fields=access_token,expires_in`
)
).json();
if (params.refresh) {
const information = await this.fetchPageInformation(
access_token,
params.refresh
);
return {
id: information.id,
name: information.name,
accessToken: information.access_token,
refreshToken: information.access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: information.picture,
username: information.username,
};
}
const {
id,
name,
@ -101,7 +102,7 @@ export class FacebookProvider implements SocialProvider {
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: expires_in,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: url,
username: '',
};
@ -122,12 +123,13 @@ export class FacebookProvider implements SocialProvider {
id,
name,
access_token,
username,
picture: {
data: { url },
},
} = await (
await fetch(
`https://graph.facebook.com/v20.0/${pageId}?fields=access_token,name,picture.type(large)&access_token=${accessToken}`
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
@ -136,6 +138,7 @@ export class FacebookProvider implements SocialProvider {
name,
access_token,
picture: url,
username,
};
}
@ -148,7 +151,7 @@ export class FacebookProvider implements SocialProvider {
let finalId = '';
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || 0) > -1) {
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
const { id: videoId, permalink_url } = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
@ -193,7 +196,11 @@ export class FacebookProvider implements SocialProvider {
})
);
const { id: postId, permalink_url } = await (
const {
id: postId,
permalink_url,
...all
} = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
{

View File

@ -6,6 +6,7 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { timer } from '@gitroom/helpers/utils/timer';
import dayjs from 'dayjs';
export class InstagramProvider implements SocialProvider {
identifier = 'instagram';
@ -13,57 +14,27 @@ export class InstagramProvider implements SocialProvider {
isBetweenSteps = true;
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
const { access_token, expires_in, ...all } = await (
await fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&fb_exchange_token=${refresh_token}`
)
).json();
const {
data: {
id,
name,
picture: {
data: { url },
},
},
} = await (
await fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture&access_token=${access_token}`
)
).json();
const {
instagram_business_account: { id: instagramId },
} = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}?fields=instagram_business_account&access_token=${access_token}`
)
).json();
return {
id: instagramId,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: expires_in,
picture: url,
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl() {
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
return {
url:
'https://www.facebook.com/v20.0/dialog/oauth' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/instagram`
`${process.env.FRONTEND_URL}/integrations/social/instagram${
refresh ? `?refresh=${refresh}` : ''
}`
)}` +
`&state=${state}` +
`&scope=${encodeURIComponent(
@ -74,13 +45,19 @@ export class InstagramProvider implements SocialProvider {
};
}
async authenticate(params: { code: string; codeVerifier: string }) {
async authenticate(params: {
code: string;
codeVerifier: string;
refresh: string;
}) {
const getAccessToken = await (
await fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/instagram`
`${process.env.FRONTEND_URL}/integrations/social/instagram${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
)}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&code=${params.code}`
@ -109,12 +86,30 @@ export class InstagramProvider implements SocialProvider {
)
).json();
if (params.refresh) {
const findPage = (await this.pages(access_token)).find(p => p.id === params.refresh);
const information = await this.fetchPageInformation(access_token, {
id: params.refresh,
pageId: findPage?.pageId!,
});
return {
id: information.id,
name: information.name,
accessToken: information.access_token,
refreshToken: information.access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: information.picture,
username: information.username,
};
}
return {
id,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: expires_in,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: url,
username: '',
};
@ -155,15 +150,15 @@ export class InstagramProvider implements SocialProvider {
accessToken: string,
data: { pageId: string; id: string }
) {
const { access_token } = await (
const { access_token, ...all } = await (
await fetch(
`https://graph.facebook.com/v20.0/${data.pageId}?fields=access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
const { id, name, profile_picture_url } = await (
const { id, name, profile_picture_url, username } = await (
await fetch(
`https://graph.facebook.com/v20.0/${data.id}?fields=name,profile_picture_url&access_token=${accessToken}`
`https://graph.facebook.com/v20.0/${data.id}?fields=username,name,profile_picture_url&access_token=${accessToken}`
)
).json();
@ -172,6 +167,7 @@ export class InstagramProvider implements SocialProvider {
name,
picture: profile_picture_url,
access_token,
username,
};
}
@ -303,7 +299,7 @@ export class InstagramProvider implements SocialProvider {
}
for (const post of theRest) {
const { id: commentId, ...all } = await (
const { id: commentId } = await (
await fetch(
`https://graph.facebook.com/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent(
post.message

View File

@ -61,13 +61,15 @@ export class LinkedinProvider implements SocialProvider {
};
}
async generateAuthUrl() {
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
const codeVerifier = makeId(30);
const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${
process.env.LINKEDIN_CLIENT_ID
}&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/linkedin`
`${process.env.FRONTEND_URL}/integrations/social/linkedin${
refresh ? `?refresh=${refresh}` : ''
}`
)}&state=${state}&scope=${encodeURIComponent(
'openid profile w_member_social r_basicprofile'
)}`;
@ -78,13 +80,19 @@ export class LinkedinProvider implements SocialProvider {
};
}
async authenticate(params: { code: string; codeVerifier: string }) {
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const body = new URLSearchParams();
body.append('grant_type', 'authorization_code');
body.append('code', params.code);
body.append(
'redirect_uri',
`${process.env.FRONTEND_URL}/integrations/social/linkedin`
`${process.env.FRONTEND_URL}/integrations/social/linkedin${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
);
body.append('client_id', process.env.LINKEDIN_CLIENT_ID!);
body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!);

View File

@ -1,7 +1,7 @@
export interface IAuthenticator {
authenticate(params: {code: string, codeVerifier: string}): Promise<AuthTokenDetails>;
authenticate(params: {code: string, codeVerifier: string, refresh?: string}): Promise<AuthTokenDetails>;
refreshToken(refreshToken: string): Promise<AuthTokenDetails>;
generateAuthUrl(): Promise<GenerateAuthUrlResponse>;
generateAuthUrl(refresh?: string): Promise<GenerateAuthUrlResponse>;
}
export type GenerateAuthUrlResponse = {

View File

@ -0,0 +1,288 @@
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
export class TiktokProvider implements SocialProvider {
identifier = 'tiktok';
name = 'Tiktok';
isBetweenSteps = false;
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
return {
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
console.log(
'https://www.tiktok.com/v2/auth/authorize' +
`?client_key=${process.env.TIKTOK_CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(
`${
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
? 'https://redirectmeto.com/'
: ''
}${process.env.FRONTEND_URL}/integrations/social/tiktok${
refresh ? `?refresh=${refresh}` : ''
}`
)}` +
`&state=${state}` +
`&response_type=code` +
`&scope=${encodeURIComponent(
'user.info.basic,video.publish,video.upload'
)}`
);
return {
url:
'https://www.tiktok.com/v2/auth/authorize' +
`?client_key=${process.env.TIKTOK_CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(
`${
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
? 'https://redirectmeto.com/'
: ''
}${process.env.FRONTEND_URL}/integrations/social/tiktok${
refresh ? `?refresh=${refresh}` : ''
}`
)}` +
`&state=${state}` +
`&response_type=code` +
`&scope=${encodeURIComponent(
'user.info.basic,video.publish,video.upload'
)}`,
codeVerifier: makeId(10),
state,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const getAccessToken = await (
await fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/facebook${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
)}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&code=${params.code}`
)
).json();
const { access_token } = await (
await fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&fb_exchange_token=${getAccessToken.access_token}&fields=access_token,expires_in`
)
).json();
if (params.refresh) {
const information = await this.fetchPageInformation(
access_token,
params.refresh
);
return {
id: information.id,
name: information.name,
accessToken: information.access_token,
refreshToken: information.access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: information.picture,
username: information.username,
};
}
const {
id,
name,
picture: {
data: { url },
},
} = await (
await fetch(
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
return {
id,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: url,
username: '',
};
}
async pages(accessToken: string) {
const { data } = await (
await fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
)
).json();
return data;
}
async fetchPageInformation(accessToken: string, pageId: string) {
const {
id,
name,
access_token,
username,
picture: {
data: { url },
},
} = await (
await fetch(
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
return {
id,
name,
access_token,
picture: url,
username,
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
): Promise<PostResponse[]> {
const [firstPost, ...comments] = postDetails;
let finalId = '';
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
const { id: videoId, permalink_url } = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
file_url: firstPost?.media?.[0]?.path!,
description: firstPost.message,
published: true,
}),
}
)
).json();
finalUrl = permalink_url;
finalId = videoId;
} else {
const uploadPhotos = !firstPost?.media?.length
? []
: await Promise.all(
firstPost.media.map(async (media) => {
const { id: photoId } = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: media.url,
published: false,
}),
}
)
).json();
return { media_fbid: photoId };
})
);
const {
id: postId,
permalink_url,
...all
} = await (
await fetch(
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(uploadPhotos?.length ? { attached_media: uploadPhotos } : {}),
message: firstPost.message,
published: true,
}),
}
)
).json();
finalUrl = permalink_url;
finalId = postId;
}
const postsArray = [];
for (const comment of comments) {
const data = await (
await fetch(
`https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(comment.media?.length
? { attachment_url: comment.media[0].url }
: {}),
message: comment.message,
}),
}
)
).json();
postsArray.push({
id: comment.id,
postId: data.id,
releaseURL: data.permalink_url,
status: 'success',
});
}
return [
{
id: firstPost.id,
postId: finalId,
releaseURL: finalUrl,
status: 'success',
},
...postsArray,
];
}
}

View File

@ -47,14 +47,16 @@ export class XProvider implements SocialProvider {
};
}
async generateAuthUrl() {
async generateAuthUrl(refresh?: string) {
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
});
const { url, oauth_token, oauth_token_secret } =
await client.generateAuthLink(
process.env.FRONTEND_URL + '/integrations/social/x',
process.env.FRONTEND_URL + `/integrations/social/x${
refresh ? `?refresh=${refresh}` : ''
}`,
{
authAccessType: 'write',
linkMode: 'authenticate',
@ -78,6 +80,7 @@ export class XProvider implements SocialProvider {
accessToken: oauth_token,
accessSecret: oauth_token_secret,
});
const { accessToken, client, accessSecret } = await startingClient.login(
code
);

View File

@ -0,0 +1,164 @@
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
import * as console from 'node:console';
import axios from 'axios';
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
const clientAndYoutube = () => {
const client = new google.auth.OAuth2({
clientId: process.env.YOUTUBE_CLIENT_ID,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET,
redirectUri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
});
const youtube = (newClient: OAuth2Client) =>
google.youtube({
version: 'v3',
auth: newClient,
});
const oauth2 = (newClient: OAuth2Client) =>
google.oauth2({
version: 'v2',
auth: newClient,
});
return { client, youtube, oauth2 };
};
export class YoutubeProvider implements SocialProvider {
identifier = 'youtube';
name = 'Youtube';
isBetweenSteps = false;
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
const { client, oauth2 } = clientAndYoutube();
client.setCredentials({ refresh_token });
const { credentials } = await client.refreshAccessToken();
const user = oauth2(client);
const expiryDate = new Date(credentials.expiry_date!);
const unixTimestamp =
Math.floor(expiryDate.getTime() / 1000) -
Math.floor(new Date().getTime() / 1000);
const { data } = await user.userinfo.get();
return {
accessToken: credentials.access_token!,
expiresIn: unixTimestamp!,
refreshToken: credentials.refresh_token!,
id: data.id!,
name: data.name!,
picture: data.picture!,
username: '',
};
}
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
const { client } = clientAndYoutube();
return {
url: client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
state,
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/youtube',
'https://www.googleapis.com/auth/youtube.force-ssl',
'https://www.googleapis.com/auth/youtube.readonly',
'https://www.googleapis.com/auth/youtube.upload',
'https://www.googleapis.com/auth/youtubepartner',
],
}),
codeVerifier: makeId(10),
state,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const { client, oauth2 } = clientAndYoutube();
const { tokens } = await client.getToken(params.code);
client.setCredentials(tokens);
const user = oauth2(client);
const { data } = await user.userinfo.get();
const expiryDate = new Date(tokens.expiry_date!);
const unixTimestamp =
Math.floor(expiryDate.getTime() / 1000) -
Math.floor(new Date().getTime() / 1000);
return {
accessToken: tokens.access_token!,
expiresIn: unixTimestamp,
refreshToken: tokens.refresh_token!,
id: data.id!,
name: data.name!,
picture: data.picture!,
username: '',
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
): Promise<PostResponse[]> {
const [firstPost, ...comments] = postDetails;
const { client, youtube } = clientAndYoutube();
client.setCredentials({ access_token: accessToken });
const youtubeClient = youtube(client);
const { settings }: { settings: YoutubeSettingsDto } = firstPost;
const response = await axios({
url: firstPost?.media?.[0]?.url,
method: 'GET',
responseType: 'stream',
});
try {
const all = await youtubeClient.videos.insert({
part: ['id', 'snippet', 'status'],
notifySubscribers: true,
requestBody: {
snippet: {
title: settings.title,
description: firstPost?.message,
tags: settings.tags.map((p) => p.label),
thumbnails: {
default: {
url: settings?.thumbnail?.path,
},
},
},
status: {
privacyStatus: 'public',
},
},
media: {
body: response.data,
},
});
console.log(all);
} catch (err) {
console.log(err);
}
return [];
}
}

View File

@ -533,10 +533,11 @@ export class StripeService {
const nextPackage = !getCurrentSubscription ? 'STANDARD' : 'PRO';
const findPricing = pricing[nextPackage];
await this._subscriptionService.createOrUpdateSubscription(
makeId(10),
organizationId,
findPricing.channel!,
getCurrentSubscription?.subscriptionTier === 'PRO' ? (getCurrentSubscription.totalChannels + 5) : findPricing.channel!,
nextPackage,
'MONTHLY',
null,
@ -546,6 +547,7 @@ export class StripeService {
return {
success: true,
};
} catch (err) {
console.log(err);
return {

178
package-lock.json generated
View File

@ -57,6 +57,7 @@
"cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.10",
"googleapis": "^137.1.0",
"ioredis": "^5.3.2",
"json-to-graphql-query": "^2.2.5",
"jsonwebtoken": "^9.0.2",
@ -15408,7 +15409,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@ -15487,6 +15487,14 @@
"node": "*"
}
},
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"engines": {
"node": "*"
}
},
"node_modules/bin-check": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz",
@ -20775,12 +20783,73 @@
"node": ">=10"
}
},
"node_modules/gaxios": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz",
"integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/https-proxy-agent": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gcd": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/gcd/-/gcd-0.0.1.tgz",
"integrity": "sha512-VNx3UEGr+ILJTiMs1+xc5SX1cMgJCrXezKPa003APUWNqQqaF6n25W8VcR7nHN6yRWbvvUTwCpZCFJeWC2kXlw==",
"dev": true
},
"node_modules/gcp-metadata": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
"dependencies": {
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
@ -21013,6 +21082,69 @@
"node": ">= 6"
}
},
"node_modules/google-auth-library": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz",
"integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/google-auth-library/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/googleapis": {
"version": "137.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==",
"dependencies": {
"google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/googleapis-common": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
"dependencies": {
"extend": "^3.0.2",
"gaxios": "^6.0.3",
"google-auth-library": "^9.7.0",
"qs": "^6.7.0",
"url-template": "^2.0.8",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -21097,6 +21229,37 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gtoken/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/gtoken/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -25996,6 +26159,14 @@
"node": ">=4"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -39139,6 +39310,11 @@
"requires-port": "^1.0.0"
}
},
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
},
"node_modules/use-composed-ref": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",

View File

@ -61,6 +61,7 @@
"cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.10",
"googleapis": "^137.1.0",
"ioredis": "^5.3.2",
"json-to-graphql-query": "^2.2.5",
"jsonwebtoken": "^9.0.2",