automod/bot/src/modules/timedActions.ts

181 lines
5.7 KiB
TypeScript

import {
DBTimedAction,
InfractionType,
ModActionType,
ServerLogCategory,
TimedActionState,
TimedActionType,
embedMsg,
fetchUser,
getDmChannel,
} from "lib";
import { client, logger } from "../index.js";
import { t } from "./i18n.js";
import { decodeTime, ulid } from "ulid";
import { dispatchLogMessage } from "./server_logs.js";
/**
* Every 60 seconds, we check the `timed_actions` collection for any actions scheduled within the next 60 seconds.
* Matches will then be stored in memory and executed at the correct time with `setTimeout()`.
*/
const processing: string[] = [];
const timeouts: { [key: string]: NodeJS.Timeout } = {};
async function processEvents() {
logger.info("Processing scheduled tasks");
try {
const tasks = await client.db.timedActions
.find({
state: TimedActionState.Pending,
date: {
$lt: new Date(Date.now() + 60000),
},
})
.toArray();
for (const task of tasks) {
scheduleExecution(task);
}
} catch (e) {
console.error(e);
}
}
function scheduleExecution(task: DBTimedAction) {
if (processing.includes(task._id)) return;
timeouts[task._id] = setTimeout(async () => {
processing.splice(processing.indexOf(task._id), 1);
delete timeouts[task._id];
try {
logger.info(`Executing task ${task._id} (${task.action})`);
await executeEvent(task);
await client.db.timedActions.updateOne(
{
_id: task._id,
},
{
$set: {
state: TimedActionState.Success,
},
},
);
} catch (e) {
logger.error(`Task ${task._id} failed`);
// Update action state to failed and store the error message
await client.db.timedActions.updateOne(
{
_id: task._id,
},
{
$set: {
state: TimedActionState.Failed,
error: `${e}`,
},
},
);
}
}, task.date.getTime() - Date.now());
}
/**
* Cancels the timer for the task with the id, if it exists already.
* Unless `markCancelled` is set to false, the state of the task in the database will be set to `Cancelled` as well.
*/
export async function cancelExecution(id: string, markCancelled = true) {
logger.info(`Unscheduling task ${id}`);
const timeout = timeouts[id];
if (timeout) clearTimeout(timeout);
const index = processing.indexOf(id);
if (index > -1) processing.splice(index, 1);
if (markCancelled) {
await client.db.timedActions.updateOne(
{ _id: id },
{ $set: { state: TimedActionState.Cancelled } },
);
}
}
setInterval(processEvents, 60_000);
await processEvents();
async function executeEvent(task: DBTimedAction) {
switch (task.action) {
case TimedActionType.Reminder: {
const user = await fetchUser(task.user, client);
if (!user) throw new Error(`Unable to fetch user ${task.user}`);
const channel = await getDmChannel(user, client);
await channel.sendMessage(
embedMsg(
await t("reminder.body", user, {
timestamp: `[<t:${Math.round(
decodeTime(task._id) / 1000,
)}:R>](${task.message})`,
message:
task.reminder ||
(await t("reminder.no_message", user, {
url: task.message,
})),
}),
await t("reminder.title", user),
"INFO",
),
);
break;
}
case TimedActionType.Unban: {
const infraction = await client.db.infractions.findOne({
_id: task.infraction,
});
if (!infraction)
throw new Error(`Infraction ${task.infraction} does not exist`);
if (infraction.type != InfractionType.BanTemporary)
throw new Error(
`Infraction ${task.infraction} is of type ${infraction.type}, expected ${InfractionType.BanTemporary}`,
);
const server =
client.servers.get(infraction.server) ||
(await client.servers.fetch(infraction.server));
if (!server.havePermission("BanMembers")) {
// May be worth sending an error notification to the log channel as well
throw new Error("No permission to manage bans");
}
await server.unbanUser(infraction.user);
await dispatchLogMessage(
{
_id: ulid(),
category: ServerLogCategory.ModAction,
server: infraction.server,
type: ModActionType.TempBanExpire,
user: infraction.user,
infraction: task.infraction,
},
client,
null,
);
break;
}
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
throw new Error(`Action ${(task as any).action} not implemented`);
}
}
export async function scheduleEvent(task: DBTimedAction) {
await client.db.timedActions.insertOne(task);
if (task.date.getTime() < Date.now() + 60000) {
scheduleExecution(task);
}
}