feat: refresh tokens fallback

This commit is contained in:
Nevo David 2024-06-02 17:25:22 +07:00
parent e0599b48c3
commit 4a5e93c356
26 changed files with 862 additions and 141 deletions

View File

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

View File

@ -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

View File

@ -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: '',
}

View File

@ -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>

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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,
};

View File

@ -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},

View File

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

View File

@ -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[]) {

View File

@ -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 = [

View File

@ -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;
}
}

View File

@ -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 [];
}
}
}

View File

@ -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',

View File

@ -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}`,

View File

@ -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');
}
}

View File

@ -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({

View File

@ -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}`,

View File

@ -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',

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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;