From 41e7d0fdebdb4c089d6618338ebf9377f14dc79f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 7 Oct 2025 17:19:18 +0700 Subject: [PATCH] Feat: agent --- .../hashnode/hashnode.publications.tsx | 2 +- .../providers/hashnode/hashnode.tags.tsx | 7 +- .../providers/listmonk/listmonk.provider.tsx | 17 +- .../src/chat/load.tools.service.ts | 2 + .../src/chat/rules.description.decorator.ts | 12 ++ .../chat/tools/integration.schedule.post.ts | 164 +++++++++++------- .../chat/tools/integration.trigger.tool.ts | 2 +- .../chat/tools/integration.validation.tool.ts | 5 +- .../src/integrations/integration.manager.ts | 16 ++ .../integrations/social/bluesky.provider.ts | 4 + .../integrations/social/farcaster.provider.ts | 4 + .../integrations/social/hashnode.provider.ts | 5 + .../integrations/social/instagram.provider.ts | 4 + .../social/instagram.standalone.provider.ts | 4 + .../social/linkedin.page.provider.ts | 4 + .../integrations/social/linkedin.provider.ts | 4 + .../integrations/social/pinterest.provider.ts | 4 + .../integrations/social/tiktok.provider.ts | 4 + .../src/integrations/social/x.provider.ts | 4 + .../integrations/social/youtube.provider.ts | 4 + 20 files changed, 190 insertions(+), 82 deletions(-) create mode 100644 libraries/nestjs-libraries/src/chat/rules.description.decorator.ts diff --git a/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.publications.tsx b/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.publications.tsx index d6052f15..038cb800 100644 --- a/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.publications.tsx +++ b/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.publications.tsx @@ -47,7 +47,7 @@ export const HashnodePublications: FC<{ value={currentMedia} > - {publications.map((publication: any) => ( + {(publications || []).map((publication: any) => ( diff --git a/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.tags.tsx b/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.tags.tsx index d16310d9..02f016b1 100644 --- a/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.tags.tsx +++ b/apps/frontend/src/components/new-launch/providers/hashnode/hashnode.tags.tsx @@ -51,7 +51,7 @@ export const HashnodeTags: FC<{ ); useEffect(() => { customFunc.get('tags').then((data) => setTags(data)); - const settings = getValues()[props.name]; + const settings = getValues()[props.name] || []; if (settings) { setTagValue(settings); } @@ -63,12 +63,13 @@ export const HashnodeTags: FC<{ if (!tags.length) { return null; } + return (
{label}
diff --git a/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx index 130192eb..7e7a1bcf 100644 --- a/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx @@ -18,7 +18,7 @@ const SettingsComponent = () => { - + ); }; @@ -29,19 +29,6 @@ export default withProvider({ SettingsComponent: SettingsComponent, CustomPreviewComponent: undefined, dto: ListmonkDto, - checkValidity: async (posts) => { - if ( - posts.some( - (p) => p.some((a) => a.path.indexOf('mp4') > -1) && p.length > 1 - ) - ) { - return 'You can only upload one video per post.'; - } - - if (posts.some((p) => p.length > 4)) { - return 'There can be maximum 4 pictures in a post.'; - } - return true; - }, + checkValidity: undefined, maximumCharacters: 300000, }); diff --git a/libraries/nestjs-libraries/src/chat/load.tools.service.ts b/libraries/nestjs-libraries/src/chat/load.tools.service.ts index c0729567..cc324174 100644 --- a/libraries/nestjs-libraries/src/chat/load.tools.service.ts +++ b/libraries/nestjs-libraries/src/chat/load.tools.service.ts @@ -58,6 +58,7 @@ export class LoadToolsService { - For X, if you don't have Premium, don't suggest a long post because it won't work. - Platform format will also be passed can be "normal", "markdown", "html", make sure you use the correct format for each platform. + - Sometimes 'integrationSchema' will return rules, make sure you follow them (these rules are set in stone, even if the user asks to ignore them) - Each socials media platform has different settings and rules, you can get them by using the integrationSchema tool. - Always make sure you use this tool before you schedule any post. - In every message I will send you the list of needed social medias (id and platform), if you already have the information use it, if not, use the integrationSchema tool to get it. @@ -65,6 +66,7 @@ export class LoadToolsService { - Before scheduling a post, always make sure you ask the user confirmation by providing all the details of the post (text, images, videos, date, time, social media platform, account). - If the user confirm, ask if they would like to get a modal with populated content without scheduling the post yet or if they want to schedule it right away. - Between tools, we will reference things like: [output:name] and [input:name] to set the information right. + - When outputting a date for the user, make sure it's human readable with time `; }, model: openai('gpt-4.1'), diff --git a/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts b/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts new file mode 100644 index 00000000..a817d629 --- /dev/null +++ b/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts @@ -0,0 +1,12 @@ +import 'reflect-metadata'; + +export function Rules(description: string) { + return function (target: any) { + // Define metadata on the class prototype (so it can be retrieved from the class) + Reflect.defineMetadata( + 'custom:rules:description', + description, + target + ); + }; +} diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts index 52af9c7f..8399d1ba 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts @@ -2,17 +2,13 @@ import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.in import { createTool } from '@mastra/core/tools'; import { z } from 'zod'; import { Injectable } from '@nestjs/common'; -import { - IntegrationManager, - socialIntegrationList, -} from '@gitroom/nestjs-libraries/integrations/integration.manager'; -import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; +import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/integration.manager'; import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; -import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -import { timer } from '@gitroom/helpers/utils/timer'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { AllProvidersSettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings'; +import { validate } from 'class-validator'; +import { Integration } from '@prisma/client'; @Injectable() export class IntegrationSchedulePostTool implements AgentToolInterface { @@ -36,69 +32,115 @@ If the user want to post a post to LinkedIn with one comment If the user want to post 20 posts for facebook each in individual days without comments - socialPost array length will be 20 - postsAndComments array length will be one + +If the tools return errors, you would need to rerun it with the right parameters, don't ask again, just run it `, - requireApproval: true, inputSchema: z.object({ - socialPost: z.array( - z.object({ - integrationId: z - .string() - .describe('The id of the integration (not internal id)'), - date: z.string().describe('The date of the post in UTC time'), - shortLink: z - .boolean() - .describe( - 'If the post has a link inside, we can ask the user if they want to add a short link' - ), - type: z - .enum(['draft', 'schedule', 'now']) - .describe( - 'The type of the post, if we pass now, we should pass the current date also' - ), - postsAndComments: z - .array( - z.object({ - content: z.string().describe('The content of the post'), - image: z - .array(z.string()) - .describe('The image of the post (URLS)'), - }) - ) - .describe( - 'first item is the post, every other item is the comments' - ), - settings: z - .array( - z.object({ - key: z.string().describe('Name of the settings key to pass'), - value: z.string().describe('Value of the key'), - }) - ) - .describe( - 'This relies on the integrationSchema tool to get the settings [input:settings]' - ), - }) - ).describe('Individual post') + socialPost: z + .array( + z.object({ + integrationId: z + .string() + .describe('The id of the integration (not internal id)'), + date: z.string().describe('The date of the post in UTC time'), + shortLink: z + .boolean() + .describe( + 'If the post has a link inside, we can ask the user if they want to add a short link' + ), + type: z + .enum(['draft', 'schedule', 'now']) + .describe( + 'The type of the post, if we pass now, we should pass the current date also' + ), + postsAndComments: z + .array( + z.object({ + content: z.string().describe('The content of the post'), + attachments: z + .array(z.string()) + .describe('The image of the post (URLS)'), + }) + ) + .describe( + 'first item is the post, every other item is the comments' + ), + settings: z + .array( + z.object({ + key: z + .string() + .describe('Name of the settings key to pass'), + value: z + .any() + .describe( + 'Value of the key, always prefer the id then label if possible' + ), + }) + ) + .describe( + 'This relies on the integrationSchema tool to get the settings [input:settings]' + ), + }) + ) + .describe('Individual post'), }), outputSchema: z.object({ - output: z.array( - z.object({ - id: z.string(), - postId: z.string(), - releaseURL: z.string(), - status: z.string(), - }) - ), + output: z + .array( + z.object({ + id: z.string(), + postId: z.string(), + releaseURL: z.string(), + status: z.string(), + }) + ) + .or(z.object({ errors: z.string() })), }), execute: async ({ runtimeContext, context }) => { + console.log(JSON.stringify(context, null, 2)); // @ts-ignore const organizationId = runtimeContext.get('organization') as string; const finalOutput = []; + + const integrations = {} as Record; + for (const platform of context.socialPost) { + integrations[platform.integrationId] = + await this._integrationService.getIntegrationById( + organizationId, + platform.integrationId + ); + + const { dto } = socialIntegrationList.find( + (p) => + p.identifier === + integrations[platform.integrationId].providerIdentifier + )!; + + if (dto) { + const newDTO = new dto(); + const obj = Object.assign( + newDTO, + platform.settings.reduce( + (acc, s) => ({ + ...acc, + [s.key]: s.value, + }), + {} as AllProvidersSettings + ) + ); + const errors = await validate(obj); + if (errors.length) { + console.log(errors); + return { + errors: JSON.stringify(errors), + }; + } + } + } + for (const post of context.socialPost) { - const integration = await this._integrationService.getIntegrationById( - organizationId, - post.integrationId - ); + const integration = integrations[post.integrationId]; if (!integration) { throw new Error('Integration not found'); @@ -123,7 +165,7 @@ If the user want to post 20 posts for facebook each in individual days without c value: post.postsAndComments.map((p) => ({ content: p.content, id: makeId(10), - image: p.image.map((p) => ({ + image: p.attachments.map((p) => ({ id: makeId(10), path: p, })), diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts index e094c3a9..6833286d 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts @@ -39,7 +39,7 @@ export class IntegrationTriggerTool implements AgentToolInterface { ), }), outputSchema: z.object({ - output: z.record(z.string(), z.any()), + output: z.array(z.object()), }), execute: async ({ runtimeContext, context }) => { console.log('triggerTool', context); diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts b/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts index 46acf8c9..2c9d84a5 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts @@ -35,6 +35,7 @@ export class IntegrationValidationTool implements AgentToolInterface { }), outputSchema: z.object({ output: z.object({ + rules: z.string(), maxLength: z .number() .describe('The maximum length of a post / comment'), @@ -77,7 +78,7 @@ export class IntegrationValidationTool implements AgentToolInterface { if (!integration) { return { - output: { maxLength: 0, settings: {}, tools: [] }, + output: { rules: '', maxLength: 0, settings: {}, tools: [] }, }; } @@ -86,9 +87,11 @@ export class IntegrationValidationTool implements AgentToolInterface { ? false : validationMetadatasToSchemas()[integration.dto.name]; const tools = this._integrationManager.getAllTools(); + const rules = this._integrationManager.getAllRulesDescription(); return { output: { + rules: rules[integration.identifier], maxLength, settings: !schemas ? 'No additional settings required' : schemas, tools: tools[integration.identifier], diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 598da9e1..dea5122d 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -96,6 +96,22 @@ export class IntegrationManager { ); } + getAllRulesDescription(): { + [key: string]: string; + } { + return socialIntegrationList.reduce( + (all, current) => ({ + ...all, + [current.identifier]: + Reflect.getMetadata( + 'custom:rules:description', + current.constructor + ) || '', + }), + {} + ); + } + getAllPlugs() { return socialIntegrationList .map((p) => { diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 8980a4dc..3e1aa59b 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -26,6 +26,7 @@ import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { timer } from '@gitroom/helpers/utils/timer'; import axios from 'axios'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; async function reduceImageBySize(url: string, maxSizeKB = 976) { try { @@ -141,6 +142,9 @@ async function uploadVideo( } satisfies AppBskyEmbedVideo.Main; } +@Rules( + 'Bluesky can have maximum 1 video or 4 pictures in one post, it can also be without attachments' +) export class BlueskyProvider extends SocialAbstract implements SocialProvider { override maxConcurrentJob = 2; // Bluesky has moderate rate limits identifier = 'bluesky'; diff --git a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts index 7d2e595f..32e68bfa 100644 --- a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts @@ -13,11 +13,15 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { groupBy } from 'lodash'; import { FarcasterDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/farcaster.dto'; import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; const client = new NeynarAPIClient({ apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000', }); +@Rules( + 'Farcaster/Warpcast can only accept pictures' +) export class FarcasterProvider extends SocialAbstract implements SocialProvider diff --git a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts index a20b0407..41c0c80f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts @@ -108,6 +108,11 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider { return tags.map((tag) => ({ value: tag.objectID, label: tag.name })); } + @Tool({ description: 'Tags', dataSchema: [] }) + tagsList() { + return tags; + } + @Tool({ description: 'Publications', dataSchema: [] }) async publications(accessToken: string) { const { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 7fe240be..2cc499e2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -11,7 +11,11 @@ import dayjs from 'dayjs'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto'; import { Integration } from '@prisma/client'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + "Instagram should have at least one attachment, if it's a story, it can have only one picture" +) export class InstagramProvider extends SocialAbstract implements SocialProvider diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts index f8261893..bc058502 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -10,9 +10,13 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto'; import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider'; import { Integration } from '@prisma/client'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; const instagramProvider = new InstagramProvider(); +@Rules( + "Instagram should have at least one attachment, if it's a story, it can have only one picture" +) export class InstagramStandaloneProvider extends SocialAbstract implements SocialProvider 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 52cf8aff..fdb66eb6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -11,7 +11,11 @@ import dayjs from 'dayjs'; import { Integration } from '@prisma/client'; import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { timer } from '@gitroom/helpers/utils/timer'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + 'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment' +) export class LinkedinPageProvider extends LinkedinProvider implements SocialProvider diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 743b4c30..4aa1c9d5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -14,7 +14,11 @@ import { PostPlug } from '@gitroom/helpers/decorators/post.plug'; import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto'; import imageToPDF from 'image-to-pdf'; import { Readable } from 'stream'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + 'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment' +) export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; name = 'LinkedIn'; diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 6e9decad..f9779010 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -13,7 +13,11 @@ import { timer } from '@gitroom/helpers/utils/timer'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import dayjs from 'dayjs'; import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + 'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one' +) export class PinterestProvider extends SocialAbstract implements SocialProvider diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 227d29ee..41b4eebc 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -12,7 +12,11 @@ import { import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto'; import { timer } from '@gitroom/helpers/utils/timer'; import { Integration } from '@prisma/client'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + 'TikTok can have one video or one picture or multiple pictures, it cannot be without an attachment' +) export class TiktokProvider extends SocialAbstract implements SocialProvider { identifier = 'tiktok'; name = 'Tiktok'; diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index d28fb976..e1ad9eee 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -18,7 +18,11 @@ import dayjs from 'dayjs'; import { uniqBy } from 'lodash'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; import { XDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/x.dto'; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; +@Rules( + 'X can have maximum 4 pictures, or maximum one video, it can also be without attachments' +) export class XProvider extends SocialAbstract implements SocialProvider { identifier = 'x'; name = 'X'; diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index e0ca0f09..bf66dc16 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -18,6 +18,7 @@ import * as process from 'node:process'; import dayjs from 'dayjs'; import { GaxiosResponse } from 'gaxios/build/src/common'; import Schema$Video = youtube_v3.Schema$Video; +import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; const clientAndYoutube = () => { const client = new google.auth.OAuth2({ @@ -47,6 +48,9 @@ const clientAndYoutube = () => { return { client, youtube, oauth2, youtubeAnalytics }; }; +@Rules( + 'YouTube must have on video attachment, it cannot be empty' +) export class YoutubeProvider extends SocialAbstract implements SocialProvider { override maxConcurrentJob = 1; // YouTube has strict upload quotas identifier = 'youtube';