From 0c1d854e78066b443dba7d074830e535118f9c14 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 22 Jun 2025 00:06:34 +0700 Subject: [PATCH] feat: integrations --- apps/backend/src/api/api.module.ts | 2 + .../src/api/routes/posts.controller.ts | 2 +- .../src/api/routes/third-party.controller.ts | 160 ++++++++++++ apps/backend/src/app.module.ts | 2 + .../public/icons/third-party/heygen.png | Bin 0 -> 7504 bytes .../src/app/(app)/(site)/third-party/page.tsx | 14 ++ .../components/launches/add.edit.model.tsx | 2 +- .../providers/high.order.provider.tsx | 2 +- .../launches/providers/reddit/subreddit.tsx | 1 + .../src/components/layout/top.menu.tsx | 7 +- .../src/components/media/media.component.tsx | 13 +- .../providers/heygen.provider.tsx | 232 +++++++++++++++++ .../third-parties/third-party.component.tsx | 165 +++++++++++++ .../third-parties/third-party.function.tsx | 88 +++++++ .../third-party.list.component.tsx | 164 ++++++++++++ .../third-parties/third-party.media.tsx | 233 ++++++++++++++++++ .../third-parties/third-party.wrapper.tsx | 16 ++ .../src/3rdparties/heygen/heygen.provider.ts | 176 +++++++++++++ .../src/3rdparties/thirdparty.interface.ts | 40 +++ .../src/3rdparties/thirdparty.manager.ts | 62 +++++ .../src/3rdparties/thirdparty.module.ts | 12 + .../src/database/prisma/database.module.ts | 4 + .../src/database/prisma/schema.prisma | 18 ++ .../third-party/third-party.repository.ts | 64 +++++ .../prisma/third-party/third-party.service.ts | 28 +++ .../integrations/social/farcaster.provider.ts | 2 +- .../src/openai/openai.service.ts | 29 ++- .../translation/locales/en/translation.json | 5 +- 28 files changed, 1535 insertions(+), 8 deletions(-) create mode 100644 apps/backend/src/api/routes/third-party.controller.ts create mode 100644 apps/frontend/public/icons/third-party/heygen.png create mode 100644 apps/frontend/src/app/(app)/(site)/third-party/page.tsx create mode 100644 apps/frontend/src/components/third-parties/providers/heygen.provider.tsx create mode 100644 apps/frontend/src/components/third-parties/third-party.component.tsx create mode 100644 apps/frontend/src/components/third-parties/third-party.function.tsx create mode 100644 apps/frontend/src/components/third-parties/third-party.list.component.tsx create mode 100644 apps/frontend/src/components/third-parties/third-party.media.tsx create mode 100644 apps/frontend/src/components/third-parties/third-party.wrapper.tsx create mode 100644 libraries/nestjs-libraries/src/3rdparties/heygen/heygen.provider.ts create mode 100644 libraries/nestjs-libraries/src/3rdparties/thirdparty.interface.ts create mode 100644 libraries/nestjs-libraries/src/3rdparties/thirdparty.manager.ts create mode 100644 libraries/nestjs-libraries/src/3rdparties/thirdparty.module.ts create mode 100644 libraries/nestjs-libraries/src/database/prisma/third-party/third-party.repository.ts create mode 100644 libraries/nestjs-libraries/src/database/prisma/third-party/third-party.service.ts diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index aef9a805..e1d47314 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -34,6 +34,7 @@ import { AutopostController } from '@gitroom/backend/api/routes/autopost.control import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service'; import { McpController } from '@gitroom/backend/api/routes/mcp.controller'; import { SetsController } from '@gitroom/backend/api/routes/sets.controller'; +import { ThirdPartyController } from '@gitroom/backend/api/routes/third-party.controller'; const authenticatedController = [ UsersController, @@ -52,6 +53,7 @@ const authenticatedController = [ SignatureController, AutopostController, SetsController, + ThirdPartyController, ]; @Module({ imports: [UploadModule], diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index 7e91f4fe..c07f3b5e 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -38,7 +38,7 @@ export class PostsController { private _starsService: StarsService, private _messagesService: MessagesService, private _agentGraphService: AgentGraphService, - private _shortLinkService: ShortLinkService + private _shortLinkService: ShortLinkService, ) {} @Get('/:id/statistics') diff --git a/apps/backend/src/api/routes/third-party.controller.ts b/apps/backend/src/api/routes/third-party.controller.ts new file mode 100644 index 00000000..26f5b794 --- /dev/null +++ b/apps/backend/src/api/routes/third-party.controller.ts @@ -0,0 +1,160 @@ +import { + Body, + Controller, + Get, + HttpException, + Param, + Post, + Delete, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ThirdPartyManager } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.manager'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { Organization } from '@prisma/client'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service'; + +@ApiTags('Third Party') +@Controller('/third-party') +export class ThirdPartyController { + private storage = UploadFactory.createStorage(); + + constructor( + private _thirdPartyManager: ThirdPartyManager, + private _mediaService: MediaService, + ) {} + + @Get('/list') + async getThirdPartyList() { + return this._thirdPartyManager.getAllThirdParties(); + } + + @Get('/') + async getSavedThirdParty(@GetOrgFromRequest() organization: Organization) { + return Promise.all( + ( + await this._thirdPartyManager.getAllThirdPartiesByOrganization( + organization.id + ) + ).map((thirdParty) => { + const { description, fields, position, title, identifier } = + this._thirdPartyManager.getThirdPartyByName(thirdParty.identifier); + return { + ...thirdParty, + title, + position, + fields, + description, + }; + }) + ); + } + + @Delete('/:id') + deleteById( + @GetOrgFromRequest() organization: Organization, + @Param('id') id: string + ) { + return this._thirdPartyManager.deleteIntegration(organization.id, id); + } + + @Post('/:id/submit') + async generate( + @GetOrgFromRequest() organization: Organization, + @Param('id') id: string, + @Body() data: any + ) { + const thirdParty = await this._thirdPartyManager.getIntegrationById( + organization.id, + id + ); + + if (!thirdParty) { + throw new HttpException('Integration not found', 404); + } + + const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName( + thirdParty.identifier + ); + + if (!thirdPartyInstance) { + throw new HttpException('Invalid identifier', 400); + } + + const loadedData = await thirdPartyInstance?.instance?.sendData( + AuthService.fixedDecryption(thirdParty.apiKey), + data + ); + + const file = await this.storage.uploadSimple(loadedData); + return this._mediaService.saveFile(organization.id, file.split('/').pop(), file); + } + + @Post('/function/:id/:functionName') + async callFunction( + @GetOrgFromRequest() organization: Organization, + @Param('id') id: string, + @Param('functionName') functionName: string, + @Body() data: any + ) { + const thirdParty = await this._thirdPartyManager.getIntegrationById( + organization.id, + id + ); + + if (!thirdParty) { + throw new HttpException('Integration not found', 404); + } + + const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName( + thirdParty.identifier + ); + + if (!thirdPartyInstance) { + throw new HttpException('Invalid identifier', 400); + } + + return thirdPartyInstance?.instance?.[functionName]( + AuthService.fixedDecryption(thirdParty.apiKey), + data + ); + } + + @Post('/:identifier') + async addApiKey( + @GetOrgFromRequest() organization: Organization, + @Param('identifier') identifier: string, + @Body('api') api: string + ) { + const thirdParty = this._thirdPartyManager.getThirdPartyByName(identifier); + if (!thirdParty) { + throw new HttpException('Invalid identifier', 400); + } + + const connect = await thirdParty.instance.checkConnection(api); + if (!connect) { + throw new HttpException('Invalid API key', 400); + } + + try { + const save = await this._thirdPartyManager.saveIntegration( + organization.id, + identifier, + api, + { + name: connect.name, + username: connect.username, + id: connect.id, + } + ); + + return { + id: save.id, + }; + } catch (e) { + console.log(e); + throw new HttpException('Integration Already Exists', 400); + } + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index a8000b3e..60967129 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -11,6 +11,7 @@ import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/t import { ThrottlerModule } from '@nestjs/throttler'; import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module'; import { McpModule } from '@gitroom/backend/mcp/mcp.module'; +import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module'; @Global() @Module({ @@ -22,6 +23,7 @@ import { McpModule } from '@gitroom/backend/mcp/mcp.module'; PublicApiModule, AgentModule, McpModule, + ThirdPartyModule, ThrottlerModule.forRoot([ { ttl: 3600000, diff --git a/apps/frontend/public/icons/third-party/heygen.png b/apps/frontend/public/icons/third-party/heygen.png new file mode 100644 index 0000000000000000000000000000000000000000..811603be43a7de4b2cc6bbaa6481584034ab48d4 GIT binary patch literal 7504 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 +} diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 044c1aec..efe8ff1a 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -558,7 +558,6 @@ export const AddEditModal: FC<{ // @ts-ignore for (const item of clipboardItems) { - console.log(item); if (item.kind === 'file') { const file = item.getAsFile(); if (file) { @@ -779,6 +778,7 @@ Here are the things you can do:
( // @ts-ignore for (const item of clipboardItems) { - console.log(item); if (item.kind === 'file') { const file = item.getAsFile(); if (file) { @@ -567,6 +566,7 @@ export const withProvider = function (
{ icon: 'plugs', path: '/plugs', }, + { + name: t('integrations', 'Integrations'), + icon: 'integrations', + path: '/third-party', + }, { name: t('billing', 'Billing'), icon: 'billing', @@ -80,7 +85,7 @@ export const TopMenu: FC = () => { const menuItems = useMenuItems(); return (
-
    +
      {menuItems .filter((f) => { if (f.hide) { diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index acd0d98b..64951a83 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -28,6 +28,7 @@ import Image from 'next/image'; import { DropFiles } from '@gitroom/frontend/components/layout/drop.files'; import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/third-party.media'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); @@ -452,6 +453,14 @@ export const MediaBox: FC<{ export const MultiMediaComponent: FC<{ label: string; description: string; + allData: { + content: string; + id?: string; + image?: Array<{ + id: string; + path: string; + }>; + }[]; value?: Array<{ path: string; id: string; @@ -471,7 +480,7 @@ export const MultiMediaComponent: FC<{ }; }) => void; }> = (props) => { - const { onOpen, onClose, name, error, text, onChange, value } = props; + const { onOpen, onClose, name, error, text, onChange, value, allData } = props; const user = useUser(); useEffect(() => { if (value) { @@ -598,6 +607,8 @@ export const MultiMediaComponent: FC<{
+ + {!!user?.tier?.ai && ( )} diff --git a/apps/frontend/src/components/third-parties/providers/heygen.provider.tsx b/apps/frontend/src/components/third-parties/providers/heygen.provider.tsx new file mode 100644 index 00000000..25a2ebc1 --- /dev/null +++ b/apps/frontend/src/components/third-parties/providers/heygen.provider.tsx @@ -0,0 +1,232 @@ +import { thirdPartyWrapper } from '@gitroom/frontend/components/third-parties/third-party.wrapper'; +import { + useThirdPartyFunction, + useThirdPartyFunctionSWR, + useThirdPartySubmit, +} from '@gitroom/frontend/components/third-parties/third-party.function'; +import { useThirdParty } from '@gitroom/frontend/components/third-parties/third-party.media'; +import { useForm, FormProvider, SubmitHandler } from 'react-hook-form'; +import { Textarea } from '@gitroom/react/form/textarea'; +import { Button } from '@gitroom/react/form/button'; +import { FC, useCallback, useState } from 'react'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import clsx from 'clsx'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { object, string } from 'zod'; +import { Select } from '@gitroom/react/form/select'; +import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; + +const aspectRatio = [ + { key: 'portrait', value: 'Portrait' }, + { key: 'story', value: 'Story' }, +]; + +const generateCaptions = [ + { key: 'yes', value: 'Yes' }, + { key: 'no', value: 'No' }, +]; + +const SelectAvatarComponent: FC<{ + avatarList: any[]; + onChange: (id: string) => void; +}> = (props) => { + const [current, setCurrent] = useState({}); + const { avatarList, onChange } = props; + + return ( +
+ {avatarList?.map((p) => ( +
{ + setCurrent(p.avatar_id === current?.avatar_id ? undefined : p); + onChange(p.avatar_id === current?.avatar_id ? {} : p.avatar_id); + }} + key={p.avatar_id} + className={clsx( + 'w-full h-full p-[20px] min-h-[100px] text-[14px] hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer', + current?.avatar_id === p.avatar_id + ? 'bg-input border border-red-500' + : 'bg-third' + )} + > +
+ +
+
{p.avatar_name}
+
+ ))} +
+ ); +}; + +const SelectVoiceComponent: FC<{ + voiceList: any[]; + onChange: (id: string) => void; +}> = (props) => { + const [current, setCurrent] = useState({}); + const { voiceList, onChange } = props; + + return ( +
+ {voiceList?.map((p) => ( +
{ + setCurrent(p.voice_id === current?.voice_id ? undefined : p); + onChange(p.voice_id === current?.voice_id ? {} : p.voice_id); + }} + key={p.avatar_id} + className={clsx( + 'w-full h-full p-[20px] min-h-[100px] text-[14px] hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer', + current?.voice_id === p.voice_id + ? 'bg-input border border-red-500' + : 'bg-third' + )} + > +
{p.name}
+
{p.language}
+
+ ))} +
+ ); +}; + +const HeygenProviderComponent = () => { + const thirdParty = useThirdParty(); + const load = useThirdPartyFunction('EVERYTIME'); + const { data } = useThirdPartyFunctionSWR('LOAD_ONCE', 'avatars'); + const { data: voices } = useThirdPartyFunctionSWR('LOAD_ONCE', 'voices'); + const send = useThirdPartySubmit(); + const [hideVoiceGenerator, setHideVoiceGenerator] = useState(false); + const [voiceLoading, setVoiceLoading] = useState(false); + + const form = useForm({ + values: { + voice: '', + avatar: '', + aspect_ratio: '', + captions: '', + selectedVoice: '', + }, + mode: 'all', + resolver: zodResolver( + object({ + voice: string().min(20, 'Voice must be at least 20 characters long'), + avatar: string().min(1, 'Avatar is required'), + selectedVoice: string().min(1, 'Voice is required'), + aspect_ratio: string().min(1, 'Aspect ratio is required'), + captions: string().min(1, 'Captions is required'), + }) + ), + }); + + const generateVoice = useCallback(async () => { + if ( + !(await deleteDialog('Are you sure? it will delete the current text')) + ) { + return; + } + + setVoiceLoading(true); + + form.setValue( + 'voice', + ( + await load('generateVoice', { + text: thirdParty.data.map((p) => p.content).join('\n'), + }) + ).voice + ); + + setVoiceLoading(false); + setHideVoiceGenerator(true); + }, [thirdParty]); + + const submit: SubmitHandler<{ voice: string; avatar: string }> = useCallback( + async (params) => { + thirdParty.onChange(await send(params)); + thirdParty.close(); + }, + [] + ); + + return ( +
+ {form.formState.isSubmitting && ( +
+ Grab a coffee and relax, this may take a while... +
+ You can also track the progress directly in HeyGen Dashboard. +
+ DO NOT CLOSE THIS WINDOW! +
+ +
+ )} + + +
+ + + + +
Voice to generate
+ {!hideVoiceGenerator && ( + + )} +