feat: refresh tokens fallback
This commit is contained in:
parent
e0599b48c3
commit
4a5e93c356
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -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: '',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<html className={interClass}>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.png" sizes="any" />
|
||||
<link rel="icon" href={!isGeneral() ? "/favicon.png" : "/postiz-fav.png"} sizes="any" />
|
||||
</head>
|
||||
<body className={chakra.className}>
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<div className="bg-black/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import useSWR from 'swr';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
|
||||
export const LinkedinContinue: FC<{
|
||||
closeModal: () => void;
|
||||
existingId: string[];
|
||||
}> = (props) => {
|
||||
const { closeModal, existingId } = props;
|
||||
const call = useCustomProviderFunction();
|
||||
const { integration } = useIntegration();
|
||||
const [page, setSelectedPage] = useState<null | {
|
||||
id: string;
|
||||
pageId: string;
|
||||
}>(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 (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div>Select Linkedin Account:</div>
|
||||
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer">
|
||||
{filteredData?.map(
|
||||
(p: {
|
||||
id: string;
|
||||
pageId: string;
|
||||
username: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
}) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-center gap-[10px] border border-input p-[10px] hover:bg-seventh',
|
||||
page?.id === p.id && 'bg-seventh'
|
||||
)}
|
||||
onClick={setPage(p)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={p.picture}
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveLinkedin}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
facebook: FacebookContinue,
|
||||
'linkedin-page': LinkedinContinue,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Partial<{ postId: string; releaseURL: string }>> {
|
||||
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[]) {
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AuthTokenDetails> {
|
||||
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<PinterestSettingsDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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<PostResponse[]> {
|
||||
return super.post(id, accessToken, postDetails, 'company');
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AuthTokenDetails> {
|
||||
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<PostResponse[]> {
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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}`,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ImageSrc> = (props) => {
|
||||
const { src, fallbackSrc, ...rest } = props;
|
||||
const [imgSrc, setImgSrc] = useState(src);
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
src={imgSrc}
|
||||
onError={() => {
|
||||
setImgSrc(fallbackSrc);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWithFallback;
|
||||
Loading…
Reference in New Issue