From c7fc90f86c1e25fe5e25ad6b48e488307819c74c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 17 May 2024 20:18:54 +0700 Subject: [PATCH] feat: add slug to integration --- .../src/api/routes/integrations.controller.ts | 53 +++++++++++++------ .../integrations/integration.repository.ts | 5 +- .../integrations/integration.service.ts | 6 ++- .../src/database/prisma/schema.prisma | 1 + .../article/article.integrations.interface.ts | 22 ++++++-- .../integrations/article/dev.to.provider.ts | 3 +- .../integrations/article/hashnode.provider.ts | 5 +- .../integrations/article/medium.provider.ts | 5 +- .../integrations/social/linkedin.provider.ts | 21 +++++++- .../integrations/social/reddit.provider.ts | 2 + .../social/social.integrations.interface.ts | 1 + .../src/integrations/social/x.provider.ts | 35 +++++++++++- 12 files changed, 128 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 21d47e8d..69dd7030 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, +} from '@nestjs/common'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; @@ -12,8 +20,8 @@ import { Sections, } from '@gitroom/backend/services/auth/permissions/permissions.service'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; -import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; -import {ApiTags} from "@nestjs/swagger"; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { ApiTags } from '@nestjs/swagger'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; @ApiTags('Integrations') @@ -51,7 +59,12 @@ export class IntegrationsController { @GetUserFromRequest() user: User, @GetOrgFromRequest() org: Organization ) { - return this._integrationService.getIntegrationForOrder(id, order, user.id, org.id); + return this._integrationService.getIntegrationForOrder( + id, + order, + user.id, + org.id + ); } @Get('/social/:integration') @@ -138,9 +151,8 @@ export class IntegrationsController { const integrationProvider = this._integrationManager.getArticlesIntegration(integration); - const { id, name, token, picture } = await integrationProvider.authenticate( - api.api - ); + const { id, name, token, picture, username } = + await integrationProvider.authenticate(api.api); if (!id) { throw new Error('Invalid api key'); @@ -153,7 +165,10 @@ export class IntegrationsController { 'article', String(id), integration, - token + token, + '', + undefined, + username ); } @@ -179,11 +194,18 @@ export class IntegrationsController { const integrationProvider = this._integrationManager.getSocialIntegration(integration); - const { accessToken, expiresIn, refreshToken, id, name, picture } = - await integrationProvider.authenticate({ - code: body.code, - codeVerifier: getCodeVerifier, - }); + const { + accessToken, + expiresIn, + refreshToken, + id, + name, + picture, + username, + } = await integrationProvider.authenticate({ + code: body.code, + codeVerifier: getCodeVerifier, + }); if (!id) { throw new Error('Invalid api key'); @@ -198,7 +220,8 @@ export class IntegrationsController { integration, accessToken, refreshToken, - expiresIn + expiresIn, + username ); } @@ -231,4 +254,4 @@ export class IntegrationsController { // @ts-ignore return this._integrationService.deleteChannel(org.id, id); } -} \ No newline at end of file +} 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 c6e8ca6b..a5972b87 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -19,7 +19,8 @@ export class IntegrationRepository { provider: string, token: string, refreshToken = '', - expiresIn = 999999999 + expiresIn = 999999999, + username?: string ) { return this._integration.model.integration.upsert({ where: { @@ -33,6 +34,7 @@ export class IntegrationRepository { name, providerIdentifier: provider, token, + profile: username, picture, refreshToken, ...(expiresIn @@ -47,6 +49,7 @@ export class IntegrationRepository { providerIdentifier: provider, token, picture, + profile: username, refreshToken, ...(expiresIn ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) } 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 f4e2cf9a..1bc4e746 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -17,7 +17,8 @@ export class IntegrationService { provider: string, token: string, refreshToken = '', - expiresIn?: number + expiresIn?: number, + username?: string ) { return this._integrationRepository.createOrUpdateIntegration( org, @@ -28,7 +29,8 @@ export class IntegrationService { provider, token, refreshToken, - expiresIn + expiresIn, + username ); } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index d1338383..b56b4d84 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -185,6 +185,7 @@ model Integration { tokenExpiration DateTime? refreshToken String? posts Post[] + profile String? deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime? @updatedAt diff --git a/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts index d63d509e..c4d1a8fb 100644 --- a/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts @@ -1,9 +1,21 @@ export interface ArticleIntegrationsInterface { - authenticate(token: string): Promise<{id: string, name: string, token: string, picture: string}>; - post(token: string, content: string, settings: object): Promise<{postId: string, releaseURL: string}>; + authenticate( + token: string + ): Promise<{ + id: string; + name: string; + token: string; + picture: string; + username: string; + }>; + post( + token: string, + content: string, + settings: object + ): Promise<{ postId: string; releaseURL: string }>; } export interface ArticleProvider extends ArticleIntegrationsInterface { - identifier: string; - name: string; -} \ No newline at end of file + identifier: string; + name: string; +} diff --git a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts index e4e27dbe..377ec289 100644 --- a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts @@ -5,7 +5,7 @@ export class DevToProvider implements ArticleProvider { identifier = 'devto'; name = 'Dev.to'; async authenticate(token: string) { - const { name, id, profile_image } = await ( + const { name, id, profile_image, username } = await ( await fetch('https://dev.to/api/users/me', { headers: { 'api-key': token, @@ -18,6 +18,7 @@ export class DevToProvider implements ArticleProvider { name, token, picture: profile_image, + username }; } diff --git a/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts index bf7ce460..5cff4376 100644 --- a/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts @@ -10,7 +10,7 @@ export class HashnodeProvider implements ArticleProvider { try { const { data: { - me: { name, id, profilePicture }, + me: { name, id, profilePicture, username }, }, } = await ( await fetch('https://gql.hashnode.com', { @@ -26,6 +26,7 @@ export class HashnodeProvider implements ArticleProvider { name, id, profilePicture + username } } `, @@ -38,6 +39,7 @@ export class HashnodeProvider implements ArticleProvider { name, token, picture: profilePicture, + username }; } catch (err) { return { @@ -45,6 +47,7 @@ export class HashnodeProvider implements ArticleProvider { name: '', token: '', picture: '', + username: '' }; } } diff --git a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts index b07af7ef..efe66b8f 100644 --- a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts @@ -7,7 +7,7 @@ export class MediumProvider implements ArticleProvider { async authenticate(token: string) { const { - data: { name, id, imageUrl }, + data: { name, id, imageUrl, username }, } = await ( await fetch('https://api.medium.com/v1/me', { headers: { @@ -21,6 +21,7 @@ export class MediumProvider implements ArticleProvider { name, token, picture: imageUrl, + username, }; } @@ -52,7 +53,7 @@ export class MediumProvider implements ArticleProvider { content, ...(settings.canonical ? { canonicalUrl: settings.canonical } : {}), ...(settings?.tags?.length - ? { tags: settings?.tags?.map(p => p.value) } + ? { tags: settings?.tags?.map((p) => p.value) } : {}), publishStatus: settings?.publication ? 'draft' : 'public', }), diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 458f32b2..074be276 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -8,7 +8,6 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import sharp from 'sharp'; import { lookup } from 'mime-types'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; -import removeMd from 'remove-markdown'; import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown'; export class LinkedinProvider implements SocialProvider { @@ -30,6 +29,14 @@ export class LinkedinProvider implements SocialProvider { }) ).json(); + const { vanityName } = await ( + await fetch('https://api.linkedin.com/v2/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + const { name, sub: id, @@ -48,6 +55,7 @@ export class LinkedinProvider implements SocialProvider { refreshToken, name, picture, + username: vanityName, }; } @@ -59,7 +67,7 @@ export class LinkedinProvider implements SocialProvider { }&redirect_uri=${encodeURIComponent( `${process.env.FRONTEND_URL}/integrations/social/linkedin` )}&state=${state}&scope=${encodeURIComponent( - 'openid profile w_member_social' + 'openid profile w_member_social r_basicprofile' )}`; return { url, @@ -105,6 +113,14 @@ export class LinkedinProvider implements SocialProvider { }) ).json(); + const { vanityName } = await ( + await fetch('https://api.linkedin.com/v2/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + return { id, accessToken, @@ -112,6 +128,7 @@ export class LinkedinProvider implements SocialProvider { expiresIn, name, picture, + username: vanityName, }; } diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index ba0ecaaa..d5208446 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -48,6 +48,7 @@ export class RedditProvider implements SocialProvider { refreshToken: newRefreshToken, expiresIn, picture: icon_img.split('?')[0], + username: name, }; } @@ -105,6 +106,7 @@ export class RedditProvider implements SocialProvider { refreshToken, expiresIn, picture: icon_img.split('?')[0], + username: name, }; } 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 02bb22b3..2af4df58 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -17,6 +17,7 @@ export type AuthTokenDetails = { refreshToken?: string; // The refresh token, if applicable expiresIn?: number; // The duration in seconds for which the access token is valid picture?: string; + username: string; }; export interface ISocialMediaIntegration { diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 1a9a727f..e0dbe480 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -8,7 +8,7 @@ import { import { lookup } from 'mime-types'; import sharp from 'sharp'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; -import removeMd from "remove-markdown"; +import removeMd from 'remove-markdown'; export class XProvider implements SocialProvider { identifier = 'x'; @@ -27,6 +27,13 @@ export class XProvider implements SocialProvider { const { data: { id, name, profile_image_url }, } = await client.v2.me(); + + const { + data: { username }, + } = await client.v2.me({ + 'user.fields': 'username', + }); + return { id, name, @@ -34,6 +41,7 @@ export class XProvider implements SocialProvider { refreshToken: newRefreshToken, expiresIn, picture: profile_image_url, + username, }; } @@ -78,6 +86,12 @@ export class XProvider implements SocialProvider { true ); + const { + data: { username }, + } = await client.v2.me({ + 'user.fields': 'username', + }); + return { id: String(id), accessToken: accessToken + ':' + accessSecret, @@ -85,6 +99,7 @@ export class XProvider implements SocialProvider { refreshToken: '', expiresIn: 999999999, picture: profile_image_url_https, + username, }; } @@ -146,7 +161,10 @@ export class XProvider implements SocialProvider { const media_ids = (uploadAll[post.id] || []).filter((f) => f); const { data }: { data: { id: string } } = await client.v2.tweet({ - text: removeMd(post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'), + text: removeMd(post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace( + '𝔫𝔢𝔴𝔩𝔦𝔫𝔢', + '\n' + ), ...(media_ids.length ? { media: { media_ids } } : {}), ...(ids.length ? { reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId } } @@ -165,4 +183,17 @@ export class XProvider implements SocialProvider { status: 'posted', })); } + + // async analytics(accessToken: string) { + // const [accessTokenSplit, accessSecretSplit] = accessToken.split(':'); + // const client = new TwitterApi({ + // appKey: process.env.X_API_KEY!, + // appSecret: process.env.X_API_SECRET!, + // accessToken: accessTokenSplit, + // accessSecret: accessSecretSplit, + // }); + // const { + // data: { username }, + // } = await client.v2; + // } }