diff --git a/src/config/database/mongo/config/config.service.ts b/src/config/database/mongo/config/config.service.ts index 242f8d0..4aaf177 100644 --- a/src/config/database/mongo/config/config.service.ts +++ b/src/config/database/mongo/config/config.service.ts @@ -11,11 +11,16 @@ import { */ @Injectable() export class MongoConfigService implements MongooseOptionsFactory { - constructor(private configService: ConfigService) { } + constructor(private configService: ConfigService) {} createMongooseOptions(): MongooseModuleOptions { if (this.username && this.password) { - return { uri: `mongodb://${this.username}:${this.password}@${this.uri}` + (this.database ? `/?authSource=${this.database}` : "") } + return { + uri: + `mongodb://${this.username}:${this.password}@${this.uri}` + + (this.database ? `/?authSource=${this.database}` : ''), + dbName: this.database, + }; } else { return { uri: this.uri }; } diff --git a/src/helpers/abstract/collection.module.abstract.ts b/src/helpers/abstract/collection.module.abstract.ts deleted file mode 100644 index 4318431..0000000 --- a/src/helpers/abstract/collection.module.abstract.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ACollectionEntry } from './collectionEntry.abstract'; - -export abstract class ACollectionModule> { - public modulesList: T[] = []; - public modulesCollection = new Map(); - - constructor(modules: T[]) { - modules.forEach((command) => { - if (command.valid) { - this.modulesList.push(command); - this.modulesCollection.set(command.name(), command); - } - }); - } - - getCommand(commandName: string): T { - if (this.modulesCollection.has(commandName)) { - return this.modulesCollection.get(commandName); - } - return null; - } -} diff --git a/src/helpers/abstract/collectionEntry.abstract.ts b/src/helpers/abstract/collectionEntry.abstract.ts deleted file mode 100644 index 3ec7e05..0000000 --- a/src/helpers/abstract/collectionEntry.abstract.ts +++ /dev/null @@ -1,26 +0,0 @@ -export abstract class ACollectionEntry { - abstract name(): string; - - public valid(): boolean { - if (this.name() && this.execute) { - console.log('command-name: ' + this.name()); - return true; - } else { - console.error("couldn't read command:"); - console.error(this); - return false; - } - } - - public abstract execute(arg: T): Promise; - - protected async run(command: () => any): Promise { - try { - await command(); - return true; - } catch (e) { - console.error(e); - return false; - } - } -} diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts deleted file mode 100644 index 8a8704e..0000000 --- a/src/helpers/utils.ts +++ /dev/null @@ -1,76 +0,0 @@ -// import 'dotenv/config'; -import { verifyKey } from 'discord-interactions'; -// import('node-fetch'); - -export class Utils { - static capitalize(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - // Simple method that returns a random emoji from list - static getRandomEmoji(): string { - const emojiList = [ - '😭', - 'πŸ˜„', - '😌', - 'πŸ€“', - '😎', - '😀', - 'πŸ€–', - 'πŸ˜Άβ€πŸŒ«οΈ', - '🌏', - 'πŸ“Έ', - 'πŸ’Ώ', - 'πŸ‘‹', - '🌊', - '✨', - ]; - return emojiList[Math.floor(Math.random() * emojiList.length)]; - } - - static async installGlobalCommands(appId, commands) { - // API endpoint to overwrite global commands - const endpoint = `applications/${appId}/commands`; - - // try { - // This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands - await this.discordRequest(endpoint, { method: 'PUT', body: commands }); - // } catch (err) { - // console.error(err); - // } - } - - static async discordRequest( - endpoint: string, - options: { method: string; body: any }, - ) { - // append endpoint to root API URL - const url = 'https://discord.com/api/v10/' + endpoint; - // Stringify payloads - if (options.body) options.body = JSON.stringify(options.body); - // Use node-fetch to make requests - const res = await fetch(url, { - headers: { - Authorization: `Bot ${process.env.DISCORD_TOKEN}`, - 'Content-Type': 'application/json; charset=UTF-8', - 'User-Agent': - 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)', - }, - ...options, - }); - return res; - } - - static verifyDiscordRequest(clientKey) { - return function (req, res, buf, encoding) { - const signature = req.get('X-Signature-Ed25519'); - const timestamp = req.get('X-Signature-Timestamp'); - - const isValidRequest = verifyKey(buf, signature, timestamp, clientKey); - if (!isValidRequest) { - res.status(401).send('Bad request signature'); - throw new Error('Bad request signature'); - } - }; - } -} diff --git a/src/modules/command/command.abstract.ts b/src/modules/command/command.abstract.ts index 4782301..b5ee918 100644 --- a/src/modules/command/command.abstract.ts +++ b/src/modules/command/command.abstract.ts @@ -10,9 +10,7 @@ export abstract class ACommand { | SlashCommandBuilder | Omit; - public abstract execute( - arg: Interaction | CommandInteraction, - ): Promise; + public abstract execute(arg: CommandInteraction): Promise; protected async run(command: () => any): Promise { try { diff --git a/src/modules/command/commands/poll.ts b/src/modules/command/commands/poll.ts index 85c5eb7..e522a81 100644 --- a/src/modules/command/commands/poll.ts +++ b/src/modules/command/commands/poll.ts @@ -3,12 +3,13 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, + CacheType, + CommandInteraction, 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 { @@ -30,69 +31,9 @@ export class PollCommand extends ACommand { .setRequired(true), ); - public execute(arg: any /*Interaction*/): Promise { + public execute(arg: CommandInteraction): 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); + this.pollService.create(arg); return true; }); } diff --git a/src/modules/event/event.abstract.ts b/src/modules/event/event.abstract.ts index cc76ccb..c050fa9 100644 --- a/src/modules/event/event.abstract.ts +++ b/src/modules/event/event.abstract.ts @@ -1,10 +1,12 @@ -import { Events } from 'discord.js'; +import { ClientEvents } from 'discord.js'; + +export type EventKey = keyof ClientEvents; export abstract class AEvent { - abstract readonly event: Events; + abstract readonly event: EventKey; abstract readonly once: boolean; - public abstract execute(arg: any): Promise; + public abstract execute(args: ClientEvents[EventKey]): Promise; protected async run(command: () => any): Promise { try { diff --git a/src/modules/event/event.service.ts b/src/modules/event/event.service.ts index 1bd9278..ca89fbc 100644 --- a/src/modules/event/event.service.ts +++ b/src/modules/event/event.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Collection } from 'discord.js'; -import { AEvent } from './event.abstract'; +import { ClientEvents, Collection, Events } from 'discord.js'; +import { AEvent, EventKey } from './event.abstract'; import { ClientReady } from './services/clientReady'; import { Interaction } from './services/interaction'; import { MessageEvent } from './services/messageEvent'; @@ -17,10 +17,17 @@ export class EventService { const events: AEvent[] = [clientReady, interaction, message]; events.forEach((event) => { console.log('register new event: ' + event.event); - (discordService.client as any)[event.once ? 'once' : 'on']( - event.event, - (args: unknown[]) => event.execute(args), - ); + if (event.once) { + discordService.client.once( + event.event, + (...args: ClientEvents[EventKey]) => event.execute(args), + ); + } else { + discordService.client.on( + event.event, + (...args: ClientEvents[EventKey]) => event.execute(args), + ); + } }); } } diff --git a/src/modules/event/services/clientReady.ts b/src/modules/event/services/clientReady.ts index 4bcaeb7..0b8ff34 100644 --- a/src/modules/event/services/clientReady.ts +++ b/src/modules/event/services/clientReady.ts @@ -1,16 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { Events } from 'discord.js'; +import { ClientEvents, Events } from 'discord.js'; import { AEvent } from '../event.abstract'; @Injectable() export class ClientReady extends AEvent { - event: Events = Events.ClientReady; + event: keyof ClientEvents = Events.ClientReady; once: boolean = true; async execute(c) { - return this.run(() => { console.log('Successfully connected to Discord'); console.log(`logged in as ${c.user?.tag}`); - }); } } diff --git a/src/modules/event/services/interaction.spec.ts b/src/modules/event/services/interaction.spec.ts index 06b5ff6..0229772 100644 --- a/src/modules/event/services/interaction.spec.ts +++ b/src/modules/event/services/interaction.spec.ts @@ -8,15 +8,17 @@ describe('Interaction', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [Interaction, - { - provide: CommandService, - useValue: {} - }, - { - provide: PollService, - useValue: {} - }], + providers: [ + Interaction, + { + provide: CommandService, + useValue: {}, + }, + { + provide: PollService, + useValue: {}, + }, + ], }).compile(); service = module.get(Interaction); diff --git a/src/modules/event/services/interaction.ts b/src/modules/event/services/interaction.ts index f98fdcf..d417274 100644 --- a/src/modules/event/services/interaction.ts +++ b/src/modules/event/services/interaction.ts @@ -1,9 +1,13 @@ import { ActionRowBuilder, ButtonBuilder, + ButtonInteraction, ButtonStyle, + CacheType, + ClientEvents, EmbedBuilder, Events, + Message, } from 'discord.js'; import { AEvent } from '../event.abstract'; import { CommandService } from '../../command/command.service'; @@ -12,7 +16,7 @@ import { PollService } from '../../poll/service/poll.service'; @Injectable() export class Interaction extends AEvent { - event: Events = Events.InteractionCreate; + event: keyof ClientEvents = Events.InteractionCreate; once: boolean = false; constructor( @@ -22,121 +26,20 @@ export class Interaction extends AEvent { super(); } - async execute(interaction: any) { - 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, - ); - } - //await interaction.deferUpdate(); - 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 interaction.update({ 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, - ); - } - //await interaction.deferUpdate(); - 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 interaction.update({ embeds: [embed], components: [buttons] }); - } else if (interaction?.customId === 'close') { - 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; - }); + async execute(args: ClientEvents[Events.InteractionCreate]): Promise { + const interaction = args[0]; + if (interaction.isCommand()) { + const { commandName } = interaction; + var command = this.commandService.getCommand(commandName); + command?.execute(interaction); + } else if (interaction.isButton()) { + if (interaction?.customId === 'up') { + await this.pollService.upVote(interaction); + } else if (interaction?.customId === 'down') { + await this.pollService.downVote(interaction); + } else if (interaction?.customId === 'close') { + await this.pollService.closePoll(interaction); + } + } } } diff --git a/src/modules/event/services/messageEvent.ts b/src/modules/event/services/messageEvent.ts index 7c9e01c..52fb044 100644 --- a/src/modules/event/services/messageEvent.ts +++ b/src/modules/event/services/messageEvent.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { Events } from 'discord.js'; -import { AEvent } from '../event.abstract'; +import { ClientEvents, Events, Message } from 'discord.js'; +import { AEvent, EventKey } from '../event.abstract'; import { IResponse, ResponseType } from '../interfaces/iresponse'; @Injectable() export class MessageEvent extends AEvent { - event: Events = Events.MessageCreate; // ShardEvents.Message; + event: keyof ClientEvents = Events.MessageCreate; // ShardEvents.Message; once: boolean = false; private responseList: Array = [ @@ -60,13 +60,13 @@ export class MessageEvent extends AEvent { }, ]; - async execute(message: any /*Message*/) { - return this.run(() => { - const { content, channel, author } = message; - if (author.bot) return; + async execute(args: ClientEvents[Events.MessageCreate]):Promise { + const message = args[0]; + const { content, channel, author } = message; + if (author.bot) return; - this.responseList.forEach((res) => { - var testRes = res.matcher.test(content); + this.responseList.forEach((res) => { + var testRes = res.matcher.test(content); if (testRes) { if (res?.responseType == ResponseType.Reply) { message.reply(res.response); @@ -75,6 +75,5 @@ export class MessageEvent extends AEvent { } } }); - }); } } diff --git a/src/modules/interaction/express.abstract.ts b/src/modules/interaction/express.abstract.ts deleted file mode 100644 index f71819a..0000000 --- a/src/modules/interaction/express.abstract.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Events } from 'discord.js'; -import { ACollectionEntry } from '../../helpers/abstract/collectionEntry.abstract'; - -export abstract class AExpress extends ACollectionEntry { - abstract readonly event: Events; - abstract readonly once: boolean; - name(): string { - return this.event; - } -} diff --git a/src/modules/interaction/express.module.ts b/src/modules/interaction/express.module.ts deleted file mode 100644 index 50056c6..0000000 --- a/src/modules/interaction/express.module.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACollectionModule } from '../../helpers/abstract/collection.module.abstract'; -import { Utils } from '../../helpers/utils'; -import { AExpress } from './express.abstract'; - -import { - InteractionResponseType, - InteractionType, - verifyKeyMiddleware, -} from 'discord-interactions'; -import { Express, Request, Response } from 'express-serve-static-core'; -const express = require('express'); -const app: Express = express(); - -export class ExpressModule extends ACollectionModule { - constructor() { - const moduleList: AExpress[] = []; - - super(moduleList); - } - - init(port: number | string) { - app.use( - express.json({ - verify: Utils.verifyDiscordRequest(process.env.PUBLIC_KEY), - }), - ); - - app.listen(port, () => { - console.log('Listening on port', port); - }); - - app.post( - '/interactions', - verifyKeyMiddleware(process.env.APP_PUBLIC_KEY), - (req: Request, res: Response) => { - const { type, data } = req.body; - - /** - * Handle verification requests - */ - if (type === InteractionType.PING) { - return res.send({ type: InteractionResponseType.PONG }); - } - - if (type === InteractionType.APPLICATION_COMMAND) { - const { name } = data; - switch (name) { - case 'test': - // Send a message into the channel where command was triggered from - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'hello world ' + Utils.getRandomEmoji(), - }, - }); - default: - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Hello world', - }, - }); - } - } - }, - ); - } -} diff --git a/src/modules/poll/module/poll.module.ts b/src/modules/poll/module/poll.module.ts index 9d1cf2e..d9c629c 100644 --- a/src/modules/poll/module/poll.module.ts +++ b/src/modules/poll/module/poll.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Poll, PollSchema } from '../../../schemas/poll.schema'; +import { DbPollService } from '../service/db-poll.service'; import { PollService } from '../service/poll.service'; @Module({ @@ -13,7 +14,7 @@ import { PollService } from '../service/poll.service'; ]), ], controllers: [], - providers: [PollService], + providers: [DbPollService, PollService], exports: [PollService], }) export class PollModule {} diff --git a/src/modules/poll/service/poll.service.spec.ts b/src/modules/poll/service/db-poll.service.spec.ts similarity index 93% rename from src/modules/poll/service/poll.service.spec.ts rename to src/modules/poll/service/db-poll.service.spec.ts index 45be9b1..71483fd 100644 --- a/src/modules/poll/service/poll.service.spec.ts +++ b/src/modules/poll/service/db-poll.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getModelToken } from '@nestjs/mongoose'; import { Model, MongooseError } from 'mongoose'; -import { PollService } from './poll.service'; +import { DbPollService } from './db-poll.service'; import { PollDocument } from '../../../schemas/poll.schema'; import { PollEntity } from '../../../schemas/poll-entity.model'; import { UpdatePollDto } from '../dto/update-poll.dto'; @@ -13,8 +13,8 @@ const mockPollModel = () => ({ findOne: jest.fn(), }); -describe('PollService', () => { - let service: PollService; +describe('DbPollService', () => { + let service: DbPollService; let pollModel: Model; let mockDate: Date = new Date(0); jest.useFakeTimers(); @@ -22,7 +22,7 @@ describe('PollService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - PollService, + DbPollService, { provide: getModelToken('Poll'), useFactory: mockPollModel, @@ -30,7 +30,7 @@ describe('PollService', () => { ], }).compile(); - service = module.get(PollService); + service = module.get(DbPollService); pollModel = module.get>(getModelToken('Poll')); }); diff --git a/src/modules/poll/service/db-poll.service.ts b/src/modules/poll/service/db-poll.service.ts new file mode 100644 index 0000000..9c7190f --- /dev/null +++ b/src/modules/poll/service/db-poll.service.ts @@ -0,0 +1,60 @@ +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 DbPollService { + 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) { + console.error(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/poll/service/poll.service.ts b/src/modules/poll/service/poll.service.ts index 25290b4..8a177b7 100644 --- a/src/modules/poll/service/poll.service.ts +++ b/src/modules/poll/service/poll.service.ts @@ -1,59 +1,192 @@ -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + CacheType, + CommandInteraction, + Embed, + EmbedBuilder, + Message, +} from 'discord.js'; +import { DbPollService } from './db-poll.service'; +import { Inject } from '@nestjs/common'; 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, + @Inject(DbPollService) + private readonly dbPollService: DbPollService, ) {} - 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(), + async create(arg: CommandInteraction) { + const author = arg.user.displayName ?? arg.user.username; + await arg.reply({ + content: `${author} has started a new poll!`, + ephemeral: true, + }); + const topic = arg.options.get('topic', true).value; + // const topic2 = 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, }); - } catch (e) { - return null; + const buttons = this.getPollButtons(); + const message = await arg.channel.send({ + embeds: [embed], + components: [buttons] as any, + }); + message.createMessageComponentCollector(); + + await this.dbPollService.create({ + msg: message.id, + upvotes: 0, + downvotes: 0, + upMembers: [], + downMembers: [], + active: true, + ownerName: arg.user.username, + createdAt: new Date(), + }); + } + + private async update( + interaction: ButtonInteraction, + msg: Message, + data: PollDocument, + ) { + await this.dbPollService.update(data); + await this.updatePollMessage(interaction, msg, data); + } + + public async upVote(interaction: ButtonInteraction) { + const data = await this.dbPollService.get(interaction.message.id); + if (!data) return; + + const msg = await interaction.channel.messages.fetch(data.msg); + + if (data.upMembers.includes(interaction.user.id)) { + return await interaction.reply({ + content: `${interaction.user} you already voted for this`, + ephemeral: true, + }); + } + if (data.downMembers.includes(interaction.user.id)) { + data.downvotes--; + data.downMembers = data.downMembers.filter( + (member) => member != interaction.user.id, + ); } + // await interaction.deferUpdate(); + data.upvotes++; + data.upMembers.push(interaction.user.id); + await this.update(interaction, msg, data); } - 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; + public async downVote(interaction: ButtonInteraction) { + const data = await this.dbPollService.get(interaction.message.id); + if (!data) return; + + const msg = await interaction.channel.messages.fetch(data.msg); + + if (data.downMembers.includes(interaction.user.id)) { + return await interaction.reply({ + content: `${interaction.user} you already voted for this`, + ephemeral: true, + }); + } + if (data.upMembers.includes(interaction.user.id)) { + data.upvotes--; + data.upMembers = data.upMembers.filter( + (member) => member != interaction.user.id, + ); } + //await interaction.deferUpdate(); + data.downvotes++; + data.downMembers.push(interaction.user.id); + await this.update(interaction, msg, data); } - async get(messageId: string): Promise { - try { - const test = await this.pollModel.findOne({msg: messageId, active:true}) - return test; - } catch(e) { - return null + public async closePoll(interaction: ButtonInteraction) { + const data = await this.dbPollService.get(interaction.message.id); + if (!data) return; + const msg = await interaction.channel.messages.fetch(data.msg); + if (interaction.user.username == data.ownerName) { + data.active = false; + await this.dbPollService.update(data); + await interaction.update({ components: [] }); + await interaction.channel.send({ + content: 'The Poll has been closed!', + }); + } else { + await interaction.channel.send({ + content: `${interaction.user} you don\'t own this poll!`, + }); } } + + private getPollButtons() { + return 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), + ); + } + + private setPollCounter(embed: Embed, data: PollDocument) { + return EmbedBuilder.from(embed).setFields( + { + name: 'Upvotes πŸ‘', + value: `> **${data.upvotes}** votes`, + inline: true, + }, + { + name: 'Downvotes πŸ‘Ž', + value: `> **${data.downvotes}** votes`, + inline: true, + }, + { name: 'Author', value: `> @${data.ownerName}` }, + ); + } + + private async updatePollMessage( + interaction: ButtonInteraction, + msg: Message, + data: PollDocument, + ) { + const buttons = this.getPollButtons(); + const embed = this.setPollCounter(msg.embeds[0], data); + await interaction.update({ + embeds: [embed], + components: [buttons] as any, + }); + } }