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