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..26d052815 --- /dev/null +++ b/src/discord/utils/helperActivityUpdate.ts @@ -0,0 +1,50 @@ +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..c83602b48 100644 --- a/src/global/utils/timer.ts +++ b/src/global/utils/timer.ts @@ -1119,6 +1119,98 @@ async function checkMoodle() { // eslint-disable-line // } } +async function pruneInactiveHelpers() { + const inactiveThreshold = new Date(); + // 2 months for production, 1 minute for dev + 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: { + 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 + 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); + if (member) { + await member.roles.remove(role); + + try { + 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 { // 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; + 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, + }, + }); + }); + + await Promise.all(promises); // Wait for all promises to resolve + log.info(F, `${inactiveHelpers.length} inactive helpers have been pruned and notified.`); +} + async function checkEvery( callback: () => Promise, interval: number, @@ -1158,6 +1250,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..d2e1272ca --- /dev/null +++ b/src/prisma/tripbot/migrations/20240930194441_auto_prune_helper/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "users" +ADD COLUMN "last_helper_activity" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP; diff --git a/src/prisma/tripbot/schema.prisma b/src/prisma/tripbot/schema.prisma index 621ce5377..6aae3a8ae 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 {