mirror of
https://github.com/hexastack/hexabot
synced 2025-01-22 18:45:57 +00:00
feat: add plugins settings i18n + typing
This commit is contained in:
parent
8d846186cc
commit
92bb4978c3
@ -24,7 +24,7 @@ export interface ChannelModuleOptions {
|
||||
folder: string;
|
||||
}
|
||||
|
||||
@InjectDynamicProviders('dist/**/*.channel.js')
|
||||
@InjectDynamicProviders('dist/extensions/**/*.channel.js')
|
||||
@Module({
|
||||
controllers: [WebhookController, ChannelController],
|
||||
providers: [ChannelService],
|
||||
|
@ -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<N extends string = string> {
|
||||
}
|
||||
|
||||
protected getGroup() {
|
||||
return this.getChannel().replaceAll('-', '_') as ChannelSetting<N>['group'];
|
||||
return hyphenToUnderscore(this.getChannel()) as ChannelSetting<N>['group'];
|
||||
}
|
||||
|
||||
async setup() {
|
||||
|
@ -59,7 +59,7 @@ export class BlockController extends BaseController<
|
||||
private readonly categoryService: CategoryService,
|
||||
private readonly labelService: LabelService,
|
||||
private readonly userService: UserService,
|
||||
private pluginsService: PluginService<BaseBlockPlugin>,
|
||||
private pluginsService: PluginService<BaseBlockPlugin<any>>,
|
||||
) {
|
||||
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: {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
0
api/src/extensions/plugins/.gitkeep
Normal file
0
api/src/extensions/plugins/.gitkeep
Normal file
@ -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<K = Record<string, unknown>>
|
||||
@ -84,41 +85,40 @@ export class I18nService<K = Record<string, unknown>>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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([
|
||||
|
@ -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<BasePlugin>) {
|
||||
public readonly settings: T;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
settings: T,
|
||||
pluginService: PluginService<BasePlugin>,
|
||||
) {
|
||||
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<StdOutgoingEnvelope>;
|
||||
|
||||
protected getArguments(block: Block) {
|
||||
if ('args' in block.message) {
|
||||
return block.message.args as SettingObject<T>;
|
||||
}
|
||||
throw new Error(`Block "${block.name}" does not have any arguments.`);
|
||||
}
|
||||
}
|
||||
|
@ -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: [
|
||||
|
@ -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';
|
||||
|
@ -22,7 +22,7 @@ export interface CustomBlocks {}
|
||||
type ChannelEvent = any;
|
||||
type BlockAttrs = Partial<BlockCreateDto> & { name: string };
|
||||
|
||||
export type PluginSetting = SettingCreateDto;
|
||||
export type PluginSetting = Omit<SettingCreateDto, 'weight'>;
|
||||
|
||||
export type PluginBlockTemplate = Omit<
|
||||
BlockAttrs,
|
||||
|
@ -9,3 +9,7 @@
|
||||
export const isEmpty = (value: string): boolean => {
|
||||
return value === undefined || value === null || value === '';
|
||||
};
|
||||
|
||||
export const hyphenToUnderscore = (str: string) => {
|
||||
return str.replaceAll('-', '_');
|
||||
};
|
||||
|
@ -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<PluginSetting[]> {
|
||||
constructor(
|
||||
pluginService: PluginService,
|
||||
private logger: LoggerService,
|
||||
) {
|
||||
super('dummy', pluginService);
|
||||
|
||||
this.title = 'Dummy';
|
||||
|
||||
this.settings = [];
|
||||
super('dummy', [], pluginService);
|
||||
|
||||
this.template = { name: 'Dummy Plugin' };
|
||||
|
@ -25,15 +25,17 @@ import { MIME_TYPES } from "@/utils/attachment";
|
||||
interface RenderSettingInputProps {
|
||||
setting: ISetting;
|
||||
field: ControllerRenderProps<any, string>;
|
||||
ns: string;
|
||||
isDisabled?: (setting: ISetting) => boolean;
|
||||
}
|
||||
|
||||
const SettingInput: React.FC<RenderSettingInputProps> = ({
|
||||
setting,
|
||||
field,
|
||||
ns,
|
||||
isDisabled = () => false,
|
||||
}) => {
|
||||
const { t } = useTranslate(setting.group);
|
||||
const { t } = useTranslate(ns);
|
||||
const label = t(`label.${setting.label}`, {
|
||||
defaultValue: setting.label,
|
||||
});
|
||||
|
@ -186,6 +186,7 @@ export const Settings = () => {
|
||||
<SettingInput
|
||||
setting={setting}
|
||||
field={field}
|
||||
ns={setting.group}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -30,8 +30,8 @@ export const CustomBlocks = () => {
|
||||
<Grid container>
|
||||
{customBlocks?.map((customBlock) => (
|
||||
<Block
|
||||
key={customBlock.name}
|
||||
title={customBlock.title}
|
||||
key={customBlock.id}
|
||||
title={t(`title.${customBlock.id}`, { ns: customBlock.id })}
|
||||
Icon={PluginIcon}
|
||||
blockTemplate={customBlock.template}
|
||||
name={customBlock.template.name}
|
||||
|
@ -109,16 +109,19 @@ export const OptionsForm = () => {
|
||||
const { onChange, ...rest } = field;
|
||||
|
||||
return (
|
||||
<AutoCompleteEntitySelect<ICustomBlockTemplate, "title">
|
||||
searchFields={["title", "name"]}
|
||||
<AutoCompleteEntitySelect<ICustomBlockTemplate, "id">
|
||||
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);
|
||||
|
@ -60,7 +60,11 @@ const PluginMessageForm = () => {
|
||||
defaultValue={message.args?.[setting.label] || setting.value}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth sx={{ paddingTop: ".75rem" }}>
|
||||
<SettingInput setting={setting} field={field} />
|
||||
<SettingInput
|
||||
setting={setting}
|
||||
field={field}
|
||||
ns={message.plugin}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -268,7 +268,7 @@ export const CustomBlockEntity = new schema.Entity(
|
||||
EntityType.CUSTOM_BLOCK,
|
||||
undefined,
|
||||
{
|
||||
idAttribute: ({ name }) => name,
|
||||
idAttribute: ({ id }) => id,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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<Format.FULL> {
|
||||
}
|
||||
|
||||
export interface ICustomBlockTemplateAttributes {
|
||||
title: string;
|
||||
name: string;
|
||||
template: IBlockAttributes;
|
||||
effects: string[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user