From e51906d485b8d374d74245405bac81c38b83111c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 26 May 2024 16:42:23 +0700 Subject: [PATCH] feat: more social media --- .env.example | 9 +- .../src/api/routes/integrations.controller.ts | 17 +- .../auth/permissions/permissions.service.ts | 2 +- apps/cron/src/cron.module.ts | 3 +- apps/cron/src/tasks/refresh.tokens.ts | 15 - .../public/icons/platforms/tiktok.png | Bin 0 -> 1416 bytes .../public/icons/platforms/youtube.png | Bin 0 -> 1566 bytes .../src/app/(site)/analytics/page.tsx | 4 +- .../src/app/(site)/billing/lifetime/page.tsx | 3 +- apps/frontend/src/app/(site)/billing/page.tsx | 4 +- .../integrations/social/[provider]/page.tsx | 3 +- .../frontend/src/app/(site)/launches/page.tsx | 4 +- .../src/app/(site)/marketplace/buyer/page.tsx | 3 +- .../src/app/(site)/marketplace/page.tsx | 4 +- .../app/(site)/marketplace/seller/page.tsx | 3 +- .../src/app/(site)/messages/[id]/page.tsx | 3 +- .../frontend/src/app/(site)/messages/page.tsx | 4 +- .../frontend/src/app/(site)/settings/page.tsx | 4 +- .../src/app/auth/forgot/[token]/page.tsx | 4 +- apps/frontend/src/app/auth/forgot/page.tsx | 4 +- apps/frontend/src/app/auth/login/page.tsx | 4 +- apps/frontend/src/app/auth/page.tsx | 4 +- .../src/components/billing/faq.component.tsx | 27 +- .../src/components/billing/lifetime.deal.tsx | 112 +++---- .../billing/main.billing.component.tsx | 14 +- .../src/components/launches/calendar.tsx | 5 +- .../helpers/use.custom.provider.function.ts | 24 +- .../launches/launches.component.tsx | 40 ++- .../facebook/facebook.continue.tsx | 18 +- .../instagram/instagram.continue.tsx | 16 +- .../providers/continue-provider/list.tsx | 2 +- .../launches/providers/show.all.providers.tsx | 5 +- .../providers/tiktok/tiktok.provider.tsx | 113 +++++++ .../providers/youtube/youtube.provider.tsx | 138 +++++++++ .../components/layout/continue.provider.tsx | 4 +- .../components/settings/teams.component.tsx | 2 +- .../integrations/integration.repository.ts | 31 +- .../integrations/integration.service.ts | 62 +++- .../database/prisma/posts/posts.repository.ts | 15 +- .../database/prisma/posts/posts.service.ts | 40 ++- .../src/database/prisma/schema.prisma | 4 + .../subscriptions/subscription.repository.ts | 1 + .../integrations/connect.integration.dto.ts | 6 +- .../src/dtos/posts/create.post.dto.ts | 2 + .../youtube.settings.dto.ts | 35 +++ .../src/integrations/integration.manager.ts | 4 + .../integrations/social/facebook.provider.ts | 87 +++--- .../integrations/social/instagram.provider.ts | 90 +++--- .../integrations/social/linkedin.provider.ts | 16 +- .../social/social.integrations.interface.ts | 4 +- .../integrations/social/tiktok.provider.ts | 288 ++++++++++++++++++ .../src/integrations/social/x.provider.ts | 7 +- .../integrations/social/youtube.provider.ts | 164 ++++++++++ .../src/services/stripe.service.ts | 4 +- package-lock.json | 178 ++++++++++- package.json | 1 + 56 files changed, 1374 insertions(+), 286 deletions(-) delete mode 100644 apps/cron/src/tasks/refresh.tokens.ts create mode 100644 apps/frontend/public/icons/platforms/tiktok.png create mode 100644 apps/frontend/public/icons/platforms/youtube.png create mode 100644 apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx create mode 100644 apps/frontend/src/components/launches/providers/youtube/youtube.provider.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts diff --git a/.env.example b/.env.example index 775dfe87..aa9e7006 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,11 @@ CLOUDFLARE_SECRET_ACCESS_KEY= CLOUDFLARE_BUCKETNAME= CLOUDFLARE_BUCKET_URL= CLOUDFLARE_REGION= -FEE_AMOUNT= \ No newline at end of file +FEE_AMOUNT= +OPENAI_API_KEY="" +FACEBOOK_APP_ID="" +FACEBOOK_APP_SECRET="" +YOUTUBE_CLIENT_ID="" +YOUTUBE_CLIENT_SECRET="" +TIKTOK_CLIENT_ID="" +TIKTOK_CLIENT_SECRET="" \ No newline at end of file diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index be30d674..aded60b2 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -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); diff --git a/apps/backend/src/services/auth/permissions/permissions.service.ts b/apps/backend/src/services/auth/permissions/permissions.service.ts index 54ccfcfc..7538dc9e 100644 --- a/apps/backend/src/services/auth/permissions/permissions.service.ts +++ b/apps/backend/src/services/auth/permissions/permissions.service.ts @@ -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) || diff --git a/apps/cron/src/cron.module.ts b/apps/cron/src/cron.module.ts index 6b09d106..151f2f99 100644 --- a/apps/cron/src/cron.module.ts +++ b/apps/cron/src/cron.module.ts @@ -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 {} diff --git a/apps/cron/src/tasks/refresh.tokens.ts b/apps/cron/src/tasks/refresh.tokens.ts deleted file mode 100644 index 62ddbf00..00000000 --- a/apps/cron/src/tasks/refresh.tokens.ts +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/apps/frontend/public/icons/platforms/tiktok.png b/apps/frontend/public/icons/platforms/tiktok.png new file mode 100644 index 0000000000000000000000000000000000000000..fde2ea8c68b05d5d201350b411521beed84cbf3e GIT binary patch literal 1416 zcmV;31$X+1P)I!Q!9RA_c6(~X?3MvsJ}B0AZU~rw+9k%3E=@T%1aZT5G2GHKm&>tK?#dNb|py5Qo6`6?X35HJ`_{B zk!d@fsigm8Ue5i#^PS(E^*cua!fhZe*$s%7`apV{`apV{`apV{j6?)+j=u-xbRcqL z%TfxnuAa#86LE{7NWg}eckV2D_7Kcyw5dST2v!`RYVwr0#YjkxSeD6srtp^GLcdWa zxdpiLXHt4$KfeX)yB#ATAc&9(6e_Jb;NVP{I)(9@&cp>IVUxYh_%(D`FoW5;H!k%j zch!3EZl;&aB_1eAUw-w~OZcdGA-d{*Q>3SVIcHDGOOb#?qyt4QSy^5#49gaj>S~-` z0CGbc{5~1?^KNmj`Bb!m5&vHh5s`mH#Fmr8fOp@)@$h6K*;yF6gLD+`)2F!d(>|)NR?}cb zLDy-sB^o*eOv+wa)Ed16`-&MEi9kPJxopn{Q3wWVtynud4TQoPHT6385kD-xvC z6pAp()pYtB{jlXsXBCHd|H`qB>#-SB>cadZ+bKMCFa8fo{-Gi@o||gjwV}j_y0b2%DQ>jug@jN6h}621#4^ zEd7@)rLm!&6*d>I*`4iJsiukAg#^JwI2mc+WTc@}ew_lbj~>OKHJ=gmUt^`iL#eIR zKW17G2!dtNl| zqrTlkU8;+c^A-HyznqlIWkTGsrZ<)}mZCeg|p&DZ8+r?{5Lo(}OJL`t=Mi zZV?J@US!ENTFtM^PrSn2f<w%Jf+olKhW zK(nnM?oucuQ}^wa zv@M&Y|K1;E@U^RQhZ&T7f1T`!hFjQLpOlN_KPfwNrX&Xh!w}Eiy)CLkhPMdS@J zWpTJsJ`V@vylJ*H`t_JtW9La^%siQwHZ(~fH(<(sKO95$-HvK~Voi8PWc1VWP4=XC zLUaKHMDu!Ob47);H(lD5Wr-X4rrAa=W5HY|Qqq~`jqA$)$3 zH5)}n&Jh_oSN7zNohhU$9Tklduq>)=!p=YqU)24{p+G&- z{c!Cnt`9vriou4HOv8zx6t!lAE73sH1w;=kAum>mP_Mgb?5g@edYk${dYf(=kbePW W8)%@UvN_NI0000&`Cr=RA_3V! zMUm_r2jW&Ri!4Mp!jQyB!i0%3F)oBbB%`2-Gomg89f^^E8X;s6O%|C&{y6Wwy61c@ zs=9l6($n2F)34JZ^F1z9z4vb2^VK=`{HwAA$-u<>3bM`Tmhe8Kur>U>7GSS%oybAM z2c3w4*@5yByCz~lHyoH35+iMEm-k)}th@cA=vhnsSwYCG|K&2t?On4Mk6jZ1P${?! zlr#x5!x_i`W*Zcw{ng7TA{8SQXdqG25sB66_sKia$tP)_{uG%386aU8B!oGgJ2FFy-Z=@{ z^)ORn>OcXCFWxIDrPp3j96hT1=*QH7riPlE!mH-RrQ+(m?tY!C7~_a|kxEexMCSM2 zqfnD1b*)Ly|6OtHo9b@pn;bXz}<&)+T0Iz@I$N%O9$|48kf|7mvmv|@Et7cm7W zZJ2+_SQmUhi3BL|bq%$9jz}2_NfD`Nsfpj@9~*Xa?qh6)d52K_T#)J`7(!#^m?Up$ETBtlWY} z_VWU)9-^~`Q&FmqNqH3>ZX9z{QWTeJWI$%Fk7%7TLfJZ{I(3n1lDbTFfBb{umYWrz z8E9^%&RtVbXQpu9WWEd0$ac6iJ@HbQlY|u-1-v)7A;C^;oHp)*Iu zyT-l~ae=v$TcU&T5u)2yr_iOyIA7LviG#QymhX{m~D-=G9BC=a-r1*wuh621) z{OEn?Kd(?8`#y2|9Zb@h*bPXcSlRZP`$9z0rg6Cda|_fGfNtq99Qgu%_%N01jna%% zZo9_9iCn3`>PaP0m%%HgPG~Rvi{)eAK%cl1J^vTdoo0!N9kb_Ah1ITwXRC78fCOxv zoZNAszI_pW=ezjRC()~~;buUkxhRn&PzD!%B`k=D`bcZN2Wu9XIcjr?7u+W#5E1By z52J@3hnHW%z2X$`MA;~9e*cg*|X%AzY0%2MH-|#uE13BsE`deqx0=O)K*xKX+fHk zKG^o3eoLPC8TH3cPym;c?kIzFC$kH3*w78@?Q+A$l~-l&;~I2BceE2sfMs7o>ib=#F5z#! zMLF|J@}cA8`SV03ZWAUnT(+qs+v{fC$UPzwsI`zS2xgdp&*#{=XW`MGkiYslku!>e z*8=J*L0>b;1(1M|MAoufM>N2zuoqs$j(?B#%x|a{F7$a34q0hxOzI4U_0f6n<=>Dk zM|kW$Rd7&}0>Aw>x|>!gfBiE6;YJ-2lt?HJa?u(rQAmq^(FJJanx5*erw?1WnM;M; z=+`d}*A@`X=&Pjdu^$f@u00^?V7zSbL4O!}@NoSVj)R8lL=GCR6FF%3KNkhs3rEOb Q3;+NC07*qoM6N<$f&qZ+)&Kwi literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/(site)/analytics/page.tsx b/apps/frontend/src/app/(site)/analytics/page.tsx index 9d98beeb..16d5d8da 100644 --- a/apps/frontend/src/app/(site)/analytics/page.tsx +++ b/apps/frontend/src/app/(site)/analytics/page.tsx @@ -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: '', } diff --git a/apps/frontend/src/app/(site)/billing/lifetime/page.tsx b/apps/frontend/src/app/(site)/billing/lifetime/page.tsx index 3d9aa7a8..8df7e829 100644 --- a/apps/frontend/src/app/(site)/billing/lifetime/page.tsx +++ b/apps/frontend/src/app/(site)/billing/lifetime/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/app/(site)/billing/page.tsx b/apps/frontend/src/app/(site)/billing/page.tsx index a27a4fd4..d7a9722c 100644 --- a/apps/frontend/src/app/(site)/billing/page.tsx +++ b/apps/frontend/src/app/(site)/billing/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx index 536ad5b8..d2679c36 100644 --- a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx @@ -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}`); } diff --git a/apps/frontend/src/app/(site)/launches/page.tsx b/apps/frontend/src/app/(site)/launches/page.tsx index d91acb1d..2c3b5948 100644 --- a/apps/frontend/src/app/(site)/launches/page.tsx +++ b/apps/frontend/src/app/(site)/launches/page.tsx @@ -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: '', } diff --git a/apps/frontend/src/app/(site)/marketplace/buyer/page.tsx b/apps/frontend/src/app/(site)/marketplace/buyer/page.tsx index 3f09e89b..a67c2404 100644 --- a/apps/frontend/src/app/(site)/marketplace/buyer/page.tsx +++ b/apps/frontend/src/app/(site)/marketplace/buyer/page.tsx @@ -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({ diff --git a/apps/frontend/src/app/(site)/marketplace/page.tsx b/apps/frontend/src/app/(site)/marketplace/page.tsx index 665b9cd4..f6d3302a 100644 --- a/apps/frontend/src/app/(site)/marketplace/page.tsx +++ b/apps/frontend/src/app/(site)/marketplace/page.tsx @@ -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({ diff --git a/apps/frontend/src/app/(site)/marketplace/seller/page.tsx b/apps/frontend/src/app/(site)/marketplace/seller/page.tsx index c9377f6a..dee662a4 100644 --- a/apps/frontend/src/app/(site)/marketplace/seller/page.tsx +++ b/apps/frontend/src/app/(site)/marketplace/seller/page.tsx @@ -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({ diff --git a/apps/frontend/src/app/(site)/messages/[id]/page.tsx b/apps/frontend/src/app/(site)/messages/[id]/page.tsx index f4d65941..a416877d 100644 --- a/apps/frontend/src/app/(site)/messages/[id]/page.tsx +++ b/apps/frontend/src/app/(site)/messages/[id]/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/app/(site)/messages/page.tsx b/apps/frontend/src/app/(site)/messages/page.tsx index f089ccce..c0525d21 100644 --- a/apps/frontend/src/app/(site)/messages/page.tsx +++ b/apps/frontend/src/app/(site)/messages/page.tsx @@ -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: '', } diff --git a/apps/frontend/src/app/(site)/settings/page.tsx b/apps/frontend/src/app/(site)/settings/page.tsx index 9abcf79a..6979dfd2 100644 --- a/apps/frontend/src/app/(site)/settings/page.tsx +++ b/apps/frontend/src/app/(site)/settings/page.tsx @@ -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({ diff --git a/apps/frontend/src/app/auth/forgot/[token]/page.tsx b/apps/frontend/src/app/auth/forgot/[token]/page.tsx index 48c581fa..44f4ddd5 100644 --- a/apps/frontend/src/app/auth/forgot/[token]/page.tsx +++ b/apps/frontend/src/app/auth/forgot/[token]/page.tsx @@ -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 } }) { diff --git a/apps/frontend/src/app/auth/forgot/page.tsx b/apps/frontend/src/app/auth/forgot/page.tsx index 8fd78ab3..8518e18e 100644 --- a/apps/frontend/src/app/auth/forgot/page.tsx +++ b/apps/frontend/src/app/auth/forgot/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/app/auth/login/page.tsx b/apps/frontend/src/app/auth/login/page.tsx index 36c26a87..eb40cada 100644 --- a/apps/frontend/src/app/auth/login/page.tsx +++ b/apps/frontend/src/app/auth/login/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/app/auth/page.tsx b/apps/frontend/src/app/auth/page.tsx index 1a052183..756d3fa2 100644 --- a/apps/frontend/src/app/auth/page.tsx +++ b/apps/frontend/src/app/auth/page.tsx @@ -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: '', }; diff --git a/apps/frontend/src/components/billing/faq.component.tsx b/apps/frontend/src/components/billing/faq.component.tsx index 267cf73b..dc82aa97 100644 --- a/apps/frontend/src/components/billing/faq.component.tsx +++ b/apps/frontend/src/components/billing/faq.component.tsx @@ -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 here for the open-source -`, + description: `We automate ChatGPT to help you write your social posts and articles`, }, ]; diff --git a/apps/frontend/src/components/billing/lifetime.deal.tsx b/apps/frontend/src/components/billing/lifetime.deal.tsx index 3c25afcf..3a967037 100644 --- a/apps/frontend/src/components/billing/lifetime.deal.tsx +++ b/apps/frontend/src/components/billing/lifetime.deal.tsx @@ -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 = () => {
- Current Package: {user?.tier?.current} + Current Package: {user?.totalChannels > 8 ? 'EXTRA' : user?.tier?.current}
@@ -162,51 +138,55 @@ export const LifetimeDeal = () => {
- {user?.tier?.current !== 'PRO' && ( -
-
Next Package: {nextPackage}
+
+
+ Next Package:{' '} + {user?.tier?.current === 'PRO' ? 'EXTRA' : !user?.tier?.current ? 'FREE' : user?.tier?.current === 'STANDARD' ? 'PRO' : 'STANDARD'} +
-
- {nextFeature.map((feature) => ( -
-
- - - -
-
{feature}
-
- ))} - -
-
- setCode(e.target.value)} - /> -
+
+ {(user?.tier?.current === 'PRO' + ? [`${(user?.totalChannels || 0) + 5} channels`] + : nextFeature + ).map((feature) => ( +
- + + +
+
{feature}
+
+ ))} + +
+
+ setCode(e.target.value)} + /> +
+
+
- )} +
); }; diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index a30a56fe..e6d05177 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -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; diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index faa51302..b32e0580 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -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(); diff --git a/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts index 8b903436..9e990aad 100644 --- a/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts +++ b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts @@ -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] ); diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 5069a909..534c9086 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -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) => (
@@ -96,8 +118,16 @@ export const LaunchesComponent = () => { integration.disabled && 'opacity-50' )} > - {integration.inBetweenSteps && ( -
+ {(integration.inBetweenSteps || + integration.refreshNeeded) && ( +
!
diff --git a/apps/frontend/src/components/launches/providers/continue-provider/facebook/facebook.continue.tsx b/apps/frontend/src/components/launches/providers/continue-provider/facebook/facebook.continue.tsx index d4f7879a..e51c5e6a 100644 --- a/apps/frontend/src/components/launches/providers/continue-provider/facebook/facebook.continue.tsx +++ b/apps/frontend/src/components/launches/providers/continue-provider/facebook/facebook.continue.tsx @@ -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); 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 ( diff --git a/apps/frontend/src/components/launches/providers/continue-provider/instagram/instagram.continue.tsx b/apps/frontend/src/components/launches/providers/continue-provider/instagram/instagram.continue.tsx index 71a7d1ce..7225635c 100644 --- a/apps/frontend/src/components/launches/providers/continue-provider/instagram/instagram.continue.tsx +++ b/apps/frontend/src/components/launches/providers/continue-provider/instagram/instagram.continue.tsx @@ -13,15 +13,23 @@ export const InstagramContinue: FC<{ const { closeModal, existingId } = props; const call = useCustomProviderFunction(); const { integration } = useIntegration(); - const [page, setSelectedPage] = useState(null); + const [page, setSelectedPage] = useState(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); }, [] diff --git a/apps/frontend/src/components/launches/providers/continue-provider/list.tsx b/apps/frontend/src/components/launches/providers/continue-provider/list.tsx index 4c9f59d7..0bec9f55 100644 --- a/apps/frontend/src/components/launches/providers/continue-provider/list.tsx +++ b/apps/frontend/src/components/launches/providers/continue-provider/list.tsx @@ -3,5 +3,5 @@ import { FacebookContinue } from '@gitroom/frontend/components/launches/provider export const continueProviderList = { instagram: InstagramContinue, - facebook: FacebookContinue, + facebook: FacebookContinue } \ No newline at end of file diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 85a62f5c..0df12fd5 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -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 ( diff --git a/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx new file mode 100644 index 00000000..8aabc469 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx @@ -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 ( +
+
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
1m
+
+
+
+
+
+        {!!firstPost?.images?.length && (
+          
+ {firstPost.images.map((image, index) => ( + + + + ))} +
+ )} +
+ {morePosts.map((p, index) => ( +
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
+ {p.text} +
+ + {!!p?.images?.length && ( +
+ {p.images.map((image, index) => ( + +
+ +
+
+ ))} +
+ )} +
+
+ ))} +
+ ); +}; + +export default withProvider(null, TikTokPreview); diff --git a/apps/frontend/src/components/launches/providers/youtube/youtube.provider.tsx b/apps/frontend/src/components/launches/providers/youtube/youtube.provider.tsx new file mode 100644 index 00000000..75b90ded --- /dev/null +++ b/apps/frontend/src/components/launches/providers/youtube/youtube.provider.tsx @@ -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 ( +
+ + +
+ +
+
+ ); +}; +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 ( +
+
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
1m
+
+
+
+
+
+        {!!firstPost?.images?.length && (
+          
+ {firstPost.images.map((image, index) => ( + + + + ))} +
+ )} +
+ {morePosts.map((p, index) => ( +
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
+ {p.text} +
+ + {!!p?.images?.length && ( +
+ {p.images.map((image, index) => ( + +
+ +
+
+ ))} +
+ )} +
+
+ ))} +
+ ); +}; + +export default withProvider( + YoutubeSettings, + YoutubePreview, + YoutubeSettingsDto +); diff --git a/apps/frontend/src/components/layout/continue.provider.tsx b/apps/frontend/src/components/layout/continue.provider.tsx index be656d91..58bf5ecc 100644 --- a/apps/frontend/src/components/layout/continue.provider.tsx +++ b/apps/frontend/src/components/layout/continue.provider.tsx @@ -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) { diff --git a/apps/frontend/src/components/settings/teams.component.tsx b/apps/frontend/src/components/settings/teams.component.tsx index f1e710b9..49038e68 100644 --- a/apps/frontend/src/components/settings/teams.component.tsx +++ b/apps/frontend/src/components/settings/teams.component.tsx @@ -189,7 +189,7 @@ export const TeamsComponent = () => {

Team Members

Account Managers

- Invite your assistant or team member to manage your Gitroom account + Invite your assistant or team member to manage your account
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index e7bd63fe..4c9677b6 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -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), - } + }, }); } diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index bf464f9c..d9ec0be0 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -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 }; diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index dc979553..2e250c6c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -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: { diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index c7b0dcbb..941d41dd 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -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( diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index e45014fc..01456f17 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -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]) diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts index c246c081..e2477a16 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts @@ -159,6 +159,7 @@ export class SubscriptionRepository { await this._usedCodes.model.usedCodes.create({ data: { code, + orgId: findOrg.id, }, }); } diff --git a/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts index db2fb937..28bf4cb9 100644 --- a/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts @@ -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; } \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts index f4336dde..92777a35 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -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' }, ], }, }) diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts new file mode 100644 index 00000000..27dbfd42 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/youtube.settings.dto.ts @@ -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[]; +} diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 7ff7f066..50b5c138 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -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 = [ diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index a4f9cf4f..a3fb41c7 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -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 { - 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`, { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 44a6f96c..9515792f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -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 { - 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 diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 22a701d8..44a4362e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -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!); diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index 711a609d..cd2b8ebc 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -1,7 +1,7 @@ export interface IAuthenticator { - authenticate(params: {code: string, codeVerifier: string}): Promise; + authenticate(params: {code: string, codeVerifier: string, refresh?: string}): Promise; refreshToken(refreshToken: string): Promise; - generateAuthUrl(): Promise; + generateAuthUrl(refresh?: string): Promise; } export type GenerateAuthUrlResponse = { diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts new file mode 100644 index 00000000..540cae96 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -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 { + 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 { + 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, + ]; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index bf1a2e17..d866f1bb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -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 ); diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts new file mode 100644 index 00000000..9fff423c --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -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 { + 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 { + 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 []; + } +} diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index a40ab775..a56da1c1 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -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 { diff --git a/package-lock.json b/package-lock.json index 875a1adb..5d4cad91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4d33679a..fd5e051a 100644 --- a/package.json +++ b/package.json @@ -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",