feat: providers
This commit is contained in:
parent
818d172d88
commit
e4eff984f8
|
|
@ -44,9 +44,11 @@ export class IntegrationsController {
|
|||
).map((p) => ({
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture,
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
type: p.type,
|
||||
})),
|
||||
};
|
||||
|
|
@ -221,7 +223,8 @@ export class IntegrationsController {
|
|||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username
|
||||
username,
|
||||
integrationProvider.isBetweenSteps
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +236,24 @@ export class IntegrationsController {
|
|||
return this._integrationService.disableChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/instagram/:id')
|
||||
async saveInstagram(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { pageId: string, id: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveInstagram(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/facebook/:id')
|
||||
async saveFacebook(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { page: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveFacebook(org.id, id, body.page);
|
||||
}
|
||||
|
||||
@Post('/enable')
|
||||
enableChannel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class MediaController {
|
|||
new ParseFilePipe({
|
||||
validators: [
|
||||
new MaxFileSizeValidator({ maxSize: 10 * 1024 * 1024 }),
|
||||
new FileTypeValidator({ fileType: 'image/*' }),
|
||||
new FileTypeValidator({ fileType: /^(image\/.+|video\/mp4)$/ }),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,21 +1,33 @@
|
|||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import {internalFetch} from "@gitroom/helpers/utils/internal.fetch";
|
||||
import {redirect} from "next/navigation";
|
||||
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page({params: {provider}, searchParams}: {params: {provider: string}, searchParams: any}) {
|
||||
if (provider === 'x') {
|
||||
searchParams = {
|
||||
...searchParams,
|
||||
state: searchParams.oauth_token || '',
|
||||
code: searchParams.oauth_verifier || ''
|
||||
};
|
||||
}
|
||||
export default async function Page({
|
||||
params: { provider },
|
||||
searchParams,
|
||||
}: {
|
||||
params: { provider: string };
|
||||
searchParams: any;
|
||||
}) {
|
||||
if (provider === 'x') {
|
||||
searchParams = {
|
||||
...searchParams,
|
||||
state: searchParams.oauth_token || '',
|
||||
code: searchParams.oauth_verifier || '',
|
||||
};
|
||||
}
|
||||
|
||||
const { id, inBetweenSteps } = await (
|
||||
await internalFetch(`/integrations/social/${provider}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(searchParams)
|
||||
});
|
||||
method: 'POST',
|
||||
body: JSON.stringify(searchParams),
|
||||
})
|
||||
).json();
|
||||
|
||||
return redirect(`/launches?added=${provider}`);
|
||||
}
|
||||
if (inBetweenSteps) {
|
||||
return redirect(`/launches?added=${provider}&continue=${id}`);
|
||||
}
|
||||
|
||||
return redirect(`/launches?added=${provider}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
|||
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
|
||||
import { GithubProvider } from '@gitroom/frontend/app/auth/providers/github.provider';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
|
||||
type Inputs = {
|
||||
email: string;
|
||||
|
|
@ -58,13 +59,19 @@ export function Login() {
|
|||
Sign In
|
||||
</h1>
|
||||
</div>
|
||||
<GithubProvider />
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-[#28344F] top-[50%] -translate-y-[50%]" />
|
||||
<div className={`absolute z-[1] ${interClass} justify-center items-center w-full left-0 top-0 flex`}>
|
||||
<div className="bg-[#0a0a0a] px-[16px]">OR</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isGeneral() && (
|
||||
<>
|
||||
<GithubProvider />
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-[#28344F] top-[50%] -translate-y-[50%]" />
|
||||
<div
|
||||
className={`absolute z-[1] ${interClass} justify-center items-center w-full left-0 top-0 flex`}
|
||||
>
|
||||
<div className="bg-[#0a0a0a] px-[16px]">OR</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="text-white">
|
||||
<Input
|
||||
label="Email"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { GithubProvider } from '@gitroom/frontend/app/auth/providers/github.prov
|
|||
import { useSearchParams } from 'next/navigation';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
|
||||
type Inputs = {
|
||||
email: string;
|
||||
|
|
@ -56,7 +57,9 @@ export function Register() {
|
|||
return <LoadingComponent />;
|
||||
}
|
||||
|
||||
return <RegisterAfter token={code} provider={provider?.toUpperCase() || 'LOCAL'} />;
|
||||
return (
|
||||
<RegisterAfter token={code} provider={provider?.toUpperCase() || 'LOCAL'} />
|
||||
);
|
||||
}
|
||||
|
||||
export function RegisterAfter({
|
||||
|
|
@ -110,11 +113,13 @@ export function RegisterAfter({
|
|||
Sign Up
|
||||
</h1>
|
||||
</div>
|
||||
{!isAfterProvider && <GithubProvider />}
|
||||
{!isAfterProvider && (
|
||||
{!isAfterProvider && !isGeneral() && <GithubProvider />}
|
||||
{!isAfterProvider && !isGeneral() && (
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-[#28344F] top-[50%] -translate-y-[50%]" />
|
||||
<div className={`absolute z-[1] ${interClass} justify-center items-center w-full left-0 top-0 flex`}>
|
||||
<div
|
||||
className={`absolute z-[1] ${interClass} justify-center items-center w-full left-0 top-0 flex`}
|
||||
>
|
||||
<div className="bg-[#0a0a0a] px-[16px]">OR</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const AddProviderComponent: FC<{
|
|||
</svg>
|
||||
</button>
|
||||
<h2 className="pt-[16px] pb-[10px]">Social</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
<div className="grid grid-cols-3 gap-[10px] justify-items-center justify-center">
|
||||
{social.map((item) => (
|
||||
<div
|
||||
key={item.identifier}
|
||||
|
|
@ -210,7 +210,7 @@ export const AddProviderComponent: FC<{
|
|||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="pb-[10px]">Articles</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
<div className="grid grid-cols-3 gap-[10px]">
|
||||
{article.map((item) => (
|
||||
<div
|
||||
key={item.identifier}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import useSWR, { useSWRConfig } from 'swr';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Post, Integration } from '@prisma/client';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
|
||||
const CalendarContext = createContext({
|
||||
currentWeek: dayjs().week(),
|
||||
|
|
@ -31,6 +32,7 @@ const CalendarContext = createContext({
|
|||
export interface Integrations {
|
||||
name: string;
|
||||
id: string;
|
||||
inBetweenSteps: boolean;
|
||||
identifier: string;
|
||||
type: string;
|
||||
picture: string;
|
||||
|
|
@ -48,6 +50,9 @@ export const CalendarWeekProvider: FC<{
|
|||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isGeneral()) {
|
||||
return [];
|
||||
}
|
||||
setTrendings(await (await fetch('/posts/predict-trending')).json());
|
||||
})();
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export const PickPlatforms: FC<{
|
|||
>
|
||||
<div className="innerComponent">
|
||||
<div className="flex">
|
||||
{integrations.map((integration) =>
|
||||
{integrations.filter(f => !f.inBetweenSteps).map((integration) =>
|
||||
!props.singleSelect ? (
|
||||
<div
|
||||
key={integration.id}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ import clsx from 'clsx';
|
|||
import { useUser } from '../layout/user.context';
|
||||
import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
|
||||
import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export const LaunchesComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
|
||||
const [reload, setReload] = useState(false);
|
||||
const load = useCallback(async (path: string) => {
|
||||
return (await (await fetch(path)).json()).integrations;
|
||||
|
|
@ -57,6 +60,10 @@ export const LaunchesComponent = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const continueIntegration = useCallback((integration: any) => async () => {
|
||||
router.push(`/launches?added=${integration.identifier}&continue=${integration.id}`);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && window.opener) {
|
||||
window.close();
|
||||
|
|
@ -89,6 +96,14 @@ export const LaunchesComponent = () => {
|
|||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.inBetweenSteps && (
|
||||
<div className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer" onClick={continueIntegration(integration)}>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-black/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
|
|
@ -114,7 +129,7 @@ export const LaunchesComponent = () => {
|
|||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-1',
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
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 FacebookContinue: FC<{closeModal: () => void, existingId: string[]}> = (props) => {
|
||||
const { closeModal, existingId } = props;
|
||||
const call = useCustomProviderFunction();
|
||||
const { integration } = useIntegration();
|
||||
const [page, setSelectedPage] = useState<null | string>(null);
|
||||
const fetch = useFetch();
|
||||
|
||||
const loadPages = useCallback(() => {
|
||||
return call.get('pages');
|
||||
}, []);
|
||||
|
||||
const setPage = useCallback(
|
||||
(id: string) => () => {
|
||||
setSelectedPage(id);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { data } = useSWR('load-pages', loadPages, {
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
|
||||
const saveInstagram = useCallback(async () => {
|
||||
await fetch(`/integrations/facebook/${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 Page:</div>
|
||||
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer">
|
||||
{filteredData?.map(
|
||||
(p: {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
picture: { data: { url: 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 === p.id && 'bg-seventh'
|
||||
)}
|
||||
onClick={setPage(p.id)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={p.picture.data.url}
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveInstagram}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
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 InstagramContinue: 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(() => {
|
||||
return call.get('pages');
|
||||
}, []);
|
||||
|
||||
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 saveInstagram = useCallback(async () => {
|
||||
await fetch(`/integrations/instagram/${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 Instagram 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: { data: { url: 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.data.url}
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveInstagram}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
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';
|
||||
|
||||
export const continueProviderList = {
|
||||
instagram: InstagramContinue,
|
||||
facebook: FacebookContinue,
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/hi
|
|||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import {afterLinkedinCompanyPreventRemove, linkedinCompanyPreventRemove} from "@gitroom/helpers/utils/linkedin.company.prevent.remove";
|
||||
import {
|
||||
afterLinkedinCompanyPreventRemove,
|
||||
linkedinCompanyPreventRemove,
|
||||
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
|
||||
const LinkedinPreview: FC = (props) => {
|
||||
const FacebookPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const mediaDir = useMediaDirectory();
|
||||
const newValues = useFormatting(topValue, {
|
||||
|
|
@ -43,7 +47,10 @@ const LinkedinPreview: FC = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre className="font-['helvetica'] text-[14px] font-[400] text-wrap" dangerouslySetInnerHTML={{__html: firstPost?.text}} />
|
||||
<pre
|
||||
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
|
||||
/>
|
||||
|
||||
{!!firstPost?.images?.length && (
|
||||
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
|
||||
|
|
@ -54,10 +61,7 @@ const LinkedinPreview: FC = (props) => {
|
|||
className="flex-1"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -89,10 +93,12 @@ const LinkedinPreview: FC = (props) => {
|
|||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-[120px] h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<div className="w-[120px] h-full">
|
||||
<VideoOrImage
|
||||
autoplay={true}
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -104,4 +110,4 @@ const LinkedinPreview: FC = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default withProvider(null, LinkedinPreview);
|
||||
export default withProvider(null, FacebookPreview);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/hi
|
|||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import {afterLinkedinCompanyPreventRemove, linkedinCompanyPreventRemove} from "@gitroom/helpers/utils/linkedin.company.prevent.remove";
|
||||
import {
|
||||
afterLinkedinCompanyPreventRemove,
|
||||
linkedinCompanyPreventRemove,
|
||||
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
|
||||
const FacebookPreview: FC = (props) => {
|
||||
const InstagramPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const mediaDir = useMediaDirectory();
|
||||
const newValues = useFormatting(topValue, {
|
||||
|
|
@ -43,8 +47,6 @@ const FacebookPreview: FC = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre className="font-['helvetica'] text-[14px] font-[400] text-wrap" dangerouslySetInnerHTML={{__html: firstPost?.text}} />
|
||||
|
||||
{!!firstPost?.images?.length && (
|
||||
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
|
||||
{firstPost.images.map((image, index) => (
|
||||
|
|
@ -54,14 +56,15 @@ const FacebookPreview: FC = (props) => {
|
|||
className="flex-1"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
|
||||
/>
|
||||
</div>
|
||||
{morePosts.map((p, index) => (
|
||||
<div className="flex gap-[8px]" key={index}>
|
||||
|
|
@ -89,10 +92,12 @@ const FacebookPreview: FC = (props) => {
|
|||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-[120px] h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<div className="w-[120px] h-full">
|
||||
<VideoOrImage
|
||||
autoplay={true}
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -104,4 +109,4 @@ const FacebookPreview: FC = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default withProvider(null, FacebookPreview);
|
||||
export default withProvider(null, InstagramPreview);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/hi
|
|||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import {afterLinkedinCompanyPreventRemove, linkedinCompanyPreventRemove} from "@gitroom/helpers/utils/linkedin.company.prevent.remove";
|
||||
import {
|
||||
afterLinkedinCompanyPreventRemove,
|
||||
linkedinCompanyPreventRemove,
|
||||
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
|
||||
const LinkedinPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
|
|
@ -43,7 +47,10 @@ const LinkedinPreview: FC = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre className="font-['helvetica'] text-[14px] font-[400] text-wrap" dangerouslySetInnerHTML={{__html: firstPost?.text}} />
|
||||
<pre
|
||||
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
|
||||
/>
|
||||
|
||||
{!!firstPost?.images?.length && (
|
||||
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
|
||||
|
|
@ -54,10 +61,7 @@ const LinkedinPreview: FC = (props) => {
|
|||
className="flex-1"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<VideoOrImage src={mediaDir.set(image.path)} autoplay={true} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -89,10 +93,12 @@ const LinkedinPreview: FC = (props) => {
|
|||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-[120px] h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<div className="w-[120px] h-full">
|
||||
<VideoOrImage
|
||||
autoplay={true}
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import LinkedinProvider from "@gitroom/frontend/components/launches/providers/li
|
|||
import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit/reddit.provider";
|
||||
import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
|
||||
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
|
||||
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
|
||||
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -14,6 +16,8 @@ export const Providers = [
|
|||
{identifier: 'reddit', component: RedditProvider},
|
||||
{identifier: 'medium', component: MediumProvider},
|
||||
{identifier: 'hashnode', component: HashnodeProvider},
|
||||
{identifier: 'facebook', component: FacebookProvider},
|
||||
{identifier: 'instagram', component: InstagramProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import clsx from 'clsx';
|
|||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
|
||||
const chirp = localFont({
|
||||
src: [
|
||||
|
|
@ -82,11 +83,13 @@ const XPreview: FC = (props) => {
|
|||
{!!value?.images?.length && (
|
||||
<div className="w-full h-[270px] rounded-[16px] flex overflow-hidden mt-[12px]">
|
||||
{value.images.map((image, index) => (
|
||||
<a key={`image_${index}`} className="flex-1" href={mediaDir.set(image.path)} target="_blank">
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
className="flex-1"
|
||||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { continueProviderList } from '@gitroom/frontend/components/launches/providers/continue-provider/list';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import dayjs from 'dayjs';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
||||
export const Null: FC<{ closeModal: () => void; existingId: string[] }> = () =>
|
||||
null;
|
||||
export const ContinueProvider: FC = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const fetch = useFetch();
|
||||
const searchParams = useSearchParams();
|
||||
const added = searchParams.get('added');
|
||||
const continueId = searchParams.get('continue');
|
||||
const router = useRouter();
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
const list = (await (await fetch(path)).json()).integrations;
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const { data: integrations } = useSWR('/integrations/list', load, {
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
mutate('/integrations/list');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('added');
|
||||
url.searchParams.delete('continue');
|
||||
router.push(url.toString());
|
||||
}, []);
|
||||
|
||||
const Provider = useMemo(() => {
|
||||
if (!added) {
|
||||
return Null;
|
||||
}
|
||||
return continueProviderList[added as keyof typeof continueProviderList];
|
||||
}, [added]);
|
||||
|
||||
if (!added || !continueId || !integrations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed left-0 top-0 w-full h-full bg-black/40 z-[499]"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<div
|
||||
className="w-[100%] max-w-[674px] absolute left-[50%] top-[65px] bg-[#0B0F1C] z-[500] -translate-x-[50%] text-white p-[16px] !pt-0 border border-[#172034] min-h-[300px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="w-full h-full relative">
|
||||
<TopTitle title="Configure Provider" />
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="outline-none absolute right-0 top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="pt-[16px]">
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
date: dayjs(),
|
||||
value: [],
|
||||
integration: {
|
||||
id: continueId,
|
||||
type: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
inBetweenSteps: true,
|
||||
identifier: added,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Provider closeModal={closeModal} existingId={integrations.map((p: any) => p.internalId)} />
|
||||
</IntegrationContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -24,6 +24,8 @@ import { ShowLinkedinCompany } from '@gitroom/frontend/components/launches/helpe
|
|||
import { SettingsComponent } from '@gitroom/frontend/components/layout/settings.component';
|
||||
import { Onboarding } from '@gitroom/frontend/components/onboarding/onboarding';
|
||||
import { Support } from '@gitroom/frontend/components/layout/support';
|
||||
import { ContinueProvider } from '@gitroom/frontend/components/layout/continue.provider';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(weekOfYear);
|
||||
|
|
@ -54,13 +56,14 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<ShowPostSelector />
|
||||
<Onboarding />
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<Link href="/" className="text-2xl flex items-center gap-[10px]">
|
||||
<div>
|
||||
<Image src="/logo.svg" width={55} height={53} alt="Logo" />
|
||||
</div>
|
||||
<div className="mt-[12px]">Gitroom</div>
|
||||
<div className="mt-[12px]">{isGeneral() ? 'Postiz' : 'Gitroom'}</div>
|
||||
</Link>
|
||||
{user?.orgId ? <TopMenu /> : <div />}
|
||||
<div className="flex items-center gap-[8px]">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const menuItems = [
|
|||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Launches',
|
||||
name: isGeneral() ? 'Calendar' : 'Launches',
|
||||
icon: 'launches',
|
||||
path: '/launches',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import EventEmitter from 'events';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import clsx from 'clsx';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { VideoFrame } from '@gitroom/react/helpers/video.frame';
|
||||
const showModalEmitter = new EventEmitter();
|
||||
|
||||
export const ShowMediaBoxModal: FC = () => {
|
||||
|
|
@ -139,10 +140,12 @@ export const MediaBox: FC<{
|
|||
<input
|
||||
type="file"
|
||||
className="absolute left-0 top-0 w-full h-full opacity-0"
|
||||
accept="image/*"
|
||||
accept="image/*,video/mp4"
|
||||
onChange={uploadMedia}
|
||||
/>
|
||||
<button className={`cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white ${interClass} border-[2px] border-[#506490]`}>
|
||||
<button
|
||||
className={`cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white ${interClass} border-[2px] border-[#506490]`}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -181,7 +184,9 @@ export const MediaBox: FC<{
|
|||
accept="image/*"
|
||||
onChange={uploadMedia}
|
||||
/>
|
||||
<button className={`cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white ${interClass} border-[2px] border-[#506490]`}>
|
||||
<button
|
||||
className={`cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white ${interClass} border-[2px] border-[#506490]`}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -208,10 +213,14 @@ export const MediaBox: FC<{
|
|||
className="w-[200px] h-[200px] border-tableBorder border-2 cursor-pointer"
|
||||
onClick={setNewMedia(media)}
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
/>
|
||||
{media.path.indexOf('mp4') > -1 ? (
|
||||
<VideoFrame url={mediaDirectory.set(media.path)} />
|
||||
) : (
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -295,11 +304,18 @@ export const MultiMediaComponent: FC<{
|
|||
currentMedia.map((media, index) => (
|
||||
<>
|
||||
<div className="cursor-pointer w-[40px] h-[40px] border-2 border-tableBorder relative">
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
<div
|
||||
onClick={() => window.open(mediaDirectory.set(media.path))}
|
||||
/>
|
||||
>
|
||||
{media.path.indexOf('mp4') > -1 ? (
|
||||
<VideoFrame url={mediaDirectory.set(media.path)} />
|
||||
) : (
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={clearMedia(index)}
|
||||
className="rounded-full w-[15px] h-[15px] bg-red-800 text-white flex justify-center items-center absolute -right-[4px] -top-[4px]"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { orderBy } from 'lodash';
|
||||
|
|
@ -7,10 +14,13 @@ import clsx from 'clsx';
|
|||
import Image from 'next/image';
|
||||
import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
|
||||
import { ApiModal } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export const ConnectChannels: FC = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const [identifier, setIdentifier] = useState<any>(undefined);
|
||||
const [popup, setPopups] = useState<undefined | string[]>(undefined);
|
||||
|
||||
const getIntegrations = useCallback(async () => {
|
||||
return (await fetch('/integrations')).json();
|
||||
|
|
@ -30,7 +40,9 @@ export const ConnectChannels: FC = () => {
|
|||
);
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
return (await (await fetch(path)).json()).integrations;
|
||||
const list = (await (await fetch(path)).json()).integrations;
|
||||
setPopups(list.map((p: any) => p.id));
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const { data: integrations, mutate } = useSWR('/integrations/list', load, {
|
||||
|
|
@ -54,6 +66,21 @@ export const ConnectChannels: FC = () => {
|
|||
);
|
||||
}, [integrations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedIntegrations.length === 0 || !popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const betweenSteps = sortedIntegrations.find((p) => p.inBetweenSteps);
|
||||
if (betweenSteps && popup.indexOf(betweenSteps.id) === -1) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.append('added', betweenSteps.identifier);
|
||||
url.searchParams.append('continue', betweenSteps.id);
|
||||
router.push(url.toString());
|
||||
setPopups([...popup, betweenSteps.id]);
|
||||
}
|
||||
}, [sortedIntegrations, popup]);
|
||||
|
||||
const update = useCallback(async (shouldReload: boolean) => {
|
||||
if (shouldReload) {
|
||||
setReload(true);
|
||||
|
|
@ -65,6 +92,16 @@ export const ConnectChannels: FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const continueIntegration = useCallback(
|
||||
(integration: any) => async () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.append('added', integration.identifier);
|
||||
url.searchParams.append('continue', integration.id);
|
||||
router.push(url.toString());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const finishUpdate = useCallback(() => {
|
||||
setIdentifier(undefined);
|
||||
update(true);
|
||||
|
|
@ -153,6 +190,17 @@ export const ConnectChannels: FC = () => {
|
|||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.inBetweenSteps && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
|
||||
onClick={continueIntegration(integration)}
|
||||
>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-black/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/pris
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
import * as console from 'node:console';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationRepository {
|
||||
|
|
@ -10,6 +12,17 @@ export class IntegrationRepository {
|
|||
private _posts: PrismaRepository<'post'>
|
||||
) {}
|
||||
|
||||
updateIntegration(id: string, params: Partial<Integration>) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
...params,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createOrUpdateIntegration(
|
||||
org: string,
|
||||
name: string,
|
||||
|
|
@ -20,7 +33,8 @@ export class IntegrationRepository {
|
|||
token: string,
|
||||
refreshToken = '',
|
||||
expiresIn = 999999999,
|
||||
username?: string
|
||||
username?: string,
|
||||
isBetweenSteps = false
|
||||
) {
|
||||
return this._integration.model.integration.upsert({
|
||||
where: {
|
||||
|
|
@ -36,6 +50,7 @@ export class IntegrationRepository {
|
|||
token,
|
||||
profile: username,
|
||||
picture,
|
||||
inBetweenSteps: isBetweenSteps,
|
||||
refreshToken,
|
||||
...(expiresIn
|
||||
? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
|
||||
|
|
@ -47,6 +62,7 @@ export class IntegrationRepository {
|
|||
type: type as any,
|
||||
name,
|
||||
providerIdentifier: provider,
|
||||
inBetweenSteps: isBetweenSteps,
|
||||
token,
|
||||
picture,
|
||||
profile: username,
|
||||
|
|
@ -67,6 +83,7 @@ export class IntegrationRepository {
|
|||
tokenExpiration: {
|
||||
lte: dayjs().add(1, 'day').toDate(),
|
||||
},
|
||||
inBetweenSteps: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
|
@ -81,7 +98,12 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getIntegrationForOrder(id: string, order: string, user: string, org: string) {
|
||||
async getIntegrationForOrder(
|
||||
id: string,
|
||||
order: string,
|
||||
user: string,
|
||||
org: string
|
||||
) {
|
||||
console.log(id, order, user, org);
|
||||
const integration = await this._posts.model.post.findFirst({
|
||||
where: {
|
||||
|
|
@ -90,12 +112,12 @@ export class IntegrationRepository {
|
|||
id: order,
|
||||
messageGroup: {
|
||||
OR: [
|
||||
{sellerId: user},
|
||||
{buyerId: user},
|
||||
{buyerOrganizationId: org},
|
||||
]
|
||||
}
|
||||
}
|
||||
{ sellerId: user },
|
||||
{ buyerId: user },
|
||||
{ buyerOrganizationId: org },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
integration: {
|
||||
|
|
@ -103,10 +125,11 @@ export class IntegrationRepository {
|
|||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
inBetweenSteps: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return integration?.integration;
|
||||
|
|
@ -170,6 +193,21 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async checkForDeletedOnceAndUpdate(org: string, page: string) {
|
||||
return this._integration.model.integration.updateMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
internalId: page,
|
||||
deletedAt: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
internalId: makeId(10),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async disableIntegrations(org: string, totalChannels: number) {
|
||||
const getChannels = await this._integration.model.integration.findMany({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
|
||||
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
|
|
@ -18,7 +20,8 @@ export class IntegrationService {
|
|||
token: string,
|
||||
refreshToken = '',
|
||||
expiresIn?: number,
|
||||
username?: string
|
||||
username?: string,
|
||||
isBetweenSteps = false
|
||||
) {
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
org,
|
||||
|
|
@ -30,7 +33,8 @@ export class IntegrationService {
|
|||
token,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username
|
||||
username,
|
||||
isBetweenSteps
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +43,12 @@ export class IntegrationService {
|
|||
}
|
||||
|
||||
getIntegrationForOrder(id: string, order: string, user: string, org: string) {
|
||||
return this._integrationRepository.getIntegrationForOrder(id, order, user, org);
|
||||
return this._integrationRepository.getIntegrationForOrder(
|
||||
id,
|
||||
order,
|
||||
user,
|
||||
org
|
||||
);
|
||||
}
|
||||
|
||||
getIntegrationById(org: string, id: string) {
|
||||
|
|
@ -86,9 +95,15 @@ export class IntegrationService {
|
|||
}
|
||||
|
||||
async deleteChannel(org: string, id: string) {
|
||||
const isTherePosts = await this._integrationRepository.countPostsForChannel(org, id);
|
||||
const isTherePosts = await this._integrationRepository.countPostsForChannel(
|
||||
org,
|
||||
id
|
||||
);
|
||||
if (isTherePosts) {
|
||||
throw new HttpException('There are posts for this channel', HttpStatus.NOT_ACCEPTABLE);
|
||||
throw new HttpException(
|
||||
'There are posts for this channel',
|
||||
HttpStatus.NOT_ACCEPTABLE
|
||||
);
|
||||
}
|
||||
|
||||
return this._integrationRepository.deleteChannel(org, id);
|
||||
|
|
@ -97,4 +112,66 @@ export class IntegrationService {
|
|||
async disableIntegrations(org: string, totalChannels: number) {
|
||||
return this._integrationRepository.disableIntegrations(org, totalChannels);
|
||||
}
|
||||
|
||||
async checkForDeletedOnceAndUpdate(org: string, page: string) {
|
||||
return this._integrationRepository.checkForDeletedOnceAndUpdate(org, page);
|
||||
}
|
||||
|
||||
async saveInstagram(org: string, id: string, data: { pageId: string, id: string }) {
|
||||
const getIntegration = await this._integrationRepository.getIntegrationById(
|
||||
org,
|
||||
id
|
||||
);
|
||||
if (getIntegration && !getIntegration.inBetweenSteps) {
|
||||
throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const instagram = this._integrationManager.getSocialIntegration(
|
||||
'instagram'
|
||||
) as InstagramProvider;
|
||||
const getIntegrationInformation = await instagram.fetchPageInformation(
|
||||
getIntegration?.token!,
|
||||
data
|
||||
);
|
||||
|
||||
await this.checkForDeletedOnceAndUpdate(org, getIntegrationInformation.id);
|
||||
await this._integrationRepository.updateIntegration(id, {
|
||||
picture: getIntegrationInformation.picture,
|
||||
internalId: getIntegrationInformation.id,
|
||||
name: getIntegrationInformation.name,
|
||||
inBetweenSteps: false,
|
||||
token: getIntegrationInformation.access_token,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async saveFacebook(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 facebook = this._integrationManager.getSocialIntegration(
|
||||
'facebook'
|
||||
) as FacebookProvider;
|
||||
const getIntegrationInformation = await facebook.fetchPageInformation(
|
||||
getIntegration?.token!,
|
||||
page
|
||||
);
|
||||
|
||||
await this.checkForDeletedOnceAndUpdate(org, getIntegrationInformation.id);
|
||||
await this._integrationRepository.updateIntegration(id, {
|
||||
picture: getIntegrationInformation.picture,
|
||||
internalId: getIntegrationInformation.id,
|
||||
name: getIntegrationInformation.name,
|
||||
inBetweenSteps: false,
|
||||
token: getIntegrationInformation.access_token,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ model Integration {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime? @updatedAt
|
||||
orderItems OrderItems[]
|
||||
inBetweenSteps Boolean @default(false)
|
||||
|
||||
@@index([updatedAt])
|
||||
@@index([deletedAt])
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
import {Injectable} from "@nestjs/common";
|
||||
import {XProvider} from "@gitroom/nestjs-libraries/integrations/social/x.provider";
|
||||
import {SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface";
|
||||
import {LinkedinProvider} from "@gitroom/nestjs-libraries/integrations/social/linkedin.provider";
|
||||
import {RedditProvider} from "@gitroom/nestjs-libraries/integrations/social/reddit.provider";
|
||||
import {DevToProvider} from "@gitroom/nestjs-libraries/integrations/article/dev.to.provider";
|
||||
import {HashnodeProvider} from "@gitroom/nestjs-libraries/integrations/article/hashnode.provider";
|
||||
import {MediumProvider} from "@gitroom/nestjs-libraries/integrations/article/medium.provider";
|
||||
import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { XProvider } from '@gitroom/nestjs-libraries/integrations/social/x.provider';
|
||||
import { SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
||||
import { RedditProvider } from '@gitroom/nestjs-libraries/integrations/social/reddit.provider';
|
||||
import { DevToProvider } from '@gitroom/nestjs-libraries/integrations/article/dev.to.provider';
|
||||
import { HashnodeProvider } from '@gitroom/nestjs-libraries/integrations/article/hashnode.provider';
|
||||
import { MediumProvider } from '@gitroom/nestjs-libraries/integrations/article/medium.provider';
|
||||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
|
||||
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
|
||||
|
||||
const socialIntegrationList = [
|
||||
new XProvider(),
|
||||
new LinkedinProvider(),
|
||||
new RedditProvider()
|
||||
new XProvider(),
|
||||
new LinkedinProvider(),
|
||||
new RedditProvider(),
|
||||
new FacebookProvider(),
|
||||
new InstagramProvider(),
|
||||
];
|
||||
|
||||
const articleIntegrationList = [
|
||||
new DevToProvider(),
|
||||
new HashnodeProvider(),
|
||||
new MediumProvider()
|
||||
new DevToProvider(),
|
||||
new HashnodeProvider(),
|
||||
new MediumProvider(),
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationManager {
|
||||
getAllIntegrations() {
|
||||
return {
|
||||
social: socialIntegrationList.map(p => ({name: p.name, identifier: p.identifier})),
|
||||
article: articleIntegrationList.map(p => ({name: p.name, identifier: p.identifier})),
|
||||
};
|
||||
}
|
||||
getAllowedSocialsIntegrations() {
|
||||
return socialIntegrationList.map(p => p.identifier);
|
||||
}
|
||||
getSocialIntegration(integration: string): SocialProvider {
|
||||
return socialIntegrationList.find(i => i.identifier === integration)!;
|
||||
}
|
||||
getAllowedArticlesIntegrations() {
|
||||
return articleIntegrationList.map(p => p.identifier);
|
||||
}
|
||||
getArticlesIntegration(integration: string): ArticleProvider {
|
||||
return articleIntegrationList.find(i => i.identifier === integration)!;
|
||||
}
|
||||
}
|
||||
getAllIntegrations() {
|
||||
return {
|
||||
social: socialIntegrationList.map((p) => ({
|
||||
name: p.name,
|
||||
identifier: p.identifier,
|
||||
})),
|
||||
article: articleIntegrationList.map((p) => ({
|
||||
name: p.name,
|
||||
identifier: p.identifier,
|
||||
})),
|
||||
};
|
||||
}
|
||||
getAllowedSocialsIntegrations() {
|
||||
return socialIntegrationList.map((p) => p.identifier);
|
||||
}
|
||||
getSocialIntegration(integration: string): SocialProvider {
|
||||
return socialIntegrationList.find((i) => i.identifier === integration)!;
|
||||
}
|
||||
getAllowedArticlesIntegrations() {
|
||||
return articleIntegrationList.map((p) => p.identifier);
|
||||
}
|
||||
getArticlesIntegration(integration: string): ArticleProvider {
|
||||
return articleIntegrationList.find((i) => i.identifier === integration)!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
export class FacebookProvider implements SocialProvider {
|
||||
identifier = 'facebook';
|
||||
name = 'Facebook Page';
|
||||
isBetweenSteps = true;
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const { access_token, expires_in, ...all } = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
'?grant_type=fb_exchange_token' +
|
||||
`&client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&fb_exchange_token=${refresh_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: expires_in,
|
||||
picture: url,
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
url:
|
||||
'https://www.facebook.com/v19.0/dialog/oauth' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook`
|
||||
)}` +
|
||||
`&state=${state}` +
|
||||
'&scope=pages_show_list,business_management,pages_manage_posts,publish_video,pages_manage_engagement,pages_read_engagement',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: { code: string; codeVerifier: string }) {
|
||||
const getAccessToken = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook`
|
||||
)}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&code=${params.code}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const { access_token, expires_in, ...all } = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
'?grant_type=fb_exchange_token' +
|
||||
`&client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&fb_exchange_token=${getAccessToken.access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: expires_in,
|
||||
picture: url,
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async pages(accessToken: string) {
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async fetchPageInformation(accessToken: string, pageId: string) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
access_token,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${pageId}?fields=access_token,name,picture.type(large)&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
access_token,
|
||||
picture: url,
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
|
||||
let finalId = '';
|
||||
let finalUrl = '';
|
||||
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || 0) > -1) {
|
||||
const { id: videoId, permalink_url } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_url: firstPost?.media?.[0]?.path!,
|
||||
description: firstPost.message,
|
||||
published: true,
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
finalUrl = permalink_url;
|
||||
finalId = videoId;
|
||||
} else {
|
||||
const uploadPhotos = !firstPost?.media?.length
|
||||
? []
|
||||
: await Promise.all(
|
||||
firstPost.media.map(async (media) => {
|
||||
const { id: photoId } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: media.url,
|
||||
published: false,
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return { media_fbid: photoId };
|
||||
})
|
||||
);
|
||||
|
||||
const { id: postId, permalink_url } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(uploadPhotos?.length ? { attached_media: uploadPhotos } : {}),
|
||||
message: firstPost.message,
|
||||
published: true,
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
finalUrl = permalink_url;
|
||||
finalId = postId;
|
||||
}
|
||||
|
||||
const postsArray = [];
|
||||
for (const comment of comments) {
|
||||
const data = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${finalId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(comment.media?.length
|
||||
? { attachment_url: comment.media[0].url }
|
||||
: {}),
|
||||
message: comment.message,
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
postsArray.push({
|
||||
id: comment.id,
|
||||
postId: data.id,
|
||||
releaseURL: data.permalink_url,
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: finalId,
|
||||
releaseURL: finalUrl,
|
||||
status: 'success',
|
||||
},
|
||||
...postsArray,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -5,14 +5,17 @@ import {
|
|||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class InstagramProvider implements SocialProvider {
|
||||
identifier = 'instagram';
|
||||
name = 'Instagram';
|
||||
isBetweenSteps = true;
|
||||
|
||||
export class FacebookProvider implements SocialProvider {
|
||||
identifier = 'facebook';
|
||||
name = 'Facebook';
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const { access_token, expires_in, ...all } = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
'https://graph.facebook.com/v20.0/oauth/access_token' +
|
||||
'?grant_type=fb_exchange_token' +
|
||||
`&client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
|
|
@ -21,19 +24,29 @@ export class FacebookProvider implements SocialProvider {
|
|||
).json();
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
data: {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const {
|
||||
instagram_business_account: { id: instagramId },
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}?fields=instagram_business_account&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
id: instagramId,
|
||||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
|
|
@ -47,14 +60,15 @@ export class FacebookProvider implements SocialProvider {
|
|||
const state = makeId(6);
|
||||
return {
|
||||
url:
|
||||
'https://www.facebook.com/v19.0/dialog/oauth' +
|
||||
'https://www.facebook.com/v20.0/dialog/oauth' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook`
|
||||
`${process.env.FRONTEND_URL}/integrations/social/instagram`
|
||||
)}` +
|
||||
`&state=${state}` +
|
||||
'&scope=email,public_profile',
|
||||
// '&scope=email,public_profile,pages_manage_posts,pages_read_engagement,publish_to_groups,groups_access_member_info',
|
||||
`&scope=${encodeURIComponent(
|
||||
'instagram_basic,pages_show_list,pages_read_engagement,business_management,instagram_content_publish,instagram_manage_comments'
|
||||
)}`,
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
|
|
@ -63,10 +77,10 @@ export class FacebookProvider implements SocialProvider {
|
|||
async authenticate(params: { code: string; codeVerifier: string }) {
|
||||
const getAccessToken = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
'https://graph.facebook.com/v20.0/oauth/access_token' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook`
|
||||
`${process.env.FRONTEND_URL}/integrations/social/instagram`
|
||||
)}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&code=${params.code}`
|
||||
|
|
@ -75,7 +89,7 @@ export class FacebookProvider implements SocialProvider {
|
|||
|
||||
const { access_token, expires_in, ...all } = await (
|
||||
await fetch(
|
||||
'https://graph.facebook.com/v19.0/oauth/access_token' +
|
||||
'https://graph.facebook.com/v20.0/oauth/access_token' +
|
||||
'?grant_type=fb_exchange_token' +
|
||||
`&client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
|
|
@ -91,7 +105,7 @@ export class FacebookProvider implements SocialProvider {
|
|||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
|
|
@ -106,11 +120,208 @@ export class FacebookProvider implements SocialProvider {
|
|||
};
|
||||
}
|
||||
|
||||
async pages(accessToken: string) {
|
||||
const { data } = await (
|
||||
await 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();
|
||||
|
||||
const onlyConnectedAccounts = await Promise.all(
|
||||
data
|
||||
.filter((f: any) => f.instagram_business_account)
|
||||
.map(async (p: any) => {
|
||||
return {
|
||||
pageId: p.id,
|
||||
...(await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${p.instagram_business_account.id}?fields=name,profile_picture_url&access_token=${accessToken}&limit=500`
|
||||
)
|
||||
).json()),
|
||||
id: p.instagram_business_account.id,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return onlyConnectedAccounts.map((p: any) => ({
|
||||
pageId: p.pageId,
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
picture: { data: { url: p.profile_picture_url } },
|
||||
}));
|
||||
}
|
||||
|
||||
async fetchPageInformation(
|
||||
accessToken: string,
|
||||
data: { pageId: string; id: string }
|
||||
) {
|
||||
const { access_token } = await (
|
||||
await 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 } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${data.id}?fields=name,profile_picture_url&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
picture: profile_picture_url,
|
||||
access_token,
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
return [];
|
||||
const [firstPost, ...theRest] = postDetails;
|
||||
|
||||
const medias = await Promise.all(
|
||||
firstPost?.media?.map(async (m) => {
|
||||
const caption =
|
||||
firstPost.media?.length === 1 ? `&caption=${firstPost.message}` : ``;
|
||||
const isCarousel =
|
||||
(firstPost?.media?.length || 0) > 1 ? `&is_carousel_item=true` : ``;
|
||||
const mediaType =
|
||||
m.path.indexOf('.mp4') > -1
|
||||
? firstPost?.media?.length === 1
|
||||
? `video_url=${m.url}&media_type=REELS`
|
||||
: `video_url=${m.url}&media_type=VIDEO`
|
||||
: `image_url=${m.url}`;
|
||||
const { id: photoId } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${caption}${isCarousel}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
let status = 'IN_PROGRESS';
|
||||
while (status === 'IN_PROGRESS') {
|
||||
const { status_code } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${photoId}?access_token=${accessToken}&fields=status_code`
|
||||
)
|
||||
).json();
|
||||
await timer(3000);
|
||||
status = status_code;
|
||||
}
|
||||
|
||||
return photoId;
|
||||
}) || []
|
||||
);
|
||||
|
||||
const arr = [];
|
||||
|
||||
let containerIdGlobal = '';
|
||||
let linkGlobal = '';
|
||||
if (medias.length === 1) {
|
||||
const { id: mediaId, ...all } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${medias[0]}&access_token=${accessToken}&field=id`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
console.log(all);
|
||||
|
||||
containerIdGlobal = mediaId;
|
||||
|
||||
const { permalink } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
linkGlobal = permalink;
|
||||
} else {
|
||||
const { id: containerId, ...all3 } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media?caption=${encodeURIComponent(
|
||||
firstPost?.message
|
||||
)}&media_type=CAROUSEL&children=${encodeURIComponent(
|
||||
medias.join(',')
|
||||
)}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
let status = 'IN_PROGRESS';
|
||||
while (status === 'IN_PROGRESS') {
|
||||
const { status_code } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${containerId}?fields=status_code&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
await timer(3000);
|
||||
status = status_code;
|
||||
}
|
||||
|
||||
const { id: mediaId, ...all4 } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${containerId}&access_token=${accessToken}&field=id`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
containerIdGlobal = mediaId;
|
||||
|
||||
const { permalink } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
linkGlobal = permalink;
|
||||
}
|
||||
|
||||
for (const post of theRest) {
|
||||
const { id: commentId, ...all } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent(
|
||||
post.message
|
||||
)}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: firstPost.id,
|
||||
postId: commentId,
|
||||
releaseURL: linkGlobal,
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown';
|
|||
export class LinkedinProvider 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', {
|
||||
|
|
@ -165,43 +167,86 @@ export class LinkedinProvider implements SocialProvider {
|
|||
}
|
||||
|
||||
private async uploadPicture(
|
||||
fileName: string,
|
||||
accessToken: string,
|
||||
personId: string,
|
||||
picture: any
|
||||
) {
|
||||
const {
|
||||
value: { uploadUrl, image },
|
||||
} = await (
|
||||
await fetch(
|
||||
'https://api.linkedin.com/rest/images?action=initializeUpload',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initializeUploadRequest: {
|
||||
owner: `urn:li:person:${personId}`,
|
||||
try {
|
||||
const {
|
||||
value: { uploadUrl, image, video, uploadInstructions, ...all },
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://api.linkedin.com/rest/${
|
||||
fileName.indexOf('mp4') > -1 ? 'videos' : 'images'
|
||||
}?action=initializeUpload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
body: JSON.stringify({
|
||||
initializeUploadRequest: {
|
||||
owner: `urn:li:person:${personId}`,
|
||||
...(fileName.indexOf('mp4') > -1
|
||||
? {
|
||||
fileSizeBytes: picture.length,
|
||||
uploadCaptions: false,
|
||||
uploadThumbnail: false,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: picture,
|
||||
});
|
||||
const sendUrlRequest = uploadInstructions?.[0]?.uploadUrl || uploadUrl;
|
||||
const finalOutput = video || image;
|
||||
|
||||
return image;
|
||||
const upload = await fetch(sendUrlRequest, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...(fileName.indexOf('mp4') > -1
|
||||
? { 'Content-Type': 'application/octet-stream' }
|
||||
: {}),
|
||||
},
|
||||
body: picture,
|
||||
});
|
||||
|
||||
if (fileName.indexOf('mp4') > -1) {
|
||||
const etag = upload.headers.get('etag');
|
||||
const a = await fetch(
|
||||
'https://api.linkedin.com/rest/videos?action=finalizeUpload',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
finalizeUploadRequest: {
|
||||
video,
|
||||
uploadToken: '',
|
||||
uploadedPartIds: [etag],
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return finalOutput;
|
||||
} catch (err: any) {
|
||||
throw 'eerr';
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
|
|
@ -217,15 +262,18 @@ export class LinkedinProvider implements SocialProvider {
|
|||
p?.media?.flatMap(async (m) => {
|
||||
return {
|
||||
id: await this.uploadPicture(
|
||||
m.path,
|
||||
accessToken,
|
||||
id,
|
||||
await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
})
|
||||
.toBuffer()
|
||||
m.path.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.path))
|
||||
: await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
})
|
||||
.toBuffer()
|
||||
),
|
||||
postId: p.id,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { groupBy } from 'lodash';
|
|||
export class RedditProvider implements SocialProvider {
|
||||
identifier = 'reddit';
|
||||
name = 'Reddit';
|
||||
isBetweenSteps = false;
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const {
|
||||
access_token: accessToken,
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ export type MediaContent = {
|
|||
export interface SocialProvider extends IAuthenticator, ISocialMediaIntegration {
|
||||
identifier: string;
|
||||
name: string;
|
||||
isBetweenSteps: boolean;
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import removeMd from 'remove-markdown';
|
|||
export class XProvider implements SocialProvider {
|
||||
identifier = 'x';
|
||||
name = 'X';
|
||||
isBetweenSteps = false;
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
|
|
@ -47,8 +49,6 @@ export class XProvider implements SocialProvider {
|
|||
|
||||
async generateAuthUrl() {
|
||||
const client = new TwitterApi({
|
||||
// clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
// clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
});
|
||||
|
|
@ -128,14 +128,16 @@ export class XProvider implements SocialProvider {
|
|||
p?.media?.flatMap(async (m) => {
|
||||
return {
|
||||
id: await client.v1.uploadMedia(
|
||||
await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
})
|
||||
.gif()
|
||||
.toBuffer(),
|
||||
m.path.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.path))
|
||||
: await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
})
|
||||
.gif()
|
||||
.toBuffer(),
|
||||
{
|
||||
mimeType: lookup(m.path) || '',
|
||||
}
|
||||
|
|
@ -183,17 +185,4 @@ export class XProvider implements SocialProvider {
|
|||
status: 'posted',
|
||||
}));
|
||||
}
|
||||
|
||||
// async analytics(accessToken: string) {
|
||||
// const [accessTokenSplit, accessSecretSplit] = accessToken.split(':');
|
||||
// const client = new TwitterApi({
|
||||
// appKey: process.env.X_API_KEY!,
|
||||
// appSecret: process.env.X_API_SECRET!,
|
||||
// accessToken: accessTokenSplit,
|
||||
// accessSecret: accessSecretSplit,
|
||||
// });
|
||||
// const {
|
||||
// data: { username },
|
||||
// } = await client.v2;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
|
||||
export const VideoFrame: FC<{ url: string }> = (props) => {
|
||||
const { url } = props;
|
||||
|
||||
return <video id="videoFrame" src={url + '#t=0.1'} preload="metadata" autoPlay={false}></video>;
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
export const VideoOrImage: FC<{ src: string; autoplay: boolean }> = (props) => {
|
||||
const { src, autoplay } = props;
|
||||
if (src.indexOf('mp4') > -1) {
|
||||
return <video src={src} autoPlay={autoplay} className="w-full h-full" muted={true} loop={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={src}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue