Merge branch 'main' into 48-request-context-vars-permanent-option

This commit is contained in:
medtaher
2024-09-28 15:01:29 +01:00
131 changed files with 3087 additions and 989 deletions

View File

@@ -23,7 +23,6 @@ import { ContextVarController } from './controllers/context-var.controller';
import { LabelController } from './controllers/label.controller';
import { MessageController } from './controllers/message.controller';
import { SubscriberController } from './controllers/subscriber.controller';
import { TranslationController } from './controllers/translation.controller';
import { BlockRepository } from './repositories/block.repository';
import { CategoryRepository } from './repositories/category.repository';
import { ContextVarRepository } from './repositories/context-var.repository';
@@ -31,7 +30,6 @@ import { ConversationRepository } from './repositories/conversation.repository';
import { LabelRepository } from './repositories/label.repository';
import { MessageRepository } from './repositories/message.repository';
import { SubscriberRepository } from './repositories/subscriber.repository';
import { TranslationRepository } from './repositories/translation.repository';
import { BlockModel } from './schemas/block.schema';
import { CategoryModel } from './schemas/category.schema';
import { ContextVarModel } from './schemas/context-var.schema';
@@ -39,10 +37,8 @@ import { ConversationModel } from './schemas/conversation.schema';
import { LabelModel } from './schemas/label.schema';
import { MessageModel } from './schemas/message.schema';
import { SubscriberModel } from './schemas/subscriber.schema';
import { TranslationModel } from './schemas/translation.schema';
import { CategorySeeder } from './seeds/category.seed';
import { ContextVarSeeder } from './seeds/context-var.seed';
import { TranslationSeeder } from './seeds/translation.seed';
import { BlockService } from './services/block.service';
import { BotService } from './services/bot.service';
import { CategoryService } from './services/category.service';
@@ -52,7 +48,6 @@ import { ConversationService } from './services/conversation.service';
import { LabelService } from './services/label.service';
import { MessageService } from './services/message.service';
import { SubscriberService } from './services/subscriber.service';
import { TranslationService } from './services/translation.service';
@Module({
imports: [
@@ -63,7 +58,6 @@ import { TranslationService } from './services/translation.service';
BlockModel,
MessageModel,
SubscriberModel,
TranslationModel,
ConversationModel,
SubscriberModel,
]),
@@ -81,7 +75,6 @@ import { TranslationService } from './services/translation.service';
BlockController,
MessageController,
SubscriberController,
TranslationController,
],
providers: [
CategoryRepository,
@@ -90,7 +83,6 @@ import { TranslationService } from './services/translation.service';
BlockRepository,
MessageRepository,
SubscriberRepository,
TranslationRepository,
ConversationRepository,
CategoryService,
ContextVarService,
@@ -98,13 +90,11 @@ import { TranslationService } from './services/translation.service';
BlockService,
MessageService,
SubscriberService,
TranslationService,
CategorySeeder,
ContextVarSeeder,
ConversationService,
ChatService,
BotService,
TranslationSeeder,
],
exports: [SubscriberService, MessageService, LabelService, BlockService],
})

View File

@@ -19,7 +19,10 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { ContentRepository } from '@/cms/repositories/content.repository';
import { ContentModel } from '@/cms/schemas/content.schema';
import { ContentService } from '@/cms/services/content.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { SettingService } from '@/setting/services/setting.service';
@@ -86,6 +89,7 @@ describe('BlockController', () => {
UserModel,
RoleModel,
PermissionModel,
LanguageModel,
]),
],
providers: [
@@ -97,6 +101,7 @@ describe('BlockController', () => {
UserRepository,
RoleRepository,
PermissionRepository,
LanguageRepository,
BlockService,
LabelService,
CategoryService,
@@ -105,10 +110,11 @@ describe('BlockController', () => {
UserService,
RoleService,
PermissionService,
LanguageService,
PluginService,
LoggerService,
{
provide: ExtendedI18nService,
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
},

View File

@@ -18,7 +18,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { ContentRepository } from '@/cms/repositories/content.repository';
import { ContentModel } from '@/cms/schemas/content.schema';
import { ContentService } from '@/cms/services/content.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { I18nService } from '@/i18n/services/i18n.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { SettingService } from '@/setting/services/setting.service';
@@ -77,7 +77,7 @@ describe('CategoryController', () => {
},
LoggerService,
{
provide: ExtendedI18nService,
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
},

View File

@@ -19,7 +19,7 @@ import { ChannelService } from '@/channel/channel.service';
import { MenuRepository } from '@/cms/repositories/menu.repository';
import { MenuModel } from '@/cms/schemas/menu.schema';
import { MenuService } from '@/cms/services/menu.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { I18nService } from '@/i18n/services/i18n.service';
import { LoggerService } from '@/logger/logger.service';
import { NlpService } from '@/nlp/services/nlp.service';
import { SettingService } from '@/setting/services/setting.service';
@@ -92,7 +92,7 @@ describe('MessageController', () => {
MenuService,
MenuRepository,
{
provide: ExtendedI18nService,
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
},

View File

@@ -1,205 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { ChannelService } from '@/channel/channel.service';
import { ContentRepository } from '@/cms/repositories/content.repository';
import { MenuRepository } from '@/cms/repositories/menu.repository';
import { ContentModel } from '@/cms/schemas/content.schema';
import { MenuModel } from '@/cms/schemas/menu.schema';
import { ContentService } from '@/cms/services/content.service';
import { MenuService } from '@/cms/services/menu.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { LoggerService } from '@/logger/logger.service';
import { NlpService } from '@/nlp/services/nlp.service';
import { PluginService } from '@/plugins/plugins.service';
import { SettingService } from '@/setting/services/setting.service';
import { NOT_FOUND_ID } from '@/utils/constants/mock';
import {
installTranslationFixtures,
translationFixtures,
} from '@/utils/test/fixtures/translation';
import { getPageQuery } from '@/utils/test/pagination';
import {
closeInMongodConnection,
rootMongooseTestModule,
} from '@/utils/test/test';
import { MessageController } from './message.controller';
import { TranslationController } from './translation.controller';
import { TranslationUpdateDto } from '../dto/translation.dto';
import { BlockRepository } from '../repositories/block.repository';
import { MessageRepository } from '../repositories/message.repository';
import { SubscriberRepository } from '../repositories/subscriber.repository';
import { TranslationRepository } from '../repositories/translation.repository';
import { BlockModel } from '../schemas/block.schema';
import { MessageModel } from '../schemas/message.schema';
import { SubscriberModel } from '../schemas/subscriber.schema';
import { Translation, TranslationModel } from '../schemas/translation.schema';
import { BlockService } from '../services/block.service';
import { MessageService } from '../services/message.service';
import { SubscriberService } from '../services/subscriber.service';
import { TranslationService } from '../services/translation.service';
describe('TranslationController', () => {
let translationController: TranslationController;
let translationService: TranslationService;
let translation: Translation;
beforeAll(async () => {
const module = await Test.createTestingModule({
controllers: [MessageController],
imports: [
rootMongooseTestModule(installTranslationFixtures),
MongooseModule.forFeature([
SubscriberModel,
TranslationModel,
MessageModel,
AttachmentModel,
MenuModel,
BlockModel,
ContentModel,
]),
],
providers: [
TranslationController,
TranslationService,
TranslationRepository,
MessageService,
MessageRepository,
SubscriberService,
SubscriberRepository,
ChannelService,
AttachmentService,
AttachmentRepository,
MenuService,
MenuRepository,
{
provide: NlpService,
useValue: {
getNLP: jest.fn(() => undefined),
},
},
{
provide: SettingService,
useValue: {
getConfig: jest.fn(() => ({
chatbot: { lang: { default: 'fr' } },
})),
getSettings: jest.fn(() => ({})),
},
},
BlockService,
BlockRepository,
ContentService,
ContentRepository,
{
provide: PluginService,
useValue: {},
},
EventEmitter2,
LoggerService,
{
provide: ExtendedI18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
initDynamicTranslations: jest.fn(),
},
},
{
provide: CACHE_MANAGER,
useValue: {
del: jest.fn(),
get: jest.fn(),
set: jest.fn(),
},
},
LoggerService,
],
}).compile();
translationService = module.get<TranslationService>(TranslationService);
translationController = module.get<TranslationController>(
TranslationController,
);
translation = await translationService.findOne({ str: 'Welcome' });
});
afterEach(jest.clearAllMocks);
afterAll(closeInMongodConnection);
describe('count', () => {
it('should count translations', async () => {
jest.spyOn(translationService, 'count');
const result = await translationController.filterCount();
expect(translationService.count).toHaveBeenCalled();
expect(result).toEqual({ count: translationFixtures.length });
});
});
describe('findOne', () => {
it('should find one translation by id', async () => {
jest.spyOn(translationService, 'findOne');
const result = await translationController.findOne(translation.id);
expect(translationService.findOne).toHaveBeenCalledWith(translation.id);
expect(result).toEqualPayload(
translationFixtures.find(({ str }) => str === translation.str),
);
});
});
describe('findPage', () => {
const pageQuery = getPageQuery<Translation>();
it('should find translations', async () => {
jest.spyOn(translationService, 'findPage');
const result = await translationController.findPage(pageQuery, {});
expect(translationService.findPage).toHaveBeenCalledWith({}, pageQuery);
expect(result).toEqualPayload(translationFixtures);
});
});
describe('updateOne', () => {
const translationUpdateDto: TranslationUpdateDto = {
str: 'Welcome !',
};
it('should update one translation by id', async () => {
jest.spyOn(translationService, 'updateOne');
const result = await translationController.updateOne(
translation.id,
translationUpdateDto,
);
expect(translationService.updateOne).toHaveBeenCalledWith(
translation.id,
translationUpdateDto,
);
expect(result).toEqualPayload({
...translationFixtures.find(({ str }) => str === translation.str),
...translationUpdateDto,
});
});
it('should throw a NotFoundException when attempting to update a translation by id', async () => {
jest.spyOn(translationService, 'updateOne');
await expect(
translationController.updateOne(NOT_FOUND_ID, translationUpdateDto),
).rejects.toThrow(NotFoundException);
});
});
});

View File

@@ -1,142 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import {
Body,
Controller,
Get,
NotFoundException,
Param,
Patch,
Query,
UseInterceptors,
Post,
} from '@nestjs/common';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
import { TFilterQuery } from 'mongoose';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
import { BaseController } from '@/utils/generics/base-controller';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { TranslationUpdateDto } from '../dto/translation.dto';
import { Translation } from '../schemas/translation.schema';
import { TranslationService } from '../services/translation.service';
@UseInterceptors(CsrfInterceptor)
@Controller('translation')
export class TranslationController extends BaseController<Translation> {
constructor(
private readonly translationService: TranslationService,
private readonly settingService: SettingService,
private readonly logger: LoggerService,
) {
super(translationService);
}
@Get()
async findPage(
@Query(PageQueryPipe) pageQuery: PageQueryDto<Translation>,
@Query(new SearchFilterPipe<Translation>({ allowedFields: ['str'] }))
filters: TFilterQuery<Translation>,
) {
return await this.translationService.findPage(filters, pageQuery);
}
/**
* Counts the filtered number of translations.
* @returns A promise that resolves to an object representing the filtered number of translations.
*/
@Get('count')
async filterCount(
@Query(
new SearchFilterPipe<Translation>({
allowedFields: ['str'],
}),
)
filters?: TFilterQuery<Translation>,
) {
return await this.count(filters);
}
@Get(':id')
async findOne(@Param('id') id: string) {
const doc = await this.translationService.findOne(id);
if (!doc) {
this.logger.warn(`Unable to find Translation by id ${id}`);
throw new NotFoundException(`Translation with ID ${id} not found`);
}
return doc;
}
@CsrfCheck(true)
@Patch(':id')
async updateOne(
@Param('id') id: string,
@Body() translationUpdate: TranslationUpdateDto,
) {
const result = await this.translationService.updateOne(
id,
translationUpdate,
);
if (!result) {
this.logger.warn(`Unable to update Translation by id ${id}`);
throw new NotFoundException(`Translation with ID ${id} not found`);
}
return result;
}
/**
* Refresh translations : Add new strings and remove old ones
* @returns {Promise<any>}
*/
@CsrfCheck(true)
@Post('refresh')
async refresh(): Promise<any> {
const settings = await this.settingService.getSettings();
const languages = settings.nlp_settings.languages;
const defaultTrans: Translation['translations'] = languages.reduce(
(acc, curr) => {
acc[curr] = '';
return acc;
},
{} as { [key: string]: string },
);
// Scan Blocks
return this.translationService
.getAllBlockStrings()
.then(async (strings: string[]) => {
const settingStrings =
await this.translationService.getSettingStrings();
// Scan global settings
strings = strings.concat(settingStrings);
// Filter unique and not empty messages
strings = strings.filter((str, pos) => {
return str && strings.indexOf(str) == pos;
});
// Perform refresh
const queue = strings.map((str) =>
this.translationService.findOneOrCreate(
{ str },
{ str, translations: defaultTrans as any, translated: 100 },
),
);
return Promise.all(queue).then(() => {
// Purge non existing translations
return this.translationService.deleteMany({
str: { $nin: strings },
});
});
});
}
}

View File

@@ -1,48 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsNotEmpty,
IsObject,
IsString,
IsOptional,
IsNumber,
} from 'class-validator';
export class TranslationCreateDto {
@ApiProperty({ description: 'Translation str', type: String })
@IsNotEmpty()
@IsString()
str: string;
@ApiProperty({ description: 'Translations', type: Object })
@IsNotEmpty()
@IsObject()
translations: Record<string, string>;
@ApiProperty({ description: 'Translated', type: Number })
@IsNotEmpty()
@IsNumber()
translated: number;
}
export class TranslationUpdateDto {
@ApiPropertyOptional({ description: 'Translation str', type: String })
@IsNotEmpty()
@IsString()
@IsOptional()
str?: string;
@ApiPropertyOptional({ description: 'Translations', type: Object })
@IsNotEmpty()
@IsObject()
@IsOptional()
translations?: Record<string, string>;
}

View File

@@ -11,6 +11,7 @@ import { Injectable, Optional } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
import { NlpSampleState } from '@/nlp/schemas/types';
@@ -36,10 +37,13 @@ export class MessageRepository extends BaseRepository<
private readonly logger: LoggerService;
private readonly languageService: LanguageService;
constructor(
@InjectModel(Message.name) readonly model: Model<AnyMessage>,
@Optional() nlpSampleService?: NlpSampleService,
@Optional() logger?: LoggerService,
@Optional() languageService?: LanguageService,
) {
super(
model,
@@ -49,6 +53,7 @@ export class MessageRepository extends BaseRepository<
);
this.logger = logger;
this.nlpSampleService = nlpSampleService;
this.languageService = languageService;
}
/**
@@ -72,10 +77,13 @@ export class MessageRepository extends BaseRepository<
'message' in _doc &&
'text' in _doc.message
) {
const defaultLang = await this.languageService?.getDefaultLanguage();
const record: NlpSampleCreateDto = {
text: _doc.message.text,
type: NlpSampleState.inbox,
trained: false,
// @TODO : We need to define the language in the message entity
language: defaultLang.id,
};
try {
await this.nlpSampleService.findOneOrCreate(record, record);

View File

@@ -1,77 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, Types } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { Translation } from '../schemas/translation.schema';
@Injectable()
export class TranslationRepository extends BaseRepository<Translation> {
constructor(
@InjectModel(Translation.name) readonly model: Model<Translation>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Translation);
}
/**
* Emits an event after a translation document is updated.
*
* @param query - The query object representing the update operation.
* @param updated - The updated translation document.
*/
async postUpdate(
_query: Query<
Document<Translation, any, any>,
Document<Translation, any, any>,
unknown,
Translation,
'findOneAndUpdate'
>,
_updated: Translation,
) {
this.eventEmitter.emit('hook:translation:update');
}
/**
* Emits an event after a new translation document is created.
*
* @param created - The newly created translation document.
*/
async postCreate(
_created: Document<unknown, unknown, Translation> &
Translation & { _id: Types.ObjectId },
) {
this.eventEmitter.emit('hook:translation:create');
}
/**
* Emits an event after a translation document is deleted.
*
* @param query - The query object representing the delete operation.
* @param result - The result of the delete operation.
*/
async postDelete(
_query: Query<
DeleteResult,
Document<Translation, any, any>,
unknown,
Translation,
'deleteOne' | 'deleteMany'
>,
_result: DeleteResult,
) {
this.eventEmitter.emit('hook:translation:delete');
}
}

View File

@@ -1,43 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
@Schema({ timestamps: true })
export class Translation extends BaseSchema {
@Prop({
type: String,
required: true,
unique: true,
})
str: string;
@Prop({
type: Object,
required: true,
})
translations: Record<string, string>;
@Prop({
type: Number,
})
translated: number;
}
export const TranslationModel: ModelDefinition = {
name: Translation.name,
schema: SchemaFactory.createForClass(Translation),
};
export type TranslationDocument = THydratedDocument<Translation>;
export default TranslationModel.schema;

View File

@@ -1,21 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { TranslationCreateDto } from '../dto/translation.dto';
export const translationModels: TranslationCreateDto[] = [
{
str: 'Welcome',
translations: {
en: 'Welcome',
fr: 'Bienvenue',
},
translated: 100,
},
];

View File

@@ -1,22 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { TranslationRepository } from '../repositories/translation.repository';
import { Translation } from '../schemas/translation.schema';
@Injectable()
export class TranslationSeeder extends BaseSeeder<Translation> {
constructor(private readonly translationRepository: TranslationRepository) {
super(translationRepository);
}
}

View File

@@ -7,6 +7,7 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
@@ -24,11 +25,14 @@ import { ContentTypeModel } from '@/cms/schemas/content-type.schema';
import { Content, ContentModel } from '@/cms/schemas/content.schema';
import { ContentTypeService } from '@/cms/services/content-type.service';
import { ContentService } from '@/cms/services/content.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import OfflineHandler from '@/extensions/channels/offline/index.channel';
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
import { Offline } from '@/extensions/channels/offline/types';
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { Settings } from '@/setting/schemas/types';
@@ -94,6 +98,7 @@ describe('BlockService', () => {
ContentModel,
AttachmentModel,
LabelModel,
LanguageModel,
]),
],
providers: [
@@ -102,18 +107,20 @@ describe('BlockService', () => {
ContentTypeRepository,
ContentRepository,
AttachmentRepository,
LanguageRepository,
BlockService,
CategoryService,
ContentTypeService,
ContentService,
AttachmentService,
LanguageService,
{
provide: PluginService,
useValue: {},
},
LoggerService,
{
provide: ExtendedI18nService,
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => {
return t === 'Welcome' ? 'Bienvenue' : t;
@@ -132,6 +139,14 @@ describe('BlockService', () => {
},
},
EventEmitter2,
{
provide: CACHE_MANAGER,
useValue: {
del: jest.fn(),
get: jest.fn(),
set: jest.fn(),
},
},
],
}).compile();
blockService = module.get<BlockService>(BlockService);

View File

@@ -13,7 +13,8 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import EventWrapper from '@/channel/lib/EventWrapper';
import { ContentService } from '@/cms/services/content.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { Nlp } from '@/nlp/lib/types';
import { PluginService } from '@/plugins/plugins.service';
@@ -44,7 +45,8 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
private readonly settingService: SettingService,
private readonly pluginService: PluginService,
private readonly logger: LoggerService,
protected readonly i18n: ExtendedI18nService,
protected readonly i18n: I18nService,
protected readonly languageService: LanguageService,
) {
super(repository);
}
@@ -109,12 +111,9 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
// Check & catch user language through NLP
const nlp = event.getNLP();
if (nlp) {
const settings = await this.settingService.getSettings();
const languages = await this.languageService.getLanguages();
const lang = nlp.entities.find((e) => e.entity === 'language');
if (
lang &&
settings.nlp_settings.languages.indexOf(lang.value) !== -1
) {
if (lang && Object.keys(languages).indexOf(lang.value) !== -1) {
const profile = event.getSender();
profile.language = lang.value;
event.setSender(profile);
@@ -372,12 +371,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
subscriberContext: SubscriberContext,
settings: Settings,
): string {
const lang =
context && context.user && context.user.language
? context.user.language
: settings.nlp_settings.default_lang;
// Translate
text = this.i18n.t(text, { lang, defaultValue: text });
text = this.i18n.t(text, {
lang: context.user.language,
defaultValue: text,
});
// Replace context tokens
text = this.processTokenReplacements(
text,

View File

@@ -25,10 +25,13 @@ import { MenuModel } from '@/cms/schemas/menu.schema';
import { ContentTypeService } from '@/cms/services/content-type.service';
import { ContentService } from '@/cms/services/content.service';
import { MenuService } from '@/cms/services/menu.service';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { offlineEventText } from '@/extensions/channels/offline/__test__/events.mock';
import OfflineHandler from '@/extensions/channels/offline/index.channel';
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { NlpEntityRepository } from '@/nlp/repositories/nlp-entity.repository';
import { NlpSampleEntityRepository } from '@/nlp/repositories/nlp-sample-entity.repository';
@@ -111,6 +114,7 @@ describe('BlockService', () => {
NlpSampleEntityModel,
NlpSampleModel,
ContextVarModel,
LanguageModel,
]),
],
providers: [
@@ -130,6 +134,7 @@ describe('BlockService', () => {
NlpEntityRepository,
NlpSampleEntityRepository,
NlpSampleRepository,
LanguageRepository,
BlockService,
CategoryService,
ContentTypeService,
@@ -149,13 +154,14 @@ describe('BlockService', () => {
NlpService,
ContextVarService,
ContextVarRepository,
LanguageService,
{
provide: PluginService,
useValue: {},
},
LoggerService,
{
provide: ExtendedI18nService,
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
},

View File

@@ -1,140 +0,0 @@
/*
* 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).
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service';
import { BlockService } from './block.service';
import { TranslationRepository } from '../repositories/translation.repository';
import { Block } from '../schemas/block.schema';
import { Translation } from '../schemas/translation.schema';
@Injectable()
export class TranslationService extends BaseService<Translation> {
constructor(
readonly repository: TranslationRepository,
private readonly blockService: BlockService,
private readonly settingService: SettingService,
private readonly i18n: ExtendedI18nService,
) {
super(repository);
this.resetI18nTranslations();
}
public async resetI18nTranslations() {
const translations = await this.findAll();
this.i18n.initDynamicTranslations(translations);
}
/**
* Return any available string inside a given block (message, button titles, fallback messages, ...)
*
* @param block - The block to parse
*
* @returns An array of strings
*/
getBlockStrings(block: Block): string[] {
let strings: string[] = [];
if (Array.isArray(block.message)) {
// Text Messages
strings = strings.concat(block.message);
} else if (typeof block.message === 'object') {
if ('plugin' in block.message) {
// plugin
Object.values(block.message.args).forEach((arg) => {
if (Array.isArray(arg)) {
// array of text
strings = strings.concat(arg);
} else if (typeof arg === 'string') {
// text
strings.push(arg);
}
});
} else if ('text' in block.message && Array.isArray(block.message.text)) {
// array of text
strings = strings.concat(block.message.text);
} else if (
'text' in block.message &&
typeof block.message.text === 'string'
) {
// text
strings.push(block.message.text);
}
if (
'quickReplies' in block.message &&
Array.isArray(block.message.quickReplies) &&
block.message.quickReplies.length > 0
) {
// Quick replies
strings = strings.concat(
block.message.quickReplies.map((qr) => qr.title),
);
} else if (
'buttons' in block.message &&
Array.isArray(block.message.buttons) &&
block.message.buttons.length > 0
) {
// Buttons
strings = strings.concat(block.message.buttons.map((btn) => btn.title));
}
}
// Add fallback messages
if (
'fallback' in block.options &&
block.options.fallback &&
'message' in block.options.fallback &&
Array.isArray(block.options.fallback.message)
) {
strings = strings.concat(block.options.fallback.message);
}
return strings;
}
/**
* Return any available string inside a block (message, button titles, fallback messages, ...)
*
* @returns A promise of all strings available in a array
*/
async getAllBlockStrings(): Promise<string[]> {
const blocks = await this.blockService.find({});
if (blocks.length === 0) {
return [];
}
return blocks.reduce((acc, block) => {
const strings = this.getBlockStrings(block);
return acc.concat(strings);
}, [] as string[]);
}
/**
* Return any available strings in settings
*
* @returns A promise of all strings available in a array
*/
async getSettingStrings(): Promise<string[]> {
let strings: string[] = [];
const settings = await this.settingService.getSettings();
if (settings.chatbot_settings.global_fallback) {
strings = strings.concat(settings.chatbot_settings.fallback_message);
}
return strings;
}
/**
* Updates the in-memory translations
*/
@OnEvent('hook:translation:*')
handleTranslationsUpdate() {
this.resetI18nTranslations();
}
}