From dcb98fc947b3e178c7bebe2e78d5f252804e15e0 Mon Sep 17 00:00:00 2001 From: Pavel DE WAVRECHIN Date: Fri, 5 Jul 2024 12:54:40 +0200 Subject: [PATCH] Add reminder command --- docs/commands.md | 12 +++ package.json | 3 +- .../20171223203915_create_tables.js | 16 +++ src/main.js | 1 + src/modules/reminder.js | 102 ++++++++++++++++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/modules/reminder.js diff --git a/docs/commands.md b/docs/commands.md index c1ac4a78d..dd4d6e94b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -116,6 +116,18 @@ Prints the ID of the current DM channel with the user Shows the DM channel ID, DM message ID, and message link of the specified user reply. `` is the message number shown in front of staff replies in the thread channel. +### `!rem ` +Adds a reminder for a task. You will be pinged as soon as the time is reached. +The reminder is stored in the database so if you restart the bot, the reminders remain active. +After you set up a reminder, the bot confirms it and gives you the reminder ID. + +**Example:** `!rem 18:40 Help this people` + +### `!delrem ` +Delete a reminder via its ID + +**Example:** `!delrem 18` + ## Anywhere on the inbox server These commands can be used anywhere on the inbox server, even outside Modmail threads. diff --git a/package.json b/package.json index a4c50d8bb..6ade262d6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "tmp": "^0.2.1", "transliteration": "^2.3.5", "uuid": "^9.0.1", - "yargs-parser": "^21.1.1" + "yargs-parser": "^21.1.1", + "cron": "^3.1.7" }, "devDependencies": { "eslint": "^8.49.0", diff --git a/src/data/migrations/20171223203915_create_tables.js b/src/data/migrations/20171223203915_create_tables.js index ac0c77813..fccd784ae 100644 --- a/src/data/migrations/20171223203915_create_tables.js +++ b/src/data/migrations/20171223203915_create_tables.js @@ -43,6 +43,18 @@ exports.up = async function(knex, Promise) { table.dateTime("created_at").notNullable(); }); } + + if (! await knex.schema.hasTable("reminders")) { + await knex.schema.createTable("reminders", table => { + table.increments('id').primary(); + table.string('thread_id').notNullable(); + table.string('user_id').notNullable(); + table.timestamp('reminder_time').notNullable(); + table.text('message').notNullable(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + table.timestamp('updated_at').defaultTo(knex.fn.now()); + }); + } }; exports.down = async function(knex, Promise) { @@ -61,4 +73,8 @@ exports.down = async function(knex, Promise) { if (await knex.schema.hasTable("snippets")) { await knex.schema.dropTable("snippets"); } + + if (await knex.schema.hasTable("reminders")) { + await knex.schema.dropTable("reminders"); + } }; diff --git a/src/main.js b/src/main.js index bce3385bc..2a6da6744 100644 --- a/src/main.js +++ b/src/main.js @@ -374,6 +374,7 @@ function getBasePlugins() { "file:./src/modules/joinLeaveNotification", "file:./src/modules/roles", "file:./src/modules/notes", + "file:./src/modules/reminder", ]; } diff --git a/src/modules/reminder.js b/src/modules/reminder.js new file mode 100644 index 000000000..3b0ab6b97 --- /dev/null +++ b/src/modules/reminder.js @@ -0,0 +1,102 @@ +const { CronJob } = require('cron'); +const threads = require("../data/threads"); + +const reminderJobs = {}; + +async function loadReminders(knex, bot) { + const reminders = await knex('reminders').select('*'); + for (const reminder of reminders) { + const { id, thread_id, user_id, reminder_time, message } = reminder; + const alertTime = new Date(reminder_time); + const now = new Date(); + + if (alertTime > now) { + const cronTime = `${alertTime.getUTCMinutes()} ${alertTime.getUTCHours()} * * *`; + const job = new CronJob(cronTime, async () => { + const thread = await threads.findById(thread_id); + if (thread) { + await thread.postSystemMessage(`<@!${user_id}> Rappel : ${message}`); + } + job.stop(); + delete reminderJobs[id]; + }, null, true, 'UTC'); + + reminderJobs[id] = job; + } + } +} + +module.exports = async ({ bot, knex, config, commands }) => { + + await loadReminders(knex, bot); + + commands.addInboxThreadCommand("rem", [{name: "message", type: "string", catchAll: true}], async (msg, args, thread) => { + + const messageParts = args.message.split(" "); + const timePart = messageParts.shift(); + const reminderMessage = messageParts.join(" "); + + const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/); + if (!timeMatch) { + await thread.postSystemMessage("Incorrect format. Use: `!rem HH:MM message`"); + return; + } + + const hour = parseInt(timeMatch[1], 10) - 2; + const minute = parseInt(timeMatch[2], 10); + const realHour = parseInt(timeMatch[1], 10); + + if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) { + await thread.postSystemMessage("Invalid time. Use: `!rem HH:MM message`"); + return; + } + + const now = new Date(); + let alertTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute); + let realAlertTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), realHour, minute); + + if (alertTime < now) { + alertTime.setDate(alertTime.getDate() + 1); + } + + const alertTimeUtc = alertTime.toISOString(); + + const [reminder] = await knex('reminders').insert({ + thread_id: thread.id, + user_id: msg.author.id, + reminder_time: alertTimeUtc, + message: reminderMessage + }).returning('id'); + + const reminderId = reminder.id; + + const cronTime = `${minute} ${hour} * * *`; + const job = new CronJob(cronTime, async () => { + await thread.postSystemMessage(`⏰ **REMINDER** <@${msg.author.id}> : \n\n >>> ${reminderMessage}`, { + allowedMentions: { + users: [msg.author.id], + }, + }); + job.stop(); + delete reminderJobs[reminderId]; + }, null, true, 'UTC'); + + reminderJobs[reminderId] = job; + + await thread.postSystemMessage(`**⏰ Reminder set for ${realAlertTime.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} :** \n\n > ${reminderMessage} \n\n **(ID: ${reminderId})**`); + }, { allowSuspended: true }); + + commands.addInboxThreadCommand("delrem", [{name: "id", type: "number"}], async (msg, args, thread) => { + await knex('reminders') + .where({ id: args.id, thread_id: thread.id }) + .delete(); + + if (reminderJobs[args.id]) { + reminderJobs[args.id].stop(); + delete reminderJobs[args.id]; + } + + await thread.postSystemMessage(`The reminder with ID **${args.id}** has been deleted.`); + }, { allowSuspended: true }); + +};