diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts
index aded60b2..2fb340df 100644
--- a/apps/backend/src/api/routes/integrations.controller.ts
+++ b/apps/backend/src/api/routes/integrations.controller.ts
@@ -261,6 +261,15 @@ export class IntegrationsController {
return this._integrationService.saveFacebook(org.id, id, body.page);
}
+ @Post('/linkedin-page/:id')
+ async saveLinkedin(
+ @Param('id') id: string,
+ @Body() body: { page: string },
+ @GetOrgFromRequest() org: Organization
+ ) {
+ return this._integrationService.saveLinkedin(org.id, id, body.page);
+ }
+
@Post('/enable')
enableChannel(
@GetOrgFromRequest() org: Organization,
diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js
index 8edcfcf5..34610438 100644
--- a/apps/frontend/next.config.js
+++ b/apps/frontend/next.config.js
@@ -12,13 +12,20 @@ const nextConfig = {
// See: https://github.com/gregberge/svgr
svgr: false,
},
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: '**',
+ },
+ ],
+ },
env: {
isBillingEnabled: String(!!process.env.STRIPE_PUBLISHABLE_KEY),
isGeneral: String(!!process.env.IS_GENERAL),
- }
+ },
};
-
const plugins = [
// Add more Next.js plugins to this list if needed.
withNx,
diff --git a/apps/frontend/public/icons/platforms/dribbble.png b/apps/frontend/public/icons/platforms/dribbble.png
new file mode 100644
index 00000000..4d3aa535
Binary files /dev/null and b/apps/frontend/public/icons/platforms/dribbble.png differ
diff --git a/apps/frontend/public/icons/platforms/linkedin-page.png b/apps/frontend/public/icons/platforms/linkedin-page.png
new file mode 100644
index 00000000..3ce04bcd
Binary files /dev/null and b/apps/frontend/public/icons/platforms/linkedin-page.png differ
diff --git a/apps/frontend/public/postiz-fav.png b/apps/frontend/public/postiz-fav.png
new file mode 100644
index 00000000..64a1cf0b
Binary files /dev/null and b/apps/frontend/public/postiz-fav.png differ
diff --git a/apps/frontend/src/app/(site)/launches/page.tsx b/apps/frontend/src/app/(site)/launches/page.tsx
index 2c3b5948..82e36764 100644
--- a/apps/frontend/src/app/(site)/launches/page.tsx
+++ b/apps/frontend/src/app/(site)/launches/page.tsx
@@ -6,7 +6,7 @@ import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches.
import {Metadata} from "next";
export const metadata: Metadata = {
- title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Launches`,
+ title: `${isGeneral() ? 'Postiz Calendar' : 'Gitroom Launches'}`,
description: '',
}
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 660310ef..9841de5c 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -7,6 +7,7 @@ import 'react-tooltip/dist/react-tooltip.css';
import LayoutContext from '@gitroom/frontend/components/layout/layout.context';
import { ReactNode } from 'react';
import { Chakra_Petch } from 'next/font/google';
+import { isGeneral } from '@gitroom/react/helpers/is.general';
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
@@ -14,7 +15,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
return (
-
+
{children}
diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx
index 534c9086..2b8b1239 100644
--- a/apps/frontend/src/components/launches/launches.component.tsx
+++ b/apps/frontend/src/components/launches/launches.component.tsx
@@ -16,6 +16,7 @@ import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator';
import { useRouter } from 'next/navigation';
import { Integration } from '@prisma/client';
+import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
export const LaunchesComponent = () => {
const fetch = useFetch();
@@ -71,10 +72,15 @@ export const LaunchesComponent = () => {
);
const refreshChannel = useCallback(
- (integration: Integration & {identifier: string}) => async () => {
- const {url} = await (await fetch(`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`, {
- method: 'GET',
- })).json();
+ (integration: Integration & { identifier: string }) => async () => {
+ const { url } = await (
+ await fetch(
+ `/integrations/social/${integration.identifier}?refresh=${integration.internalId}`,
+ {
+ method: 'GET',
+ }
+ )
+ ).json();
window.location.href = url;
},
@@ -134,7 +140,8 @@ export const LaunchesComponent = () => {
)}
-
void;
+ existingId: string[];
+}> = (props) => {
+ const { closeModal, existingId } = props;
+ const call = useCustomProviderFunction();
+ const { integration } = useIntegration();
+ const [page, setSelectedPage] = useState(null);
+ const fetch = useFetch();
+
+ const loadPages = useCallback(async () => {
+ try {
+ const pages = await call.get('companies');
+ return pages;
+ } catch (e) {
+ closeModal();
+ }
+ }, []);
+
+ const setPage = useCallback(
+ (param: { id: string; pageId: string }) => () => {
+ setSelectedPage(param);
+ },
+ []
+ );
+
+ const { data } = useSWR('load-pages', loadPages, {
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ revalidateOnMount: true,
+ revalidateOnReconnect: false,
+ refreshInterval: 0,
+ });
+
+ const saveLinkedin = useCallback(async () => {
+ await fetch(`/integrations/linkedin-page/${integration?.id}`, {
+ method: 'POST',
+ body: JSON.stringify(page),
+ });
+
+ closeModal();
+ }, [integration, page]);
+
+ const filteredData = useMemo(() => {
+ return (
+ data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
+ );
+ }, [data]);
+
+ return (
+
+
Select Linkedin Account:
+
+ {filteredData?.map(
+ (p: {
+ id: string;
+ pageId: string;
+ username: string;
+ name: string;
+ picture: string;
+ }) => (
+
+
+

+
+
{p.name}
+
+ )
+ )}
+
+
+
+
+
+ );
+};
diff --git a/apps/frontend/src/components/launches/providers/continue-provider/list.tsx b/apps/frontend/src/components/launches/providers/continue-provider/list.tsx
index 0bec9f55..7ff24102 100644
--- a/apps/frontend/src/components/launches/providers/continue-provider/list.tsx
+++ b/apps/frontend/src/components/launches/providers/continue-provider/list.tsx
@@ -1,7 +1,9 @@
import { InstagramContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/instagram/instagram.continue';
import { FacebookContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/facebook/facebook.continue';
+import { LinkedinContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/linkedin/linkedin.continue';
export const continueProviderList = {
instagram: InstagramContinue,
- facebook: FacebookContinue
-}
\ No newline at end of file
+ facebook: FacebookContinue,
+ 'linkedin-page': LinkedinContinue,
+};
diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx
index a4996015..2ee9aa87 100644
--- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx
+++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx
@@ -16,6 +16,7 @@ export const Providers = [
{identifier: 'devto', component: DevtoProvider},
{identifier: 'x', component: XProvider},
{identifier: 'linkedin', component: LinkedinProvider},
+ {identifier: 'linkedin-page', component: LinkedinProvider},
{identifier: 'reddit', component: RedditProvider},
{identifier: 'medium', component: MediumProvider},
{identifier: 'hashnode', component: HashnodeProvider},
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
index d9ec0be0..2f201b73 100644
--- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
@@ -4,9 +4,9 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
import { SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
-import { Integration, Organization } from '@prisma/client';
+import { Integration } from '@prisma/client';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
-import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider';
+import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider';
@Injectable()
export class IntegrationService {
@@ -86,6 +86,10 @@ export class IntegrationService {
);
}
+ async refreshNeeded(org: string, id: string) {
+ return this._integrationRepository.refreshNeeded(org, id);
+ }
+
async refreshTokens() {
const integrations = await this._integrationRepository.needsToBeRefreshed();
for (const integration of integrations) {
@@ -195,6 +199,38 @@ export class IntegrationService {
return { success: true };
}
+ async saveLinkedin(org: string, id: string, page: string) {
+ const getIntegration = await this._integrationRepository.getIntegrationById(
+ org,
+ id
+ );
+ if (getIntegration && !getIntegration.inBetweenSteps) {
+ throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST);
+ }
+
+ const linkedin = this._integrationManager.getSocialIntegration(
+ 'linkedin-page'
+ ) as LinkedinPageProvider;
+
+ const getIntegrationInformation = await linkedin.fetchPageInformation(
+ getIntegration?.token!,
+ page
+ );
+
+ await this.checkForDeletedOnceAndUpdate(org, String(getIntegrationInformation.id));
+
+ await this._integrationRepository.updateIntegration(String(id), {
+ picture: getIntegrationInformation.picture,
+ internalId: String(getIntegrationInformation.id),
+ name: getIntegrationInformation.name,
+ inBetweenSteps: false,
+ token: getIntegrationInformation.access_token,
+ profile: getIntegrationInformation.username,
+ });
+
+ return { success: true };
+ }
+
async saveFacebook(org: string, id: string, page: string) {
const getIntegration = await this._integrationRepository.getIntegrationById(
org,
diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
index 7bacf814..80002293 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
@@ -16,6 +16,7 @@ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
type PostWithConditionals = Post & {
integration?: Integration;
@@ -145,7 +146,9 @@ export class PostsService {
await this._notificationService.inAppNotification(
firstPost.organizationId,
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
- `An error occurred while posting on ${firstPost.integration?.providerIdentifier} ${JSON.stringify(err)}`,
+ `An error occurred while posting on ${
+ firstPost.integration?.providerIdentifier
+ } ${JSON.stringify(err)}`,
true
);
}
@@ -173,19 +176,33 @@ export class PostsService {
return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]);
}
- private async postSocial(integration: Integration, posts: Post[]) {
+ private async postSocial(
+ integration: Integration,
+ posts: Post[],
+ forceRefresh = false
+ ): Promise> {
const getIntegration = this._integrationManager.getSocialIntegration(
integration.providerIdentifier
);
if (!getIntegration) {
- return;
+ return {};
}
- if (dayjs(integration?.tokenExpiration).isBefore(dayjs())) {
+ if (dayjs(integration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) {
const { accessToken, expiresIn, refreshToken } =
await getIntegration.refreshToken(integration.refreshToken!);
+ if (!accessToken) {
+ await this._integrationService.refreshNeeded(
+ integration.organizationId,
+ integration.id
+ );
+
+ await this._integrationService.informAboutRefreshError(integration.organizationId, integration);
+ return {};
+ }
+
await this._integrationService.createOrUpdateIntegration(
integration.organizationId,
integration.name,
@@ -203,51 +220,59 @@ export class PostsService {
const newPosts = await this.updateTags(integration.organizationId, posts);
- const publishedPosts = await getIntegration.post(
- integration.internalId,
- integration.token,
- newPosts.map((p) => ({
- id: p.id,
- message: p.content,
- settings: JSON.parse(p.settings || '{}'),
- media: (JSON.parse(p.image || '[]') as Media[]).map((m) => ({
- url:
- m.path.indexOf('http') === -1
- ? process.env.FRONTEND_URL +
- '/' +
- process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
- m.path
- : m.path,
- type: 'image',
- path:
- m.path.indexOf('http') === -1
- ? process.env.UPLOAD_DIRECTORY + m.path
- : m.path,
- })),
- }))
- );
-
- for (const post of publishedPosts) {
- await this._postRepository.updatePost(
- post.id,
- post.postId,
- post.releaseURL
+ try {
+ const publishedPosts = await getIntegration.post(
+ integration.internalId,
+ integration.token,
+ newPosts.map((p) => ({
+ id: p.id,
+ message: p.content,
+ settings: JSON.parse(p.settings || '{}'),
+ media: (JSON.parse(p.image || '[]') as Media[]).map((m) => ({
+ url:
+ m.path.indexOf('http') === -1
+ ? process.env.FRONTEND_URL +
+ '/' +
+ process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
+ m.path
+ : m.path,
+ type: 'image',
+ path:
+ m.path.indexOf('http') === -1
+ ? process.env.UPLOAD_DIRECTORY + m.path
+ : m.path,
+ })),
+ }))
);
+
+ for (const post of publishedPosts) {
+ await this._postRepository.updatePost(
+ post.id,
+ post.postId,
+ post.releaseURL
+ );
+ }
+
+ await this._notificationService.inAppNotification(
+ integration.organizationId,
+ `Your post has been published on ${capitalize(
+ integration.providerIdentifier
+ )}`,
+ `Your post has been published at ${publishedPosts[0].releaseURL}`,
+ true
+ );
+
+ return {
+ postId: publishedPosts[0].postId,
+ releaseURL: publishedPosts[0].releaseURL,
+ };
+ } catch (err) {
+ if (err instanceof RefreshToken) {
+ return this.postSocial(integration, posts, true);
+ }
+
+ throw err;
}
-
- await this._notificationService.inAppNotification(
- integration.organizationId,
- `Your post has been published on ${capitalize(
- integration.providerIdentifier
- )}`,
- `Your post has been published at ${publishedPosts[0].releaseURL}`,
- true
- );
-
- return {
- postId: publishedPosts[0].postId,
- releaseURL: publishedPosts[0].releaseURL,
- };
}
private async postArticle(integration: Integration, posts: Post[]) {
diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
index efa47538..c0e93cda 100644
--- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts
+++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
@@ -12,16 +12,20 @@ import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social
import { YoutubeProvider } from '@gitroom/nestjs-libraries/integrations/social/youtube.provider';
import { TiktokProvider } from '@gitroom/nestjs-libraries/integrations/social/tiktok.provider';
import { PinterestProvider } from '@gitroom/nestjs-libraries/integrations/social/pinterest.provider';
+import { DribbbleProvider } from '@gitroom/nestjs-libraries/integrations/social/dribbble.provider';
+import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider';
const socialIntegrationList = [
new XProvider(),
new LinkedinProvider(),
+ new LinkedinPageProvider(),
new RedditProvider(),
new FacebookProvider(),
new InstagramProvider(),
new YoutubeProvider(),
new TiktokProvider(),
- new PinterestProvider()
+ new PinterestProvider(),
+ new DribbbleProvider(),
];
const articleIntegrationList = [
diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts
new file mode 100644
index 00000000..52439fdb
--- /dev/null
+++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts
@@ -0,0 +1,13 @@
+export class RefreshToken {
+}
+
+export abstract class SocialAbstract {
+ async fetch(url: string, options: RequestInit = {}) {
+ const request = await fetch(url, options);
+ if (request.status === 401) {
+ throw new RefreshToken();
+ }
+
+ return request;
+ }
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts
new file mode 100644
index 00000000..e892e9ae
--- /dev/null
+++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts
@@ -0,0 +1,271 @@
+import {
+ AuthTokenDetails,
+ PostDetails,
+ PostResponse,
+ SocialProvider,
+} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
+import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto';
+import axios from 'axios';
+import FormData from 'form-data';
+import { timer } from '@gitroom/helpers/utils/timer';
+import dayjs from 'dayjs';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
+
+export class DribbbleProvider extends SocialAbstract implements SocialProvider {
+ identifier = 'dribbble';
+ name = 'Dribbbble';
+ isBetweenSteps = false;
+
+ async refreshToken(refreshToken: string): Promise {
+ const { access_token, expires_in } = await (
+ await this.fetch('https://api-sandbox.pinterest.com/v5/oauth/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Authorization: `Basic ${Buffer.from(
+ `${process.env.PINTEREST_CLIENT_ID}:${process.env.PINTEREST_CLIENT_SECRET}`
+ ).toString('base64')}`,
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ scope:
+ 'boards:read,boards:write,pins:read,pins:write,user_accounts:read',
+ redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/pinterest`,
+ }),
+ })
+ ).json();
+
+ const { id, profile_image, username } = await (
+ await this.fetch('https://api-sandbox.pinterest.com/v5/user_account', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${access_token}`,
+ },
+ })
+ ).json();
+
+ return {
+ id: id,
+ name: username,
+ accessToken: access_token,
+ refreshToken: refreshToken,
+ expiresIn: expires_in,
+ picture: profile_image,
+ username,
+ };
+ }
+
+ async teams(accessToken: string) {
+ const { teams } = await (
+ await this.fetch('https://api.dribbble.com/v2/user', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ return teams?.map((team: any) => ({
+ id: team.id,
+ name: team.name,
+ })) || [];
+ }
+
+ async generateAuthUrl(refresh?: string) {
+ const state = makeId(6);
+ return {
+ url: `https://dribbble.com/oauth/authorize?client_id=${
+ process.env.DRIBBBLE_CLIENT_ID
+ }&redirect_uri=${encodeURIComponent(
+ `${process.env.FRONTEND_URL}/integrations/social/dribbble${
+ refresh ? `?refresh=${refresh}` : ''
+ }`
+ )}&response_type=code&scope=public+upload&state=${state}`,
+ codeVerifier: makeId(10),
+ state,
+ };
+ }
+
+ async authenticate(params: {
+ code: string;
+ codeVerifier: string;
+ refresh: string;
+ }) {
+ const { access_token } = await (
+ await this.fetch(
+ `https://dribbble.com/oauth/token?client_id=${process.env.DRIBBBLE_CLIENT_ID}&client_secret=${process.env.DRIBBBLE_CLIENT_SECRET}&code=${params.code}&redirect_uri=${process.env.FRONTEND_URL}/integrations/social/dribbble`,
+ {
+ method: 'POST',
+ }
+ )
+ ).json();
+
+ const { id, name, avatar_url, login } = await (
+ await this.fetch('https://api.dribbble.com/v2/user', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${access_token}`,
+ },
+ })
+ ).json();
+
+ return {
+ id: id,
+ name,
+ accessToken: access_token,
+ refreshToken: '',
+ expiresIn: 999999999,
+ picture: avatar_url,
+ username: login,
+ };
+ }
+
+ async boards(accessToken: string) {
+ const { items } = await (
+ await this.fetch('https://api-sandbox.pinterest.com/v5/boards', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ return (
+ items?.map((item: any) => ({
+ name: item.name,
+ id: item.id,
+ })) || []
+ );
+ }
+
+ async post(
+ id: string,
+ accessToken: string,
+ postDetails: PostDetails[]
+ ): Promise {
+ let mediaId = '';
+ const findMp4 = postDetails?.[0]?.media?.find(
+ (p) => (p.path?.indexOf('mp4') || -1) > -1
+ );
+ const picture = postDetails?.[0]?.media?.find(
+ (p) => (p.path?.indexOf('mp4') || -1) === -1
+ );
+
+ if (findMp4) {
+ const { upload_url, media_id, upload_parameters } = await (
+ await this.fetch('https://api-sandbox.pinterest.com/v5/media', {
+ method: 'POST',
+ body: JSON.stringify({
+ media_type: 'video',
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ const { data, status } = await axios.get(
+ postDetails?.[0]?.media?.[0]?.url!,
+ {
+ responseType: 'stream',
+ }
+ );
+
+ const formData = Object.keys(upload_parameters)
+ .filter((f) => f)
+ .reduce((acc, key) => {
+ acc.append(key, upload_parameters[key]);
+ return acc;
+ }, new FormData());
+
+ formData.append('file', data);
+ await axios.post(upload_url, formData);
+
+ let statusCode = '';
+ while (statusCode !== 'succeeded') {
+ console.log('trying');
+ const mediafile = await (
+ await this.fetch(
+ 'https://api-sandbox.pinterest.com/v5/media/' + media_id,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ )
+ ).json();
+
+ await timer(3000);
+ statusCode = mediafile.status;
+ }
+
+ mediaId = media_id;
+ }
+
+ const mapImages = postDetails?.[0]?.media?.map((m) => ({
+ url: m.url,
+ }));
+
+ try {
+ const {
+ id: pId,
+ link,
+ ...all
+ } = await (
+ await this.fetch('https://api-sandbox.pinterest.com/v5/pins', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ ...(postDetails?.[0]?.settings.link
+ ? { link: postDetails?.[0]?.settings.link }
+ : {}),
+ ...(postDetails?.[0]?.settings.title
+ ? { title: postDetails?.[0]?.settings.title }
+ : {}),
+ ...(postDetails?.[0]?.settings.description
+ ? { title: postDetails?.[0]?.settings.description }
+ : {}),
+ ...(postDetails?.[0]?.settings.dominant_color
+ ? { title: postDetails?.[0]?.settings.dominant_color }
+ : {}),
+ board_id: postDetails?.[0]?.settings.board,
+ media_source: mediaId
+ ? {
+ source_type: 'video_id',
+ media_id: mediaId,
+ cover_image_url: picture?.url,
+ }
+ : mapImages?.length === 1
+ ? {
+ source_type: 'image_url',
+ url: mapImages?.[0]?.url,
+ }
+ : {
+ source_type: 'multiple_image_urls',
+ items: mapImages,
+ },
+ }),
+ })
+ ).json();
+
+ return [
+ {
+ id: postDetails?.[0]?.id,
+ postId: pId,
+ releaseURL: `https://www.pinterest.com/pin/${pId}`,
+ status: 'success',
+ },
+ ];
+ } catch (err) {
+ console.log(err);
+ return [];
+ }
+ }
+}
diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
index 8785dc08..d8824364 100644
--- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
@@ -6,8 +6,9 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class FacebookProvider implements SocialProvider {
+export class FacebookProvider extends SocialAbstract implements SocialProvider {
identifier = 'facebook';
name = 'Facebook Page';
isBetweenSteps = true;
@@ -46,7 +47,7 @@ export class FacebookProvider implements SocialProvider {
refresh?: string;
}) {
const getAccessToken = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
@@ -60,7 +61,7 @@ export class FacebookProvider implements SocialProvider {
).json();
const { access_token } = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
@@ -92,7 +93,7 @@ export class FacebookProvider implements SocialProvider {
data: { url },
},
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
@@ -110,7 +111,7 @@ export class FacebookProvider implements SocialProvider {
async pages(accessToken: string) {
const { data } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
)
).json();
@@ -128,7 +129,7 @@ export class FacebookProvider implements SocialProvider {
data: { url },
},
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
@@ -153,7 +154,7 @@ export class FacebookProvider implements SocialProvider {
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
const { id: videoId, permalink_url } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
@@ -177,7 +178,7 @@ export class FacebookProvider implements SocialProvider {
: await Promise.all(
firstPost.media.map(async (media) => {
const { id: photoId } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`,
{
method: 'POST',
@@ -201,7 +202,7 @@ export class FacebookProvider implements SocialProvider {
permalink_url,
...all
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
@@ -224,7 +225,7 @@ export class FacebookProvider implements SocialProvider {
const postsArray = [];
for (const comment of comments) {
const data = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
index 8f11cca8..37b6be8f 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
@@ -7,8 +7,9 @@ import {
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { timer } from '@gitroom/helpers/utils/timer';
import dayjs from 'dayjs';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class InstagramProvider implements SocialProvider {
+export class InstagramProvider extends SocialAbstract implements SocialProvider {
identifier = 'instagram';
name = 'Instagram';
isBetweenSteps = true;
@@ -51,7 +52,7 @@ export class InstagramProvider implements SocialProvider {
refresh: string;
}) {
const getAccessToken = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
@@ -65,7 +66,7 @@ export class InstagramProvider implements SocialProvider {
).json();
const { access_token, expires_in, ...all } = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
@@ -81,7 +82,7 @@ export class InstagramProvider implements SocialProvider {
data: { url },
},
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
@@ -117,7 +118,7 @@ export class InstagramProvider implements SocialProvider {
async pages(accessToken: string) {
const { data } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,instagram_business_account,username,name,picture.type(large)&access_token=${accessToken}&limit=500`
)
).json();
@@ -129,7 +130,7 @@ export class InstagramProvider implements SocialProvider {
return {
pageId: p.id,
...(await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${p.instagram_business_account.id}?fields=name,profile_picture_url&access_token=${accessToken}&limit=500`
)
).json()),
@@ -151,13 +152,13 @@ export class InstagramProvider implements SocialProvider {
data: { pageId: string; id: string }
) {
const { access_token, ...all } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${data.pageId}?fields=access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
const { id, name, profile_picture_url, username } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${data.id}?fields=username,name,profile_picture_url&access_token=${accessToken}`
)
).json();
@@ -191,7 +192,7 @@ export class InstagramProvider implements SocialProvider {
: `video_url=${m.url}&media_type=VIDEO`
: `image_url=${m.url}`;
const { id: photoId } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${caption}${isCarousel}&access_token=${accessToken}`,
{
method: 'POST',
@@ -202,7 +203,7 @@ export class InstagramProvider implements SocialProvider {
let status = 'IN_PROGRESS';
while (status === 'IN_PROGRESS') {
const { status_code } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${photoId}?access_token=${accessToken}&fields=status_code`
)
).json();
@@ -220,7 +221,7 @@ export class InstagramProvider implements SocialProvider {
let linkGlobal = '';
if (medias.length === 1) {
const { id: mediaId } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${medias[0]}&access_token=${accessToken}&field=id`,
{
method: 'POST',
@@ -231,7 +232,7 @@ export class InstagramProvider implements SocialProvider {
containerIdGlobal = mediaId;
const { permalink } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
)
).json();
@@ -246,7 +247,7 @@ export class InstagramProvider implements SocialProvider {
linkGlobal = permalink;
} else {
const { id: containerId, ...all3 } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/media?caption=${encodeURIComponent(
firstPost?.message
)}&media_type=CAROUSEL&children=${encodeURIComponent(
@@ -261,7 +262,7 @@ export class InstagramProvider implements SocialProvider {
let status = 'IN_PROGRESS';
while (status === 'IN_PROGRESS') {
const { status_code } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${containerId}?fields=status_code&access_token=${accessToken}`
)
).json();
@@ -270,7 +271,7 @@ export class InstagramProvider implements SocialProvider {
}
const { id: mediaId, ...all4 } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${containerId}&access_token=${accessToken}&field=id`,
{
method: 'POST',
@@ -281,7 +282,7 @@ export class InstagramProvider implements SocialProvider {
containerIdGlobal = mediaId;
const { permalink } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
)
).json();
@@ -298,7 +299,7 @@ export class InstagramProvider implements SocialProvider {
for (const post of theRest) {
const { id: commentId } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent(
post.message
)}&access_token=${accessToken}`,
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
new file mode 100644
index 00000000..cef78db9
--- /dev/null
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
@@ -0,0 +1,198 @@
+import {
+ AuthTokenDetails,
+ PostDetails,
+ PostResponse,
+ SocialProvider,
+} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
+import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
+
+export class LinkedinPageProvider
+ extends LinkedinProvider
+ implements SocialProvider
+{
+ override identifier = 'linkedin-page';
+ override name = 'LinkedIn Page';
+ override isBetweenSteps = true;
+
+ override async refreshToken(
+ refresh_token: string
+ ): Promise {
+ const { access_token: accessToken, refresh_token: refreshToken } = await (
+ await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token,
+ client_id: process.env.LINKEDIN_CLIENT_ID!,
+ client_secret: process.env.LINKEDIN_CLIENT_SECRET!,
+ }),
+ })
+ ).json();
+
+ const { vanityName } = await (
+ await fetch('https://api.linkedin.com/v2/me', {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ const {
+ name,
+ sub: id,
+ picture,
+ } = await (
+ await fetch('https://api.linkedin.com/v2/userinfo', {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ return {
+ id,
+ accessToken,
+ refreshToken,
+ name,
+ picture,
+ username: vanityName,
+ };
+ }
+
+ override async generateAuthUrl(refresh?: string) {
+ const state = makeId(6);
+ const codeVerifier = makeId(30);
+ const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${
+ process.env.LINKEDIN_CLIENT_ID
+ }&redirect_uri=${encodeURIComponent(
+ `${process.env.FRONTEND_URL}/integrations/social/linkedin-page${
+ refresh ? `?refresh=${refresh}` : ''
+ }`
+ )}&state=${state}&scope=${encodeURIComponent(
+ 'openid profile w_member_social r_basicprofile rw_organization_admin w_organization_social r_organization_social'
+ )}`;
+ return {
+ url,
+ codeVerifier,
+ state,
+ };
+ }
+
+ async companies(accessToken: string) {
+ const { elements } = await (
+ await fetch(
+ 'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(localizedName,vanityName,logoV2(original~:playableStreams))))',
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ )
+ ).json();
+
+ return (elements || []).map((e: any) => ({
+ id: e.organizationalTarget.split(':').pop(),
+ page: e.organizationalTarget.split(':').pop(),
+ username: e['organizationalTarget~'].vanityName,
+ name: e['organizationalTarget~'].localizedName,
+ picture:
+ e['organizationalTarget~'].logoV2?.['original~']?.elements?.[0]
+ ?.identifiers?.[0]?.identifier,
+ }));
+ }
+
+ async fetchPageInformation(accessToken: string, pageId: string) {
+ const data = await (
+ await fetch(
+ `https://api.linkedin.com/v2/organizations/${pageId}?projection=(id,localizedName,vanityName,logoV2(original~:playableStreams))`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ )
+ ).json();
+
+ return {
+ id: data.id,
+ name: data.localizedName,
+ access_token: accessToken,
+ picture: data?.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0].identifier,
+ username: data.vanityName,
+ };
+ }
+
+ override async authenticate(params: {
+ code: string;
+ codeVerifier: string;
+ refresh?: string;
+ }) {
+ const body = new URLSearchParams();
+ body.append('grant_type', 'authorization_code');
+ body.append('code', params.code);
+ body.append(
+ 'redirect_uri',
+ `${process.env.FRONTEND_URL}/integrations/social/linkedin-page${
+ params.refresh ? `?refresh=${params.refresh}` : ''
+ }`
+ );
+ body.append('client_id', process.env.LINKEDIN_CLIENT_ID!);
+ body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!);
+
+ const {
+ access_token: accessToken,
+ expires_in: expiresIn,
+ refresh_token: refreshToken,
+ } = await (
+ await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body,
+ })
+ ).json();
+
+ const {
+ name,
+ sub: id,
+ picture,
+ } = await (
+ await fetch('https://api.linkedin.com/v2/userinfo', {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ const { vanityName } = await (
+ await fetch('https://api.linkedin.com/v2/me', {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ ).json();
+
+ return {
+ id,
+ accessToken,
+ refreshToken,
+ expiresIn,
+ name,
+ picture,
+ username: vanityName,
+ };
+ }
+
+ override async post(
+ id: string,
+ accessToken: string,
+ postDetails: PostDetails[]
+ ): Promise {
+ return super.post(id, accessToken, postDetails, 'company');
+ }
+}
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
index 44a4362e..25fe4b88 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
@@ -9,15 +9,16 @@ import sharp from 'sharp';
import { lookup } from 'mime-types';
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class LinkedinProvider implements SocialProvider {
+export class LinkedinProvider extends SocialAbstract implements SocialProvider {
identifier = 'linkedin';
name = 'LinkedIn';
isBetweenSteps = false;
async refreshToken(refresh_token: string): Promise {
const { access_token: accessToken, refresh_token: refreshToken } = await (
- await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
+ await this.fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -32,7 +33,7 @@ export class LinkedinProvider implements SocialProvider {
).json();
const { vanityName } = await (
- await fetch('https://api.linkedin.com/v2/me', {
+ await this.fetch('https://api.linkedin.com/v2/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -44,7 +45,7 @@ export class LinkedinProvider implements SocialProvider {
sub: id,
picture,
} = await (
- await fetch('https://api.linkedin.com/v2/userinfo', {
+ await this.fetch('https://api.linkedin.com/v2/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -102,7 +103,7 @@ export class LinkedinProvider implements SocialProvider {
expires_in: expiresIn,
refresh_token: refreshToken,
} = await (
- await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
+ await this.fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -116,7 +117,7 @@ export class LinkedinProvider implements SocialProvider {
sub: id,
picture,
} = await (
- await fetch('https://api.linkedin.com/v2/userinfo', {
+ await this.fetch('https://api.linkedin.com/v2/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -124,7 +125,7 @@ export class LinkedinProvider implements SocialProvider {
).json();
const { vanityName } = await (
- await fetch('https://api.linkedin.com/v2/me', {
+ await this.fetch('https://api.linkedin.com/v2/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -152,7 +153,7 @@ export class LinkedinProvider implements SocialProvider {
}
const { elements } = await (
- await fetch(
+ await this.fetch(
`https://api.linkedin.com/rest/organizations?q=vanityName&vanityName=${getCompanyVanity[1]}`,
{
method: 'GET',
@@ -174,17 +175,18 @@ export class LinkedinProvider implements SocialProvider {
};
}
- private async uploadPicture(
+ protected async uploadPicture(
fileName: string,
accessToken: string,
personId: string,
- picture: any
+ picture: any,
+ type = 'personal' as 'company' | 'personal'
) {
try {
const {
value: { uploadUrl, image, video, uploadInstructions, ...all },
} = await (
- await fetch(
+ await this.fetch(
`https://api.linkedin.com/rest/${
fileName.indexOf('mp4') > -1 ? 'videos' : 'images'
}?action=initializeUpload`,
@@ -198,7 +200,10 @@ export class LinkedinProvider implements SocialProvider {
},
body: JSON.stringify({
initializeUploadRequest: {
- owner: `urn:li:person:${personId}`,
+ owner:
+ type === 'personal'
+ ? `urn:li:person:${personId}`
+ : `urn:li:organization:${personId}`,
...(fileName.indexOf('mp4') > -1
? {
fileSizeBytes: picture.length,
@@ -215,7 +220,7 @@ export class LinkedinProvider implements SocialProvider {
const sendUrlRequest = uploadInstructions?.[0]?.uploadUrl || uploadUrl;
const finalOutput = video || image;
- const upload = await fetch(sendUrlRequest, {
+ const upload = await this.fetch(sendUrlRequest, {
method: 'PUT',
headers: {
'X-Restli-Protocol-Version': '2.0.0',
@@ -230,7 +235,7 @@ export class LinkedinProvider implements SocialProvider {
if (fileName.indexOf('mp4') > -1) {
const etag = upload.headers.get('etag');
- const a = await fetch(
+ const a = await this.fetch(
'https://api.linkedin.com/rest/videos?action=finalizeUpload',
{
method: 'POST',
@@ -260,7 +265,8 @@ export class LinkedinProvider implements SocialProvider {
async post(
id: string,
accessToken: string,
- postDetails: PostDetails[]
+ postDetails: PostDetails[],
+ type = 'personal' as 'company' | 'personal'
): Promise {
const [firstPost, ...restPosts] = postDetails;
@@ -281,7 +287,8 @@ export class LinkedinProvider implements SocialProvider {
.resize({
width: 1000,
})
- .toBuffer()
+ .toBuffer(),
+ type
),
postId: p.id,
};
@@ -300,7 +307,7 @@ export class LinkedinProvider implements SocialProvider {
const media_ids = (uploadAll[firstPost.id] || []).filter((f) => f);
- const data = await fetch('https://api.linkedin.com/v2/posts', {
+ const data = await this.fetch('https://api.linkedin.com/v2/posts', {
method: 'POST',
headers: {
'X-Restli-Protocol-Version': '2.0.0',
@@ -308,7 +315,10 @@ export class LinkedinProvider implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
- author: `urn:li:person:${id}`,
+ author:
+ type === 'personal'
+ ? `urn:li:person:${id}`
+ : `urn:li:organization:${id}`,
commentary: removeMarkdown({
text: firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
@@ -350,6 +360,7 @@ export class LinkedinProvider implements SocialProvider {
}
const topPostId = data.headers.get('x-restli-id')!;
+
const ids = [
{
status: 'posted',
@@ -360,7 +371,7 @@ export class LinkedinProvider implements SocialProvider {
];
for (const post of restPosts) {
const { object } = await (
- await fetch(
+ await this.fetch(
`https://api.linkedin.com/v2/socialActions/${decodeURIComponent(
topPostId
)}/comments`,
@@ -371,7 +382,7 @@ export class LinkedinProvider implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
- actor: `urn:li:person:${id}`,
+ actor: type === 'personal' ? `urn:li:person:${id}` : `urn:li:organization:${id}`,
object: topPostId,
message: {
text: removeMarkdown({
diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
index 415e64bb..951beead 100644
--- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
@@ -9,15 +9,16 @@ import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provi
import axios from 'axios';
import FormData from 'form-data';
import { timer } from '@gitroom/helpers/utils/timer';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class PinterestProvider implements SocialProvider {
+export class PinterestProvider extends SocialAbstract implements SocialProvider {
identifier = 'pinterest';
name = 'Pinterest';
isBetweenSteps = false;
async refreshToken(refreshToken: string): Promise {
const { access_token, expires_in } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/oauth/token', {
+ await this.fetch('https://api.pinterest.com/v5/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -36,7 +37,7 @@ export class PinterestProvider implements SocialProvider {
).json();
const { id, profile_image, username } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/user_account', {
+ await this.fetch('https://api.pinterest.com/v5/user_account', {
method: 'GET',
headers: {
Authorization: `Bearer ${access_token}`,
@@ -78,7 +79,7 @@ export class PinterestProvider implements SocialProvider {
refresh: string;
}) {
const { access_token, refresh_token, expires_in } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/oauth/token', {
+ await this.fetch('https://api.pinterest.com/v5/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -95,7 +96,7 @@ export class PinterestProvider implements SocialProvider {
).json();
const { id, profile_image, username } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/user_account', {
+ await this.fetch('https://api.pinterest.com/v5/user_account', {
method: 'GET',
headers: {
Authorization: `Bearer ${access_token}`,
@@ -116,7 +117,7 @@ export class PinterestProvider implements SocialProvider {
async boards(accessToken: string) {
const { items } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/boards', {
+ await this.fetch('https://api.pinterest.com/v5/boards', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -147,7 +148,7 @@ export class PinterestProvider implements SocialProvider {
if (findMp4) {
const { upload_url, media_id, upload_parameters } = await (
- await fetch('https://api-sandbox.pinterest.com/v5/media', {
+ await this.fetch('https://api.pinterest.com/v5/media', {
method: 'POST',
body: JSON.stringify({
media_type: 'video',
@@ -180,8 +181,8 @@ export class PinterestProvider implements SocialProvider {
while (statusCode !== 'succeeded') {
console.log('trying');
const mediafile = await (
- await fetch(
- 'https://api-sandbox.pinterest.com/v5/media/' + media_id,
+ await this.fetch(
+ 'https://api.pinterest.com/v5/media/' + media_id,
{
method: 'GET',
headers: {
@@ -208,7 +209,7 @@ export class PinterestProvider implements SocialProvider {
link,
...all
} = await (
- await fetch('https://api-sandbox.pinterest.com/v5/pins', {
+ await this.fetch('https://api.pinterest.com/v5/pins', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
index 2a9c0858..db4ffbf2 100644
--- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
@@ -8,8 +8,9 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
import { timer } from '@gitroom/helpers/utils/timer';
import { groupBy } from 'lodash';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class RedditProvider implements SocialProvider {
+export class RedditProvider extends SocialAbstract implements SocialProvider {
identifier = 'reddit';
name = 'Reddit';
isBetweenSteps = false;
@@ -20,7 +21,7 @@ export class RedditProvider implements SocialProvider {
refresh_token: newRefreshToken,
expires_in: expiresIn,
} = await (
- await fetch('https://www.reddit.com/api/v1/access_token', {
+ await this.fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -36,7 +37,7 @@ export class RedditProvider implements SocialProvider {
).json();
const { name, id, icon_img } = await (
- await fetch('https://oauth.reddit.com/api/v1/me', {
+ await this.fetch('https://oauth.reddit.com/api/v1/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -77,7 +78,7 @@ export class RedditProvider implements SocialProvider {
refresh_token: refreshToken,
expires_in: expiresIn,
} = await (
- await fetch('https://www.reddit.com/api/v1/access_token', {
+ await this.fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -94,7 +95,7 @@ export class RedditProvider implements SocialProvider {
).json();
const { name, id, icon_img } = await (
- await fetch('https://oauth.reddit.com/api/v1/me', {
+ await this.fetch('https://oauth.reddit.com/api/v1/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
@@ -126,7 +127,7 @@ export class RedditProvider implements SocialProvider {
data: { id, name, url },
},
} = await (
- await fetch('https://oauth.reddit.com/api/submit', {
+ await this.fetch('https://oauth.reddit.com/api/submit', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -181,7 +182,7 @@ export class RedditProvider implements SocialProvider {
},
},
} = await (
- await fetch('https://oauth.reddit.com/api/comment', {
+ await this.fetch('https://oauth.reddit.com/api/comment', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -226,7 +227,7 @@ export class RedditProvider implements SocialProvider {
const {
data: { children },
} = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`,
{
method: 'GET',
@@ -271,7 +272,7 @@ export class RedditProvider implements SocialProvider {
const {
data: { submission_type, allow_images },
} = await (
- await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, {
+ await this.fetch(`https://oauth.reddit.com/${data.subreddit}/about`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -281,7 +282,7 @@ export class RedditProvider implements SocialProvider {
).json();
const { is_flair_required } = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/api/v1/${
data.subreddit.split('/r/')[1]
}/post_requirements`,
@@ -296,7 +297,7 @@ export class RedditProvider implements SocialProvider {
).json();
const newData = await (
- await fetch(
+ await this.fetch(
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
{
method: 'GET',
diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
index 540cae96..b873958e 100644
--- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
@@ -6,8 +6,9 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class TiktokProvider implements SocialProvider {
+export class TiktokProvider extends SocialAbstract implements SocialProvider {
identifier = 'tiktok';
name = 'Tiktok';
isBetweenSteps = false;
@@ -73,7 +74,7 @@ export class TiktokProvider implements SocialProvider {
refresh?: string;
}) {
const getAccessToken = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
@@ -87,7 +88,7 @@ export class TiktokProvider implements SocialProvider {
).json();
const { access_token } = await (
- await fetch(
+ await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
@@ -119,7 +120,7 @@ export class TiktokProvider implements SocialProvider {
data: { url },
},
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
@@ -137,7 +138,7 @@ export class TiktokProvider implements SocialProvider {
async pages(accessToken: string) {
const { data } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
)
).json();
@@ -155,7 +156,7 @@ export class TiktokProvider implements SocialProvider {
data: { url },
},
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
@@ -180,7 +181,7 @@ export class TiktokProvider implements SocialProvider {
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
const { id: videoId, permalink_url } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
@@ -204,7 +205,7 @@ export class TiktokProvider implements SocialProvider {
: await Promise.all(
firstPost.media.map(async (media) => {
const { id: photoId } = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`,
{
method: 'POST',
@@ -228,7 +229,7 @@ export class TiktokProvider implements SocialProvider {
permalink_url,
...all
} = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
@@ -251,7 +252,7 @@ export class TiktokProvider implements SocialProvider {
const postsArray = [];
for (const comment of comments) {
const data = await (
- await fetch(
+ await this.fetch(
`https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index d866f1bb..2470a3f9 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -9,8 +9,9 @@ import { lookup } from 'mime-types';
import sharp from 'sharp';
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import removeMd from 'remove-markdown';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
-export class XProvider implements SocialProvider {
+export class XProvider extends SocialAbstract implements SocialProvider {
identifier = 'x';
name = 'X';
isBetweenSteps = false;
diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
index 9fff423c..8ddc3278 100644
--- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
@@ -10,6 +10,7 @@ import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
import * as console from 'node:console';
import axios from 'axios';
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
+import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
const clientAndYoutube = () => {
const client = new google.auth.OAuth2({
@@ -33,7 +34,7 @@ const clientAndYoutube = () => {
return { client, youtube, oauth2 };
};
-export class YoutubeProvider implements SocialProvider {
+export class YoutubeProvider extends SocialAbstract implements SocialProvider {
identifier = 'youtube';
name = 'Youtube';
isBetweenSteps = false;
diff --git a/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx
new file mode 100644
index 00000000..d0b754b3
--- /dev/null
+++ b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx
@@ -0,0 +1,27 @@
+import { FC, useState } from 'react';
+import Image from 'next/image';
+
+interface ImageSrc {
+ src: string;
+ fallbackSrc: string;
+ width: number;
+ height: number;
+ [key: string]: any;
+}
+
+const ImageWithFallback: FC = (props) => {
+ const { src, fallbackSrc, ...rest } = props;
+ const [imgSrc, setImgSrc] = useState(src);
+
+ return (
+ {
+ setImgSrc(fallbackSrc);
+ }}
+ />
+ );
+};
+
+export default ImageWithFallback;
\ No newline at end of file