From d10344e49267dd7fe97e4851fd3d3beb1c9dc48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Sun, 19 Sep 2021 14:36:35 +0100 Subject: [PATCH 1/5] Rename attendance polls to polls --- src/bot.ts | 14 +-- src/modules/{attendance.ts => polls.ts} | 96 +++++++++---------- .../migration.sql | 3 + src/prisma/schema.prisma | 4 +- 4 files changed, 57 insertions(+), 60 deletions(-) rename src/modules/{attendance.ts => polls.ts} (73%) create mode 100644 src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql diff --git a/src/bot.ts b/src/bot.ts index 93b6b1e..602382e 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -11,7 +11,7 @@ import { PrismaClient } from "@prisma/client"; import { InteractionHandlers, CommandProvider, Chore } from "./bot.d"; import * as utils from "./modules/utils"; -import * as attendance from "./modules/attendance"; +import * as polls from "./modules/polls"; import * as roleSelection from "./modules/roleSelection"; import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; @@ -48,7 +48,7 @@ const client = new Discord.Client({ }); const commandProviders: CommandProvider[] = [ - attendance.provideCommands, + polls.provideCommands, roleSelection.provideCommands, sudo.provideCommands, misc.provideCommands, @@ -62,7 +62,7 @@ const commandHandlers: InteractionHandlers = {}; // two above will be dynamically loaded const buttonHandlers: InteractionHandlers = { - attendance: attendance.handleAttendanceButton, + polls: polls.handlePollButton, roleSelection: roleSelection.handleRoleSelectionButton, }; @@ -72,18 +72,18 @@ const menuHandlers: InteractionHandlers = { const startupChores: Chore[] = [ { - summary: "Schedule attendance polls", + summary: "Schedule polls", fn: async () => - await attendance.scheduleAttendancePolls( + await polls.schedulePolls( client, prisma, - await prisma.attendancePoll.findMany({ + await prisma.poll.findMany({ where: { type: "scheduled", }, }) ), - complete: "All attendance polls scheduled", + complete: "All polls scheduled", }, { summary: "Send role selection messages", diff --git a/src/modules/attendance.ts b/src/modules/polls.ts similarity index 73% rename from src/modules/attendance.ts rename to src/modules/polls.ts index 032c74d..d111e96 100644 --- a/src/modules/attendance.ts +++ b/src/modules/polls.ts @@ -1,4 +1,4 @@ -// Handler for attendance polls +// Handler for polls import { ButtonInteraction, @@ -14,28 +14,28 @@ import { import * as Builders from "@discordjs/builders"; import cron from "node-cron"; -import { PrismaClient, AttendancePoll } from "@prisma/client"; +import { PrismaClient, Poll } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; -const ATTENDANCE_POLL_MSG = "Attendance Poll"; -const ATTENDANCE_POLL_ACTION_ROW = new MessageActionRow(); -ATTENDANCE_POLL_ACTION_ROW.addComponents([ +const POLL_MSG = "Poll"; +const POLL_ACTION_ROW = new MessageActionRow(); +POLL_ACTION_ROW.addComponents([ new MessageButton() .setLabel("Yes") .setStyle("SUCCESS") - .setCustomId("attendance:yes"), + .setCustomId("poll:yes"), new MessageButton() .setLabel("No") .setStyle("DANGER") - .setCustomId("attendance:no"), + .setCustomId("poll:no"), new MessageButton() .setLabel("Clear") .setStyle("SECONDARY") - .setCustomId("attendance:clear"), + .setCustomId("poll:clear"), ]); -const ATTENDANCE_NO_ONE = "*No one*"; +const POLL_NO_ONE = "*No one*"; -export const handleAttendanceButton = async ( +export const handlePollButton = async ( interaction: ButtonInteraction ): Promise => { await interaction.deferReply({ ephemeral: true }); @@ -51,22 +51,22 @@ export const handleAttendanceButton = async ( fieldIndex = 1; } - const newEmbed = getNewEmbed( + const newEmbed = getNewPollEmbed( oldEmbeds[0] as MessageEmbed, fieldIndex, interaction.user.id ); (interaction.message as Message).edit({ - content: ATTENDANCE_POLL_MSG, + content: POLL_MSG, embeds: [newEmbed], - components: [ATTENDANCE_POLL_ACTION_ROW], + components: [POLL_ACTION_ROW], }); await interaction.editReply("Response recorded!"); }; -export const getNewEmbed = ( +export const getNewPollEmbed = ( oldEmbed: MessageEmbed, fieldIndex: number, userId: Snowflake @@ -76,10 +76,9 @@ export const getNewEmbed = ( field.value .split("\n") .filter( - (user) => - user !== ATTENDANCE_NO_ONE && user !== `<@${userId}>` + (user) => user !== POLL_NO_ONE && user !== `<@${userId}>` ) - .join("\n") || (fieldIndex === i ? "" : ATTENDANCE_NO_ONE); + .join("\n") || (fieldIndex === i ? "" : POLL_NO_ONE); }); if (oldEmbed.fields[fieldIndex]) oldEmbed.fields[ @@ -89,8 +88,8 @@ export const getNewEmbed = ( return oldEmbed; }; -export const sendAttendanceEmbed = async ( - embed: AttendancePoll, +export const sendPollEmbed = async ( + embed: Poll, channel: TextChannel ): Promise => { const pinnedMessages = await channel.messages.fetchPinned(); @@ -106,25 +105,25 @@ export const sendAttendanceEmbed = async ( ); const message = await channel.send({ - content: ATTENDANCE_POLL_MSG, + content: POLL_MSG, embeds: [ new MessageEmbed() .setTitle(embed.title) - .addField("Attending", ATTENDANCE_NO_ONE, true) - .addField("Not Attending", ATTENDANCE_NO_ONE, true) + .addField("Yes", POLL_NO_ONE, true) + .addField("No", POLL_NO_ONE, true) .setFooter(embed.id) .setTimestamp(), ], - components: [ATTENDANCE_POLL_ACTION_ROW], + components: [POLL_ACTION_ROW], }); await message.pin(); }; -export const scheduleAttendancePolls = async ( +export const schedulePolls = async ( client: Client, prisma: PrismaClient, - polls: AttendancePoll[] + polls: Poll[] ): Promise => { await Promise.all( polls.map(async (poll) => { @@ -134,7 +133,7 @@ export const scheduleAttendancePolls = async ( if (!channel) { console.error( - `Couldn't fetch channel ${poll.channelId} for attendance poll ${poll.id}` + `Couldn't fetch channel ${poll.channelId} for poll ${poll.id}` ); return; } @@ -142,17 +141,14 @@ export const scheduleAttendancePolls = async ( cron.schedule(poll.cron, async () => { try { // make sure it wasn't deleted / edited in the meantime - const p = await prisma.attendancePoll.findFirst({ + const p = await prisma.poll.findFirst({ where: { id: poll.id }, }); if (p !== null) { - await sendAttendanceEmbed(p, channel as TextChannel); + await sendPollEmbed(p, channel as TextChannel); } } catch (e) { - console.error( - "Could not verify (& send) attendance poll:", - e.message - ); + console.error("Could not verify (& send) poll:", e.message); } }); }) @@ -161,12 +157,12 @@ export const scheduleAttendancePolls = async ( export function provideCommands(): CommandDescriptor[] { const cmd = new Builders.SlashCommandBuilder() - .setName("attendance") - .setDescription("Manage attendance polls"); + .setName("poll") + .setDescription("Manage polls"); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("add") - .setDescription("Create a new scheduled attendance poll") + .setDescription("Create a new scheduled poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -176,7 +172,7 @@ export function provideCommands(): CommandDescriptor[] { .addStringOption( new Builders.SlashCommandStringOption() .setName("title") - .setDescription("Attendance poll title") + .setDescription("Poll title") .setRequired(true) ) .addStringOption( @@ -197,7 +193,7 @@ export function provideCommands(): CommandDescriptor[] { cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("remove") - .setDescription("Remove an existing attendance poll") + .setDescription("Remove an existing poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -208,12 +204,12 @@ export function provideCommands(): CommandDescriptor[] { cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("list") - .setDescription("List existing attendance polls") + .setDescription("List existing polls") ); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("info") - .setDescription("Get information for an existing attendance poll") + .setDescription("Get information for an existing poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -239,7 +235,7 @@ export async function handleCommand( // TODO: don't take this at face value // ^ how important is this? in principle admins won't mess up - const poll = await prisma.attendancePoll.create({ + const poll = await prisma.poll.create({ data: { id, type: "scheduled", @@ -249,9 +245,7 @@ export async function handleCommand( }, }); - await scheduleAttendancePolls(interaction.client, prisma, [ - poll, - ]); + await schedulePolls(interaction.client, prisma, [poll]); await interaction.editReply( "✅ Successfully added and scheduled." @@ -268,7 +262,7 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); - await prisma.attendancePoll.delete({ where: { id } }); + await prisma.poll.delete({ where: { id } }); await interaction.editReply("✅ Successfully removed."); } catch (e) { @@ -279,18 +273,18 @@ export async function handleCommand( } case "list": { try { - const polls = await prisma.attendancePoll.findMany(); + const polls = await prisma.poll.findMany(); await interaction.editReply({ embeds: [ new MessageEmbed() - .setTitle("Attendance Polls") + .setTitle("Polls") .setDescription( polls.length - ? "Below is a list of all attendance polls with their title and ID" - : "No attendance polls found" + ? "Below is a list of all polls with their title and ID" + : "No polls found" ) .addFields( - polls.map((p) => ({ + polls.map((p: Poll) => ({ name: p.title, value: p.id, inline: true, @@ -308,7 +302,7 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); - const poll = await prisma.attendancePoll.findFirst({ + const poll = await prisma.poll.findFirst({ where: { id }, }); @@ -320,7 +314,7 @@ export async function handleCommand( await interaction.editReply({ embeds: [ new MessageEmbed() - .setTitle("Attendance Poll Information") + .setTitle("Poll Information") .addField("ID", poll.id, true) .addField("Type", poll.type, true) .addField("Title", poll.title, true) diff --git a/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql b/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql new file mode 100644 index 0000000..f9585f1 --- /dev/null +++ b/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql @@ -0,0 +1,3 @@ +PRAGMA foreign_keys=off; +ALTER TABLE "attendance_polls" RENAME TO "polls"; +PRAGMA foreign_keys=on; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index c56b775..ea97dc6 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -15,14 +15,14 @@ model Config { @@map("config") } -model AttendancePoll { +model Poll { id String @id /// identifier used to keep track of embed on pinned messages type String title String cron String /// cron schedule channelId String @map("channel_id") /// channel where to post the poll - @@map("attendance_polls") + @@map("polls") } model RoleGroup { From b6de6847e6b95e10b7ad1a71fbaf624e9540e9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Sun, 19 Sep 2021 22:15:48 +0100 Subject: [PATCH 2/5] Support for one-shot polls --- src/modules/polls.ts | 50 +++++++++++-------- .../migration.sql | 14 ++++++ src/prisma/schema.prisma | 2 +- 3 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql diff --git a/src/modules/polls.ts b/src/modules/polls.ts index d111e96..7bed836 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -23,15 +23,15 @@ POLL_ACTION_ROW.addComponents([ new MessageButton() .setLabel("Yes") .setStyle("SUCCESS") - .setCustomId("poll:yes"), + .setCustomId("polls:yes"), new MessageButton() .setLabel("No") .setStyle("DANGER") - .setCustomId("poll:no"), + .setCustomId("polls:no"), new MessageButton() .setLabel("Clear") .setStyle("SECONDARY") - .setCustomId("poll:clear"), + .setCustomId("polls:clear"), ]); const POLL_NO_ONE = "*No one*"; @@ -138,7 +138,7 @@ export const schedulePolls = async ( return; } - cron.schedule(poll.cron, async () => { + cron.schedule(poll.cron as string, async () => { try { // make sure it wasn't deleted / edited in the meantime const p = await prisma.poll.findFirst({ @@ -162,7 +162,7 @@ export function provideCommands(): CommandDescriptor[] { cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("add") - .setDescription("Create a new scheduled poll") + .setDescription("Create a new poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -175,20 +175,20 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("Poll title") .setRequired(true) ) - .addStringOption( - new Builders.SlashCommandStringOption() - .setName("cron") - .setDescription( - "Cron schedule string; BE VERY CAREFUL THIS IS CORRECT!" - ) - .setRequired(true) - ) .addChannelOption( new Builders.SlashCommandChannelOption() .setName("channel") .setDescription("Where polls will be sent") .setRequired(true) ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("schedule") + .setDescription( + "Cron schedule string; BE VERY CAREFUL THIS IS CORRECT! If none, send one-shot poll." + ) + .setRequired(false) + ) ); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() @@ -229,26 +229,30 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); const title = interaction.options.getString("title", true); - const cron = interaction.options.getString("cron", true); const channel = interaction.options.getChannel("channel", true); - - // TODO: don't take this at face value - // ^ how important is this? in principle admins won't mess up + const cron = interaction.options.getString( + "schedule", + false + ) as string | null; const poll = await prisma.poll.create({ data: { id, - type: "scheduled", + type: cron ? "scheduled" : "one-shot", title, cron, channelId: channel.id, }, }); - await schedulePolls(interaction.client, prisma, [poll]); + if (cron) { + await schedulePolls(interaction.client, prisma, [poll]); + } else { + await sendPollEmbed(poll, channel as TextChannel); + } await interaction.editReply( - "✅ Successfully added and scheduled." + `✅ Successfully added${cron ? " and scheduled" : ""}.` ); } catch (e) { await interaction.editReply( @@ -318,7 +322,11 @@ export async function handleCommand( .addField("ID", poll.id, true) .addField("Type", poll.type, true) .addField("Title", poll.title, true) - .addField("Cron Schedule", poll.cron, true) + .addField( + "Schedule", + poll.cron ? poll.cron : "N/A", + true + ) .addField( "Channel", `<#${poll.channelId}>`, diff --git a/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql b/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql new file mode 100644 index 0000000..e03b92f --- /dev/null +++ b/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql @@ -0,0 +1,14 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_polls" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "cron" TEXT, + "channel_id" TEXT NOT NULL +); +INSERT INTO "new_polls" ("channel_id", "cron", "id", "title", "type") SELECT "channel_id", "cron", "id", "title", "type" FROM "polls"; +DROP TABLE "polls"; +ALTER TABLE "new_polls" RENAME TO "polls"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index ea97dc6..1dc6022 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -19,7 +19,7 @@ model Poll { id String @id /// identifier used to keep track of embed on pinned messages type String title String - cron String /// cron schedule + cron String? /// cron schedule channelId String @map("channel_id") /// channel where to post the poll @@map("polls") From 6c6abcddeaaf36b292e15a2fcb8cd6dd7338b8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Mon, 20 Sep 2021 01:21:06 +0100 Subject: [PATCH 3/5] Small fixes following up @RafDevX's review --- src/bot.ts | 13 +++---------- src/modules/polls.ts | 43 ++++++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 602382e..f00c45b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -73,16 +73,9 @@ const menuHandlers: InteractionHandlers = { const startupChores: Chore[] = [ { summary: "Schedule polls", - fn: async () => - await polls.schedulePolls( - client, - prisma, - await prisma.poll.findMany({ - where: { - type: "scheduled", - }, - }) - ), + fn: async () => { + await polls.scheduleAllScheduledPolls(client, prisma); + }, complete: "All polls scheduled", }, { diff --git a/src/modules/polls.ts b/src/modules/polls.ts index 7bed836..9806637 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -89,7 +89,7 @@ export const getNewPollEmbed = ( }; export const sendPollEmbed = async ( - embed: Poll, + poll: Poll, channel: TextChannel ): Promise => { const pinnedMessages = await channel.messages.fetchPinned(); @@ -97,7 +97,7 @@ export const sendPollEmbed = async ( pinnedMessages.map((msg) => { if ( msg.embeds?.some( - (msgEmbed) => msgEmbed.footer?.text === embed.id + (msgEmbed) => msgEmbed.footer?.text === poll.id ) ) return msg.unpin(); @@ -108,10 +108,10 @@ export const sendPollEmbed = async ( content: POLL_MSG, embeds: [ new MessageEmbed() - .setTitle(embed.title) + .setTitle(poll.title) .addField("Yes", POLL_NO_ONE, true) .addField("No", POLL_NO_ONE, true) - .setFooter(embed.id) + .setFooter(poll.id) .setTimestamp(), ], components: [POLL_ACTION_ROW], @@ -123,7 +123,7 @@ export const sendPollEmbed = async ( export const schedulePolls = async ( client: Client, prisma: PrismaClient, - polls: Poll[] + polls: (Poll & { cron: string })[] ): Promise => { await Promise.all( polls.map(async (poll) => { @@ -138,7 +138,7 @@ export const schedulePolls = async ( return; } - cron.schedule(poll.cron as string, async () => { + cron.schedule(poll.cron, async () => { try { // make sure it wasn't deleted / edited in the meantime const p = await prisma.poll.findFirst({ @@ -148,13 +148,31 @@ export const schedulePolls = async ( await sendPollEmbed(p, channel as TextChannel); } } catch (e) { - console.error("Could not verify (& send) poll:", e.message); + console.error( + "Could not verify (& send) poll:", + (e as Error).message + ); } }); }) ); }; +export const scheduleAllScheduledPolls = async ( + client: Client, + prisma: PrismaClient +): Promise => { + await schedulePolls( + client, + prisma, + (await prisma.poll.findMany({ + where: { + type: "scheduled", + }, + })) as (Poll & { cron: string })[] + ); +}; + export function provideCommands(): CommandDescriptor[] { const cmd = new Builders.SlashCommandBuilder() .setName("poll") @@ -230,10 +248,7 @@ export async function handleCommand( const id = interaction.options.getString("id", true); const title = interaction.options.getString("title", true); const channel = interaction.options.getChannel("channel", true); - const cron = interaction.options.getString( - "schedule", - false - ) as string | null; + const cron = interaction.options.getString("schedule", false); const poll = await prisma.poll.create({ data: { @@ -246,7 +261,9 @@ export async function handleCommand( }); if (cron) { - await schedulePolls(interaction.client, prisma, [poll]); + await schedulePolls(interaction.client, prisma, [ + poll as Poll & { cron: string }, + ]); } else { await sendPollEmbed(poll, channel as TextChannel); } @@ -288,7 +305,7 @@ export async function handleCommand( : "No polls found" ) .addFields( - polls.map((p: Poll) => ({ + polls.map((p) => ({ name: p.title, value: p.id, inline: true, From 8876b8a0909868fdc08dc0d728b6d8140ea0f321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Mon, 20 Sep 2021 01:42:45 +0100 Subject: [PATCH 4/5] Unpin deleted threads --- src/modules/polls.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/modules/polls.ts b/src/modules/polls.ts index 9806637..dfd20f8 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -88,7 +88,7 @@ export const getNewPollEmbed = ( return oldEmbed; }; -export const sendPollEmbed = async ( +export const unpinPoll = async ( poll: Poll, channel: TextChannel ): Promise => { @@ -103,6 +103,13 @@ export const sendPollEmbed = async ( return msg.unpin(); }) ); +}; + +export const sendPollEmbed = async ( + poll: Poll, + channel: TextChannel +): Promise => { + await unpinPoll(poll, channel); const message = await channel.send({ content: POLL_MSG, @@ -283,6 +290,14 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); + const p = (await prisma.poll.findFirst({ + where: { id }, + })) as Poll; + const channel = (await interaction.client.channels.fetch( + p.channelId as Snowflake + )) as TextChannel; + await unpinPoll(p, channel); + await prisma.poll.delete({ where: { id } }); await interaction.editReply("✅ Successfully removed."); From 164167014f0bb3168b47ffc15ea8ecd7f33620b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Mon, 20 Sep 2021 15:10:32 +0100 Subject: [PATCH 5/5] Remove unnecessary "Poll" message --- src/modules/polls.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/modules/polls.ts b/src/modules/polls.ts index dfd20f8..8969e25 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -17,7 +17,6 @@ import cron from "node-cron"; import { PrismaClient, Poll } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; -const POLL_MSG = "Poll"; const POLL_ACTION_ROW = new MessageActionRow(); POLL_ACTION_ROW.addComponents([ new MessageButton() @@ -58,7 +57,6 @@ export const handlePollButton = async ( ); (interaction.message as Message).edit({ - content: POLL_MSG, embeds: [newEmbed], components: [POLL_ACTION_ROW], }); @@ -112,7 +110,6 @@ export const sendPollEmbed = async ( await unpinPoll(poll, channel); const message = await channel.send({ - content: POLL_MSG, embeds: [ new MessageEmbed() .setTitle(poll.title)