commit
b17727bccc
|
|
@ -1,13 +1,5 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseFilters,
|
||||
Body, Controller, Delete, Get, Param, Post, Put, Query, UseFilters
|
||||
} from '@nestjs/common';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
|
||||
|
|
@ -30,6 +22,7 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import {
|
||||
NotEnoughScopes,
|
||||
RefreshToken,
|
||||
|
|
@ -538,4 +531,35 @@ export class IntegrationsController {
|
|||
|
||||
return this._integrationService.deleteChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/plug/list')
|
||||
async getPlugList() {
|
||||
return { plugs: this._integrationManager.getAllPlugs() };
|
||||
}
|
||||
|
||||
@Get('/:id/plugs')
|
||||
async getPlugsByIntegrationId(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.getPlugsByIntegrationId(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/plugs')
|
||||
async postPlugsByIntegrationId(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: PlugDto
|
||||
) {
|
||||
return this._integrationService.createOrUpdatePlug(org.id, id, body);
|
||||
}
|
||||
|
||||
@Put('/plugs/:id/activate')
|
||||
async changePlugActivation(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body('status') status: boolean
|
||||
) {
|
||||
return this._integrationService.changePlugActivation(org.id, id, status);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Plugs } from '@gitroom/frontend/components/plugs/plugs';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Plugs`,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default async function Index() {
|
||||
return (
|
||||
<>
|
||||
<Plugs />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -44,14 +44,9 @@ export const useMenuItems = () => {
|
|||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Marketplace',
|
||||
icon: 'marketplace',
|
||||
path: '/marketplace',
|
||||
},
|
||||
{
|
||||
name: 'Messages',
|
||||
icon: 'messages',
|
||||
path: '/messages',
|
||||
name: 'Plugs',
|
||||
icon: 'plugs',
|
||||
path: '/plugs',
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
'use client';
|
||||
import {
|
||||
PlugSettings,
|
||||
PlugsInterface,
|
||||
usePlugs,
|
||||
} from '@gitroom/frontend/components/plugs/plugs.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import {
|
||||
FormProvider,
|
||||
SubmitHandler,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { string, object } from 'yup';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export function convertBackRegex(s: string) {
|
||||
const matches = s.match(/\/(.*)\/([a-z]*)/);
|
||||
const pattern = matches?.[1] || '';
|
||||
const flags = matches?.[2] || '';
|
||||
|
||||
return new RegExp(pattern, flags);
|
||||
}
|
||||
|
||||
export const TextArea: FC<{ name: string; placeHolder: string }> = (props) => {
|
||||
const form = useFormContext();
|
||||
const { onChange, onBlur, ...all } = form.register(props.name);
|
||||
const value = form.watch(props.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea className="hidden" {...all}></textarea>
|
||||
<CopilotTextarea
|
||||
disableBranding={true}
|
||||
placeholder={props.placeHolder}
|
||||
value={value}
|
||||
className={clsx(
|
||||
'!min-h-40 !max-h-80 p-[24px] overflow-hidden bg-customColor2 outline-none rounded-[4px] border-fifth border'
|
||||
)}
|
||||
onChange={(e) => {
|
||||
onChange({ target: { name: props.name, value: e.target.value } });
|
||||
}}
|
||||
autosuggestionsConfig={{
|
||||
textareaPurpose: `Assist me in writing social media posts.`,
|
||||
chatApiConfigs: {},
|
||||
}}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px]">
|
||||
{form?.formState?.errors?.[props.name]?.message as string}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlugPop: FC<{
|
||||
plug: PlugsInterface;
|
||||
settings: PlugSettings;
|
||||
data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
};
|
||||
}> = (props) => {
|
||||
const { plug, settings, data } = props;
|
||||
const { closeAll } = useModals();
|
||||
const fetch = useFetch();
|
||||
const toaster = useToaster();
|
||||
|
||||
const values = useMemo(() => {
|
||||
if (!data?.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(data.data).reduce((acc: any, current: any) => {
|
||||
return {
|
||||
...acc,
|
||||
[current.name]: current.value,
|
||||
};
|
||||
}, {} as any);
|
||||
}, []);
|
||||
|
||||
const yupSchema = useMemo(() => {
|
||||
return object(
|
||||
plug.fields.reduce((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[field.name]: field.validation
|
||||
? string().matches(convertBackRegex(field.validation), {
|
||||
message: 'Invalid value',
|
||||
})
|
||||
: null,
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
resolver: yupResolver(yupSchema),
|
||||
values,
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const submit: SubmitHandler<any> = useCallback(async (data) => {
|
||||
await fetch(`/integrations/${settings.providerId}/plugs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
func: plug.methodName,
|
||||
fields: Object.keys(data).map((key) => ({
|
||||
name: key,
|
||||
value: data[key],
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
toaster.show('Plug updated', 'success');
|
||||
closeAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<div className="fixed left-0 top-0 bg-primary/80 z-[300] w-full min-h-full p-4 md:p-[60px] animate-fade">
|
||||
<div className="max-w-[1000px] w-full h-full bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative mx-auto">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={`Auto Plug: ${plug.title}`} />
|
||||
</div>
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary 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>
|
||||
<div className="my-[20px]">{plug.description}</div>
|
||||
<div>
|
||||
{plug.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
{field.type === 'richtext' ? (
|
||||
<TextArea
|
||||
name={field.name}
|
||||
placeHolder={field.placeholder}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
name={field.name}
|
||||
label={field.description}
|
||||
className="w-full mt-[8px] p-[8px] border border-tableBorder rounded-md text-black"
|
||||
placeholder={field.placeholder}
|
||||
type={field.type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-[20px]">
|
||||
<Button type="submit">Activate</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlugItem: FC<{
|
||||
plug: PlugsInterface;
|
||||
addPlug: (data: any) => void;
|
||||
data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
};
|
||||
}> = (props) => {
|
||||
const { plug, addPlug, data } = props;
|
||||
const [activated, setActivated] = useState(!!data?.activated);
|
||||
useEffect(() => {
|
||||
setActivated(!!data?.activated);
|
||||
}, [data?.activated]);
|
||||
const fetch = useFetch();
|
||||
|
||||
const changeActivated = useCallback(
|
||||
async (status: 'on' | 'off') => {
|
||||
await fetch(`/integrations/plugs/${data?.id}/activate`, {
|
||||
body: JSON.stringify({
|
||||
status: status === 'on',
|
||||
}),
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
setActivated(status === 'on');
|
||||
},
|
||||
[activated]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => addPlug(data)}
|
||||
key={plug.title}
|
||||
className="w-full h-[300px] bg-customColor48 hover:bg-customColor2 hover:border-customColor48 hover:border"
|
||||
>
|
||||
<div key={plug.title} className="p-[16px] h-full flex flex-col flex-1">
|
||||
<div className="flex">
|
||||
<div className="text-[20px] mb-[8px] flex-1">{plug.title}</div>
|
||||
{!!data && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Slider
|
||||
value={activated ? 'on' : 'off'}
|
||||
onChange={changeActivated}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">{plug.description}</div>
|
||||
<Button>{!data ? 'Set Plug' : 'Edit Plug'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Plug = () => {
|
||||
const plug = usePlugs();
|
||||
const modals = useModals();
|
||||
const fetch = useFetch();
|
||||
const load = useCallback(async () => {
|
||||
return (await fetch(`/integrations/${plug.providerId}/plugs`)).json();
|
||||
}, [plug.providerId]);
|
||||
|
||||
const { data, isLoading, mutate } = useSWR(`plugs-${plug.providerId}`, load);
|
||||
|
||||
const addEditPlug = useCallback(
|
||||
(p: PlugsInterface) =>
|
||||
(data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
}) => {
|
||||
modals.openModal({
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
withCloseButton: false,
|
||||
onClose() {
|
||||
mutate();
|
||||
},
|
||||
size: '100%',
|
||||
children: (
|
||||
<PlugPop
|
||||
plug={p}
|
||||
data={data}
|
||||
settings={{
|
||||
identifier: plug.identifier,
|
||||
providerId: plug.providerId,
|
||||
name: plug.name,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-[30px]">
|
||||
{plug.plugs.map((p) => (
|
||||
<PlugItem
|
||||
key={p.title + '-' + plug.providerId}
|
||||
addPlug={addEditPlug(p)}
|
||||
plug={p}
|
||||
data={data?.find((a: any) => a.plugFunction === p.methodName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface PlugSettings {
|
||||
providerId: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export interface PlugInterface extends PlugSettings {
|
||||
plugs: PlugsInterface[];
|
||||
}
|
||||
|
||||
export interface FieldsInterface {
|
||||
name: string;
|
||||
type: string;
|
||||
validation: string;
|
||||
placeholder: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface PlugsInterface {
|
||||
title: string;
|
||||
description: string;
|
||||
runEveryMilliseconds: number;
|
||||
methodName: string;
|
||||
fields: FieldsInterface[];
|
||||
}
|
||||
|
||||
export const PlugsContext = createContext<PlugInterface>({
|
||||
providerId: '',
|
||||
name: '',
|
||||
identifier: '',
|
||||
plugs: [
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
runEveryMilliseconds: 0,
|
||||
methodName: '',
|
||||
fields: [{ name: '', type: '', placeholder: '', description: '', validation: '' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const usePlugs = () => useContext(PlugsContext);
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { capitalize, orderBy } from 'lodash';
|
||||
import clsx from 'clsx';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { PlugsContext } from '@gitroom/frontend/components/plugs/plugs.context';
|
||||
import { Plug } from '@gitroom/frontend/components/plugs/plug';
|
||||
|
||||
export const Plugs = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const toaster = useToaster();
|
||||
const load = useCallback(async () => {
|
||||
return (await (await fetch('/integrations/list')).json()).integrations;
|
||||
}, []);
|
||||
|
||||
const load2 = useCallback(async (path: string) => {
|
||||
return await (await fetch(path)).json();
|
||||
}, []);
|
||||
|
||||
const { data: plugList, isLoading: plugLoading } = useSWR(
|
||||
'/integrations/plug/list',
|
||||
load2,
|
||||
{
|
||||
fallbackData: [],
|
||||
}
|
||||
);
|
||||
|
||||
const { data, isLoading } = useSWR('analytics-list', load, {
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
data.filter((integration: any) =>
|
||||
plugList?.plugs?.some(
|
||||
(f: any) => f.identifier === integration.identifier
|
||||
)
|
||||
),
|
||||
// data.filter((integration) => !integration.disabled),
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
);
|
||||
}, [data, plugList]);
|
||||
|
||||
const currentIntegration = useMemo(() => {
|
||||
return sortedIntegrations[current];
|
||||
}, [current, sortedIntegrations]);
|
||||
|
||||
const currentIntegrationPlug = useMemo(() => {
|
||||
const plug = plugList?.plugs?.find(
|
||||
(f: any) => f?.identifier === currentIntegration?.identifier
|
||||
);
|
||||
|
||||
if (!plug) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
providerId: currentIntegration.id,
|
||||
...plug,
|
||||
};
|
||||
}, [currentIntegration, plugList]);
|
||||
|
||||
if (isLoading || plugLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!sortedIntegrations.length && !isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center mt-[100px] gap-[27px] text-center">
|
||||
<div>
|
||||
<img src="/peoplemarketplace.svg" />
|
||||
</div>
|
||||
<div className="text-[48px]">
|
||||
There are not plugs matching your channels
|
||||
<br />
|
||||
You have to add: X or LinkedIn or Threads
|
||||
</div>
|
||||
<Button onClick={() => router.push('/launches')}>
|
||||
Go to the calendar to add channels
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-[30px] flex-1">
|
||||
<div className="p-[16px] bg-customColor48 overflow-hidden flex w-[220px]">
|
||||
<div className="flex gap-[16px] flex-col overflow-hidden">
|
||||
<div className="text-[20px] mb-[8px]">Channels</div>
|
||||
{sortedIntegrations.map((integration, index) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
onClick={() => {
|
||||
if (integration.refreshNeeded) {
|
||||
toaster.show(
|
||||
'Please refresh the integration from the calendar',
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setRefresh(true);
|
||||
setTimeout(() => {
|
||||
setRefresh(false);
|
||||
}, 10);
|
||||
setCurrent(index);
|
||||
}}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
currentIntegration.id !== integration.id &&
|
||||
'opacity-20 hover:opacity-100 cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer">
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full left-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<PlugsContext.Provider value={currentIntegrationPlug}>
|
||||
<Plug />
|
||||
</PlugsContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { StarsController } from './stars.controller';
|
||||
import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database.module";
|
||||
import {TrendingService} from "@gitroom/nestjs-libraries/services/trending.service";
|
||||
import {PostsController} from "@gitroom/workers/app/posts.controller";
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { TrendingService } from '@gitroom/nestjs-libraries/services/trending.service';
|
||||
import { PostsController } from '@gitroom/workers/app/posts.controller';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PlugsController } from '@gitroom/workers/app/plugs.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, BullMqModule],
|
||||
controllers: [...!process.env.IS_GENERAL ? [StarsController] : [], PostsController],
|
||||
controllers: [
|
||||
...(!process.env.IS_GENERAL ? [StarsController] : []),
|
||||
PostsController,
|
||||
PlugsController,
|
||||
],
|
||||
providers: [TrendingService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
|
||||
@Controller()
|
||||
export class PlugsController {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
|
||||
@EventPattern('plugs', Transport.REDIS)
|
||||
async plug(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
return this._integrationService.processPlugs(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ export class PostsController {
|
|||
}
|
||||
|
||||
@EventPattern('submit', Transport.REDIS)
|
||||
async payout(data: { id: string, releaseURL: string }) {
|
||||
async payout(data: { id: string; releaseURL: string }) {
|
||||
return this._postsService.payout(data.id, data.releaseURL);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function Plug(params: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
runEveryMilliseconds: number;
|
||||
totalRuns: number;
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
validation?: RegExp;
|
||||
}[];
|
||||
}) {
|
||||
return function (target: Object, propertyKey: string | symbol, descriptor: any) {
|
||||
// Retrieve existing metadata or initialize an empty array
|
||||
const existingMetadata = Reflect.getMetadata('custom:plug', target) || [];
|
||||
|
||||
// Add the metadata information for this method
|
||||
existingMetadata.push({ methodName: propertyKey, ...params });
|
||||
|
||||
// Define metadata on the class prototype (so it can be retrieved from the class)
|
||||
Reflect.defineMetadata('custom:plug', existingMetadata, target);
|
||||
};
|
||||
}
|
||||
|
|
@ -40,8 +40,8 @@ export class BullMqClient extends ClientProxy {
|
|||
const job = await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: !packet.data.options.attempts,
|
||||
removeOnFail: !packet.data.options.attempts,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ import { Integration } from '@prisma/client';
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationRepository {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _integration: PrismaRepository<'integration'>,
|
||||
private _customers: PrismaRepository<'customer'>,
|
||||
private _posts: PrismaRepository<'post'>
|
||||
private _posts: PrismaRepository<'post'>,
|
||||
private _plugs: PrismaRepository<'plugs'>,
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
|
||||
private _customers: PrismaRepository<'customer'>
|
||||
) {}
|
||||
|
||||
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
|
||||
|
|
@ -30,6 +33,35 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getPlug(plugId: string) {
|
||||
return this._plugs.model.plugs.findFirst({
|
||||
where: {
|
||||
id: plugId,
|
||||
},
|
||||
include: {
|
||||
integration: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._plugs.model.plugs.findMany({
|
||||
where: {
|
||||
integrationId,
|
||||
organizationId: orgId,
|
||||
activated: true,
|
||||
},
|
||||
include: {
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateIntegration(id: string, params: Partial<Integration>) {
|
||||
if (
|
||||
params.picture &&
|
||||
|
|
@ -378,4 +410,80 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
getPlugsByIntegrationId(org: string, id: string) {
|
||||
return this._plugs.model.plugs.findMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
integrationId: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createOrUpdatePlug(org: string, integrationId: string, body: PlugDto) {
|
||||
return this._plugs.model.plugs.upsert({
|
||||
where: {
|
||||
organizationId: org,
|
||||
plugFunction_integrationId: {
|
||||
integrationId,
|
||||
plugFunction: body.func,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
integrationId,
|
||||
organizationId: org,
|
||||
plugFunction: body.func,
|
||||
data: JSON.stringify(body.fields),
|
||||
activated: true,
|
||||
},
|
||||
update: {
|
||||
data: JSON.stringify(body.fields),
|
||||
},
|
||||
select: {
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
changePlugActivation(orgId: string, plugId: string, status: boolean) {
|
||||
return this._plugs.model.plugs.update({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
id: plugId,
|
||||
},
|
||||
data: {
|
||||
activated: !!status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) {
|
||||
return this._exisingPlugData.model.exisingPlugData.findMany({
|
||||
where: {
|
||||
integrationId,
|
||||
methodName,
|
||||
value: {
|
||||
in: id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async saveExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
value: string[]
|
||||
) {
|
||||
return this._exisingPlugData.model.exisingPlugData.createMany({
|
||||
data: value.map((p) => ({
|
||||
integrationId,
|
||||
methodName,
|
||||
value: p,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
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';
|
||||
import { AnalyticsData, SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import {
|
||||
AnalyticsData,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { Integration, Organization } from '@prisma/client';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider';
|
||||
|
|
@ -17,6 +16,9 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
|||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
|
|
@ -24,10 +26,15 @@ export class IntegrationService {
|
|||
constructor(
|
||||
private _integrationRepository: IntegrationRepository,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService
|
||||
private _notificationService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
|
||||
async setTimes(orgId: string, integrationId: string, times: IntegrationTimeDto) {
|
||||
async setTimes(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
times: IntegrationTimeDto
|
||||
) {
|
||||
return this._integrationRepository.setTimes(orgId, integrationId, times);
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +54,9 @@ export class IntegrationService {
|
|||
timezone?: number,
|
||||
customInstanceDetails?: string
|
||||
) {
|
||||
const uploadedPicture = picture ? await this.storage.uploadSimple(picture) : undefined;
|
||||
const uploadedPicture = picture
|
||||
? await this.storage.uploadSimple(picture)
|
||||
: undefined;
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
org,
|
||||
name,
|
||||
|
|
@ -295,7 +304,12 @@ export class IntegrationService {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
async checkAnalytics(org: Organization, integration: string, date: string, forceRefresh = false): Promise<AnalyticsData[]> {
|
||||
async checkAnalytics(
|
||||
org: Organization,
|
||||
integration: string,
|
||||
date: string,
|
||||
forceRefresh = false
|
||||
): Promise<AnalyticsData[]> {
|
||||
const getIntegration = await this.getIntegrationById(org.id, integration);
|
||||
|
||||
if (!getIntegration) {
|
||||
|
|
@ -310,7 +324,10 @@ export class IntegrationService {
|
|||
getIntegration.providerIdentifier
|
||||
);
|
||||
|
||||
if (dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) {
|
||||
if (
|
||||
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
|
||||
forceRefresh
|
||||
) {
|
||||
const { accessToken, expiresIn, refreshToken } =
|
||||
await integrationProvider.refreshToken(getIntegration.refreshToken!);
|
||||
|
||||
|
|
@ -374,4 +391,113 @@ export class IntegrationService {
|
|||
customers(orgId: string) {
|
||||
return this._integrationRepository.customers(orgId);
|
||||
}
|
||||
|
||||
getPlugsByIntegrationId(org: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugsByIntegrationId(
|
||||
org,
|
||||
integrationId
|
||||
);
|
||||
}
|
||||
|
||||
async processPlugs(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
|
||||
if (!getPlugById) {
|
||||
return ;
|
||||
}
|
||||
|
||||
const integration = this._integrationManager.getSocialIntegration(
|
||||
getPlugById.integration.providerIdentifier
|
||||
);
|
||||
|
||||
const findPlug = this._integrationManager
|
||||
.getAllPlugs()
|
||||
.find(
|
||||
(p) => p.identifier === getPlugById.integration.providerIdentifier
|
||||
)!;
|
||||
|
||||
console.log(data.postId);
|
||||
|
||||
// @ts-ignore
|
||||
const process = await integration[getPlugById.plugFunction](
|
||||
getPlugById.integration,
|
||||
data.postId,
|
||||
JSON.parse(getPlugById.data).reduce((all: any, current: any) => {
|
||||
all[current.name] = current.value;
|
||||
return all;
|
||||
}, {})
|
||||
);
|
||||
|
||||
if (process) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (data.totalRuns === data.currentRun) {
|
||||
return ;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + data.postId + '_' + findPlug.identifier,
|
||||
options: {
|
||||
delay: 0, // runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
plugId: data.plugId,
|
||||
postId: data.postId,
|
||||
delay: data.delay,
|
||||
totalRuns: data.totalRuns,
|
||||
currentRun: data.currentRun + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdatePlug(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
body: PlugDto
|
||||
) {
|
||||
const { activated } = await this._integrationRepository.createOrUpdatePlug(
|
||||
orgId,
|
||||
integrationId,
|
||||
body
|
||||
);
|
||||
|
||||
return {
|
||||
activated,
|
||||
};
|
||||
}
|
||||
|
||||
async changePlugActivation(orgId: string, plugId: string, status: boolean) {
|
||||
const { id, integrationId, plugFunction } =
|
||||
await this._integrationRepository.changePlugActivation(
|
||||
orgId,
|
||||
plugId,
|
||||
status
|
||||
);
|
||||
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugs(orgId, integrationId);
|
||||
}
|
||||
|
||||
async loadExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) {
|
||||
const exisingData = await this._integrationRepository.loadExisingData(
|
||||
methodName,
|
||||
integrationId,
|
||||
id
|
||||
);
|
||||
const loadOnlyIds = exisingData.map((p) => p.value);
|
||||
return difference(id, loadOnlyIds);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,13 @@ export class PostsService {
|
|||
true
|
||||
);
|
||||
|
||||
await this.checkPlugs(
|
||||
integration.organizationId,
|
||||
getIntegration.identifier,
|
||||
integration.id,
|
||||
publishedPosts[0].postId
|
||||
);
|
||||
|
||||
return {
|
||||
postId: publishedPosts[0].postId,
|
||||
releaseURL: publishedPosts[0].releaseURL,
|
||||
|
|
@ -312,6 +319,42 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
|
||||
private async checkPlugs(
|
||||
orgId: string,
|
||||
providerName: string,
|
||||
integrationId: string,
|
||||
postId: string
|
||||
) {
|
||||
const loadAllPlugs = this._integrationManager.getAllPlugs();
|
||||
const getPlugs = await this._integrationService.getPlugs(
|
||||
orgId,
|
||||
integrationId
|
||||
);
|
||||
|
||||
const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
|
||||
|
||||
for (const plug of getPlugs) {
|
||||
const runPlug = currentPlug?.plugs?.find((p: any) => p.methodName === plug.plugFunction)!;
|
||||
if (!runPlug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + postId + '_' + runPlug.identifier,
|
||||
options: {
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
plugId: plug.id,
|
||||
postId,
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
totalRuns: runPlug.totalRuns,
|
||||
currentRun: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async postArticle(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getArticlesIntegration(
|
||||
integration.providerIdentifier
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ model Organization {
|
|||
buyerOrganization MessagesGroup[]
|
||||
usedCodes UsedCodes[]
|
||||
credits Credits[]
|
||||
plugs Plugs[]
|
||||
customers Customer[]
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +281,8 @@ model Integration {
|
|||
customInstanceDetails String?
|
||||
customerId String?
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
plugs Plugs[]
|
||||
exisingPlugData ExisingPlugData[]
|
||||
|
||||
@@index([updatedAt])
|
||||
@@index([deletedAt])
|
||||
|
|
@ -455,6 +458,30 @@ model Messages {
|
|||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Plugs {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
plugFunction String
|
||||
data String
|
||||
integrationId String
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
activated Boolean @default(true)
|
||||
|
||||
@@unique([plugFunction, integrationId])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model ExisingPlugData {
|
||||
id String @id @default(uuid())
|
||||
integrationId String
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
methodName String
|
||||
value String
|
||||
|
||||
@@unique([integrationId, methodName, value])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { IsDefined, IsString, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FieldsDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class PlugDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
func: string;
|
||||
|
||||
@Type(() => FieldsDto)
|
||||
@ValidateNested({ each: true })
|
||||
@IsDefined()
|
||||
fields: FieldsDto[];
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
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';
|
||||
|
|
@ -64,6 +66,27 @@ export class IntegrationManager {
|
|||
})),
|
||||
};
|
||||
}
|
||||
|
||||
getAllPlugs() {
|
||||
return socialIntegrationList
|
||||
.map((p) => {
|
||||
return {
|
||||
name: p.name,
|
||||
identifier: p.identifier,
|
||||
plugs: (
|
||||
Reflect.getMetadata('custom:plug', p.constructor.prototype) || []
|
||||
).map((p: any) => ({
|
||||
...p,
|
||||
fields: p.fields.map((c: any) => ({
|
||||
...c,
|
||||
validation: c?.validation?.toString(),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter((f) => f.plugs.length);
|
||||
}
|
||||
|
||||
getAllowedSocialsIntegrations() {
|
||||
return socialIntegrationList.map((p) => p.identifier);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import dayjs from 'dayjs';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import sharp from 'sharp';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'bluesky';
|
||||
|
|
@ -116,7 +118,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let loadCid = '';
|
||||
let loadUri = '';
|
||||
const cidUrl = [] as { cid: string; url: string, rev: string }[];
|
||||
const cidUrl = [] as { cid: string; url: string; rev: string }[];
|
||||
for (const post of postDetails) {
|
||||
const images = await Promise.all(
|
||||
post.media?.map(async (p) => {
|
||||
|
|
@ -134,9 +136,9 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
const rt = new RichText({
|
||||
text: post.message,
|
||||
})
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent)
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
|
|
@ -179,9 +181,143 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
return postDetails.map((p, index) => ({
|
||||
id: p.id,
|
||||
postId: cidUrl[index].cid,
|
||||
postId: cidUrl[index].url,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url.split('/').pop()}`,
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url
|
||||
.split('/')
|
||||
.pop()}`,
|
||||
}));
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await agent.repost(
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.cid
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
const rt = new RichText({
|
||||
text: fields.post,
|
||||
});
|
||||
|
||||
await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
reply: {
|
||||
root: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
parent: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
|
|
@ -97,7 +99,7 @@ export class LinkedinPageProvider
|
|||
}
|
||||
|
||||
async companies(accessToken: string) {
|
||||
const { elements } = await (
|
||||
const { elements, ...all } = await (
|
||||
await fetch(
|
||||
'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(localizedName,vanityName,logoV2(original~:playableStreams))))',
|
||||
{
|
||||
|
|
@ -360,6 +362,150 @@ export class LinkedinPageProvider
|
|||
percentageChange: 5,
|
||||
}));
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'linkedin-page-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
if (totalLikes >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:organization:${integration.internalId}`,
|
||||
commentary: '',
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
targetEntities: [],
|
||||
thirdPartyDistributionChannels: [],
|
||||
},
|
||||
lifecycleState: 'PUBLISHED',
|
||||
isReshareDisabledByAuthor: false,
|
||||
reshareContext: {
|
||||
parent: id,
|
||||
},
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'linkedin-page-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
if (totalLikes >= fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${decodeURIComponent(
|
||||
id
|
||||
)}/comments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
actor: `urn:li:organization:${integration.internalId}`,
|
||||
object: id,
|
||||
message: {
|
||||
text: this.fixText(fields.post)
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Root {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
SocialAbstract,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
|
|
@ -22,6 +21,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
scopes = ['openid', 'profile', 'w_member_social', 'r_basicprofile'];
|
||||
refreshWait = true;
|
||||
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const {
|
||||
access_token: accessToken,
|
||||
|
|
@ -282,7 +282,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
private fixText(text: string) {
|
||||
protected fixText(text: string) {
|
||||
const pattern = /@\[.+?]\(urn:li:organization.+?\)/g;
|
||||
const matches = text.match(pattern) || [];
|
||||
const splitAll = text.split(pattern);
|
||||
|
|
@ -431,7 +431,9 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
? `urn:li:person:${id}`
|
||||
: `urn:li:organization:${id}`,
|
||||
object: topPostId,
|
||||
message: this.fixText(post.message),
|
||||
message: {
|
||||
text: this.fixText(post.message)
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { capitalize, chunk } from 'lodash';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'threads';
|
||||
|
|
@ -152,7 +154,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let globalThread = '';
|
||||
let link = '';
|
||||
|
||||
if (firstPost?.media?.length! <= 1) {
|
||||
const type = !firstPost?.media?.[0]?.url
|
||||
? undefined
|
||||
|
|
@ -345,4 +346,73 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'threads-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/insights?metric=likes&access_token=${integration.token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const {
|
||||
values: [value],
|
||||
} = data.find((p: any) => p.name === 'likes');
|
||||
|
||||
if (value.value >= fields.likesAmount) {
|
||||
await timer(2000);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('media_type', 'TEXT');
|
||||
form.append('text', fields.post);
|
||||
form.append('reply_to_id', id);
|
||||
form.append('access_token', integration.token);
|
||||
|
||||
const { id: replyId } = await (
|
||||
await this.fetch('https://graph.threads.net/v1.0/me/threads', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${integration.internalId}/threads_publish?creation_id=${replyId}&access_token=${integration.token}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ 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';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class XProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'x';
|
||||
|
|
@ -17,10 +20,112 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
await client.v2.retweet(integration.internalId, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
|
||||
await client.v2.tweet({
|
||||
text: removeMd(fields.post.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace(
|
||||
'𝔫𝔢𝔴𝔩𝔦𝔫𝔢',
|
||||
'\n'
|
||||
),
|
||||
reply: { in_reply_to_tweet_id: id },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID! || process.env.X_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET! || process.env.X_CLIENT_SECRET!,
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
});
|
||||
const {
|
||||
accessToken,
|
||||
|
|
|
|||
Loading…
Reference in New Issue