From 4a5e93c35618747abbaeec231f5b3ebe57a71871 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 2 Jun 2024 17:25:22 +0700 Subject: [PATCH] feat: refresh tokens fallback --- .../src/api/routes/integrations.controller.ts | 9 + apps/frontend/next.config.js | 11 +- .../public/icons/platforms/dribbble.png | Bin 0 -> 2787 bytes .../public/icons/platforms/linkedin-page.png | Bin 0 -> 1071 bytes apps/frontend/public/postiz-fav.png | Bin 0 -> 3300 bytes .../frontend/src/app/(site)/launches/page.tsx | 2 +- apps/frontend/src/app/layout.tsx | 3 +- .../launches/launches.component.tsx | 17 +- .../linkedin/linkedin.continue.tsx | 102 +++++++ .../providers/continue-provider/list.tsx | 6 +- .../launches/providers/show.all.providers.tsx | 1 + .../integrations/integration.service.ts | 40 ++- .../database/prisma/posts/posts.service.ts | 119 +++++--- .../src/integrations/integration.manager.ts | 6 +- .../src/integrations/social.abstract.ts | 13 + .../integrations/social/dribbble.provider.ts | 271 ++++++++++++++++++ .../integrations/social/facebook.provider.ts | 21 +- .../integrations/social/instagram.provider.ts | 35 +-- .../social/linkedin.page.provider.ts | 198 +++++++++++++ .../integrations/social/linkedin.provider.ts | 51 ++-- .../integrations/social/pinterest.provider.ts | 21 +- .../integrations/social/reddit.provider.ts | 23 +- .../integrations/social/tiktok.provider.ts | 21 +- .../src/integrations/social/x.provider.ts | 3 +- .../integrations/social/youtube.provider.ts | 3 +- .../src/helpers/image.with.fallback.tsx | 27 ++ 26 files changed, 862 insertions(+), 141 deletions(-) create mode 100644 apps/frontend/public/icons/platforms/dribbble.png create mode 100644 apps/frontend/public/icons/platforms/linkedin-page.png create mode 100644 apps/frontend/public/postiz-fav.png create mode 100644 apps/frontend/src/components/launches/providers/continue-provider/linkedin/linkedin.continue.tsx create mode 100644 libraries/nestjs-libraries/src/integrations/social.abstract.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts create mode 100644 libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index aded60b2..2fb340df 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -261,6 +261,15 @@ export class IntegrationsController { return this._integrationService.saveFacebook(org.id, id, body.page); } + @Post('/linkedin-page/:id') + async saveLinkedin( + @Param('id') id: string, + @Body() body: { page: string }, + @GetOrgFromRequest() org: Organization + ) { + return this._integrationService.saveLinkedin(org.id, id, body.page); + } + @Post('/enable') enableChannel( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 8edcfcf5..34610438 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -12,13 +12,20 @@ const nextConfig = { // See: https://github.com/gregberge/svgr svgr: false, }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, env: { isBillingEnabled: String(!!process.env.STRIPE_PUBLISHABLE_KEY), isGeneral: String(!!process.env.IS_GENERAL), - } + }, }; - const plugins = [ // Add more Next.js plugins to this list if needed. withNx, diff --git a/apps/frontend/public/icons/platforms/dribbble.png b/apps/frontend/public/icons/platforms/dribbble.png new file mode 100644 index 0000000000000000000000000000000000000000..4d3aa5354bc4d892d62a5fc34acfccca5c5b358b GIT binary patch literal 2787 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/public/icons/platforms/linkedin-page.png b/apps/frontend/public/icons/platforms/linkedin-page.png new file mode 100644 index 0000000000000000000000000000000000000000..3ce04bcdfc9bcd0bf84817bbb0e29e98c5cd6989 GIT binary patch literal 1071 zcmV+~1kn45P) zO{H$yN<1VMtl02au|r}36@Nihu>*;MR0%1f5|2%aDgkOm1O(J1&^C<%uAMk>{J4)9 z7WOsqz1R2J!cL0nZ(;e4M>D^B&Y5%0nDD_g?mn^Y1_;&ws~Zily3t*VrF1k)N(cY| zBTzqaTt!(+0+!Gjjiq9l9hxE}Y_SDcq1KXsBaYvndgtiqllzBFA*SY5-n)F`dcF*m zZyy%qv!gpcJ9#*z*QfE9A4*T`j-R;j^X-xerH^qLwsb<}uRfk}nHrkhmw0g^4I%r- z6oF-E{L%D~fBe3b!LgkE9kQmDWcyJLIIv1-+AOf(sblE^DEyku0roD2?LGX zE8XuMdp-&afGtpU00{{&LQDbMlT0r7`{kS!VBZcX=`;q-%P57TB5aO&{x507V2 zb3*p*ab43ndubxwj;StAbn?)iquHIX+LCQg8#p?wJvTlQ;>3S*B{Fmir0SGk(i6{RZ1qq7&ye>=1SjxjEh8AM%?Sa%UI73!UyIC%+GIE&vV&OOur4pIt-7Uy zGs;)&I~5y500d!bA)krs&HjWDmNqOf&)d>(h3eEop;>VZg-?8Cko=FEh8RJ%?k9sxKm_t(wO14tQTWXXEXXzi>x9`R pVAam2ZlwlT-DrT-jRsiV=pT4MRA*p0Mi~GA002ovPDHLkV1nCR`w##C literal 0 HcmV?d00001 diff --git a/apps/frontend/public/postiz-fav.png b/apps/frontend/public/postiz-fav.png new file mode 100644 index 0000000000000000000000000000000000000000..64a1cf0bfbbb1ea54a989d2a0b4cafb2d1ad309c GIT binary patch literal 3300 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/(site)/launches/page.tsx b/apps/frontend/src/app/(site)/launches/page.tsx index 2c3b5948..82e36764 100644 --- a/apps/frontend/src/app/(site)/launches/page.tsx +++ b/apps/frontend/src/app/(site)/launches/page.tsx @@ -6,7 +6,7 @@ import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches. import {Metadata} from "next"; export const metadata: Metadata = { - title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Launches`, + title: `${isGeneral() ? 'Postiz Calendar' : 'Gitroom Launches'}`, description: '', } diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 660310ef..9841de5c 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -7,6 +7,7 @@ import 'react-tooltip/dist/react-tooltip.css'; import LayoutContext from '@gitroom/frontend/components/layout/layout.context'; import { ReactNode } from 'react'; import { Chakra_Petch } from 'next/font/google'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] }); @@ -14,7 +15,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { return ( - + {children} diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 534c9086..2b8b1239 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -16,6 +16,7 @@ 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'; +import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback'; export const LaunchesComponent = () => { const fetch = useFetch(); @@ -71,10 +72,15 @@ export const LaunchesComponent = () => { ); const refreshChannel = useCallback( - (integration: Integration & {identifier: string}) => async () => { - const {url} = await (await fetch(`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`, { - method: 'GET', - })).json(); + (integration: Integration & { identifier: string }) => async () => { + const { url } = await ( + await fetch( + `/integrations/social/${integration.identifier}?refresh=${integration.internalId}`, + { + method: 'GET', + } + ) + ).json(); window.location.href = url; }, @@ -134,7 +140,8 @@ export const LaunchesComponent = () => {
)} - {integration.identifier} 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(async () => { + try { + const pages = await call.get('companies'); + return pages; + } catch (e) { + closeModal(); + } + }, []); + + const setPage = useCallback( + (param: { id: string; pageId: string }) => () => { + setSelectedPage(param); + }, + [] + ); + + const { data } = useSWR('load-pages', loadPages, { + refreshWhenHidden: false, + refreshWhenOffline: false, + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnMount: true, + revalidateOnReconnect: false, + refreshInterval: 0, + }); + + const saveLinkedin = useCallback(async () => { + await fetch(`/integrations/linkedin-page/${integration?.id}`, { + method: 'POST', + body: JSON.stringify(page), + }); + + closeModal(); + }, [integration, page]); + + const filteredData = useMemo(() => { + return ( + data?.filter((p: { id: string }) => !existingId.includes(p.id)) || [] + ); + }, [data]); + + return ( +
+
Select Linkedin Account:
+
+ {filteredData?.map( + (p: { + id: string; + pageId: string; + username: string; + name: string; + picture: string; + }) => ( +
+
+ profile +
+
{p.name}
+
+ ) + )} +
+
+ +
+
+ ); +}; 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 0bec9f55..7ff24102 100644 --- a/apps/frontend/src/components/launches/providers/continue-provider/list.tsx +++ b/apps/frontend/src/components/launches/providers/continue-provider/list.tsx @@ -1,7 +1,9 @@ import { InstagramContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/instagram/instagram.continue'; import { FacebookContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/facebook/facebook.continue'; +import { LinkedinContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/linkedin/linkedin.continue'; export const continueProviderList = { instagram: InstagramContinue, - facebook: FacebookContinue -} \ No newline at end of file + facebook: FacebookContinue, + 'linkedin-page': LinkedinContinue, +}; 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 a4996015..2ee9aa87 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -16,6 +16,7 @@ export const Providers = [ {identifier: 'devto', component: DevtoProvider}, {identifier: 'x', component: XProvider}, {identifier: 'linkedin', component: LinkedinProvider}, + {identifier: 'linkedin-page', component: LinkedinProvider}, {identifier: 'reddit', component: RedditProvider}, {identifier: 'medium', component: MediumProvider}, {identifier: 'hashnode', component: HashnodeProvider}, 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 d9ec0be0..2f201b73 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -4,9 +4,9 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ 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 { Integration } from '@prisma/client'; import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; -import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider'; +import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider'; @Injectable() export class IntegrationService { @@ -86,6 +86,10 @@ export class IntegrationService { ); } + async refreshNeeded(org: string, id: string) { + return this._integrationRepository.refreshNeeded(org, id); + } + async refreshTokens() { const integrations = await this._integrationRepository.needsToBeRefreshed(); for (const integration of integrations) { @@ -195,6 +199,38 @@ export class IntegrationService { return { success: true }; } + async saveLinkedin(org: string, id: string, page: string) { + const getIntegration = await this._integrationRepository.getIntegrationById( + org, + id + ); + if (getIntegration && !getIntegration.inBetweenSteps) { + throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST); + } + + const linkedin = this._integrationManager.getSocialIntegration( + 'linkedin-page' + ) as LinkedinPageProvider; + + const getIntegrationInformation = await linkedin.fetchPageInformation( + getIntegration?.token!, + page + ); + + await this.checkForDeletedOnceAndUpdate(org, String(getIntegrationInformation.id)); + + await this._integrationRepository.updateIntegration(String(id), { + picture: getIntegrationInformation.picture, + internalId: String(getIntegrationInformation.id), + name: getIntegrationInformation.name, + inBetweenSteps: false, + token: getIntegrationInformation.access_token, + profile: getIntegrationInformation.username, + }); + + return { success: true }; + } + async saveFacebook(org: string, id: string, page: string) { const getIntegration = await this._integrationRepository.getIntegrationById( org, 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 7bacf814..80002293 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -16,6 +16,7 @@ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto'; import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract'; type PostWithConditionals = Post & { integration?: Integration; @@ -145,7 +146,9 @@ export class PostsService { 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} ${JSON.stringify(err)}`, + `An error occurred while posting on ${ + firstPost.integration?.providerIdentifier + } ${JSON.stringify(err)}`, true ); } @@ -173,19 +176,33 @@ export class PostsService { return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]); } - private async postSocial(integration: Integration, posts: Post[]) { + private async postSocial( + integration: Integration, + posts: Post[], + forceRefresh = false + ): Promise> { const getIntegration = this._integrationManager.getSocialIntegration( integration.providerIdentifier ); if (!getIntegration) { - return; + return {}; } - if (dayjs(integration?.tokenExpiration).isBefore(dayjs())) { + if (dayjs(integration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) { const { accessToken, expiresIn, refreshToken } = await getIntegration.refreshToken(integration.refreshToken!); + if (!accessToken) { + await this._integrationService.refreshNeeded( + integration.organizationId, + integration.id + ); + + await this._integrationService.informAboutRefreshError(integration.organizationId, integration); + return {}; + } + await this._integrationService.createOrUpdateIntegration( integration.organizationId, integration.name, @@ -203,51 +220,59 @@ export class PostsService { const newPosts = await this.updateTags(integration.organizationId, posts); - const publishedPosts = await getIntegration.post( - integration.internalId, - integration.token, - newPosts.map((p) => ({ - id: p.id, - message: p.content, - settings: JSON.parse(p.settings || '{}'), - media: (JSON.parse(p.image || '[]') as Media[]).map((m) => ({ - url: - m.path.indexOf('http') === -1 - ? process.env.FRONTEND_URL + - '/' + - process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + - m.path - : m.path, - type: 'image', - path: - m.path.indexOf('http') === -1 - ? process.env.UPLOAD_DIRECTORY + m.path - : m.path, - })), - })) - ); - - for (const post of publishedPosts) { - await this._postRepository.updatePost( - post.id, - post.postId, - post.releaseURL + try { + const publishedPosts = await getIntegration.post( + integration.internalId, + integration.token, + newPosts.map((p) => ({ + id: p.id, + message: p.content, + settings: JSON.parse(p.settings || '{}'), + media: (JSON.parse(p.image || '[]') as Media[]).map((m) => ({ + url: + m.path.indexOf('http') === -1 + ? process.env.FRONTEND_URL + + '/' + + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + + m.path + : m.path, + type: 'image', + path: + m.path.indexOf('http') === -1 + ? process.env.UPLOAD_DIRECTORY + m.path + : m.path, + })), + })) ); + + for (const post of publishedPosts) { + await this._postRepository.updatePost( + post.id, + post.postId, + post.releaseURL + ); + } + + await this._notificationService.inAppNotification( + integration.organizationId, + `Your post has been published on ${capitalize( + integration.providerIdentifier + )}`, + `Your post has been published at ${publishedPosts[0].releaseURL}`, + true + ); + + return { + postId: publishedPosts[0].postId, + releaseURL: publishedPosts[0].releaseURL, + }; + } catch (err) { + if (err instanceof RefreshToken) { + return this.postSocial(integration, posts, true); + } + + throw err; } - - await this._notificationService.inAppNotification( - integration.organizationId, - `Your post has been published on ${capitalize( - integration.providerIdentifier - )}`, - `Your post has been published at ${publishedPosts[0].releaseURL}`, - true - ); - - return { - postId: publishedPosts[0].postId, - releaseURL: publishedPosts[0].releaseURL, - }; } private async postArticle(integration: Integration, posts: Post[]) { diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index efa47538..c0e93cda 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -12,16 +12,20 @@ import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider'; import { TiktokProvider } from '@gitroom/nestjs-libraries/integrations/social/tiktok.provider'; import { PinterestProvider } from '@gitroom/nestjs-libraries/integrations/social/pinterest.provider'; +import { DribbbleProvider } from '@gitroom/nestjs-libraries/integrations/social/dribbble.provider'; +import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider'; const socialIntegrationList = [ new XProvider(), new LinkedinProvider(), + new LinkedinPageProvider(), new RedditProvider(), new FacebookProvider(), new InstagramProvider(), new YoutubeProvider(), new TiktokProvider(), - new PinterestProvider() + new PinterestProvider(), + new DribbbleProvider(), ]; const articleIntegrationList = [ diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts new file mode 100644 index 00000000..52439fdb --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -0,0 +1,13 @@ +export class RefreshToken { +} + +export abstract class SocialAbstract { + async fetch(url: string, options: RequestInit = {}) { + const request = await fetch(url, options); + if (request.status === 401) { + throw new RefreshToken(); + } + + return request; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts new file mode 100644 index 00000000..e892e9ae --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts @@ -0,0 +1,271 @@ +import { + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto'; +import axios from 'axios'; +import FormData from 'form-data'; +import { timer } from '@gitroom/helpers/utils/timer'; +import dayjs from 'dayjs'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; + +export class DribbbleProvider extends SocialAbstract implements SocialProvider { + identifier = 'dribbble'; + name = 'Dribbbble'; + isBetweenSteps = false; + + async refreshToken(refreshToken: string): Promise { + const { access_token, expires_in } = await ( + await this.fetch('https://api-sandbox.pinterest.com/v5/oauth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + `${process.env.PINTEREST_CLIENT_ID}:${process.env.PINTEREST_CLIENT_SECRET}` + ).toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + scope: + 'boards:read,boards:write,pins:read,pins:write,user_accounts:read', + redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/pinterest`, + }), + }) + ).json(); + + const { id, profile_image, username } = await ( + await this.fetch('https://api-sandbox.pinterest.com/v5/user_account', { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + id: id, + name: username, + accessToken: access_token, + refreshToken: refreshToken, + expiresIn: expires_in, + picture: profile_image, + username, + }; + } + + async teams(accessToken: string) { + const { teams } = await ( + await this.fetch('https://api.dribbble.com/v2/user', { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + return teams?.map((team: any) => ({ + id: team.id, + name: team.name, + })) || []; + } + + async generateAuthUrl(refresh?: string) { + const state = makeId(6); + return { + url: `https://dribbble.com/oauth/authorize?client_id=${ + process.env.DRIBBBLE_CLIENT_ID + }&redirect_uri=${encodeURIComponent( + `${process.env.FRONTEND_URL}/integrations/social/dribbble${ + refresh ? `?refresh=${refresh}` : '' + }` + )}&response_type=code&scope=public+upload&state=${state}`, + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh: string; + }) { + const { access_token } = await ( + await this.fetch( + `https://dribbble.com/oauth/token?client_id=${process.env.DRIBBBLE_CLIENT_ID}&client_secret=${process.env.DRIBBBLE_CLIENT_SECRET}&code=${params.code}&redirect_uri=${process.env.FRONTEND_URL}/integrations/social/dribbble`, + { + method: 'POST', + } + ) + ).json(); + + const { id, name, avatar_url, login } = await ( + await this.fetch('https://api.dribbble.com/v2/user', { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + id: id, + name, + accessToken: access_token, + refreshToken: '', + expiresIn: 999999999, + picture: avatar_url, + username: login, + }; + } + + async boards(accessToken: string) { + const { items } = await ( + await this.fetch('https://api-sandbox.pinterest.com/v5/boards', { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + return ( + items?.map((item: any) => ({ + name: item.name, + id: item.id, + })) || [] + ); + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise { + let mediaId = ''; + const findMp4 = postDetails?.[0]?.media?.find( + (p) => (p.path?.indexOf('mp4') || -1) > -1 + ); + const picture = postDetails?.[0]?.media?.find( + (p) => (p.path?.indexOf('mp4') || -1) === -1 + ); + + if (findMp4) { + const { upload_url, media_id, upload_parameters } = await ( + await this.fetch('https://api-sandbox.pinterest.com/v5/media', { + method: 'POST', + body: JSON.stringify({ + media_type: 'video', + }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + const { data, status } = await axios.get( + postDetails?.[0]?.media?.[0]?.url!, + { + responseType: 'stream', + } + ); + + const formData = Object.keys(upload_parameters) + .filter((f) => f) + .reduce((acc, key) => { + acc.append(key, upload_parameters[key]); + return acc; + }, new FormData()); + + formData.append('file', data); + await axios.post(upload_url, formData); + + let statusCode = ''; + while (statusCode !== 'succeeded') { + console.log('trying'); + const mediafile = await ( + await this.fetch( + 'https://api-sandbox.pinterest.com/v5/media/' + media_id, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).json(); + + await timer(3000); + statusCode = mediafile.status; + } + + mediaId = media_id; + } + + const mapImages = postDetails?.[0]?.media?.map((m) => ({ + url: m.url, + })); + + try { + const { + id: pId, + link, + ...all + } = await ( + await this.fetch('https://api-sandbox.pinterest.com/v5/pins', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...(postDetails?.[0]?.settings.link + ? { link: postDetails?.[0]?.settings.link } + : {}), + ...(postDetails?.[0]?.settings.title + ? { title: postDetails?.[0]?.settings.title } + : {}), + ...(postDetails?.[0]?.settings.description + ? { title: postDetails?.[0]?.settings.description } + : {}), + ...(postDetails?.[0]?.settings.dominant_color + ? { title: postDetails?.[0]?.settings.dominant_color } + : {}), + board_id: postDetails?.[0]?.settings.board, + media_source: mediaId + ? { + source_type: 'video_id', + media_id: mediaId, + cover_image_url: picture?.url, + } + : mapImages?.length === 1 + ? { + source_type: 'image_url', + url: mapImages?.[0]?.url, + } + : { + source_type: 'multiple_image_urls', + items: mapImages, + }, + }), + }) + ).json(); + + return [ + { + id: postDetails?.[0]?.id, + postId: pId, + releaseURL: `https://www.pinterest.com/pin/${pId}`, + status: 'success', + }, + ]; + } catch (err) { + console.log(err); + return []; + } + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index 8785dc08..d8824364 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -6,8 +6,9 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import dayjs from 'dayjs'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class FacebookProvider implements SocialProvider { +export class FacebookProvider extends SocialAbstract implements SocialProvider { identifier = 'facebook'; name = 'Facebook Page'; isBetweenSteps = true; @@ -46,7 +47,7 @@ export class FacebookProvider implements SocialProvider { refresh?: string; }) { const getAccessToken = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + `?client_id=${process.env.FACEBOOK_APP_ID}` + `&redirect_uri=${encodeURIComponent( @@ -60,7 +61,7 @@ export class FacebookProvider implements SocialProvider { ).json(); const { access_token } = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + '?grant_type=fb_exchange_token' + `&client_id=${process.env.FACEBOOK_APP_ID}` + @@ -92,7 +93,7 @@ export class FacebookProvider implements SocialProvider { data: { url }, }, } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}` ) ).json(); @@ -110,7 +111,7 @@ export class FacebookProvider implements SocialProvider { async pages(accessToken: string) { const { data } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}` ) ).json(); @@ -128,7 +129,7 @@ export class FacebookProvider implements SocialProvider { data: { url }, }, } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}` ) ).json(); @@ -153,7 +154,7 @@ export class FacebookProvider implements SocialProvider { let finalUrl = ''; if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) { const { id: videoId, permalink_url } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', @@ -177,7 +178,7 @@ export class FacebookProvider implements SocialProvider { : await Promise.all( firstPost.media.map(async (media) => { const { id: photoId } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`, { method: 'POST', @@ -201,7 +202,7 @@ export class FacebookProvider implements SocialProvider { permalink_url, ...all } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', @@ -224,7 +225,7 @@ export class FacebookProvider implements SocialProvider { const postsArray = []; for (const comment of comments) { const data = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 8f11cca8..37b6be8f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -7,8 +7,9 @@ import { import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { timer } from '@gitroom/helpers/utils/timer'; import dayjs from 'dayjs'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class InstagramProvider implements SocialProvider { +export class InstagramProvider extends SocialAbstract implements SocialProvider { identifier = 'instagram'; name = 'Instagram'; isBetweenSteps = true; @@ -51,7 +52,7 @@ export class InstagramProvider implements SocialProvider { refresh: string; }) { const getAccessToken = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + `?client_id=${process.env.FACEBOOK_APP_ID}` + `&redirect_uri=${encodeURIComponent( @@ -65,7 +66,7 @@ export class InstagramProvider implements SocialProvider { ).json(); const { access_token, expires_in, ...all } = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + '?grant_type=fb_exchange_token' + `&client_id=${process.env.FACEBOOK_APP_ID}` + @@ -81,7 +82,7 @@ export class InstagramProvider implements SocialProvider { data: { url }, }, } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}` ) ).json(); @@ -117,7 +118,7 @@ export class InstagramProvider implements SocialProvider { async pages(accessToken: string) { const { data } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/me/accounts?fields=id,instagram_business_account,username,name,picture.type(large)&access_token=${accessToken}&limit=500` ) ).json(); @@ -129,7 +130,7 @@ export class InstagramProvider implements SocialProvider { return { pageId: p.id, ...(await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${p.instagram_business_account.id}?fields=name,profile_picture_url&access_token=${accessToken}&limit=500` ) ).json()), @@ -151,13 +152,13 @@ export class InstagramProvider implements SocialProvider { data: { pageId: string; id: string } ) { const { access_token, ...all } = await ( - await fetch( + await this.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, username } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${data.id}?fields=username,name,profile_picture_url&access_token=${accessToken}` ) ).json(); @@ -191,7 +192,7 @@ export class InstagramProvider implements SocialProvider { : `video_url=${m.url}&media_type=VIDEO` : `image_url=${m.url}`; const { id: photoId } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/media?${mediaType}${caption}${isCarousel}&access_token=${accessToken}`, { method: 'POST', @@ -202,7 +203,7 @@ export class InstagramProvider implements SocialProvider { let status = 'IN_PROGRESS'; while (status === 'IN_PROGRESS') { const { status_code } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${photoId}?access_token=${accessToken}&fields=status_code` ) ).json(); @@ -220,7 +221,7 @@ export class InstagramProvider implements SocialProvider { let linkGlobal = ''; if (medias.length === 1) { const { id: mediaId } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${medias[0]}&access_token=${accessToken}&field=id`, { method: 'POST', @@ -231,7 +232,7 @@ export class InstagramProvider implements SocialProvider { containerIdGlobal = mediaId; const { permalink } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` ) ).json(); @@ -246,7 +247,7 @@ export class InstagramProvider implements SocialProvider { linkGlobal = permalink; } else { const { id: containerId, ...all3 } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/media?caption=${encodeURIComponent( firstPost?.message )}&media_type=CAROUSEL&children=${encodeURIComponent( @@ -261,7 +262,7 @@ export class InstagramProvider implements SocialProvider { let status = 'IN_PROGRESS'; while (status === 'IN_PROGRESS') { const { status_code } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${containerId}?fields=status_code&access_token=${accessToken}` ) ).json(); @@ -270,7 +271,7 @@ export class InstagramProvider implements SocialProvider { } const { id: mediaId, ...all4 } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${containerId}&access_token=${accessToken}&field=id`, { method: 'POST', @@ -281,7 +282,7 @@ export class InstagramProvider implements SocialProvider { containerIdGlobal = mediaId; const { permalink } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` ) ).json(); @@ -298,7 +299,7 @@ export class InstagramProvider implements SocialProvider { for (const post of theRest) { const { id: commentId } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent( post.message )}&access_token=${accessToken}`, diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts new file mode 100644 index 00000000..cef78db9 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -0,0 +1,198 @@ +import { + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider'; + +export class LinkedinPageProvider + extends LinkedinProvider + implements SocialProvider +{ + override identifier = 'linkedin-page'; + override name = 'LinkedIn Page'; + override isBetweenSteps = true; + + override async refreshToken( + refresh_token: string + ): Promise { + const { access_token: accessToken, refresh_token: refreshToken } = await ( + await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token, + client_id: process.env.LINKEDIN_CLIENT_ID!, + client_secret: process.env.LINKEDIN_CLIENT_SECRET!, + }), + }) + ).json(); + + const { vanityName } = await ( + await fetch('https://api.linkedin.com/v2/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + const { + name, + sub: id, + picture, + } = await ( + await fetch('https://api.linkedin.com/v2/userinfo', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + return { + id, + accessToken, + refreshToken, + name, + picture, + username: vanityName, + }; + } + + override 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-page${ + refresh ? `?refresh=${refresh}` : '' + }` + )}&state=${state}&scope=${encodeURIComponent( + 'openid profile w_member_social r_basicprofile rw_organization_admin w_organization_social r_organization_social' + )}`; + return { + url, + codeVerifier, + state, + }; + } + + async companies(accessToken: string) { + const { elements } = await ( + await fetch( + 'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(localizedName,vanityName,logoV2(original~:playableStreams))))', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).json(); + + return (elements || []).map((e: any) => ({ + id: e.organizationalTarget.split(':').pop(), + page: e.organizationalTarget.split(':').pop(), + username: e['organizationalTarget~'].vanityName, + name: e['organizationalTarget~'].localizedName, + picture: + e['organizationalTarget~'].logoV2?.['original~']?.elements?.[0] + ?.identifiers?.[0]?.identifier, + })); + } + + async fetchPageInformation(accessToken: string, pageId: string) { + const data = await ( + await fetch( + `https://api.linkedin.com/v2/organizations/${pageId}?projection=(id,localizedName,vanityName,logoV2(original~:playableStreams))`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).json(); + + return { + id: data.id, + name: data.localizedName, + access_token: accessToken, + picture: data?.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0].identifier, + username: data.vanityName, + }; + } + + override 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-page${ + params.refresh ? `?refresh=${params.refresh}` : '' + }` + ); + body.append('client_id', process.env.LINKEDIN_CLIENT_ID!); + body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!); + + const { + access_token: accessToken, + expires_in: expiresIn, + refresh_token: refreshToken, + } = await ( + await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body, + }) + ).json(); + + const { + name, + sub: id, + picture, + } = await ( + await fetch('https://api.linkedin.com/v2/userinfo', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + const { vanityName } = await ( + await fetch('https://api.linkedin.com/v2/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + return { + id, + accessToken, + refreshToken, + expiresIn, + name, + picture, + username: vanityName, + }; + } + + override async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise { + return super.post(id, accessToken, postDetails, 'company'); + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 44a4362e..25fe4b88 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -9,15 +9,16 @@ import sharp from 'sharp'; import { lookup } from 'mime-types'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class LinkedinProvider implements SocialProvider { +export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; name = 'LinkedIn'; isBetweenSteps = false; async refreshToken(refresh_token: string): Promise { const { access_token: accessToken, refresh_token: refreshToken } = await ( - await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + await this.fetch('https://www.linkedin.com/oauth/v2/accessToken', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -32,7 +33,7 @@ export class LinkedinProvider implements SocialProvider { ).json(); const { vanityName } = await ( - await fetch('https://api.linkedin.com/v2/me', { + await this.fetch('https://api.linkedin.com/v2/me', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -44,7 +45,7 @@ export class LinkedinProvider implements SocialProvider { sub: id, picture, } = await ( - await fetch('https://api.linkedin.com/v2/userinfo', { + await this.fetch('https://api.linkedin.com/v2/userinfo', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -102,7 +103,7 @@ export class LinkedinProvider implements SocialProvider { expires_in: expiresIn, refresh_token: refreshToken, } = await ( - await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + await this.fetch('https://www.linkedin.com/oauth/v2/accessToken', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -116,7 +117,7 @@ export class LinkedinProvider implements SocialProvider { sub: id, picture, } = await ( - await fetch('https://api.linkedin.com/v2/userinfo', { + await this.fetch('https://api.linkedin.com/v2/userinfo', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -124,7 +125,7 @@ export class LinkedinProvider implements SocialProvider { ).json(); const { vanityName } = await ( - await fetch('https://api.linkedin.com/v2/me', { + await this.fetch('https://api.linkedin.com/v2/me', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -152,7 +153,7 @@ export class LinkedinProvider implements SocialProvider { } const { elements } = await ( - await fetch( + await this.fetch( `https://api.linkedin.com/rest/organizations?q=vanityName&vanityName=${getCompanyVanity[1]}`, { method: 'GET', @@ -174,17 +175,18 @@ export class LinkedinProvider implements SocialProvider { }; } - private async uploadPicture( + protected async uploadPicture( fileName: string, accessToken: string, personId: string, - picture: any + picture: any, + type = 'personal' as 'company' | 'personal' ) { try { const { value: { uploadUrl, image, video, uploadInstructions, ...all }, } = await ( - await fetch( + await this.fetch( `https://api.linkedin.com/rest/${ fileName.indexOf('mp4') > -1 ? 'videos' : 'images' }?action=initializeUpload`, @@ -198,7 +200,10 @@ export class LinkedinProvider implements SocialProvider { }, body: JSON.stringify({ initializeUploadRequest: { - owner: `urn:li:person:${personId}`, + owner: + type === 'personal' + ? `urn:li:person:${personId}` + : `urn:li:organization:${personId}`, ...(fileName.indexOf('mp4') > -1 ? { fileSizeBytes: picture.length, @@ -215,7 +220,7 @@ export class LinkedinProvider implements SocialProvider { const sendUrlRequest = uploadInstructions?.[0]?.uploadUrl || uploadUrl; const finalOutput = video || image; - const upload = await fetch(sendUrlRequest, { + const upload = await this.fetch(sendUrlRequest, { method: 'PUT', headers: { 'X-Restli-Protocol-Version': '2.0.0', @@ -230,7 +235,7 @@ export class LinkedinProvider implements SocialProvider { if (fileName.indexOf('mp4') > -1) { const etag = upload.headers.get('etag'); - const a = await fetch( + const a = await this.fetch( 'https://api.linkedin.com/rest/videos?action=finalizeUpload', { method: 'POST', @@ -260,7 +265,8 @@ export class LinkedinProvider implements SocialProvider { async post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails[], + type = 'personal' as 'company' | 'personal' ): Promise { const [firstPost, ...restPosts] = postDetails; @@ -281,7 +287,8 @@ export class LinkedinProvider implements SocialProvider { .resize({ width: 1000, }) - .toBuffer() + .toBuffer(), + type ), postId: p.id, }; @@ -300,7 +307,7 @@ export class LinkedinProvider implements SocialProvider { const media_ids = (uploadAll[firstPost.id] || []).filter((f) => f); - const data = await fetch('https://api.linkedin.com/v2/posts', { + const data = await this.fetch('https://api.linkedin.com/v2/posts', { method: 'POST', headers: { 'X-Restli-Protocol-Version': '2.0.0', @@ -308,7 +315,10 @@ export class LinkedinProvider implements SocialProvider { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - author: `urn:li:person:${id}`, + author: + type === 'personal' + ? `urn:li:person:${id}` + : `urn:li:organization:${id}`, commentary: removeMarkdown({ text: firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'), except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g], @@ -350,6 +360,7 @@ export class LinkedinProvider implements SocialProvider { } const topPostId = data.headers.get('x-restli-id')!; + const ids = [ { status: 'posted', @@ -360,7 +371,7 @@ export class LinkedinProvider implements SocialProvider { ]; for (const post of restPosts) { const { object } = await ( - await fetch( + await this.fetch( `https://api.linkedin.com/v2/socialActions/${decodeURIComponent( topPostId )}/comments`, @@ -371,7 +382,7 @@ export class LinkedinProvider implements SocialProvider { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - actor: `urn:li:person:${id}`, + actor: type === 'personal' ? `urn:li:person:${id}` : `urn:li:organization:${id}`, object: topPostId, message: { text: removeMarkdown({ diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 415e64bb..951beead 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -9,15 +9,16 @@ import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provi import axios from 'axios'; import FormData from 'form-data'; import { timer } from '@gitroom/helpers/utils/timer'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class PinterestProvider implements SocialProvider { +export class PinterestProvider extends SocialAbstract implements SocialProvider { identifier = 'pinterest'; name = 'Pinterest'; isBetweenSteps = false; async refreshToken(refreshToken: string): Promise { const { access_token, expires_in } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/oauth/token', { + await this.fetch('https://api.pinterest.com/v5/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -36,7 +37,7 @@ export class PinterestProvider implements SocialProvider { ).json(); const { id, profile_image, username } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/user_account', { + await this.fetch('https://api.pinterest.com/v5/user_account', { method: 'GET', headers: { Authorization: `Bearer ${access_token}`, @@ -78,7 +79,7 @@ export class PinterestProvider implements SocialProvider { refresh: string; }) { const { access_token, refresh_token, expires_in } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/oauth/token', { + await this.fetch('https://api.pinterest.com/v5/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -95,7 +96,7 @@ export class PinterestProvider implements SocialProvider { ).json(); const { id, profile_image, username } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/user_account', { + await this.fetch('https://api.pinterest.com/v5/user_account', { method: 'GET', headers: { Authorization: `Bearer ${access_token}`, @@ -116,7 +117,7 @@ export class PinterestProvider implements SocialProvider { async boards(accessToken: string) { const { items } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/boards', { + await this.fetch('https://api.pinterest.com/v5/boards', { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, @@ -147,7 +148,7 @@ export class PinterestProvider implements SocialProvider { if (findMp4) { const { upload_url, media_id, upload_parameters } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/media', { + await this.fetch('https://api.pinterest.com/v5/media', { method: 'POST', body: JSON.stringify({ media_type: 'video', @@ -180,8 +181,8 @@ export class PinterestProvider implements SocialProvider { while (statusCode !== 'succeeded') { console.log('trying'); const mediafile = await ( - await fetch( - 'https://api-sandbox.pinterest.com/v5/media/' + media_id, + await this.fetch( + 'https://api.pinterest.com/v5/media/' + media_id, { method: 'GET', headers: { @@ -208,7 +209,7 @@ export class PinterestProvider implements SocialProvider { link, ...all } = await ( - await fetch('https://api-sandbox.pinterest.com/v5/pins', { + await this.fetch('https://api.pinterest.com/v5/pins', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index 2a9c0858..db4ffbf2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -8,8 +8,9 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; import { timer } from '@gitroom/helpers/utils/timer'; import { groupBy } from 'lodash'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class RedditProvider implements SocialProvider { +export class RedditProvider extends SocialAbstract implements SocialProvider { identifier = 'reddit'; name = 'Reddit'; isBetweenSteps = false; @@ -20,7 +21,7 @@ export class RedditProvider implements SocialProvider { refresh_token: newRefreshToken, expires_in: expiresIn, } = await ( - await fetch('https://www.reddit.com/api/v1/access_token', { + await this.fetch('https://www.reddit.com/api/v1/access_token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -36,7 +37,7 @@ export class RedditProvider implements SocialProvider { ).json(); const { name, id, icon_img } = await ( - await fetch('https://oauth.reddit.com/api/v1/me', { + await this.fetch('https://oauth.reddit.com/api/v1/me', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -77,7 +78,7 @@ export class RedditProvider implements SocialProvider { refresh_token: refreshToken, expires_in: expiresIn, } = await ( - await fetch('https://www.reddit.com/api/v1/access_token', { + await this.fetch('https://www.reddit.com/api/v1/access_token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -94,7 +95,7 @@ export class RedditProvider implements SocialProvider { ).json(); const { name, id, icon_img } = await ( - await fetch('https://oauth.reddit.com/api/v1/me', { + await this.fetch('https://oauth.reddit.com/api/v1/me', { headers: { Authorization: `Bearer ${accessToken}`, }, @@ -126,7 +127,7 @@ export class RedditProvider implements SocialProvider { data: { id, name, url }, }, } = await ( - await fetch('https://oauth.reddit.com/api/submit', { + await this.fetch('https://oauth.reddit.com/api/submit', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, @@ -181,7 +182,7 @@ export class RedditProvider implements SocialProvider { }, }, } = await ( - await fetch('https://oauth.reddit.com/api/comment', { + await this.fetch('https://oauth.reddit.com/api/comment', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, @@ -226,7 +227,7 @@ export class RedditProvider implements SocialProvider { const { data: { children }, } = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`, { method: 'GET', @@ -271,7 +272,7 @@ export class RedditProvider implements SocialProvider { const { data: { submission_type, allow_images }, } = await ( - await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, { + await this.fetch(`https://oauth.reddit.com/${data.subreddit}/about`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, @@ -281,7 +282,7 @@ export class RedditProvider implements SocialProvider { ).json(); const { is_flair_required } = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/api/v1/${ data.subreddit.split('/r/')[1] }/post_requirements`, @@ -296,7 +297,7 @@ export class RedditProvider implements SocialProvider { ).json(); const newData = await ( - await fetch( + await this.fetch( `https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`, { method: 'GET', diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 540cae96..b873958e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -6,8 +6,9 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import dayjs from 'dayjs'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class TiktokProvider implements SocialProvider { +export class TiktokProvider extends SocialAbstract implements SocialProvider { identifier = 'tiktok'; name = 'Tiktok'; isBetweenSteps = false; @@ -73,7 +74,7 @@ export class TiktokProvider implements SocialProvider { refresh?: string; }) { const getAccessToken = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + `?client_id=${process.env.FACEBOOK_APP_ID}` + `&redirect_uri=${encodeURIComponent( @@ -87,7 +88,7 @@ export class TiktokProvider implements SocialProvider { ).json(); const { access_token } = await ( - await fetch( + await this.fetch( 'https://graph.facebook.com/v20.0/oauth/access_token' + '?grant_type=fb_exchange_token' + `&client_id=${process.env.FACEBOOK_APP_ID}` + @@ -119,7 +120,7 @@ export class TiktokProvider implements SocialProvider { data: { url }, }, } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}` ) ).json(); @@ -137,7 +138,7 @@ export class TiktokProvider implements SocialProvider { async pages(accessToken: string) { const { data } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}` ) ).json(); @@ -155,7 +156,7 @@ export class TiktokProvider implements SocialProvider { data: { url }, }, } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}` ) ).json(); @@ -180,7 +181,7 @@ export class TiktokProvider implements SocialProvider { let finalUrl = ''; if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) { const { id: videoId, permalink_url } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', @@ -204,7 +205,7 @@ export class TiktokProvider implements SocialProvider { : await Promise.all( firstPost.media.map(async (media) => { const { id: photoId } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`, { method: 'POST', @@ -228,7 +229,7 @@ export class TiktokProvider implements SocialProvider { permalink_url, ...all } = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', @@ -251,7 +252,7 @@ export class TiktokProvider implements SocialProvider { const postsArray = []; for (const comment of comments) { const data = await ( - await fetch( + await this.fetch( `https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`, { method: 'POST', diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index d866f1bb..2470a3f9 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -9,8 +9,9 @@ import { lookup } from 'mime-types'; import sharp from 'sharp'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import removeMd from 'remove-markdown'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -export class XProvider implements SocialProvider { +export class XProvider extends SocialAbstract implements SocialProvider { identifier = 'x'; name = 'X'; isBetweenSteps = false; diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index 9fff423c..8ddc3278 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -10,6 +10,7 @@ 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'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; const clientAndYoutube = () => { const client = new google.auth.OAuth2({ @@ -33,7 +34,7 @@ const clientAndYoutube = () => { return { client, youtube, oauth2 }; }; -export class YoutubeProvider implements SocialProvider { +export class YoutubeProvider extends SocialAbstract implements SocialProvider { identifier = 'youtube'; name = 'Youtube'; isBetweenSteps = false; diff --git a/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx new file mode 100644 index 00000000..d0b754b3 --- /dev/null +++ b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx @@ -0,0 +1,27 @@ +import { FC, useState } from 'react'; +import Image from 'next/image'; + +interface ImageSrc { + src: string; + fallbackSrc: string; + width: number; + height: number; + [key: string]: any; +} + +const ImageWithFallback: FC = (props) => { + const { src, fallbackSrc, ...rest } = props; + const [imgSrc, setImgSrc] = useState(src); + + return ( + { + setImgSrc(fallbackSrc); + }} + /> + ); +}; + +export default ImageWithFallback; \ No newline at end of file