From 30b87b5b771a69230c42130af49d8352dc912da4 Mon Sep 17 00:00:00 2001 From: theimperious1 Date: Wed, 2 Oct 2024 18:38:19 -0400 Subject: [PATCH 1/3] new users column & migration, helper activity tracking, timer for autopruning inactive helpers --- src/discord/events/messageCreate.ts | 2 + src/discord/utils/helperActivityUpdate.ts | 51 ++++++++++++ src/global/utils/timer.ts | 83 +++++++++++++++++++ .../migration.sql | 2 + src/prisma/tripbot/schema.prisma | 1 + 5 files changed, 139 insertions(+) create mode 100644 src/discord/utils/helperActivityUpdate.ts create mode 100644 src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql diff --git a/src/discord/events/messageCreate.ts b/src/discord/events/messageCreate.ts index 4646a15dd..3c48532ab 100644 --- a/src/discord/events/messageCreate.ts +++ b/src/discord/events/messageCreate.ts @@ -17,6 +17,7 @@ import { imagesOnly } from '../utils/imagesOnly'; import { countMessage } from '../commands/guild/d.counting'; import { bridgeMessage } from '../utils/bridge'; import { discordAiModerate } from '../commands/global/d.ai'; +import { helperActivityUpdate } from '../utils/helperActivityUpdate'; // import { awayMessage } from '../utils/awayMessage'; // import log from '../../global/utils/log'; // import {parse} from 'path'; @@ -89,6 +90,7 @@ export const messageCreate: MessageCreateEvent = { karma(message); imagesOnly(message); discordAiModerate(message); + helperActivityUpdate(message); // Disabled for testing // thoughtPolice(message); diff --git a/src/discord/utils/helperActivityUpdate.ts b/src/discord/utils/helperActivityUpdate.ts new file mode 100644 index 000000000..bee5b8092 --- /dev/null +++ b/src/discord/utils/helperActivityUpdate.ts @@ -0,0 +1,51 @@ +import { + Message, TextChannel, +} from 'discord.js'; + +export default helperActivityUpdate; + +// const F = f(__filename); + +/** + * helperActivityUpdate + * @param {Message} message The message that was sent + * @return {Promise} +* */ +export async function helperActivityUpdate(message: Message): Promise { + if (!message.guild) return; // If not in a guild then ignore all messages + if (message.guild.id !== env.DISCORD_GUILD_ID) return; // If not in tripsit ignore all messages + + // Determine if the message was sent in a TextChannel + if (!(message.channel instanceof TextChannel)) return; + + if (message.channel.parentId !== env.CATEGORY_HARMREDUCTIONCENTRE) return; + + const guildData = await db.discord_guilds.upsert({ + where: { + id: message.guild?.id, + }, + create: { + id: message.guild?.id, + }, + update: {}, + }); + + if(!guildData.role_helper) return; + + const role = await message.guild?.roles.fetch(guildData.role_helper); + + if (!message.member || !role || !message.member.roles.cache.has(role.id)) return; + + await db.users.upsert({ + where: { + discord_id: message.author.id, + }, + create: { + discord_id: message.author.id + }, + update: { + last_helper_activity: new Date() + }, + }); + +} diff --git a/src/global/utils/timer.ts b/src/global/utils/timer.ts index 2591d0356..296e02b47 100644 --- a/src/global/utils/timer.ts +++ b/src/global/utils/timer.ts @@ -1119,6 +1119,88 @@ async function checkMoodle() { // eslint-disable-line // } } +async function pruneInactiveHelpers() { + const inactiveThreshold = new Date(); + // 2 months for production, 1 minute for dev + env.NODE_ENV === 'production' + ? inactiveThreshold.setMonth(inactiveThreshold.getMonth() - 2) + : inactiveThreshold.setMinutes(inactiveThreshold.getMinutes() - 1); + + const inactiveHelpers = await db.users.findMany({ + where: { + last_helper_activity: { + lt: inactiveThreshold, + }, + }, + }); + + const guild = discordClient.guilds.cache.get(env.DISCORD_GUILD_ID); + if (!guild) { + log.error(F, `Guild with ID ${env.DISCORD_GUILD_ID} not found.`); + return; + } + + const guildData = await db.discord_guilds.upsert({ + where: { + id: guild.id, + }, + create: { + id: guild.id, + }, + update: {}, + }); + + if (!guildData.role_helper) { + log.error(F, `Unable to fetch helper role from db in pruneInactiveHelpers for guild ID ${env.DISCORD_GUILD_ID}`); + return; + } + + const role = guild.roles.cache.get(guildData.role_helper); + if (!role) { + log.error(F, `Unable to fetch helper role from Discord in pruneInactiveHelpers for guild ID ${env.DISCORD_GUILD_ID}`); + return; + } + + // Loop through the inactive helpers and check their guild membership + for (const user of inactiveHelpers) { + + if(!user.discord_id) { + log.info(F, `Failed to fetch discord ID for db entry ${user.id}`) + continue; + } + + const member = await guild.members.fetch(user.discord_id).catch(() => null); + if (member) { + await member.roles.remove(role); + + try { + await member.send(`Your helper role has been removed due to inactivity. You can reapply for it at any time from the #volunteering channel if you find you have the time to help out again.`); + } catch (error) { + log.error(F, `Failed to send DM to ${member.user.username}: ${error}`); + } + } else { + const target = await guild.client.users.fetch(user.discord_id); + log.info(F, `Helper ${target.username}(${user.discord_id}) is no longer a member of the guild :(`); + const channelTripsitters = await guild.channels.fetch(env.CHANNEL_TRIPSITTERS) as TextChannel; + await channelTripsitters.send(stripIndents`Helper ${target.username}(${user.discord_id}) is no longer a member of the guild :(`); + } + + // Reset activity to prevent looping the same user again + await db.users.upsert({ + where: { + discord_id: user.discord_id, + }, + create: { + discord_id: user.discord_id, + }, + update: { + last_helper_activity: null, + }, + }); + } + log.info(F, `${inactiveHelpers.length} inactive helpers have been pruned and notified.`); +} + async function checkEvery( callback: () => Promise, interval: number, @@ -1158,6 +1240,7 @@ async function runTimer() { { callback: checkMoodle, interval: env.NODE_ENV === 'production' ? seconds60 : seconds5 }, // { callback: checkLpm, interval: env.NODE_ENV === 'production' ? seconds10 : seconds5 }, { callback: updateDb, interval: env.NODE_ENV === 'production' ? hours24 : hours48 }, + { callback: pruneInactiveHelpers, interval: env.NODE_ENV === 'production' ? hours48 : seconds60 }, ]; timers.forEach(timer => { diff --git a/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql b/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql new file mode 100644 index 000000000..3252a3d01 --- /dev/null +++ b/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "last_helper_activity" TIMESTAMP(3); diff --git a/src/prisma/tripbot/schema.prisma b/src/prisma/tripbot/schema.prisma index a815453dd..da2070554 100644 --- a/src/prisma/tripbot/schema.prisma +++ b/src/prisma/tripbot/schema.prisma @@ -174,6 +174,7 @@ model users { wordle_scores wordle_scores[] connections_scores connections_scores[] mini_scores mini_scores[] + last_helper_activity DateTime? } model wordle_scores { From 7c1a2c95cb34feb5ea95e407649c7a5bb8905726 Mon Sep 17 00:00:00 2001 From: theimperious1 Date: Thu, 3 Oct 2024 10:05:02 -0400 Subject: [PATCH 2/3] fix: channel reference, kinder notification msg, compliant loops --- src/discord/utils/helperActivityUpdate.ts | 7 ++--- src/global/utils/timer.ts | 36 +++++++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/discord/utils/helperActivityUpdate.ts b/src/discord/utils/helperActivityUpdate.ts index bee5b8092..26d052815 100644 --- a/src/discord/utils/helperActivityUpdate.ts +++ b/src/discord/utils/helperActivityUpdate.ts @@ -30,7 +30,7 @@ export async function helperActivityUpdate(message: Message): Promise { update: {}, }); - if(!guildData.role_helper) return; + if (!guildData.role_helper) return; const role = await message.guild?.roles.fetch(guildData.role_helper); @@ -41,11 +41,10 @@ export async function helperActivityUpdate(message: Message): Promise { discord_id: message.author.id, }, create: { - discord_id: message.author.id + discord_id: message.author.id, }, update: { - last_helper_activity: new Date() + last_helper_activity: new Date(), }, }); - } diff --git a/src/global/utils/timer.ts b/src/global/utils/timer.ts index 296e02b47..c83602b48 100644 --- a/src/global/utils/timer.ts +++ b/src/global/utils/timer.ts @@ -1122,10 +1122,12 @@ async function checkMoodle() { // eslint-disable-line async function pruneInactiveHelpers() { const inactiveThreshold = new Date(); // 2 months for production, 1 minute for dev - env.NODE_ENV === 'production' - ? inactiveThreshold.setMonth(inactiveThreshold.getMonth() - 2) - : inactiveThreshold.setMinutes(inactiveThreshold.getMinutes() - 1); - + if (env.NODE_ENV === 'production') { + inactiveThreshold.setMonth(inactiveThreshold.getMonth() - 2); + } else { + inactiveThreshold.setMinutes(inactiveThreshold.getMinutes() - 1); + } + const inactiveHelpers = await db.users.findMany({ where: { last_helper_activity: { @@ -1154,7 +1156,7 @@ async function pruneInactiveHelpers() { log.error(F, `Unable to fetch helper role from db in pruneInactiveHelpers for guild ID ${env.DISCORD_GUILD_ID}`); return; } - + const role = guild.roles.cache.get(guildData.role_helper); if (!role) { log.error(F, `Unable to fetch helper role from Discord in pruneInactiveHelpers for guild ID ${env.DISCORD_GUILD_ID}`); @@ -1162,11 +1164,10 @@ async function pruneInactiveHelpers() { } // Loop through the inactive helpers and check their guild membership - for (const user of inactiveHelpers) { - - if(!user.discord_id) { - log.info(F, `Failed to fetch discord ID for db entry ${user.id}`) - continue; + const promises = inactiveHelpers.map(async user => { + if (!user.discord_id) { + log.info(F, `Failed to fetch discord ID for db entry ${user.id}`); + return; // No need to continue for this user } const member = await guild.members.fetch(user.discord_id).catch(() => null); @@ -1174,11 +1175,18 @@ async function pruneInactiveHelpers() { await member.roles.remove(role); try { - await member.send(`Your helper role has been removed due to inactivity. You can reapply for it at any time from the #volunteering channel if you find you have the time to help out again.`); + const channelHowToVolunteer = await guild.channels.fetch(env.CHANNEL_HOW_TO_VOLUNTEER); + await member.send(stripIndents` + Your helper role has been automatically removed on TripSit due to inactivity, + but no worries—you can easily reapply for it at any time through ${channelHowToVolunteer} + if you’d like to start helping out again. + + Thank you for all your past contributions, and we’d love to have you back whenever you're ready! + `); } catch (error) { log.error(F, `Failed to send DM to ${member.user.username}: ${error}`); } - } else { + } else { // If we run into a Helper who is no longer a member, then notify the Tripsitters. const target = await guild.client.users.fetch(user.discord_id); log.info(F, `Helper ${target.username}(${user.discord_id}) is no longer a member of the guild :(`); const channelTripsitters = await guild.channels.fetch(env.CHANNEL_TRIPSITTERS) as TextChannel; @@ -1197,7 +1205,9 @@ async function pruneInactiveHelpers() { last_helper_activity: null, }, }); - } + }); + + await Promise.all(promises); // Wait for all promises to resolve log.info(F, `${inactiveHelpers.length} inactive helpers have been pruned and notified.`); } From 6ee9764d6f81752f53fd48da4f02729e2fcdf8fd Mon Sep 17 00:00:00 2001 From: theimperious1 Date: Thu, 3 Oct 2024 13:46:38 -0400 Subject: [PATCH 3/3] Add default value for new column last_helper_activity --- .../migrations/20240930194441_auto_prune_helper/migration.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql b/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql index 3252a3d01..d2e1272ca 100644 --- a/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql +++ b/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql @@ -1,2 +1,3 @@ -- AlterTable -ALTER TABLE "users" ADD COLUMN "last_helper_activity" TIMESTAMP(3); +ALTER TABLE "users" +ADD COLUMN "last_helper_activity" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP;