From 81ef628590c55eca3c33b83424d33b06eb98c202 Mon Sep 17 00:00:00 2001 From: sogehige Date: Wed, 6 Mar 2024 13:58:16 +0100 Subject: [PATCH] feat(keywords): add response randomizer --- src/database/entity/keyword.ts | 25 +---- .../1678892044040-changeKeywordsResponses.ts | 34 ++++++ .../1678892044040-changeKeywordsResponses.ts | 35 ++++++ .../1678892044040-changeKeywordsResponses.ts | 33 ++++++ src/systems/keywords.ts | 93 +++++++--------- test/tests/cooldowns/check.js | 27 ++++- test/tests/cooldowns/check_default_values.js | 9 +- .../default_cooldown_can_be_overrided.js | 9 +- .../#4860_group_filter_and_permissions.js | 100 ++++++++---------- 9 files changed, 232 insertions(+), 133 deletions(-) create mode 100644 src/database/migration/mysql/22.x/1678892044040-changeKeywordsResponses.ts create mode 100644 src/database/migration/postgres/22.x/1678892044040-changeKeywordsResponses.ts create mode 100644 src/database/migration/sqlite/22.x/1678892044040-changeKeywordsResponses.ts diff --git a/src/database/entity/keyword.ts b/src/database/entity/keyword.ts index 755f7a205e1..a7eb8947a60 100644 --- a/src/database/entity/keyword.ts +++ b/src/database/entity/keyword.ts @@ -1,4 +1,3 @@ -import { ManyToOne, OneToMany } from 'typeorm'; import { BaseEntity, Column, Entity, Index, PrimaryColumn } from 'typeorm'; import { z } from 'zod'; @@ -23,32 +22,18 @@ export class Keyword extends BotEntity { @Column({ nullable: true, type: String }) group: string | null; - @OneToMany(() => KeywordResponses, (item) => item.keyword) - responses: KeywordResponses[]; -} + @Column({ default: false }) + areResponsesRandomized: boolean; -@Entity() -export class KeywordResponses extends BaseEntity { - @PrimaryColumn({ generated: 'uuid', type: 'uuid' }) + @Column({ type: (process.env.TYPEORM_CONNECTION ?? 'better-sqlite3') !== 'better-sqlite3' ? 'json' : 'simple-json' }) + responses: { id: string; - - @Column() order: number; - - @Column({ type: 'text' }) response: string; - - @Column() stopIfExecuted: boolean; - - @Column({ nullable: true, type: String }) permission: string | null; - - @Column() filter: string; - - @ManyToOne(() => Keyword, (item) => item.responses, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - keyword: Keyword; + }[] = []; } @Entity() diff --git a/src/database/migration/mysql/22.x/1678892044040-changeKeywordsResponses.ts b/src/database/migration/mysql/22.x/1678892044040-changeKeywordsResponses.ts new file mode 100644 index 00000000000..415c8ee9eed --- /dev/null +++ b/src/database/migration/mysql/22.x/1678892044040-changeKeywordsResponses.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +import { insertItemIntoTable } from '../../../insertItemIntoTable.js'; + +export class changeKeywordsResponses1678892044040 implements MigrationInterface { + name = 'changeKeywordsResponses1678892044040'; + + public async up(queryRunner: QueryRunner): Promise { + const items = await queryRunner.query(`SELECT * from \`keyword\``); + const items2 = await queryRunner.query(`SELECT * from \`keyword_responses\``); + + await queryRunner.query(`DELETE from \`keyword_responses\` WHERE 1=1`); + await queryRunner.query(`DELETE from \`keyword\` WHERE 1=1`); + + await queryRunner.query(`DROP TABLE \`keyword_responses\``); + await queryRunner.query(`DROP TABLE \`keyword\``); + + await queryRunner.query(`CREATE TABLE \`keyword\` (\`id\` varchar(36) NOT NULL, \`keyword\` varchar(255) NOT NULL, \`enabled\` tinyint NOT NULL, \`group\` varchar(255) NULL, \`areResponsesRandomized\` tinyint NOT NULL DEFAULT 0, \`responses\` json NOT NULL, INDEX \`IDX_35e3ff88225eef1d85c951e229\` (\`keyword\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + + for (const item of items) { + item.responses = JSON.stringify(items2.filter((o: any) => o.keywordId === item.id)); + await insertItemIntoTable('keyword', { + ...item, + }, queryRunner); + } + + return; + } + + public async down(queryRunner: QueryRunner): Promise { + return; + } + +} diff --git a/src/database/migration/postgres/22.x/1678892044040-changeKeywordsResponses.ts b/src/database/migration/postgres/22.x/1678892044040-changeKeywordsResponses.ts new file mode 100644 index 00000000000..b6551fff0c5 --- /dev/null +++ b/src/database/migration/postgres/22.x/1678892044040-changeKeywordsResponses.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +import { insertItemIntoTable } from '../../../insertItemIntoTable.js'; + +export class changeKeywordsResponses1678892044040 implements MigrationInterface { + name = 'changeKeywordsResponses1678892044040'; + + public async up(queryRunner: QueryRunner): Promise { + const items = await queryRunner.query(`SELECT * from "keyword"`); + const items2 = await queryRunner.query(`SELECT * from "keyword_responses"`); + + await queryRunner.query(`DELETE from "keyword_responses" WHERE 1=1`); + await queryRunner.query(`DELETE from "keyword" WHERE 1=1`); + + await queryRunner.query(`DROP TABLE "keyword_responses"`); + await queryRunner.query(`DROP TABLE "keyword"`); + + await queryRunner.query(`CREATE TABLE "keyword" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "areResponsesRandomized" boolean NOT NULL DEFAULT false, "keyword" character varying NOT NULL, "enabled" boolean NOT NULL, "group" character varying, "responses" json NOT NULL, CONSTRAINT "PK_affdb8c8fa5b442900cb3aa21dc" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_35e3ff88225eef1d85c951e229" ON "keyword" ("keyword") `); + + for (const item of items) { + item.responses = JSON.stringify(items2.filter((o: any) => o.keywordId === item.id)); + await insertItemIntoTable('keyword', { + ...item, + }, queryRunner); + } + + return; + } + + public async down(queryRunner: QueryRunner): Promise { + return; + } + +} diff --git a/src/database/migration/sqlite/22.x/1678892044040-changeKeywordsResponses.ts b/src/database/migration/sqlite/22.x/1678892044040-changeKeywordsResponses.ts new file mode 100644 index 00000000000..4109ae5910d --- /dev/null +++ b/src/database/migration/sqlite/22.x/1678892044040-changeKeywordsResponses.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +import { insertItemIntoTable } from '../../../insertItemIntoTable.js'; + +export class changeKeywordsResponses1678892044040 implements MigrationInterface { + name = 'changeKeywordsResponses1678892044040'; + + public async up(queryRunner: QueryRunner): Promise { + const items = await queryRunner.query(`SELECT * from "keyword"`); + const items2 = await queryRunner.query(`SELECT * from "keyword_responses"`); + + await queryRunner.query(`DELETE from "keyword_responses" WHERE 1=1`); + await queryRunner.query(`DELETE from "keyword" WHERE 1=1`); + + await queryRunner.query(`DROP TABLE "keyword_responses"`); + await queryRunner.query(`DROP TABLE "keyword"`); + + await queryRunner.query(`CREATE TABLE "keyword" ("id" varchar PRIMARY KEY NOT NULL, "areResponsesRandomized" boolean NOT NULL DEFAULT (0), "keyword" varchar NOT NULL, "enabled" boolean NOT NULL, "group" varchar, "responses" text NOT NULL)`); + await queryRunner.query(`CREATE INDEX "IDX_35e3ff88225eef1d85c951e229" ON "keyword" ("keyword") `); + + for (const item of items) { + item.responses = JSON.stringify(items2.filter((o: any) => o.keywordId === item.id)); + await insertItemIntoTable('keyword', { + ...item, + }, queryRunner); + } + } + + public async down(queryRunner: QueryRunner): Promise { + return; + } + +} diff --git a/src/systems/keywords.ts b/src/systems/keywords.ts index 70c78ae542d..39daee57b1f 100644 --- a/src/systems/keywords.ts +++ b/src/systems/keywords.ts @@ -1,7 +1,9 @@ +import { randomUUID } from 'node:crypto'; + import { - Keyword, KeywordGroup, KeywordResponses, + Keyword, KeywordGroup, } from '@entity/keyword.js'; -import _, { merge } from 'lodash-es'; +import _, { orderBy, shuffle } from 'lodash-es'; import XRegExp from 'xregexp'; import System from './_interface.js'; @@ -11,7 +13,6 @@ import { } from '../decorators.js'; import { Expects } from '../expects.js'; -import { AppDataSource } from '~/database.js'; import { checkFilter } from '~/helpers/checkFilter.js'; import { isUUID, prepare } from '~/helpers/commons/index.js'; import { @@ -40,7 +41,7 @@ class Keywords extends System { app.get('/api/systems/keywords', adminMiddleware, async (req, res) => { res.send({ - data: await Keyword.find({ relations: ['responses'] }), + data: await Keyword.find(), }); }); app.get('/api/systems/keywords/groups/', adminMiddleware, async (req, res) => { @@ -69,7 +70,7 @@ class Keywords extends System { }); app.get('/api/systems/keywords/:id', adminMiddleware, async (req, res) => { res.send({ - data: await Keyword.findOne({ where: { id: req.params.id }, relations: ['responses'] }), + data: await Keyword.findOne({ where: { id: req.params.id } }), }); }); app.delete('/api/systems/keywords/groups/:name', adminMiddleware, async (req, res) => { @@ -90,15 +91,6 @@ class Keywords extends System { app.post('/api/systems/keywords', adminMiddleware, async (req, res) => { try { const itemToSave = await Keyword.create(req.body).save(); - - await AppDataSource.getRepository(KeywordResponses).delete({ keyword: { id: itemToSave.id } }); - const responses = req.body.responses; - for (const response of responses) { - const resToSave = new KeywordResponses(); - merge(resToSave, response); - resToSave.keyword = itemToSave; - await resToSave.save(); - } res.send({ data: itemToSave }); } catch (e) { res.status(400).send({ errors: e }); @@ -142,8 +134,7 @@ class Keywords extends System { .toArray(); let kDb = await Keyword.findOne({ - relations: ['responses'], - where: { keyword: keywordRegex }, + where: { keyword: keywordRegex }, }); if (!kDb) { kDb = new Keyword(); @@ -158,14 +149,16 @@ class Keywords extends System { throw Error('Permission ' + userlevel + ' not found.'); } - const newResponse = new KeywordResponses(); - newResponse.keyword = kDb; - newResponse.order = kDb.responses.length; - newResponse.permission = pItem.id ?? defaultPermissions.VIEWERS; - newResponse.stopIfExecuted = stopIfExecuted; - newResponse.response = response; - newResponse.filter = ''; - await newResponse.save(); + kDb.responses.push({ + id: randomUUID(), + order: kDb.responses.length, + permission: pItem.id ?? defaultPermissions.VIEWERS, + stopIfExecuted: stopIfExecuted, + response: response, + filter: '', + }); + + await kDb.save(); return [{ response: prepare('keywords.keyword-was-added', kDb), ...opts, id: kDb.id, }]; @@ -204,9 +197,9 @@ class Keywords extends System { let keywords: Required[] = []; if (isUUID(keywordRegexOrUUID)) { - keywords = await Keyword.find({ where: { id: keywordRegexOrUUID }, relations: ['responses'] }); + keywords = await Keyword.find({ where: { id: keywordRegexOrUUID } }); } else { - keywords = await Keyword.find({ where: { keyword: keywordRegexOrUUID }, relations: ['responses'] }); + keywords = await Keyword.find({ where: { keyword: keywordRegexOrUUID } }); } if (keywords.length === 0) { @@ -230,7 +223,7 @@ class Keywords extends System { if (stopIfExecuted) { responseDb.stopIfExecuted = stopIfExecuted; } - await responseDb.save(); + await keyword.save(); return [{ response: prepare('keywords.keyword-was-edited', { keyword: keyword.keyword, response }), ...opts }]; } } catch (e: any) { @@ -258,8 +251,7 @@ class Keywords extends System { // print responses const keyword_with_responses = await Keyword.findOne({ - relations: ['responses'], - where: isUUID(keyword) ? { id: keyword } : { keyword }, + where: isUUID(keyword) ? { id: keyword } : { keyword }, }); if (!keyword_with_responses || keyword_with_responses.responses.length === 0) { @@ -309,28 +301,22 @@ class Keywords extends System { return [{ response: prepare('keywords.keyword-is-ambiguous'), ...opts }]; } else { const keyword = keywords[0]; - if (rId) { - const responseDb = keyword.responses.find(o => o.order === (rId - 1)); - if (!responseDb) { - return [{ response: prepare('keywords.response-was-not-found'), ...opts }]; - } - // remove and reorder - let count = 0; - for (let i = 0; i < keyword.responses.length; i++) { - const response = _.orderBy(keyword.responses, 'order', 'asc')[i]; - if (responseDb.id !== response.id) { - response.order = count; - count++; - await response.save(); - } else { - await response.remove(); - } - } - return [{ response: prepare('keywords.response-was-removed', keyword), ...opts }]; + let response = prepare('keywords.keyword-was-removed', keyword); + if (rId >= 1) { + const responseDb = keyword.responses.filter(o => o.order !== (rId - 1)); + + // reorder + responseDb.forEach((item, index) => { + item.order = index; + }); + + await keyword.save(); + response = prepare('keywords.response-was-removed', { keyword, response: rId }); } else { - await Keyword.remove(keyword); - return [{ response: prepare('keywords.keyword-was-removed', keyword), ...opts }]; + await keyword.remove(); } + return [{ response, ...opts }]; + } } catch (e: any) { error(e.stack); @@ -391,7 +377,7 @@ class Keywords extends System { return true; } - const keywords = (await Keyword.find({ relations: ['responses'] })).filter((o) => { + const keywords = (await Keyword.find()).filter((o) => { const regexp = `([!"#$%&'()*+,-.\\/:;<=>?\\b\\s]${o.keyword}[!"#$%&'()*+,-.\\/:;<=>?\\b\\s])|(^${o.keyword}[!"#$%&'()*+,-.\\/:;<=>?\\b\\s])|([!"#$%&'()*+,-.\\/:;<=>?\\b\\s]${o.keyword}$)|(^${o.keyword}$)`; const isFoundInMessage = XRegExp(regexp, 'giu').test(opts.message); const isEnabled = o.enabled; @@ -405,7 +391,7 @@ class Keywords extends System { let atLeastOnePermissionOk = false; for (const k of keywords) { debug('keywords.run', JSON.stringify({ k })); - const _responses: KeywordResponses[] = []; + const _responses: Keyword['responses'] = []; // check group filter first let group: Readonly> | null; @@ -422,7 +408,8 @@ class Keywords extends System { } } - for (const r of _.orderBy(k.responses, 'order', 'asc')) { + const responses = k.areResponsesRandomized ? shuffle(k.responses) : orderBy(k.responses, 'order', 'asc'); + for (const r of responses) { let permission = r.permission ?? groupPermission; // show warning if null permission if (!permission) { @@ -448,7 +435,7 @@ class Keywords extends System { return atLeastOnePermissionOk; } - async sendResponse(responses: (KeywordResponses)[], opts: { sender: CommandOptions['sender'], discord: CommandOptions['discord'], id: string }) { + async sendResponse(responses: (Keyword['responses']), opts: { sender: CommandOptions['sender'], discord: CommandOptions['discord'], id: string }) { for (let i = 0; i < responses.length; i++) { // check if response have new line, then split it and send it as separate messages for (const response of responses[i].response.split('\n')) { diff --git a/test/tests/cooldowns/check.js b/test/tests/cooldowns/check.js index f241080e016..4dc9b84aa9d 100644 --- a/test/tests/cooldowns/check.js +++ b/test/tests/cooldowns/check.js @@ -172,7 +172,14 @@ describe('Cooldowns - @func3 - check()', () => { it('Add koncha to keywords', async () => { await AppDataSource.getRepository(Keyword).save({ keyword: 'koncha', - response: '$sender KonCha', + responses: [{ + id: '1234', + order: 0, + response: '$sender KonCha', + stopIfExecuted: false, + permission: '0efd7b1c-e460-4167-8e06-8aaf2c170311', + filter: '', + }], enabled: true, }); }); @@ -716,7 +723,14 @@ describe('Cooldowns - @func3 - check()', () => { it('test', async () => { await AppDataSource.getRepository(Keyword).save({ keyword: 'me', - response: '(!me)', + responses: [{ + id: '1234', + order: 0, + response: '(!me)', + stopIfExecuted: false, + permission: '0efd7b1c-e460-4167-8e06-8aaf2c170311', + filter: '', + }], enabled: true, }); @@ -765,7 +779,14 @@ describe('Cooldowns - @func3 - check()', () => { it('test', async () => { await AppDataSource.getRepository(Keyword).save({ keyword: 'me', - response: '(!me)', + responses: [{ + id: '1234', + order: 0, + response: '(!me)', + stopIfExecuted: false, + permission: '0efd7b1c-e460-4167-8e06-8aaf2c170311', + filter: '', + }], enabled: true, }); diff --git a/test/tests/cooldowns/check_default_values.js b/test/tests/cooldowns/check_default_values.js index 92cba062014..01a0c034108 100644 --- a/test/tests/cooldowns/check_default_values.js +++ b/test/tests/cooldowns/check_default_values.js @@ -111,7 +111,14 @@ describe('Cooldowns - @func3 - default check', () => { it('test', async () => { await AppDataSource.getRepository(Keyword).save({ keyword: 'me', - response: '(!me)', + responses: [{ + id: '1234', + order: 0, + response: '(!me)', + stopIfExecuted: false, + permission: defaultPermissions.VIEWERS, + filter: '', + }], enabled: true, }); diff --git a/test/tests/cooldowns/default_cooldown_can_be_overrided.js b/test/tests/cooldowns/default_cooldown_can_be_overrided.js index d9199fe37cd..36ce89be6ac 100644 --- a/test/tests/cooldowns/default_cooldown_can_be_overrided.js +++ b/test/tests/cooldowns/default_cooldown_can_be_overrided.js @@ -98,7 +98,14 @@ describe('Cooldowns - @func3 - default cooldown can be overrided', () => { it('Create me keyword', async () => { await AppDataSource.getRepository(Keyword).save({ keyword: 'me', - response: '(!me)', + responses: [{ + id: '1234', + order: 0, + response: '(!me)', + stopIfExecuted: false, + permission: defaultPermissions.VIEWERS, + filter: '', + }], enabled: true, }); }); diff --git a/test/tests/keywords/#4860_group_filter_and_permissions.js b/test/tests/keywords/#4860_group_filter_and_permissions.js index 37654f1af44..306a7195776 100644 --- a/test/tests/keywords/#4860_group_filter_and_permissions.js +++ b/test/tests/keywords/#4860_group_filter_and_permissions.js @@ -2,7 +2,7 @@ import assert from 'assert'; import('../../general.js'); -import { Keyword, KeywordGroup, KeywordResponses } from '../../../dest/database/entity/keyword.js'; +import { Keyword, KeywordGroup } from '../../../dest/database/entity/keyword.js'; import { prepare } from '../../../dest/helpers/commons/prepare.js'; import { defaultPermissions } from '../../../dest/helpers/permissions/defaultPermissions.js'; import keywords from '../../../dest/systems/keywords.js'; @@ -53,35 +53,31 @@ describe('Keywords - @func3 - #4860 - keywords group permissions and filter shou keyword.enabled = true; keyword.visible = true; keyword.group = 'filterGroup'; + keyword.responses = [ + { + stopIfExecuted: false, + response: 'bad449ae-f0b3-488c-a7b0-39a853d5333f', + filter: '', + order: 0, + permission: defaultPermissions.VIEWERS, + }, + { + stopIfExecuted: false, + response: 'c0f68c62-630b-412b-9c97-f5b1afc734d2', + filter: '$title === \'test\'', + order: 1, + permission: defaultPermissions.VIEWERS, + }, + { + stopIfExecuted: false, + response: '4b310000-b105-475a-8a85-a573a0bca1b7', + filter: '$title !== \'test\'', + order: 2, + permission: defaultPermissions.VIEWERS, + }, + ] await keyword.save(); testfilter = keyword.id; - - const response1 = new KeywordResponses(); - response1.stopIfExecuted = false; - response1.response = 'bad449ae-f0b3-488c-a7b0-39a853d5333f'; - response1.filter = ''; - response1.order = 0; - response1.permission = defaultPermissions.VIEWERS; - response1.keyword = keyword; - await response1.save(); - - const response2 = new KeywordResponses(); - response2.stopIfExecuted = false; - response2.response = 'c0f68c62-630b-412b-9c97-f5b1afc734d2'; - response2.filter = '$title === \'test\''; - response2.order = 1; - response2.permission = defaultPermissions.VIEWERS; - response2.keyword = keyword; - await response2.save(); - - const response3 = new KeywordResponses(); - response3.stopIfExecuted = false; - response3.response = '4b310000-b105-475a-8a85-a573a0bca1b7'; - response3.filter = '$title !== \'test\''; - response3.order = 2; - response3.permission = defaultPermissions.VIEWERS; - response3.keyword = keyword; - await response3.save(); }); it('create keyword testpermnull with permGroup', async () => { @@ -90,16 +86,14 @@ describe('Keywords - @func3 - #4860 - keywords group permissions and filter shou keyword.enabled = true; keyword.visible = true; keyword.group = 'permGroup'; + keyword.responses = [{ + stopIfExecuted: false, + response: '430ea834-da5f-48b1-bf2f-3acaf1f04c63', + filter: '', + order: 0, + permission: null, + }]; await keyword.save(); - - const response1 = new KeywordResponses(); - response1.stopIfExecuted = false; - response1.response = '430ea834-da5f-48b1-bf2f-3acaf1f04c63'; - response1.filter = ''; - response1.order = 0; - response1.permission = null; - response1.keyword = keyword; - await response1.save(); }); let testpermnull2 = ''; @@ -110,17 +104,15 @@ describe('Keywords - @func3 - #4860 - keywords group permissions and filter shou keyword.enabled = true; keyword.visible = true; keyword.group = 'permGroup2'; + keyword.responses = [{ + stopIfExecuted: false, + response: '1594a86e-158d-4b7d-9898-0f80bd6a0c98', + filter: '', + order: 0, + permission: null, + }]; await keyword.save(); testpermnull2 = keyword.id; - - const response1 = new KeywordResponses(); - response1.stopIfExecuted = false; - response1.response = '1594a86e-158d-4b7d-9898-0f80bd6a0c98'; - response1.filter = ''; - response1.order = 0; - response1.permission = null; - response1.keyword = keyword; - await response1.save(); }); it('create keyword testpermmods with permGroup2', async () => { @@ -129,16 +121,14 @@ describe('Keywords - @func3 - #4860 - keywords group permissions and filter shou keyword.enabled = true; keyword.visible = true; keyword.group = 'permGroup2'; + keyword.responses = [{ + stopIfExecuted: false, + response: 'cae8f74f-046a-4756-b6c5-f2219d9a0f4e', + filter: '', + order: 0, + permission: defaultPermissions.MODERATORS, + }]; await keyword.save(); - - const response1 = new KeywordResponses(); - response1.stopIfExecuted = false; - response1.response = 'cae8f74f-046a-4756-b6c5-f2219d9a0f4e'; - response1.filter = ''; - response1.order = 0; - response1.permission = defaultPermissions.MODERATORS; - response1.keyword = keyword; - await response1.save(); }); it('!testpermnull should be triggered by CASTER', async () => {