From b3b8f55055fc17614a3548addac1bbc3b1fef6c1 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Thu, 2 Nov 2023 17:53:27 +0100 Subject: [PATCH 01/12] test(interaction): test suite --- src/modules/event/services/interaction.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/modules/event/services/interaction.spec.ts diff --git a/src/modules/event/services/interaction.spec.ts b/src/modules/event/services/interaction.spec.ts new file mode 100644 index 0000000..6a3ceef --- /dev/null +++ b/src/modules/event/services/interaction.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Interaction } from './interaction'; + +describe('Interaction', () => { + var service: Interaction; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [Interaction], + }).compile(); + + service = module.get(Interaction); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); From f2d967da0f38a6883381ae5b59859234e46f8530 Mon Sep 17 00:00:00 2001 From: Matteo Juen <45847997+Blvckleg@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:50:31 +0100 Subject: [PATCH 02/12] Update README.md --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0cb0e24..173f59c 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,22 @@ Latest: 1.1.0 🤖 Description: -BingusBoingus is the bot you never knew you needed, mainly because it doesn't exist yet! This Discord bot repository is currently just a twinkle in the eyes of our imaginary developers. With BingusBoingus, expect the unexpected, and prepare for some hilariously quirky and utterly pointless commands that will make you wonder, "Why does this bot even exist?" +BingusBoingus is the bot you never knew you needed, mainly because it not really useful and just very silly instead! With BingusBoingus, expect the unexpected, and prepare for some hilariously quirky and utterly pointless commands that will make you wonder, "Why does this bot even exist?" -🪄 Features (not really): +🪄 Features: -Random Responses: BingusBoingus excels at delivering responses that are both baffling and comical. Ask it a question, and you might get a recipe for mashed potatoes in return. +Random Responses: BingusBoingus excels at delivering responses that are both baffling and comical. -Virtual Tea Party: Join the bot in its daily virtual tea party where it discusses the weather with itself. +Coinflip: Ask Bingus to toss a coin for you. It might land on heads. It might land on tails. Or it might... + +Saving Quotes: BingusBoingus is able to save quotes in a database. 404 Error: This bot's purpose is as elusive as a unicorn, so prepare to have your expectations shattered. +And more.... + +For more information on commands and how to use them please check the [BingusBoingus Wiki](https://github.com/Blvckleg/BingusBoingus/wiki) + 🎉 Coming Soon (or not): - Nothingness: BingusBoingus promises to introduce a groundbreaking feature called "Nothingness" that will redefine the concept of non-functionality. @@ -22,15 +28,11 @@ Virtual Tea Party: Join the bot in its daily virtual tea party where it discusse - Occasional Nonsense: The bot will occasionally send you random, nonsensical messages to keep you on your toes. -- Also: - -- And coming soon: - -- maybe even: +- Virtual Tea Party: Join the bot in its daily virtual tea party where it discusses the weather with itself. Stay tuned for more updates on BingusBoingus, the Discord bot that's sillier than a rubber chicken in a tuxedo! -This repository follows a structured Git branching workflow: +## This repository follows a structured Git branching workflow: - Branch Creation: Start a new branch for each feature or bug fix. - Commits: Make changes and commit with clear messages following the conventional commit messages. From 110ee45c2d35e25e62f2af26b06c0aeb2eec8dda Mon Sep 17 00:00:00 2001 From: Matteo Juen <45847997+Blvckleg@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:51:06 +0100 Subject: [PATCH 03/12] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 173f59c..0ad7fb8 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ BingusBoingus is the bot you never knew you needed, mainly because it not really 🪄 Features: -Random Responses: BingusBoingus excels at delivering responses that are both baffling and comical. +- Random Responses: BingusBoingus excels at delivering responses that are both baffling and comical. -Coinflip: Ask Bingus to toss a coin for you. It might land on heads. It might land on tails. Or it might... +- Coinflip: Ask Bingus to toss a coin for you. It might land on heads. It might land on tails. Or it might... -Saving Quotes: BingusBoingus is able to save quotes in a database. +- Saving Quotes: BingusBoingus is able to save quotes in a database. -404 Error: This bot's purpose is as elusive as a unicorn, so prepare to have your expectations shattered. +- 404 Error: This bot's purpose is as elusive as a unicorn, so prepare to have your expectations shattered. And more.... From ae70a71229ce56c90e5d22df953189e327f5e26c Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Fri, 23 Feb 2024 23:27:15 +0100 Subject: [PATCH 04/12] feat(vsc launch); launch setting for dev apllication --- .vscode/launch.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 24e06db..49f2254 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,12 +7,23 @@ { "type": "node", "request": "launch", - "name": "start:dev", + "name": "start", "runtimeExecutable": "npm", "console": "integratedTerminal", "envFile": "${workspaceFolder}/.env", "runtimeArgs": ["run-script", "start:debug"], "skipFiles": ["/**"] + }, + { + "type": "node", + "request": "launch", + "name": "start:dev", + "runtimeExecutable": "npm", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env.dev", + "runtimeArgs": ["run-script", "start:debug"], + "skipFiles": ["/**"] } ] + } From af3af6ed60b2eb728bf738557342bc8a11269293 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Fri, 23 Feb 2024 23:27:34 +0100 Subject: [PATCH 05/12] tests(quote-service); testing suite --- .../event/services/interaction.spec.ts | 7 +- .../someone-once-said.service.spec.ts | 126 ++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/modules/someone-once-said/services/someone-once-said.service.spec.ts diff --git a/src/modules/event/services/interaction.spec.ts b/src/modules/event/services/interaction.spec.ts index 6a3ceef..2ea7805 100644 --- a/src/modules/event/services/interaction.spec.ts +++ b/src/modules/event/services/interaction.spec.ts @@ -1,12 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Interaction } from './interaction'; +import { CommandService } from '../../command/command.service'; describe('Interaction', () => { var service: Interaction; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [Interaction], + providers: [Interaction, + { + provide: CommandService, + useValue: {} + }], }).compile(); service = module.get(Interaction); diff --git a/src/modules/someone-once-said/services/someone-once-said.service.spec.ts b/src/modules/someone-once-said/services/someone-once-said.service.spec.ts new file mode 100644 index 0000000..bd24ed9 --- /dev/null +++ b/src/modules/someone-once-said/services/someone-once-said.service.spec.ts @@ -0,0 +1,126 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Model, Error, MongooseError } from 'mongoose'; +import { SomeoneOnceSaidService } from './someone-once-said.service'; +import { SomeoneOnceSaidEntity } from '../../../schemas/someone-once-said-entity.model'; +import { SomeoneOnceSaidDocument } from '../../../schemas/someone-once-said.schema'; +import { getModelToken } from '@nestjs/mongoose'; +describe('SomeoneOnceSaidService', () => { + let service: SomeoneOnceSaidService; + let modelMock: Model; + let mockDate: Date = new Date(0); + jest.useFakeTimers(); + jest.setSystemTime(mockDate); + const mockQuoteDto: SomeoneOnceSaidEntity = { + phrase: 'Test quote', + username: 'testUser', + secName: 'Teschter', + createdAt: mockDate, + }; + const mockQuoteDocument: SomeoneOnceSaidDocument = { + phrase: 'Test quote', + username: 'testUser', + secName: 'Teschter', + createdAt: mockDate, + } as SomeoneOnceSaidDocument; + + beforeEach(async () => { + jest.setSystemTime(0); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SomeoneOnceSaidService, + { + provide: getModelToken('SomeoneOnceSaid'), + useValue: { + create: jest.fn((p) => mockQuoteDocument), + deleteMany: jest.fn((p) => [mockQuoteDocument]), + countDocuments: jest.fn((p) => 1), + findOne: jest.fn((p) => mockQuoteDocument), + }, + }, + ], + }).compile(); + + service = module.get(SomeoneOnceSaidService); + modelMock = module.get>( + getModelToken('SomeoneOnceSaid'), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create a quote', async () => { + (modelMock as any).create = jest.fn((p) => mockQuoteDocument); + + const result = await service.create(mockQuoteDto); + + expect(result).toEqual(mockQuoteDocument); + expect(modelMock.create).toHaveBeenCalledWith(mockQuoteDto); + }); + + it('should return null when there is an error', async () => { + const mockQuoteDto: SomeoneOnceSaidEntity = { + phrase: 'Test quote', + username: 'testUser', + createdAt: mockDate, + }; + (modelMock as any).create = jest.fn((p) => new Error('Test error')); + + const result = await service.create(mockQuoteDto); + + expect(result).toStrictEqual(new MongooseError('Test error')); + }); + }); + + describe('deleteProductionOrderForUser', () => { + it('should delete quotes for a user', async () => { + const username = 'testUser'; + + const result = await service.deleteProductionOrderForUser(username); + + expect(result).toBeUndefined(); + expect(modelMock.deleteMany).toHaveBeenCalledWith({ username }); + }); + + it('should return an error when there is an error', async () => { + const username = 'testUser'; + (modelMock as any).deleteMany = jest + .fn() + .mockRejectedValue(new Error('test')); + + const result = await service.deleteProductionOrderForUser(username); + expect(result).toStrictEqual(new MongooseError(`Error deleting Quotes for username: ${username}`)) + + }); + }); + + describe('getRandomQuote', () => { + it('should return a random quote', async () => { + (modelMock as any).findOne = jest.fn((p) => ({ + skip: () => ({ + limit: () => mockQuoteDocument, + }), + })); + const countSpy = jest.spyOn(modelMock, 'countDocuments'); + const findOneSpy = jest.spyOn(modelMock, 'findOne'); + + const result = await service.getRandomQuote(); + + expect(result).toStrictEqual(mockQuoteDocument); + expect(countSpy).toHaveBeenCalled(); + expect(findOneSpy).toHaveBeenCalledTimes(1); + }); + + it('should return null when there is an error', async () => { + (modelMock as any).countDocuments = jest.fn( + (p) => new Error('Test error'), + ); + + const result = await service.getRandomQuote(); + + expect(result).toBeNull(); + }); + }); +}); From fa71345a99fc9922dc7d6c1dcba5aa2c3e18ff4e Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 10:17:39 +0100 Subject: [PATCH 06/12] style: rm unused tag in readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 0ad7fb8..9e4a8f5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -Status: In Development 🛠️ - -Latest: 1.1.0 - 🤖 Description: BingusBoingus is the bot you never knew you needed, mainly because it not really useful and just very silly instead! With BingusBoingus, expect the unexpected, and prepare for some hilariously quirky and utterly pointless commands that will make you wonder, "Why does this bot even exist?" From 76ee28b6289b7e481e52d50dcbe368bb3b49b304 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 10:20:32 +0100 Subject: [PATCH 07/12] style: remove init script, doesn't work --- infra/mongo/init.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 infra/mongo/init.js diff --git a/infra/mongo/init.js b/infra/mongo/init.js deleted file mode 100644 index 79c8291..0000000 --- a/infra/mongo/init.js +++ /dev/null @@ -1,5 +0,0 @@ -db.createUser({ - user: _getEnv('MONGO_USER'), - pwd: _getEnv('MONGO_PASSWORD'), - roles: ['readWrite', 'dbAdmin'], -}); From fb3caec1048d133f6362076b5a15e15d59ba72b6 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 11:35:36 +0100 Subject: [PATCH 08/12] fix: compose mount --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8b52ade..ebd54b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: volumes: - ./infra/mongo:/db/data:rw - ./infra/mongo/mongo.conf:/etc/mongo/mongo.conf:ro - - ./infra/mongo/init.js:/docker-entrypoint-initdb.d/init.js:ro env_file: - ./infra/mongo/.env command: From 012c7c9995e7ff6f7b60380737ec42a48b57db4b Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 13:23:44 +0100 Subject: [PATCH 09/12] chore: class validator --- package-lock.json | 29 +++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 30 insertions(+) diff --git a/package-lock.json b/package-lock.json index 75a10e8..74d4ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/platform-express": "^10.2.6", "@nestjs/testing": "^10.2.6", "@types/jest": "^29.5.5", + "class-validator": "^0.14.1", "discord-interactions": "^3.4.0", "discord.js": "^14.13.0", "dotenv": "^16.3.1", @@ -1776,6 +1777,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -2310,6 +2316,16 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4301,6 +4317,11 @@ "node": ">=6" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.57", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.57.tgz", + "integrity": "sha512-OjsEd9y4LgcX+Ig09SbxWqcGESxliDDFNVepFhB9KEsQZTrnk3UdEU+cO0sW1APvLprHstQpS23OQpZ3bwxy6Q==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6064,6 +6085,14 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 474cd89..0b06112 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/platform-express": "^10.2.6", "@nestjs/testing": "^10.2.6", "@types/jest": "^29.5.5", + "class-validator": "^0.14.1", "discord-interactions": "^3.4.0", "discord.js": "^14.13.0", "dotenv": "^16.3.1", From 07c9d60cd0f45a27e02726be18a2ae04927e9339 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 13:24:02 +0100 Subject: [PATCH 10/12] feat(poll); poll command and service. --- src/modules/command/command.module.ts | 7 +- src/modules/command/command.service.ts | 3 + src/modules/command/commands/get-a-quote.ts | 2 +- src/modules/command/commands/poll.ts | 99 ++++++++++++++ .../command/commands/someone-once-said.ts | 2 +- src/modules/event/event.module.ts | 3 +- src/modules/event/services/interaction.ts | 129 ++++++++++++++++-- src/modules/poll/dto/update-poll.dto.ts | 26 ++++ src/modules/poll/module/poll.module.ts | 19 +++ src/modules/poll/service/poll.service.ts | 59 ++++++++ .../someone-once-said.module.ts | 2 +- .../someone-once-said.service.spec.ts | 0 .../someone-once-said.service.ts | 0 src/schemas/poll-entity.model.ts | 11 ++ src/schemas/poll.schema.ts | 37 +++++ 15 files changed, 384 insertions(+), 15 deletions(-) create mode 100644 src/modules/command/commands/poll.ts create mode 100644 src/modules/poll/dto/update-poll.dto.ts create mode 100644 src/modules/poll/module/poll.module.ts create mode 100644 src/modules/poll/service/poll.service.ts rename src/modules/someone-once-said/{modules => module}/someone-once-said.module.ts (86%) rename src/modules/someone-once-said/{services => service}/someone-once-said.service.spec.ts (100%) rename src/modules/someone-once-said/{services => service}/someone-once-said.service.ts (100%) create mode 100644 src/schemas/poll-entity.model.ts create mode 100644 src/schemas/poll.schema.ts diff --git a/src/modules/command/command.module.ts b/src/modules/command/command.module.ts index 8866f6a..884ac59 100644 --- a/src/modules/command/command.module.ts +++ b/src/modules/command/command.module.ts @@ -8,8 +8,10 @@ import { BugReport } from './commands/bug'; import { CoinflipCommand } from './commands/coinflip'; import { GoldCommand } from './commands/gold'; import SomeoneOnceSaidCommand from './commands/someone-once-said'; -import { SomeoneOnceSaidModule } from '../someone-once-said/modules/someone-once-said.module'; +import { SomeoneOnceSaidModule } from '../someone-once-said/module/someone-once-said.module'; import GetRandomQuote from './commands/get-a-quote'; +import { PollCommand } from './commands/poll'; +import { PollModule } from '../poll/module/poll.module'; @Module({ providers: [ @@ -23,8 +25,9 @@ import GetRandomQuote from './commands/get-a-quote'; GoldCommand, SomeoneOnceSaidCommand, GetRandomQuote, + PollCommand ], - imports: [SomeoneOnceSaidModule], + imports: [SomeoneOnceSaidModule, PollModule], exports: [CommandService], }) export class CommandModule {} diff --git a/src/modules/command/command.service.ts b/src/modules/command/command.service.ts index c8c7eeb..8c6863a 100644 --- a/src/modules/command/command.service.ts +++ b/src/modules/command/command.service.ts @@ -8,6 +8,7 @@ import { CoinflipCommand } from './commands/coinflip'; import { GoldCommand } from './commands/gold'; import SomeoneOnceSaidCommand from './commands/someone-once-said'; import GetRandomQuote from './commands/get-a-quote'; +import { PollCommand } from './commands/poll'; @Injectable() export class CommandService { @@ -23,6 +24,7 @@ export class CommandService { goldModule: GoldCommand, someoneOnceSaidModule: SomeoneOnceSaidCommand, getRandomQuoteModule: GetRandomQuote, + pollModule: PollCommand ) { const commands: ACommand[] = [ //pingpongModule, @@ -34,6 +36,7 @@ export class CommandService { goldModule, someoneOnceSaidModule, getRandomQuoteModule, + pollModule ]; commands.forEach((command) => { if (command.data.name && command.execute) { diff --git a/src/modules/command/commands/get-a-quote.ts b/src/modules/command/commands/get-a-quote.ts index d94614d..3e46868 100644 --- a/src/modules/command/commands/get-a-quote.ts +++ b/src/modules/command/commands/get-a-quote.ts @@ -5,7 +5,7 @@ import { SlashCommandBuilder, } from 'discord.js'; import { ACommand } from '../command.abstract'; -import { SomeoneOnceSaidService } from '../../someone-once-said/services/someone-once-said.service'; +import { SomeoneOnceSaidService } from '../../someone-once-said/service/someone-once-said.service'; import { Inject } from '@nestjs/common'; export default class GetRandomQuote extends ACommand { diff --git a/src/modules/command/commands/poll.ts b/src/modules/command/commands/poll.ts new file mode 100644 index 0000000..85c5eb7 --- /dev/null +++ b/src/modules/command/commands/poll.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder, +} from 'discord.js'; +import { ACommand } from '../command.abstract'; +import { PollService } from '../../poll/service/poll.service'; +import { PollEntity } from '../../../schemas/poll-entity.model'; + +@Injectable() +export class PollCommand extends ACommand { + constructor( + @Inject(PollService) + private readonly pollService: PollService, + ) { + super(); + } + data = new SlashCommandBuilder() + .setName('poll') + .setDescription('Start a poll in your channel!') + .addStringOption((option) => + option + .setName('topic') + .setDescription('the topic of your poll') + .setMinLength(5) + .setMaxLength(200) + .setRequired(true), + ); + + public execute(arg: any /*Interaction*/): Promise { + return this.run(async () => { + const author = arg.user.displayName ?? arg.user.username; + await arg.reply({ + content: `${author} has started a new poll!`, + ephemeral: true, + }); + + const topic = await arg.options.getString('topic'); + + const embed = new EmbedBuilder() + .setColor('Aqua') + .setAuthor({ name: author }) + .setFooter({ text: 'poll started 🤚' }) + .setTimestamp() + .setTitle('📍 vote now!') + .setDescription(`> ${topic}`) + .addFields({ + name: 'Upvotes 👍', + value: '> **No votes**', + inline: true, + }) + .addFields({ + name: 'Downvotes 👎', + value: '> **No votes**', + inline: true, + }) + .addFields({ + name: 'Author', + value: `> ${arg.user}`, + inline: true, + }); + + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('up') + .setLabel('⬆️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('down') + .setLabel('⬇️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('close') + .setLabel('⚠️ close') + .setStyle(ButtonStyle.Danger), + ); + + const message = await arg.channel.send({ embeds: [embed], components: [buttons]}); + + message.createMessageComponentCollector(); + + let pollentity: PollEntity = { + msg: message.id, + upvotes: 0, + downvotes: 0, + upMembers: [], + downMembers: [], + active: true, + ownerName: arg.user.username, + createdAt: new Date() + } + await this.pollService.create(pollentity); + return true; + }); + } +} diff --git a/src/modules/command/commands/someone-once-said.ts b/src/modules/command/commands/someone-once-said.ts index f25c252..62990c2 100644 --- a/src/modules/command/commands/someone-once-said.ts +++ b/src/modules/command/commands/someone-once-said.ts @@ -5,7 +5,7 @@ import { SlashCommandBuilder, } from 'discord.js'; import { ACommand } from '../command.abstract'; -import { SomeoneOnceSaidService } from '../../someone-once-said/services/someone-once-said.service'; +import { SomeoneOnceSaidService } from '../../someone-once-said/service/someone-once-said.service'; import { Inject } from '@nestjs/common'; import { SomeoneOnceSaid } from '../../../schemas/someone-once-said.schema'; diff --git a/src/modules/event/event.module.ts b/src/modules/event/event.module.ts index 2280d24..4bef5c3 100644 --- a/src/modules/event/event.module.ts +++ b/src/modules/event/event.module.ts @@ -5,9 +5,10 @@ import { Interaction } from './services/interaction'; import { MessageEvent } from './services/messageEvent'; import { DiscordModule } from '../discord/discord.module'; import { CommandModule } from '../command/command.module'; +import { PollModule } from '../poll/module/poll.module'; @Module({ - imports: [DiscordModule, CommandModule], + imports: [DiscordModule, CommandModule, PollModule], providers: [EventService, ClientReady, Interaction, MessageEvent], exports: [EventService], }) diff --git a/src/modules/event/services/interaction.ts b/src/modules/event/services/interaction.ts index 6911cbb..4f4d168 100644 --- a/src/modules/event/services/interaction.ts +++ b/src/modules/event/services/interaction.ts @@ -1,25 +1,136 @@ -import { Events } from 'discord.js'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + Events, +} from 'discord.js'; import { AEvent } from '../event.abstract'; import { CommandService } from '../../command/command.service'; import { Inject, Injectable } from '@nestjs/common'; +import { PollService } from '../../poll/service/poll.service'; @Injectable() export class Interaction extends AEvent { event: Events = Events.InteractionCreate; once: boolean = false; - constructor(private readonly commandService: CommandService) { + constructor( + private readonly commandService: CommandService, + private readonly pollService: PollService, + ) { super(); } async execute(interaction: any) { - return this.run(() => { - if (!interaction.isCommand()) { - return; - } - const { commandName } = interaction; - var command = this.commandService.getCommand(commandName); - command?.execute(interaction); + return await this.run(async () => { + if (interaction.isCommand()) { + const { commandName } = interaction; + var command = this.commandService.getCommand(commandName); + command?.execute(interaction); + } else if (interaction.isButton()) { + const data = await this.pollService.get(interaction.message.id); + if (!data) return; + const msg = await interaction.channel.messages.fetch(data.msg); + if (interaction?.customId === 'up') { + if (data.upMembers.includes(interaction.user.id)) { + return await interaction.reply({ + content: `${interaction.user} you already voted for this`, + ephermal: true, + }); + } + if (data.downMembers.includes(interaction.user.id)) { + data.downvotes--; + data.downMembers = data.downMembers.filter( + (member) => member != interaction.user.id, + ); + } + data.upvotes++; + data.upMembers.push(interaction.user.id); + await this.pollService.update(data); + + const embed = EmbedBuilder.from(msg.embeds[0]).setFields( + { + name: 'Upvotes 👍', + value: `> **${data.upvotes}** votes`, + inline: true, + }, + { + name: 'Downvotes 👎', + value: `> **${data.downvotes}** votes`, + inline: true, + }, + { name: 'Author', value: `> @${data.ownerName}` }, + ); + + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('up') + .setLabel('⬆️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('down') + .setLabel('⬇️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('close') + .setLabel('⚠️ close') + .setStyle(ButtonStyle.Danger), + ); + return await msg.edit({ embeds: [embed], components: [buttons] }); + } else if (interaction?.customId === 'down') { + if (data.downMembers.includes(interaction.user.id)) { + return await interaction.reply({ + content: `${interaction.user} you already voted for this`, + ephermal: true, + }); + } + if (data.upMembers.includes(interaction.user.id)) { + data.upvotes--; + data.upMembers = data.upMembers.filter( + (member) => member != interaction.user.id, + ); + } + data.downvotes++; + data.downMembers.push(interaction.user.id); + await this.pollService.update(data); + + const embed = EmbedBuilder.from(msg.embeds[0]).setFields( + { + name: 'Upvotes 👍', + value: `> **${data.upvotes}** votes`, + inline: true, + }, + { + name: 'Downvotes 👎', + value: `> **${data.downvotes}** votes`, + inline: true, + }, + { name: 'Author', value: `> @${data.ownerName}` }, + ); + + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('up') + .setLabel('⬆️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('down') + .setLabel('⬇️') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('close') + .setLabel('⚠️ close') + .setStyle(ButtonStyle.Danger), + ); + return await msg.edit({ embeds: [embed], components: [buttons] }); + } else if (interaction?.customId === 'close') { + data.active = false; + await this.pollService.update(data); + await msg.edit({components: []}) + return await interaction.channel.send({content: 'The Poll has been closed!'}) + } + } else return false; }); } } diff --git a/src/modules/poll/dto/update-poll.dto.ts b/src/modules/poll/dto/update-poll.dto.ts new file mode 100644 index 0000000..ade9487 --- /dev/null +++ b/src/modules/poll/dto/update-poll.dto.ts @@ -0,0 +1,26 @@ +import { IsNotEmpty, IsOptional } from "class-validator"; + +export class UpdatePollDto { + @IsNotEmpty() + msg: string; + + @IsNotEmpty() + upvotes: number; + + @IsNotEmpty() + downvotes: number; + + @IsOptional() + active: boolean; + + @IsOptional() + ownerName: string; + + @IsOptional() + downMembers: string[] + + @IsOptional() + upMembers: string[] + + +} \ No newline at end of file diff --git a/src/modules/poll/module/poll.module.ts b/src/modules/poll/module/poll.module.ts new file mode 100644 index 0000000..9d1cf2e --- /dev/null +++ b/src/modules/poll/module/poll.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Poll, PollSchema } from '../../../schemas/poll.schema'; +import { PollService } from '../service/poll.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: Poll.name, + schema: PollSchema, + }, + ]), + ], + controllers: [], + providers: [PollService], + exports: [PollService], +}) +export class PollModule {} diff --git a/src/modules/poll/service/poll.service.ts b/src/modules/poll/service/poll.service.ts new file mode 100644 index 0000000..25290b4 --- /dev/null +++ b/src/modules/poll/service/poll.service.ts @@ -0,0 +1,59 @@ +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { PollDocument } from '../../../schemas/poll.schema'; +import { PollEntity } from '../../../schemas/poll-entity.model'; +import { UpdatePollDto } from '../dto/update-poll.dto'; + +export class PollService { + constructor( + @InjectModel('Poll') + private readonly pollModel: Model, + ) {} + + async create( + pollDto: PollEntity, + ): Promise { + try { + return await this.pollModel.create({ + msg: pollDto.msg, + ownerName: pollDto.ownerName, + upvotes: pollDto.upvotes, + downvotes: pollDto.downvotes, + upMembers: pollDto?.upMembers ?? [], + downMembers: pollDto?.downMembers ?? [], + active: true, + createdAt: new Date(), + }); + } catch (e) { + return null; + } + } + + async update( + updateDto: UpdatePollDto, + ): Promise { + try { + return await this.pollModel.findOneAndUpdate({msg: updateDto.msg}, + { + upvotes: updateDto.upvotes, + downvotes: updateDto?.downvotes, + upMembers: updateDto?.upMembers, + downMembers: updateDto?.downMembers, + active: updateDto?.active + }, { new: true} + + ); + } catch (e) { + return null; + } + } + + async get(messageId: string): Promise { + try { + const test = await this.pollModel.findOne({msg: messageId, active:true}) + return test; + } catch(e) { + return null + } + } +} diff --git a/src/modules/someone-once-said/modules/someone-once-said.module.ts b/src/modules/someone-once-said/module/someone-once-said.module.ts similarity index 86% rename from src/modules/someone-once-said/modules/someone-once-said.module.ts rename to src/modules/someone-once-said/module/someone-once-said.module.ts index a11a9b9..20deb78 100644 --- a/src/modules/someone-once-said/modules/someone-once-said.module.ts +++ b/src/modules/someone-once-said/module/someone-once-said.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { SomeoneOnceSaidService } from '../services/someone-once-said.service'; +import { SomeoneOnceSaidService } from '../service/someone-once-said.service'; import { SomeoneOnceSaid, SomeoneOnceSaidSchema, diff --git a/src/modules/someone-once-said/services/someone-once-said.service.spec.ts b/src/modules/someone-once-said/service/someone-once-said.service.spec.ts similarity index 100% rename from src/modules/someone-once-said/services/someone-once-said.service.spec.ts rename to src/modules/someone-once-said/service/someone-once-said.service.spec.ts diff --git a/src/modules/someone-once-said/services/someone-once-said.service.ts b/src/modules/someone-once-said/service/someone-once-said.service.ts similarity index 100% rename from src/modules/someone-once-said/services/someone-once-said.service.ts rename to src/modules/someone-once-said/service/someone-once-said.service.ts diff --git a/src/schemas/poll-entity.model.ts b/src/schemas/poll-entity.model.ts new file mode 100644 index 0000000..ebd4a96 --- /dev/null +++ b/src/schemas/poll-entity.model.ts @@ -0,0 +1,11 @@ +export class PollEntity { + msg: string; + ownerName: string; + upvotes: number; + downvotes: number; + upMembers?: string[]; + downMembers?: string[]; + active: boolean; + createdAt: Date; + } + \ No newline at end of file diff --git a/src/schemas/poll.schema.ts b/src/schemas/poll.schema.ts new file mode 100644 index 0000000..35d77fe --- /dev/null +++ b/src/schemas/poll.schema.ts @@ -0,0 +1,37 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; + +@Schema() +export class Poll { + @Prop({ required: true }) + msg: string; + + @Prop({ required: true }) + ownerName: string; + + @Prop({ required: true }) + upvotes: number; + + @Prop({ required: true }) + downvotes: number; + + @Prop({ required: false }) + upMembers: string[]; + + @Prop({ required: false }) + downMembers: string[]; + + @Prop({ required: true }) + active: boolean; + + @Prop({ required: true }) + createdAt: Date; + + constructor(data) { + Object.assign(this, data); + } +} + +export type PollDocument = Poll & Document; + +export const PollSchema = + SchemaFactory.createForClass(Poll); From 36d2c83a938e6fc064751a30dafaa85e1980c10f Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 16:57:03 +0100 Subject: [PATCH 11/12] fix(poll): token timeout --- src/modules/event/services/interaction.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/modules/event/services/interaction.ts b/src/modules/event/services/interaction.ts index 4f4d168..f98fdcf 100644 --- a/src/modules/event/services/interaction.ts +++ b/src/modules/event/services/interaction.ts @@ -45,6 +45,7 @@ export class Interaction extends AEvent { (member) => member != interaction.user.id, ); } + //await interaction.deferUpdate(); data.upvotes++; data.upMembers.push(interaction.user.id); await this.pollService.update(data); @@ -77,7 +78,7 @@ export class Interaction extends AEvent { .setLabel('⚠️ close') .setStyle(ButtonStyle.Danger), ); - return await msg.edit({ embeds: [embed], components: [buttons] }); + return await interaction.update({ embeds: [embed], components: [buttons] }); } else if (interaction?.customId === 'down') { if (data.downMembers.includes(interaction.user.id)) { return await interaction.reply({ @@ -91,6 +92,7 @@ export class Interaction extends AEvent { (member) => member != interaction.user.id, ); } + //await interaction.deferUpdate(); data.downvotes++; data.downMembers.push(interaction.user.id); await this.pollService.update(data); @@ -123,12 +125,16 @@ export class Interaction extends AEvent { .setLabel('⚠️ close') .setStyle(ButtonStyle.Danger), ); - return await msg.edit({ embeds: [embed], components: [buttons] }); + return await interaction.update({ embeds: [embed], components: [buttons] }); } else if (interaction?.customId === 'close') { - data.active = false; - await this.pollService.update(data); - await msg.edit({components: []}) - return await interaction.channel.send({content: 'The Poll has been closed!'}) + if(interaction.user.username == data.ownerName) { + data.active = false; + await this.pollService.update(data); + await interaction.update({components: []}) + return await interaction.channel.send({content: 'The Poll has been closed!'}) + } else { + return await interaction.channel.send({content: `${interaction.user} you don\'t own this poll!`}); + } } } else return false; }); From cb30c9bd191ce7372b3e30345bee184b12ca21e1 Mon Sep 17 00:00:00 2001 From: matteom0165 Date: Sat, 24 Feb 2024 17:06:31 +0100 Subject: [PATCH 12/12] tests: nest dependencies --- .../event/services/interaction.spec.ts | 5 + src/modules/poll/service/poll.service.spec.ts | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/modules/poll/service/poll.service.spec.ts diff --git a/src/modules/event/services/interaction.spec.ts b/src/modules/event/services/interaction.spec.ts index 2ea7805..06b5ff6 100644 --- a/src/modules/event/services/interaction.spec.ts +++ b/src/modules/event/services/interaction.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Interaction } from './interaction'; import { CommandService } from '../../command/command.service'; +import { PollService } from '../../poll/service/poll.service'; describe('Interaction', () => { var service: Interaction; @@ -11,6 +12,10 @@ describe('Interaction', () => { { provide: CommandService, useValue: {} + }, + { + provide: PollService, + useValue: {} }], }).compile(); diff --git a/src/modules/poll/service/poll.service.spec.ts b/src/modules/poll/service/poll.service.spec.ts new file mode 100644 index 0000000..45be9b1 --- /dev/null +++ b/src/modules/poll/service/poll.service.spec.ts @@ -0,0 +1,100 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getModelToken } from '@nestjs/mongoose'; +import { Model, MongooseError } from 'mongoose'; +import { PollService } from './poll.service'; +import { PollDocument } from '../../../schemas/poll.schema'; +import { PollEntity } from '../../../schemas/poll-entity.model'; +import { UpdatePollDto } from '../dto/update-poll.dto'; + +// Mocking the pollModel +const mockPollModel = () => ({ + create: jest.fn(), + findOneAndUpdate: jest.fn(), + findOne: jest.fn(), +}); + +describe('PollService', () => { + let service: PollService; + let pollModel: Model; + let mockDate: Date = new Date(0); + jest.useFakeTimers(); + jest.setSystemTime(mockDate); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PollService, + { + provide: getModelToken('Poll'), + useFactory: mockPollModel, + }, + ], + }).compile(); + + service = module.get(PollService); + pollModel = module.get>(getModelToken('Poll')); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create a poll', async () => { + const mockPoll: PollEntity = { + msg: 'Test poll message', + ownerName: 'Test owner', + upvotes: 0, + downvotes: 0, + upMembers: [], + downMembers: [], + } as PollEntity; + + const mockCreatedPoll: PollDocument = { + ...mockPoll, + active: true, + createdAt: mockDate, + } as PollDocument; + + (pollModel as any).create = jest.fn((p) => mockCreatedPoll); + + const result = await service.create(mockPoll); + + expect(result).toStrictEqual(mockCreatedPoll); + }); + + it('should return null if an error occurs', async () => { + (pollModel as any).create = jest.fn().mockRejectedValue(new MongooseError('test')); + + const result = await service.create({} as PollEntity); + + expect(result).toBeNull(); + }); + }); + + describe('update', () => { + it('should update a poll', async () => { + const mockPoll: PollEntity = { + msg: 'Test poll message', + ownerName: 'Test owner', + upvotes: 0, + downvotes: 0, + upMembers: [], + downMembers: [], + } as PollEntity; + + const mockCreatedPoll: PollDocument = { + ...mockPoll, + active: true, + createdAt: mockDate, + } as PollDocument; + + (pollModel as any).findOneAndUpdate = jest.fn((p) => mockCreatedPoll); + + const result = await service.update(mockPoll as UpdatePollDto); + + expect(result).toStrictEqual(mockCreatedPoll); + }); + }) + + // Add similar tests for update and get methods +});