diff --git a/api/src/channel/channel.module.ts b/api/src/channel/channel.module.ts index dc4c039e..c4924735 100644 --- a/api/src/channel/channel.module.ts +++ b/api/src/channel/channel.module.ts @@ -24,7 +24,7 @@ export interface ChannelModuleOptions { folder: string; } -@InjectDynamicProviders('dist/**/*.channel.js') +@InjectDynamicProviders('dist/extensions/**/*.channel.js') @Module({ controllers: [WebhookController, ChannelController], providers: [ChannelService], diff --git a/api/src/channel/lib/Handler.ts b/api/src/channel/lib/Handler.ts index 1532dc85..174c047d 100644 --- a/api/src/channel/lib/Handler.ts +++ b/api/src/channel/lib/Handler.ts @@ -19,6 +19,7 @@ import { LoggerService } from '@/logger/logger.service'; import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper'; import { NlpService } from '@/nlp/services/nlp.service'; import { SettingService } from '@/setting/services/setting.service'; +import { hyphenToUnderscore } from '@/utils/helpers/misc'; import { SocketRequest } from '@/websocket/utils/socket-request'; import { SocketResponse } from '@/websocket/utils/socket-response'; @@ -56,7 +57,7 @@ export default abstract class ChannelHandler { } protected getGroup() { - return this.getChannel().replaceAll('-', '_') as ChannelSetting['group']; + return hyphenToUnderscore(this.getChannel()) as ChannelSetting['group']; } async setup() { diff --git a/api/src/chat/controllers/block.controller.ts b/api/src/chat/controllers/block.controller.ts index 83da093d..e68e0788 100644 --- a/api/src/chat/controllers/block.controller.ts +++ b/api/src/chat/controllers/block.controller.ts @@ -59,7 +59,7 @@ export class BlockController extends BaseController< private readonly categoryService: CategoryService, private readonly labelService: LabelService, private readonly userService: UserService, - private pluginsService: PluginService, + private pluginsService: PluginService>, ) { super(blockService); } @@ -122,8 +122,7 @@ export class BlockController extends BaseController< const plugins = this.pluginsService .getAllByType(PluginType.block) .map((p) => ({ - title: p.title, - name: p.id, + id: p.id, template: { ...p.template, message: { diff --git a/api/src/chat/services/message.service.ts b/api/src/chat/services/message.service.ts index 1c1f465b..c66d0903 100644 --- a/api/src/chat/services/message.service.ts +++ b/api/src/chat/services/message.service.ts @@ -116,4 +116,23 @@ export class MessageService extends BaseService< limit, ); } + + /** + * Retrieves the latest messages for a given subscriber + * + * @param subscriber - The subscriber whose message history is being retrieved. + * @param limit - The maximum number of messages to return (defaults to 5). + * + * @returns The message history since the specified date. + */ + async findLastMessages(subscriber: Subscriber, limit: number = 5) { + const lastMessages = await this.findPage( + { + $or: [{ sender: subscriber.id }, { recipient: subscriber.id }], + }, + { sort: ['createdAt', 'desc'], skip: 0, limit }, + ); + + return lastMessages.reverse(); + } } diff --git a/api/src/extensions/plugins/.gitkeep b/api/src/extensions/plugins/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/api/src/i18n/services/i18n.service.ts b/api/src/i18n/services/i18n.service.ts index 3e06910b..018e5246 100644 --- a/api/src/i18n/services/i18n.service.ts +++ b/api/src/i18n/services/i18n.service.ts @@ -22,6 +22,7 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types'; import { config } from '@/config'; import { Translation } from '@/i18n/schemas/translation.schema'; +import { hyphenToUnderscore } from '@/utils/helpers/misc'; @Injectable() export class I18nService> @@ -84,41 +85,40 @@ export class I18nService> } async loadExtensionI18nTranslations() { - const extensionsDir = path.join( - __dirname, - '..', - '..', - 'extensions', - 'channels', - ); + const baseDir = path.join(__dirname, '..', '..', 'extensions'); + const extensionTypes = ['channels', 'plugins']; + try { - const extensionFolders = await fs.readdir(extensionsDir, { - withFileTypes: true, - }); + for (const type of extensionTypes) { + const extensionsDir = path.join(baseDir, type); + const extensionFolders = await fs.readdir(extensionsDir, { + withFileTypes: true, + }); - for (const folder of extensionFolders) { - if (folder.isDirectory()) { - const i18nPath = path.join(extensionsDir, folder.name, 'i18n'); - const extensionName = folder.name.replaceAll('-', '_'); - try { - // Check if the i18n directory exists - await fs.access(i18nPath); + for (const folder of extensionFolders) { + if (folder.isDirectory()) { + const i18nPath = path.join(extensionsDir, folder.name, 'i18n'); + const namespace = hyphenToUnderscore(folder.name); + try { + // Check if the i18n directory exists + await fs.access(i18nPath); - // Load and merge translations - const i18nLoader = new I18nJsonLoader({ path: i18nPath }); - const translations = await i18nLoader.load(); - for (const lang in translations) { - if (!this.extensionTranslations[lang]) { - this.extensionTranslations[lang] = { - [extensionName]: translations[lang], - }; - } else { - this.extensionTranslations[lang][extensionName] = - translations[lang]; + // Load and merge translations + const i18nLoader = new I18nJsonLoader({ path: i18nPath }); + const translations = await i18nLoader.load(); + for (const lang in translations) { + if (!this.extensionTranslations[lang]) { + this.extensionTranslations[lang] = { + [namespace]: translations[lang], + }; + } else { + this.extensionTranslations[lang][namespace] = + translations[lang]; + } } + } catch (error) { + // If the i18n folder does not exist or error in reading, skip this folder } - } catch (error) { - // If the i18n folder does not exist or error in reading, skip this folder } } } diff --git a/api/src/nlp/nlp.module.ts b/api/src/nlp/nlp.module.ts index c2e816d8..05c7896b 100644 --- a/api/src/nlp/nlp.module.ts +++ b/api/src/nlp/nlp.module.ts @@ -33,7 +33,7 @@ import { NlpSampleService } from './services/nlp-sample.service'; import { NlpValueService } from './services/nlp-value.service'; import { NlpService } from './services/nlp.service'; -@InjectDynamicProviders('dist/**/*.nlp.helper.js') +@InjectDynamicProviders('dist/extensions/**/*.nlp.helper.js') @Module({ imports: [ MongooseModule.forFeature([ diff --git a/api/src/plugins/base-block-plugin.ts b/api/src/plugins/base-block-plugin.ts index ab01da5f..47808fa3 100644 --- a/api/src/plugins/base-block-plugin.ts +++ b/api/src/plugins/base-block-plugin.ts @@ -22,17 +22,22 @@ import { } from './types'; @Injectable() -export abstract class BaseBlockPlugin extends BasePlugin { +export abstract class BaseBlockPlugin< + T extends PluginSetting[], +> extends BasePlugin { public readonly type: PluginType = PluginType.block; - constructor(id: string, pluginService: PluginService) { + public readonly settings: T; + + constructor( + id: string, + settings: T, + pluginService: PluginService, + ) { super(id, pluginService); + this.settings = settings; } - title: string; - - settings: PluginSetting[]; - template: PluginBlockTemplate; effects?: PluginEffects; @@ -42,4 +47,11 @@ export abstract class BaseBlockPlugin extends BasePlugin { context: Context, convId?: string, ): Promise; + + protected getArguments(block: Block) { + if ('args' in block.message) { + return block.message.args as SettingObject; + } + throw new Error(`Block "${block.name}" does not have any arguments.`); + } } diff --git a/api/src/plugins/plugins.module.ts b/api/src/plugins/plugins.module.ts index 3baf52f6..2579f06f 100644 --- a/api/src/plugins/plugins.module.ts +++ b/api/src/plugins/plugins.module.ts @@ -20,7 +20,7 @@ import { ContentModel } from '@/cms/schemas/content.schema'; import { PluginService } from './plugins.service'; -@InjectDynamicProviders('dist/**/*.plugin.js') +@InjectDynamicProviders('dist/extensions/**/*.plugin.js') @Global() @Module({ imports: [ diff --git a/api/src/plugins/plugins.service.spec.ts b/api/src/plugins/plugins.service.spec.ts index 81282e55..31f15d6b 100644 --- a/api/src/plugins/plugins.service.spec.ts +++ b/api/src/plugins/plugins.service.spec.ts @@ -8,8 +8,8 @@ import { Test } from '@nestjs/testing'; -import { DummyPlugin } from '@/extensions/plugins/dummy.plugin'; import { LoggerModule } from '@/logger/logger.module'; +import { DummyPlugin } from '@/utils/test/dummy/dummy.plugin'; import { BaseBlockPlugin } from './base-block-plugin'; import { PluginService } from './plugins.service'; diff --git a/api/src/plugins/types.ts b/api/src/plugins/types.ts index 13a7eeef..e557c999 100644 --- a/api/src/plugins/types.ts +++ b/api/src/plugins/types.ts @@ -22,7 +22,7 @@ export interface CustomBlocks {} type ChannelEvent = any; type BlockAttrs = Partial & { name: string }; -export type PluginSetting = SettingCreateDto; +export type PluginSetting = Omit; export type PluginBlockTemplate = Omit< BlockAttrs, diff --git a/api/src/utils/helpers/misc.ts b/api/src/utils/helpers/misc.ts index 530b47bd..be1aafb4 100644 --- a/api/src/utils/helpers/misc.ts +++ b/api/src/utils/helpers/misc.ts @@ -9,3 +9,7 @@ export const isEmpty = (value: string): boolean => { return value === undefined || value === null || value === ''; }; + +export const hyphenToUnderscore = (str: string) => { + return str.replaceAll('-', '_'); +}; diff --git a/api/src/extensions/plugins/dummy.plugin.ts b/api/src/utils/test/dummy/dummy.plugin.ts similarity index 89% rename from api/src/extensions/plugins/dummy.plugin.ts rename to api/src/utils/test/dummy/dummy.plugin.ts index 267a2649..271343dd 100644 --- a/api/src/extensions/plugins/dummy.plugin.ts +++ b/api/src/utils/test/dummy/dummy.plugin.ts @@ -15,18 +15,15 @@ import { import { LoggerService } from '@/logger/logger.service'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { PluginService } from '@/plugins/plugins.service'; +import { PluginSetting } from '@/plugins/types'; @Injectable() -export class DummyPlugin extends BaseBlockPlugin { +export class DummyPlugin extends BaseBlockPlugin { constructor( pluginService: PluginService, private logger: LoggerService, ) { - super('dummy', pluginService); - - this.title = 'Dummy'; - - this.settings = []; + super('dummy', [], pluginService); this.template = { name: 'Dummy Plugin' }; diff --git a/frontend/src/components/settings/SettingInput.tsx b/frontend/src/components/settings/SettingInput.tsx index ece79d4f..11c02ce9 100644 --- a/frontend/src/components/settings/SettingInput.tsx +++ b/frontend/src/components/settings/SettingInput.tsx @@ -25,15 +25,17 @@ import { MIME_TYPES } from "@/utils/attachment"; interface RenderSettingInputProps { setting: ISetting; field: ControllerRenderProps; + ns: string; isDisabled?: (setting: ISetting) => boolean; } const SettingInput: React.FC = ({ setting, field, + ns, isDisabled = () => false, }) => { - const { t } = useTranslate(setting.group); + const { t } = useTranslate(ns); const label = t(`label.${setting.label}`, { defaultValue: setting.label, }); diff --git a/frontend/src/components/settings/index.tsx b/frontend/src/components/settings/index.tsx index 5d5c3505..4697b224 100644 --- a/frontend/src/components/settings/index.tsx +++ b/frontend/src/components/settings/index.tsx @@ -186,6 +186,7 @@ export const Settings = () => { diff --git a/frontend/src/components/visual-editor/CustomBlocks.tsx b/frontend/src/components/visual-editor/CustomBlocks.tsx index 27921080..a2ebc173 100644 --- a/frontend/src/components/visual-editor/CustomBlocks.tsx +++ b/frontend/src/components/visual-editor/CustomBlocks.tsx @@ -30,8 +30,8 @@ export const CustomBlocks = () => { {customBlocks?.map((customBlock) => ( { const { onChange, ...rest } = field; return ( - - searchFields={["title", "name"]} + + searchFields={["id"]} entity={EntityType.CUSTOM_BLOCK} format={Format.BASIC} - idKey="name" - labelKey="title" + idKey="id" + labelKey="id" label={t("label.effects")} multiple={true} + getOptionLabel={(option) => { + return t(`title.${option.id}`, { ns: option.id }); + }} onChange={(_e, selected) => - onChange(selected.map(({ name }) => name)) + onChange(selected.map(({ id }) => id)) } preprocess={(options) => { return options.filter(({ effects }) => effects.length > 0); diff --git a/frontend/src/components/visual-editor/form/PluginMessageForm.tsx b/frontend/src/components/visual-editor/form/PluginMessageForm.tsx index 6db304a6..2da0f39d 100644 --- a/frontend/src/components/visual-editor/form/PluginMessageForm.tsx +++ b/frontend/src/components/visual-editor/form/PluginMessageForm.tsx @@ -60,7 +60,11 @@ const PluginMessageForm = () => { defaultValue={message.args?.[setting.label] || setting.value} render={({ field }) => ( - + )} /> diff --git a/frontend/src/services/entities.ts b/frontend/src/services/entities.ts index 3b23128a..1074646f 100644 --- a/frontend/src/services/entities.ts +++ b/frontend/src/services/entities.ts @@ -268,7 +268,7 @@ export const CustomBlockEntity = new schema.Entity( EntityType.CUSTOM_BLOCK, undefined, { - idAttribute: ({ name }) => name, + idAttribute: ({ id }) => id, }, ); diff --git a/frontend/src/types/block.types.ts b/frontend/src/types/block.types.ts index 337a086a..251f35d8 100644 --- a/frontend/src/types/block.types.ts +++ b/frontend/src/types/block.types.ts @@ -11,15 +11,15 @@ import { EntityType, Format } from "@/services/types"; import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; import { ILabel } from "./label.types"; import { - StdOutgoingQuickRepliesMessage, + AttachmentForeignKey, + ContentOptions, + PayloadType, StdOutgoingAttachmentMessage, StdOutgoingButtonsMessage, StdOutgoingListMessage, + StdOutgoingQuickRepliesMessage, StdOutgoingTextMessage, - AttachmentForeignKey, StdPluginMessage, - ContentOptions, - PayloadType, } from "./message.types"; import { IUser } from "./user.types"; @@ -132,8 +132,6 @@ export interface IBlockFull extends IBlockStub, IFormat { } export interface ICustomBlockTemplateAttributes { - title: string; - name: string; template: IBlockAttributes; effects: string[]; }