181 lines
5.7 KiB
TypeScript
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);
|
|
}
|
|
}
|