diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts
index a394fe51..a6ea63ef 100644
--- a/apps/backend/src/api/routes/integrations.controller.ts
+++ b/apps/backend/src/api/routes/integrations.controller.ts
@@ -1,5 +1,5 @@
import {
- Body, Controller, Delete, Get, Param, Post, 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';
@@ -23,6 +23,7 @@ import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { NotEnoughScopes } from '@gitroom/nestjs-libraries/integrations/social.abstract';
+import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
@ApiTags('Integrations')
@Controller('/integrations')
@@ -352,7 +353,9 @@ export class IntegrationsController {
}
if (refresh && id !== refresh) {
- throw new NotEnoughScopes('Please refresh the channel that needs to be refreshed');
+ throw new NotEnoughScopes(
+ 'Please refresh the channel that needs to be refreshed'
+ );
}
return this._integrationService.createOrUpdateIntegration(
@@ -444,4 +447,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);
+ }
}
diff --git a/apps/frontend/src/app/(site)/plugs/page.tsx b/apps/frontend/src/app/(site)/plugs/page.tsx
new file mode 100644
index 00000000..64a417e3
--- /dev/null
+++ b/apps/frontend/src/app/(site)/plugs/page.tsx
@@ -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 (
+ <>
+
+ >
+ );
+}
diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx
index 29666236..39b75cb6 100644
--- a/apps/frontend/src/components/launches/launches.component.tsx
+++ b/apps/frontend/src/components/launches/launches.component.tsx
@@ -31,6 +31,7 @@ export const LaunchesComponent = () => {
const load = useCallback(async (path: string) => {
return (await (await fetch(path)).json()).integrations;
}, []);
+
const user = useUser();
const {
@@ -132,7 +133,10 @@ export const LaunchesComponent = () => {
'Channel disconnected, click to reconnect.',
})}
key={integration.id}
- className={clsx("flex gap-[8px] items-center", integration.refreshNeeded && 'cursor-pointer')}
+ className={clsx(
+ 'flex gap-[8px] items-center',
+ integration.refreshNeeded && 'cursor-pointer'
+ )}
>
{
- const {isGeneral} = useVariables();
+ const { isGeneral } = useVariables();
return [
...(!isGeneral
? [
- {
- name: 'Analytics',
- icon: 'analytics',
- path: '/analytics',
- },
- ]
+ {
+ name: 'Analytics',
+ icon: 'analytics',
+ path: '/analytics',
+ },
+ ]
: []),
{
name: isGeneral ? 'Calendar' : 'Launches',
@@ -26,32 +26,27 @@ export const useMenuItems = () => {
},
...(isGeneral
? [
- {
- name: 'Analytics',
- icon: 'analytics',
- path: '/analytics',
- },
- ]
+ {
+ name: 'Analytics',
+ icon: 'analytics',
+ path: '/analytics',
+ },
+ ]
: []),
...(!isGeneral
? [
- {
- name: 'Settings',
- icon: 'settings',
- path: '/settings',
- role: ['ADMIN', 'SUPERADMIN'],
- },
- ]
+ {
+ name: 'Settings',
+ icon: 'settings',
+ path: '/settings',
+ role: ['ADMIN', 'SUPERADMIN'],
+ },
+ ]
: []),
{
- name: 'Marketplace',
- icon: 'marketplace',
- path: '/marketplace',
- },
- {
- name: 'Messages',
- icon: 'messages',
- path: '/messages',
+ name: 'Plugs',
+ icon: 'plugs',
+ path: '/plugs',
},
{
name: 'Billing',
@@ -61,12 +56,12 @@ export const useMenuItems = () => {
requireBilling: true,
},
];
-}
+};
export const TopMenu: FC = () => {
const path = usePathname();
const user = useUser();
- const {billingEnabled} = useVariables();
+ const { billingEnabled } = useVariables();
const menuItems = useMenuItems();
return (
diff --git a/apps/frontend/src/components/plugs/plug.tsx b/apps/frontend/src/components/plugs/plug.tsx
new file mode 100644
index 00000000..8982f3a9
--- /dev/null
+++ b/apps/frontend/src/components/plugs/plug.tsx
@@ -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 (
+ <>
+
+
{
+ onChange({ target: { name: props.name, value: e.target.value } });
+ }}
+ autosuggestionsConfig={{
+ textareaPurpose: `Assist me in writing social media posts.`,
+ chatApiConfigs: {},
+ }}
+ />
+
+ {form?.formState?.errors?.[props.name]?.message as string}
+
+ >
+ );
+};
+
+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 = 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 (
+
+
+
+ );
+};
+
+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 (
+ addPlug(data)}
+ key={plug.title}
+ className="w-full h-[300px] bg-customColor48 hover:bg-customColor2 hover:border-customColor48 hover:border"
+ >
+
+
+
{plug.title}
+ {!!data && (
+
e.stopPropagation()}>
+
+
+ )}
+
+
{plug.description}
+
+
+
+ );
+};
+
+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: (
+
+ ),
+ });
+ },
+ [data]
+ );
+
+ if (isLoading) {
+ return null;
+ }
+
+ return (
+
+ {plug.plugs.map((p) => (
+
a.plugFunction === p.methodName)}
+ />
+ ))}
+
+ );
+};
diff --git a/apps/frontend/src/components/plugs/plugs.context.ts b/apps/frontend/src/components/plugs/plugs.context.ts
new file mode 100644
index 00000000..2b9bb6a0
--- /dev/null
+++ b/apps/frontend/src/components/plugs/plugs.context.ts
@@ -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({
+ providerId: '',
+ name: '',
+ identifier: '',
+ plugs: [
+ {
+ title: '',
+ description: '',
+ runEveryMilliseconds: 0,
+ methodName: '',
+ fields: [{ name: '', type: '', placeholder: '', description: '', validation: '' }],
+ },
+ ],
+});
+
+export const usePlugs = () => useContext(PlugsContext);
diff --git a/apps/frontend/src/components/plugs/plugs.tsx b/apps/frontend/src/components/plugs/plugs.tsx
new file mode 100644
index 00000000..c399a723
--- /dev/null
+++ b/apps/frontend/src/components/plugs/plugs.tsx
@@ -0,0 +1,174 @@
+'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]);
+
+ console.log(currentIntegrationPlug);
+ if (isLoading || plugLoading) {
+ return null;
+ }
+
+ if (!sortedIntegrations.length && !isLoading) {
+ return (
+
+
+

+
+
+ Can{"'"}t show analytics yet
+
+ You have to add Social Media channels
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
Channels
+ {sortedIntegrations.map((integration, index) => (
+
{
+ 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'
+ )}
+ >
+
+ {(integration.inBetweenSteps || integration.refreshNeeded) && (
+
+ )}
+
+
+
+
+ {integration.name}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/apps/workers/src/app/app.module.ts b/apps/workers/src/app/app.module.ts
index 20d2e381..d008584a 100644
--- a/apps/workers/src/app/app.module.ts
+++ b/apps/workers/src/app/app.module.ts
@@ -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 {}
diff --git a/apps/workers/src/app/plugs.controller.ts b/apps/workers/src/app/plugs.controller.ts
new file mode 100644
index 00000000..a91916d5
--- /dev/null
+++ b/apps/workers/src/app/plugs.controller.ts
@@ -0,0 +1,52 @@
+import { Controller } from '@nestjs/common';
+import { EventPattern, Transport } from '@nestjs/microservices';
+import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
+import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
+
+@Controller()
+export class PlugsController {
+ constructor(
+ private _workerServiceProducer: BullMqClient,
+ private _integrationService: IntegrationService
+ ) {}
+
+ @EventPattern('plugs', Transport.REDIS)
+ async plug(data: {
+ orgId: string;
+ integrationId: string;
+ funcName: string;
+ retry: number;
+ delay: number;
+ }) {
+ try {
+ await this._integrationService.startPlug(data);
+
+ if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') {
+ return this._workerServiceProducer.emit('plugs', {
+ id: data.integrationId + '-' + data.funcName,
+ options: {
+ delay: 6000, // delay,
+ },
+ payload: {
+ ...data,
+ retry: data.retry,
+ },
+ });
+ }
+ } catch (e) {
+ if (data.retry > 3) {
+ return;
+ }
+ return this._workerServiceProducer.emit('plugs', {
+ id: data.integrationId + '-' + data.funcName,
+ options: {
+ delay: data.delay, // delay,
+ },
+ payload: {
+ ...data,
+ retry: data.retry + 1,
+ },
+ });
+ }
+ }
+}
diff --git a/apps/workers/src/app/posts.controller.ts b/apps/workers/src/app/posts.controller.ts
index a0232ddd..a8c1aa9f 100644
--- a/apps/workers/src/app/posts.controller.ts
+++ b/apps/workers/src/app/posts.controller.ts
@@ -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);
}
}
diff --git a/libraries/helpers/src/decorators/plug.decorator.ts b/libraries/helpers/src/decorators/plug.decorator.ts
new file mode 100644
index 00000000..78900344
--- /dev/null
+++ b/libraries/helpers/src/decorators/plug.decorator.ts
@@ -0,0 +1,25 @@
+import 'reflect-metadata';
+
+export function Plug(params: {
+ title: string;
+ description: string;
+ runEveryMilliseconds: number;
+ fields: {
+ name: string;
+ description: string;
+ type: string;
+ placeholder: string;
+ validation?: RegExp;
+ }[];
+}) {
+ return function (target: Object, propertyKey: string | symbol) {
+ // 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);
+ };
+}
diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts
index 2aed109f..eddde7cd 100644
--- a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts
+++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts
@@ -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 {
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
index 72a9c8db..ab8e4c93 100644
--- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
@@ -5,13 +5,15 @@ 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 _posts: PrismaRepository<'post'>
+ private _posts: PrismaRepository<'post'>,
+ private _plugs: PrismaRepository<'plugs'>
) {}
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
@@ -310,4 +312,50 @@ 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,
+ },
+ });
+ }
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
index 8ac2aba5..84e42bda 100644
--- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
@@ -1,27 +1,23 @@
-import {
- HttpException,
- HttpStatus,
- Injectable,
- Param,
- Query,
-} 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';
-import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
-import axios from 'axios';
-import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
import { timer } from '@gitroom/helpers/utils/timer';
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';
@Injectable()
export class IntegrationService {
@@ -29,10 +25,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);
}
@@ -292,7 +293,12 @@ export class IntegrationService {
return { success: true };
}
- async checkAnalytics(org: Organization, integration: string, date: string, forceRefresh = false): Promise {
+ async checkAnalytics(
+ org: Organization,
+ integration: string,
+ date: string,
+ forceRefresh = false
+ ): Promise {
const getIntegration = await this.getIntegrationById(org.id, integration);
if (!getIntegration) {
@@ -307,7 +313,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!);
@@ -367,4 +376,133 @@ export class IntegrationService {
return [];
}
+
+ getPlugsByIntegrationId(org: string, integrationId: string) {
+ return this._integrationRepository.getPlugsByIntegrationId(
+ org,
+ integrationId
+ );
+ }
+
+ processPlugs(
+ orgId: string,
+ integrationId: string,
+ delay: number,
+ funcName: string
+ ) {
+ return this._workerServiceProducer.emit('plugs', {
+ id: integrationId + '-' + funcName,
+ options: {
+ delay: 0, // delay,
+ },
+ payload: {
+ retry: 1,
+ delay,
+ orgId,
+ integrationId: integrationId,
+ funcName: funcName,
+ },
+ });
+ }
+
+ async activatedPlug(
+ orgId: string,
+ integrationId: string,
+ funcName: string,
+ type: 'add' | 'remove'
+ ) {
+ const loadIntegration = await this.getIntegrationById(orgId, integrationId);
+ const allPlugs = this._integrationManager.getAllPlugs();
+ const findPlug = allPlugs.find(
+ (p) => p.identifier === loadIntegration?.providerIdentifier!
+ )!;
+ const plug = findPlug.plugs.find((p: any) => p.methodName === funcName)!;
+
+ if (type === 'add') {
+ return this.processPlugs(
+ orgId,
+ integrationId,
+ plug.runEveryMilliseconds,
+ funcName
+ );
+ } else {
+ console.log(integrationId + '-' + funcName);
+ return this._workerServiceProducer.delete(
+ 'plugs',
+ integrationId + '-' + funcName
+ );
+ }
+ }
+
+ async createOrUpdatePlug(
+ orgId: string,
+ integrationId: string,
+ body: PlugDto
+ ) {
+ const { activated } = await this._integrationRepository.createOrUpdatePlug(
+ orgId,
+ integrationId,
+ body
+ );
+
+ if (activated) {
+ await this.activatedPlug(orgId, integrationId, body.func, 'add');
+ }
+ }
+
+ async changePlugActivation(orgId: string, plugId: string, status: boolean) {
+ const { id, integrationId, plugFunction } =
+ await this._integrationRepository.changePlugActivation(
+ orgId,
+ plugId,
+ status
+ );
+
+ if (status) {
+ await this.activatedPlug(orgId, integrationId, plugFunction, 'add');
+ } else {
+ await this.activatedPlug(orgId, integrationId, plugFunction, 'remove');
+ }
+
+ return { id };
+ }
+
+ async startPlug(data: {
+ orgId: string;
+ integrationId: string;
+ funcName: string;
+ retry: number;
+ delay: number;
+ }) {
+ const integration = await this.getIntegrationById(
+ data.orgId,
+ data.integrationId
+ );
+
+ if (!integration) {
+ return;
+ }
+
+ const plugInformation = (
+ await this._integrationRepository.getPlugsByIntegrationId(
+ data.orgId,
+ data.integrationId
+ )
+ ).find((p) => p.plugFunction === data.funcName)!;
+
+ const plugData = JSON.parse(plugInformation.data).reduce(
+ (all: any, current: any) => ({
+ ...all,
+ [current.name]: current.value,
+ }),
+ {}
+ );
+
+ const integrationInstance = this._integrationManager.getSocialIntegration(
+ integration.providerIdentifier
+ );
+
+ // @ts-ignore
+ return integrationInstance[data.funcName](integration, plugData);
+ }
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index a2858330..214090d5 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -29,6 +29,7 @@ model Organization {
buyerOrganization MessagesGroup[]
usedCodes UsedCodes[]
credits Credits[]
+ plugs Plugs[]
}
model User {
@@ -264,6 +265,7 @@ model Integration {
refreshNeeded Boolean @default(false)
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
customInstanceDetails String?
+ plugs Plugs[]
@@index([updatedAt])
@@index([deletedAt])
@@ -439,6 +441,20 @@ 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])
+}
+
enum OrderStatus {
PENDING
ACCEPTED
diff --git a/libraries/nestjs-libraries/src/dtos/plugs/plug.dto.ts b/libraries/nestjs-libraries/src/dtos/plugs/plug.dto.ts
new file mode 100644
index 00000000..50af4354
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/plugs/plug.dto.ts
@@ -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[];
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
index e4e951d8..09756576 100644
--- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts
+++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
@@ -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);
}
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
index 2bd66365..f6553c51 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
@@ -9,6 +9,7 @@ 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';
export class LinkedinPageProvider
extends LinkedinProvider
@@ -97,17 +98,20 @@ 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))))',
{
headers: {
Authorization: `Bearer ${accessToken}`,
+ 'X-Restli-Protocol-Version': '2.0.0',
+ 'LinkedIn-Version': '202402',
},
}
)
).json();
+ console.log(all);
return (elements || []).map((e: any) => ({
id: e.organizationalTarget.split(':').pop(),
page: e.organizationalTarget.split(':').pop(),
@@ -124,7 +128,10 @@ export class LinkedinPageProvider
requiredId: string,
accessToken: string
): Promise {
- const information = await this.fetchPageInformation(accessToken, requiredId);
+ const information = await this.fetchPageInformation(
+ accessToken,
+ requiredId
+ );
return {
id: information.id,
@@ -355,6 +362,41 @@ export class LinkedinPageProvider
percentageChange: 5,
}));
}
+
+ @Plug({
+ title: 'Auto Repost Posts',
+ description:
+ 'When a post reached a certain number of likes, repost it to increase engagement',
+ runEveryMilliseconds: 7200000,
+ fields: [
+ {
+ name: 'likesAmount',
+ type: 'number',
+ placeholder: 'Amount of likes',
+ description: 'The amount of likes to trigger the post',
+ validation: /^\d+$/,
+ },
+ ],
+ })
+ async autoAddPost(integration: Integration, fields: { likesAmount: number }) {
+ const a = await fetch(
+ `https://api.linkedin.com/rest/posts?author=${encodeURIComponent(
+ `urn:li:organization:${integration.internalId}`
+ )}&q=author&count=10&sortBy=LAST_MODIFIED`,
+ {
+ method: 'GET',
+ headers: {
+ 'X-Restli-Protocol-Version': '2.0.0',
+ 'Content-Type': 'application/json',
+ 'LinkedIn-Version': '202402',
+ Authorization: `Bearer ${integration.token}`,
+ },
+ }
+ );
+
+ console.log(await a.json());
+ return;
+ }
}
export interface Root {
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
index 441e9084..ab4d3b0a 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
@@ -14,6 +14,8 @@ import {
SocialAbstract,
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { Integration } from '@prisma/client';
+import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
+import { string } from 'yup';
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
identifier = 'linkedin';
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index 8bc9f0ac..d78548f1 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -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 { string } from 'yup';
+import { Integration } from '@prisma/client';
export class XProvider extends SocialAbstract implements SocialProvider {
identifier = 'x';