From bb2dfa7ac0f2c6138f8d740ede251fe288f59a5e Mon Sep 17 00:00:00 2001 From: Yassine Date: Wed, 29 Jan 2025 16:04:37 +0100 Subject: [PATCH 1/4] feat: cleanup orphan extensions settings --- api/src/extra/extension-cleanup.service.ts | 73 +++++++++++++++++++++ api/src/extra/index.ts | 13 +++- api/src/setting/services/setting.service.ts | 25 +++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 api/src/extra/extension-cleanup.service.ts diff --git a/api/src/extra/extension-cleanup.service.ts b/api/src/extra/extension-cleanup.service.ts new file mode 100644 index 000000000..2e181397e --- /dev/null +++ b/api/src/extra/extension-cleanup.service.ts @@ -0,0 +1,73 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { Injectable, OnModuleInit } from '@nestjs/common'; + +import { ChannelService } from '@/channel/channel.service'; +import { HelperService } from '@/helper/helper.service'; +import { LoggerService } from '@/logger/logger.service'; +import { PluginService } from '@/plugins/plugins.service'; +import { SettingService } from '@/setting/services/setting.service'; + +@Injectable() +export class ExtensionCleanupService implements OnModuleInit { + constructor( + private readonly helperService: HelperService, + private readonly pluginService: PluginService, + private readonly channelService: ChannelService, + private readonly settingService: SettingService, + private readonly logger: LoggerService, + ) {} + + async onModuleInit() { + await this.cleanupOrphanedSettings(); + } + + private async cleanupOrphanedSettings() { + enum ExtensionType { + 'helper', + 'plugin', + 'channel', + } + const activeExtensions = [ + ...this.getHelperNames(), + ...this.getPluginNames(), + ...this.getChannelNames(), + ].reduce((acc, name) => { + if (!acc.includes(name)) { + acc.push(name.replace(/-/g, '_')); + } + return acc; + }, []); + + this.logger.debug('Cleaning up orphaned settings...'); + + const settingGroups = await this.settingService.getAllGroups(); + + for (const group of settingGroups) { + const extensionType = group.split('_').pop() ?? ''; + debugger; + if (extensionType in ExtensionType && !activeExtensions.includes(group)) { + this.logger.debug(`Deleting orphaned settings for ${group}...`); + await this.settingService.deleteGroup(group); + } + } + } + + private getHelperNames(): string[] { + return this.helperService.getAll().map((handler) => handler.getName()); + } + + private getPluginNames(): string[] { + return this.pluginService.getAll().map((handler) => handler.getName()); + } + + private getChannelNames(): string[] { + return this.channelService.getAll().map((handler) => handler.getName()); + } +} diff --git a/api/src/extra/index.ts b/api/src/extra/index.ts index d773e1360..03e079243 100644 --- a/api/src/extra/index.ts +++ b/api/src/extra/index.ts @@ -1,9 +1,18 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -export default []; +import { Module } from '@nestjs/common'; + +import { ExtensionCleanupService } from './extension-cleanup.service'; + +@Module({ + providers: [ExtensionCleanupService], +}) +class ExtensionCleanupModule {} + +export default [ExtensionCleanupModule]; diff --git a/api/src/setting/services/setting.service.ts b/api/src/setting/services/setting.service.ts index 56fffba3a..8d9c69b73 100644 --- a/api/src/setting/services/setting.service.ts +++ b/api/src/setting/services/setting.service.ts @@ -51,6 +51,15 @@ export class SettingService extends BaseService { } } + /** + * Removes all settings that belong to the provided group. + * + * @param group - The group of settings to remove. + */ + async deleteGroup(group: string) { + await this.repository.deleteMany({ group }); + } + /** * Loads all settings and returns them grouped in ascending order by weight. * @@ -100,6 +109,22 @@ export class SettingService extends BaseService { ); } + /** + * Retrieves a list of all setting groups. + * + * @returns A promise that resolves to a list of setting groups. + */ + async getAllGroups(): Promise { + const settings = await this.findAll(); + const groups = settings.reduce((acc, setting) => { + if (!acc.includes(setting.group)) { + acc.push(setting.group); + } + return acc; + }, []); + return groups; + } + /** * Retrieves the application configuration object. * From 5eb15cc86a19be12476cb41c6f33ce02578a5407 Mon Sep 17 00:00:00 2001 From: Yassine Date: Wed, 29 Jan 2025 21:33:05 +0100 Subject: [PATCH 2/4] fix: renaming --- ...on-cleanup.service.ts => cleanup.service.ts} | 17 +++++++---------- api/src/extra/index.ts | 8 ++++---- 2 files changed, 11 insertions(+), 14 deletions(-) rename api/src/extra/{extension-cleanup.service.ts => cleanup.service.ts} (87%) diff --git a/api/src/extra/extension-cleanup.service.ts b/api/src/extra/cleanup.service.ts similarity index 87% rename from api/src/extra/extension-cleanup.service.ts rename to api/src/extra/cleanup.service.ts index 2e181397e..9c40468b1 100644 --- a/api/src/extra/extension-cleanup.service.ts +++ b/api/src/extra/cleanup.service.ts @@ -15,7 +15,7 @@ import { PluginService } from '@/plugins/plugins.service'; import { SettingService } from '@/setting/services/setting.service'; @Injectable() -export class ExtensionCleanupService implements OnModuleInit { +export class CleanupService implements OnModuleInit { constructor( private readonly helperService: HelperService, private readonly pluginService: PluginService, @@ -38,12 +38,7 @@ export class ExtensionCleanupService implements OnModuleInit { ...this.getHelperNames(), ...this.getPluginNames(), ...this.getChannelNames(), - ].reduce((acc, name) => { - if (!acc.includes(name)) { - acc.push(name.replace(/-/g, '_')); - } - return acc; - }, []); + ]; this.logger.debug('Cleaning up orphaned settings...'); @@ -60,14 +55,16 @@ export class ExtensionCleanupService implements OnModuleInit { } private getHelperNames(): string[] { - return this.helperService.getAll().map((handler) => handler.getName()); + return this.helperService.getAll().map((handler) => handler.getNamespace()); } private getPluginNames(): string[] { - return this.pluginService.getAll().map((handler) => handler.getName()); + return this.pluginService.getAll().map((handler) => handler.getNamespace()); } private getChannelNames(): string[] { - return this.channelService.getAll().map((handler) => handler.getName()); + return this.channelService + .getAll() + .map((handler) => handler.getNamespace()); } } diff --git a/api/src/extra/index.ts b/api/src/extra/index.ts index 03e079243..1c6f0b7d0 100644 --- a/api/src/extra/index.ts +++ b/api/src/extra/index.ts @@ -8,11 +8,11 @@ import { Module } from '@nestjs/common'; -import { ExtensionCleanupService } from './extension-cleanup.service'; +import { CleanupService } from './cleanup.service'; @Module({ - providers: [ExtensionCleanupService], + providers: [CleanupService], }) -class ExtensionCleanupModule {} +class CleanupModule {} -export default [ExtensionCleanupModule]; +export default [CleanupModule]; From eacd1a186eb70c5deba7c8d1235412519fd8e59a Mon Sep 17 00:00:00 2001 From: Yassine Date: Thu, 30 Jan 2025 20:13:10 +0100 Subject: [PATCH 3/4] fix: cleanup method per extension type --- api/src/channel/channel.service.ts | 37 ++++++++++++- api/src/extra/cleanup.service.ts | 70 ------------------------- api/src/extra/index.ts | 13 +---- api/src/helper/helper.service.ts | 35 ++++++++++++- api/src/plugins/plugins.service.spec.ts | 12 ++++- api/src/plugins/plugins.service.ts | 45 ++++++++++++++-- 6 files changed, 123 insertions(+), 89 deletions(-) delete mode 100644 api/src/extra/cleanup.service.ts diff --git a/api/src/channel/channel.service.ts b/api/src/channel/channel.service.ts index 00c987c00..3ddf04082 100644 --- a/api/src/channel/channel.service.ts +++ b/api/src/channel/channel.service.ts @@ -6,13 +6,18 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + OnApplicationBootstrap, + UnauthorizedException, +} from '@nestjs/common'; import { Request, Response } from 'express'; import { SubscriberService } from '@/chat/services/subscriber.service'; import { CONSOLE_CHANNEL_NAME } from '@/extensions/channels/console/settings'; import { WEB_CHANNEL_NAME } from '@/extensions/channels/web/settings'; import { LoggerService } from '@/logger/logger.service'; +import { SettingService } from '@/setting/services/setting.service'; import { getSessionStore } from '@/utils/constants/session-store'; import { SocketGet, @@ -27,14 +32,42 @@ import ChannelHandler from './lib/Handler'; import { ChannelName } from './types'; @Injectable() -export class ChannelService { +export class ChannelService implements OnApplicationBootstrap { private registry: Map> = new Map(); constructor( private readonly logger: LoggerService, + private readonly settingService: SettingService, private readonly subscriberService: SubscriberService, ) {} + async onApplicationBootstrap(): Promise { + await this.cleanup(); + } + + /** + * Cleanups the unregisterd channels from settings. + * + */ + async cleanup(): Promise { + const activePlugins = this.getAll().map((handler) => + handler.getNamespace(), + ); + + const channelSettings = (await this.settingService.getAllGroups()).filter( + (group) => group.split('_').pop() === 'channel', + ) as HyphenToUnderscore[]; + + const orphanSettings = channelSettings.filter( + (group) => !activePlugins.includes(group), + ); + + for (const group of orphanSettings) { + this.logger.log(`Deleting orphaned settings for ${group}...`); + await this.settingService.deleteGroup(group); + } + } + /** * Registers a channel with a specific handler. * diff --git a/api/src/extra/cleanup.service.ts b/api/src/extra/cleanup.service.ts deleted file mode 100644 index 9c40468b1..000000000 --- a/api/src/extra/cleanup.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright © 2025 Hexastack. All rights reserved. - * - * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: - * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. - * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). - */ - -import { Injectable, OnModuleInit } from '@nestjs/common'; - -import { ChannelService } from '@/channel/channel.service'; -import { HelperService } from '@/helper/helper.service'; -import { LoggerService } from '@/logger/logger.service'; -import { PluginService } from '@/plugins/plugins.service'; -import { SettingService } from '@/setting/services/setting.service'; - -@Injectable() -export class CleanupService implements OnModuleInit { - constructor( - private readonly helperService: HelperService, - private readonly pluginService: PluginService, - private readonly channelService: ChannelService, - private readonly settingService: SettingService, - private readonly logger: LoggerService, - ) {} - - async onModuleInit() { - await this.cleanupOrphanedSettings(); - } - - private async cleanupOrphanedSettings() { - enum ExtensionType { - 'helper', - 'plugin', - 'channel', - } - const activeExtensions = [ - ...this.getHelperNames(), - ...this.getPluginNames(), - ...this.getChannelNames(), - ]; - - this.logger.debug('Cleaning up orphaned settings...'); - - const settingGroups = await this.settingService.getAllGroups(); - - for (const group of settingGroups) { - const extensionType = group.split('_').pop() ?? ''; - debugger; - if (extensionType in ExtensionType && !activeExtensions.includes(group)) { - this.logger.debug(`Deleting orphaned settings for ${group}...`); - await this.settingService.deleteGroup(group); - } - } - } - - private getHelperNames(): string[] { - return this.helperService.getAll().map((handler) => handler.getNamespace()); - } - - private getPluginNames(): string[] { - return this.pluginService.getAll().map((handler) => handler.getNamespace()); - } - - private getChannelNames(): string[] { - return this.channelService - .getAll() - .map((handler) => handler.getNamespace()); - } -} diff --git a/api/src/extra/index.ts b/api/src/extra/index.ts index 1c6f0b7d0..d773e1360 100644 --- a/api/src/extra/index.ts +++ b/api/src/extra/index.ts @@ -1,18 +1,9 @@ /* - * Copyright © 2025 Hexastack. All rights reserved. + * Copyright © 2024 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Module } from '@nestjs/common'; - -import { CleanupService } from './cleanup.service'; - -@Module({ - providers: [CleanupService], -}) -class CleanupModule {} - -export default [CleanupModule]; +export default []; diff --git a/api/src/helper/helper.service.ts b/api/src/helper/helper.service.ts index db0a88008..285784923 100644 --- a/api/src/helper/helper.service.ts +++ b/api/src/helper/helper.service.ts @@ -6,7 +6,11 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + InternalServerErrorException, + OnApplicationBootstrap, +} from '@nestjs/common'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; @@ -15,7 +19,7 @@ import BaseHelper from './lib/base-helper'; import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types'; @Injectable() -export class HelperService { +export class HelperService implements OnApplicationBootstrap { private registry: HelperRegistry = new Map(); constructor( @@ -28,6 +32,33 @@ export class HelperService { }); } + async onApplicationBootstrap(): Promise { + await this.cleanup(); + } + + /** + * Cleanups the unregisterd helpers from settings. + * + */ + async cleanup(): Promise { + const activeHelpers = this.getAll().map((handler) => + handler.getNamespace(), + ); + + const helperSettings = (await this.settingService.getAllGroups()).filter( + (group) => group.split('_').pop() === 'helper', + ) as HyphenToUnderscore[]; + + const orphanSettings = helperSettings.filter( + (group) => !activeHelpers.includes(group), + ); + + for (const group of orphanSettings) { + this.logger.log(`Deleting orphaned settings for ${group}...`); + await this.settingService.deleteGroup(group); + } + } + /** * Registers a helper. * diff --git a/api/src/plugins/plugins.service.spec.ts b/api/src/plugins/plugins.service.spec.ts index bb9f42e4f..ba34d0c1d 100644 --- a/api/src/plugins/plugins.service.spec.ts +++ b/api/src/plugins/plugins.service.spec.ts @@ -9,17 +9,27 @@ import { Test } from '@nestjs/testing'; import { LoggerModule } from '@/logger/logger.module'; +import { SettingService } from '@/setting/services/setting.service'; import { DummyPlugin } from '@/utils/test/dummy/dummy.plugin'; import { BaseBlockPlugin } from './base-block-plugin'; import { PluginService } from './plugins.service'; import { PluginType } from './types'; +// Mock services +const mockSettingService = { + get: jest.fn(), +} as unknown as SettingService; + describe('PluginsService', () => { let pluginsService: PluginService; beforeAll(async () => { const module = await Test.createTestingModule({ - providers: [PluginService, DummyPlugin], + providers: [ + PluginService, + DummyPlugin, + { provide: SettingService, useValue: mockSettingService }, + ], imports: [LoggerModule], }).compile(); pluginsService = module.get(PluginService); diff --git a/api/src/plugins/plugins.service.ts b/api/src/plugins/plugins.service.ts index 8a2667c94..51edc39e7 100644 --- a/api/src/plugins/plugins.service.ts +++ b/api/src/plugins/plugins.service.ts @@ -6,7 +6,14 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + InternalServerErrorException, + OnApplicationBootstrap, +} from '@nestjs/common'; + +import { LoggerService } from '@/logger/logger.service'; +import { SettingService } from '@/setting/services/setting.service'; import { BasePlugin } from './base-plugin.service'; import { PluginInstance } from './map-types'; @@ -26,7 +33,9 @@ import { PluginName, PluginType } from './types'; * @typeparam T - The plugin type, which extends from `BasePlugin`. By default, it uses `BaseBlockPlugin`. */ @Injectable() -export class PluginService { +export class PluginService + implements OnApplicationBootstrap +{ /** * The registry of plugins, stored as a map where the first key is the type of plugin, * the second key is the name of the plugin and the value is a plugin of type `T`. @@ -35,7 +44,37 @@ export class PluginService { Object.keys(PluginType).map((t) => [t as PluginType, new Map()]), ); - constructor() {} + constructor( + private readonly settingService: SettingService, + private readonly logger: LoggerService, + ) {} + + async onApplicationBootstrap(): Promise { + await this.cleanup(); + } + + /** + * Cleanups the unregisterd plugins from settings. + * + */ + async cleanup(): Promise { + const activePlugins = this.getAll().map((handler) => + handler.getNamespace(), + ); + + const pluginSettings = (await this.settingService.getAllGroups()).filter( + (group) => group.split('_').pop() === 'plugin', + ) as HyphenToUnderscore[]; + + const orphanSettings = pluginSettings.filter( + (group) => !activePlugins.includes(group), + ); + + for (const group of orphanSettings) { + this.logger.log(`Deleting orphaned settings for ${group}...`); + await this.settingService.deleteGroup(group); + } + } /** * Registers a plugin with a given name. From c3a47f56c3c7f73fb427f2d2f28ebbd0d483ea33 Mon Sep 17 00:00:00 2001 From: Yassine Date: Fri, 31 Jan 2025 11:51:39 +0100 Subject: [PATCH 4/4] feat: generic extension service --- api/src/channel/channel.service.ts | 43 +++++--------- api/src/helper/helper.service.ts | 41 ++++---------- api/src/plugins/plugins.service.ts | 47 +++++----------- api/src/setting/services/setting.service.ts | 16 ++++-- api/src/utils/generics/extension-service.ts | 62 +++++++++++++++++++++ 5 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 api/src/utils/generics/extension-service.ts diff --git a/api/src/channel/channel.service.ts b/api/src/channel/channel.service.ts index 3ddf04082..9e414d7b1 100644 --- a/api/src/channel/channel.service.ts +++ b/api/src/channel/channel.service.ts @@ -6,11 +6,7 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { - Injectable, - OnApplicationBootstrap, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Request, Response } from 'express'; import { SubscriberService } from '@/chat/services/subscriber.service'; @@ -19,6 +15,7 @@ import { WEB_CHANNEL_NAME } from '@/extensions/channels/web/settings'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; import { getSessionStore } from '@/utils/constants/session-store'; +import { ExtensionService } from '@/utils/generics/extension-service'; import { SocketGet, SocketPost, @@ -32,40 +29,26 @@ import ChannelHandler from './lib/Handler'; import { ChannelName } from './types'; @Injectable() -export class ChannelService implements OnApplicationBootstrap { +export class ChannelService extends ExtensionService< + ChannelHandler +> { private registry: Map> = new Map(); constructor( - private readonly logger: LoggerService, - private readonly settingService: SettingService, + protected readonly logger: LoggerService, + protected readonly settingService: SettingService, private readonly subscriberService: SubscriberService, - ) {} - - async onApplicationBootstrap(): Promise { - await this.cleanup(); + ) { + super(settingService, logger); } /** - * Cleanups the unregisterd channels from settings. + * Retrieves the type of extension this service manages. * + * @returns The type of extension this service manages. */ - async cleanup(): Promise { - const activePlugins = this.getAll().map((handler) => - handler.getNamespace(), - ); - - const channelSettings = (await this.settingService.getAllGroups()).filter( - (group) => group.split('_').pop() === 'channel', - ) as HyphenToUnderscore[]; - - const orphanSettings = channelSettings.filter( - (group) => !activePlugins.includes(group), - ); - - for (const group of orphanSettings) { - this.logger.log(`Deleting orphaned settings for ${group}...`); - await this.settingService.deleteGroup(group); - } + public getExtensionType(): 'channel' { + return 'channel'; } /** diff --git a/api/src/helper/helper.service.ts b/api/src/helper/helper.service.ts index 285784923..e4a6fd432 100644 --- a/api/src/helper/helper.service.ts +++ b/api/src/helper/helper.service.ts @@ -6,57 +6,38 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { - Injectable, - InternalServerErrorException, - OnApplicationBootstrap, -} from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; +import { ExtensionService } from '@/utils/generics/extension-service'; import BaseHelper from './lib/base-helper'; import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types'; @Injectable() -export class HelperService implements OnApplicationBootstrap { +export class HelperService extends ExtensionService { private registry: HelperRegistry = new Map(); constructor( - private readonly settingService: SettingService, - private readonly logger: LoggerService, + protected readonly settingService: SettingService, + protected readonly logger: LoggerService, ) { + super(settingService, logger); + // Init empty registry Object.values(HelperType).forEach((type: HelperType) => { this.registry.set(type, new Map()); }); } - async onApplicationBootstrap(): Promise { - await this.cleanup(); - } - /** - * Cleanups the unregisterd helpers from settings. + * Retrieves the type of extension this service manages. * + * @returns The type of extension this service manages. */ - async cleanup(): Promise { - const activeHelpers = this.getAll().map((handler) => - handler.getNamespace(), - ); - - const helperSettings = (await this.settingService.getAllGroups()).filter( - (group) => group.split('_').pop() === 'helper', - ) as HyphenToUnderscore[]; - - const orphanSettings = helperSettings.filter( - (group) => !activeHelpers.includes(group), - ); - - for (const group of orphanSettings) { - this.logger.log(`Deleting orphaned settings for ${group}...`); - await this.settingService.deleteGroup(group); - } + protected getExtensionType(): 'helper' { + return 'helper'; } /** diff --git a/api/src/plugins/plugins.service.ts b/api/src/plugins/plugins.service.ts index 51edc39e7..c3a700a44 100644 --- a/api/src/plugins/plugins.service.ts +++ b/api/src/plugins/plugins.service.ts @@ -1,19 +1,16 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { - Injectable, - InternalServerErrorException, - OnApplicationBootstrap, -} from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; +import { ExtensionService } from '@/utils/generics/extension-service'; import { BasePlugin } from './base-plugin.service'; import { PluginInstance } from './map-types'; @@ -33,9 +30,9 @@ import { PluginName, PluginType } from './types'; * @typeparam T - The plugin type, which extends from `BasePlugin`. By default, it uses `BaseBlockPlugin`. */ @Injectable() -export class PluginService - implements OnApplicationBootstrap -{ +export class PluginService< + T extends BasePlugin = BasePlugin, +> extends ExtensionService { /** * The registry of plugins, stored as a map where the first key is the type of plugin, * the second key is the name of the plugin and the value is a plugin of type `T`. @@ -45,35 +42,19 @@ export class PluginService ); constructor( - private readonly settingService: SettingService, - private readonly logger: LoggerService, - ) {} - - async onApplicationBootstrap(): Promise { - await this.cleanup(); + protected readonly settingService: SettingService, + protected readonly logger: LoggerService, + ) { + super(settingService, logger); } /** - * Cleanups the unregisterd plugins from settings. + * Retrieves the type of extension this service manages. * + * @returns The type of extension this service manages. */ - async cleanup(): Promise { - const activePlugins = this.getAll().map((handler) => - handler.getNamespace(), - ); - - const pluginSettings = (await this.settingService.getAllGroups()).filter( - (group) => group.split('_').pop() === 'plugin', - ) as HyphenToUnderscore[]; - - const orphanSettings = pluginSettings.filter( - (group) => !activePlugins.includes(group), - ); - - for (const group of orphanSettings) { - this.logger.log(`Deleting orphaned settings for ${group}...`); - await this.settingService.deleteGroup(group); - } + public getExtensionType(): 'plugin' { + return 'plugin'; } /** diff --git a/api/src/setting/services/setting.service.ts b/api/src/setting/services/setting.service.ts index 8d9c69b73..1c37f08d1 100644 --- a/api/src/setting/services/setting.service.ts +++ b/api/src/setting/services/setting.service.ts @@ -20,6 +20,7 @@ import { } from '@/utils/constants/cache'; import { Cacheable } from '@/utils/decorators/cacheable.decorator'; import { BaseService } from '@/utils/generics/base-service'; +import { ExtensionName } from '@/utils/types/extension'; import { SettingCreateDto } from '../dto/setting.dto'; import { SettingRepository } from '../repositories/setting.repository'; @@ -110,18 +111,25 @@ export class SettingService extends BaseService { } /** - * Retrieves a list of all setting groups. + * + * Retrieves a list of all setting groups for a specific extension type. + * + * @param extensionType - The extension type to filter by. * * @returns A promise that resolves to a list of setting groups. */ - async getAllGroups(): Promise { - const settings = await this.findAll(); + async getExtensionSettings( + extensionType: 'channel' | 'plugin' | 'helper', + ): Promise[]> { + const settings = await this.find({ + group: { $regex: `_${extensionType}$` }, + }); const groups = settings.reduce((acc, setting) => { if (!acc.includes(setting.group)) { acc.push(setting.group); } return acc; - }, []); + }, []) as HyphenToUnderscore[]; return groups; } diff --git a/api/src/utils/generics/extension-service.ts b/api/src/utils/generics/extension-service.ts new file mode 100644 index 000000000..6d9b537f2 --- /dev/null +++ b/api/src/utils/generics/extension-service.ts @@ -0,0 +1,62 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { OnApplicationBootstrap } from '@nestjs/common'; + +import { LoggerService } from '@/logger/logger.service'; +import { SettingService } from '@/setting/services/setting.service'; + +export abstract class ExtensionService< + T extends { getNamespace(): string; getName(): string }, +> implements OnApplicationBootstrap +{ + constructor( + protected readonly settingService: SettingService, + protected readonly logger: LoggerService, + ) {} + + async onApplicationBootstrap(): Promise { + await this.cleanup(this.getExtensionType()); + } + + /** + * Cleanups the unregistered extensions from settings. + * + * @param extensionType - The type of extension (e.g., 'plugin', 'helper', 'channel'). + */ + async cleanup(extensionType: 'channel' | 'plugin' | 'helper'): Promise { + const activeExtensions = this.getAll().map((handler) => + handler.getNamespace(), + ); + + const orphanSettings = ( + await this.settingService.getExtensionSettings(extensionType) + ).filter((group) => !activeExtensions.includes(group)); + + await Promise.all( + orphanSettings.map(async (group) => { + this.logger.log(`Deleting orphaned settings for ${group}...`); + return this.settingService.deleteGroup(group); + }), + ); + } + + /** + * Retrieves all registered extensions as an array. + * + * @returns An array containing all the registered extensions. + */ + public abstract getAll(): T[]; + + /** + * Abstract method to get the type of extension this service manages. + * + * @returns The type of extension (e.g., 'plugin', 'helper', 'channel'). + */ + protected abstract getExtensionType(): 'channel' | 'plugin' | 'helper'; +}