diff --git a/src/commands/guild/tempban.ts b/src/commands/guild/tempban.ts new file mode 100644 index 0000000..1f84aff --- /dev/null +++ b/src/commands/guild/tempban.ts @@ -0,0 +1,68 @@ +import { CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../../types/interaction'; +import { PermissionLevel } from '../../utils/permissions'; +import * as scheduler from '../../utils/scheduler'; +import {formatDate, formatUserRaw} from "../../utils/utils"; +import {unbanHandler} from "../shared/tempban"; + +const TempBan: Command = { + permissionLevel: PermissionLevel.MODERATOR, + + data: new SlashCommandBuilder() + .setName('tempban') + .setDescription('Bans the given user for X time.') + .addUserOption(option => option + .setName('user') + .setDescription('The user to ban') + .setRequired(true)) + .addIntegerOption(option => option + .setName('m') + .setDescription('Amount of minutes before unban') + .setRequired(false)) + .addIntegerOption(option => option + .setName('h') + .setDescription('Amount of hours before unban') + .setRequired(false)) + .addIntegerOption(option => option + .setName('d') + .setDescription('Amount of days before unban') + .setRequired(false)) + .addIntegerOption(option => option + .setName('w') + .setDescription('Amount of weeks before unban') + .setRequired(false)) + .addIntegerOption(option => option + .setName('M') + .setDescription('Amount of months before unban') + .setRequired(false)) + .addIntegerOption(option => option + .setName('y') + .setDescription('Amount of years before unban') + .setRequired(false)), + + async execute(interaction: CommandInteraction) { + if (!interaction.isChatInputCommand()) + return; + + if (!interaction.inGuild() || !interaction.guild) + return interaction.reply({ content: 'This command must be ran in a guild.', ephemeral: true }); + + const user = interaction.options.getUser('user', true); + const minutes = interaction.options.getInteger( 'm', false ) || 0; + const hours = interaction.options.getInteger( 'h', false ) || 0; + const days = interaction.options.getInteger( 'd', false ) || 0; + const weeks = interaction.options.getInteger( 'w', false ) || 0; + const months = interaction.options.getInteger( 'M', false ) || 0; + const years = interaction.options.getInteger( 'y', false ) || 0; + + if ( minutes + hours + days + weeks + months + years == 0 ) + return interaction.reply({ content: 'This command requires at least one non-zero parameter.', ephemeral: true }); + + const task = scheduler.schedule( interaction.guild.id, { delay: 0 }, unbanHandler ); + + await interaction.guild.bans.create(user); + + return interaction.reply({ content: `Banned ${formatUserRaw(user)} until ${formatDate(task.date)}`, ephemeral: true }); + } +}; +export default TempBan; diff --git a/src/commands/guild/warn.ts b/src/commands/guild/warn.ts index c45747f..d87d334 100644 --- a/src/commands/guild/warn.ts +++ b/src/commands/guild/warn.ts @@ -2,7 +2,6 @@ import { CommandInteraction, EmbedBuilder, SlashCommandBuilder } from 'discord.j import { Command } from '../../types/interaction'; import { PermissionLevel } from '../../utils/permissions'; import { LogLevelColor } from '../../utils/log'; -import { formatDate, formatUserRaw } from '../../utils/utils'; import * as persist from '../../utils/persist'; import { getWarnList } from '../shared/warnlist'; @@ -75,7 +74,7 @@ const Warn: Command = { .setColor(LogLevelColor.WARNING) .setTitle('WARN') .setFields({ name: 'You have been warned for the following reason:', value: `\`${reason}\`` }) - .setFooter({ text: `Issued by ${formatUserRaw(interaction.user)} in "${interaction.guild.name}"` }) + .setFooter({ text: `Issued in "${interaction.guild.name}"` }) .setTimestamp(); await user.send({ embeds: [embed] }); diff --git a/src/commands/shared/tempban.ts b/src/commands/shared/tempban.ts new file mode 100644 index 0000000..5c5dac0 --- /dev/null +++ b/src/commands/shared/tempban.ts @@ -0,0 +1,19 @@ +import {MoralityCoreClient} from '../../types/client'; + +import * as persist from '../../utils/persist'; +import {resume} from '../../utils/scheduler'; +import { ok as assert } from 'assert'; +import {ScheduledTask} from '../../types/scheduler'; + +export function register( client: MoralityCoreClient ): void { + for ( const guild of client.guilds.valueOf().values() ) + for ( const { taskId } of persist.data(guild.id).moderation.tempban ) + resume( guild.id, taskId, unbanHandler ); +} + +export function unbanHandler( task: ScheduledTask, data: unknown ): void { + assert( data instanceof { guild: String }, 'unbanHandler was passed a non-string parameter!!' ); + + const client = MoralityCoreClient.get(); + client.guilds.fetch( ) +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index d7d2f8a..7a5b294 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,8 @@ import { updateCommands, updateCommandsForGuild } from './utils/update_commands' import * as config from './config.json'; import * as log from './utils/log'; import * as persist from './utils/persist'; +import * as scheduler from './utils/scheduler'; +import * as tempban from './commands/shared/tempban'; // Make console output better import consoleStamp from 'console-stamp'; @@ -344,13 +346,20 @@ async function main() { } }); + // start the scheduler + scheduler.run(); + // Log in await client.login(config.token); + // register all tempban stuff again + tempban.register( client ); + process.on('SIGINT', () => { const date = new Date(); log.writeToLog(undefined, `--- BOT END AT ${date.toDateString()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()} ---`); client.destroy(); + scheduler.shutdown(); persist.saveAll(); process.exit(); }); diff --git a/src/types/client.ts b/src/types/client.ts index 514bec7..e03f56e 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -60,6 +60,7 @@ export class Callbacks { } export class MoralityCoreClient extends Client { + private static instance: MoralityCoreClient; commands: Collection; callbacks: Callbacks; @@ -67,5 +68,8 @@ export class MoralityCoreClient extends Client { super(options); this.commands = new Collection(); this.callbacks = new Callbacks(); + MoralityCoreClient.instance = this; } + + static get = () => MoralityCoreClient.instance; } diff --git a/src/types/persist.ts b/src/types/persist.ts index 16d7bb4..2bcff17 100644 --- a/src/types/persist.ts +++ b/src/types/persist.ts @@ -35,6 +35,12 @@ export interface PersistentData { issuer: string, }[], }, + tempban: { + taskId: string, + user: string, + reason: string, + issuer: string, + }[] }, reaction_roles: { [message: string]: { @@ -48,9 +54,17 @@ export interface PersistentData { responses: { [response_id: string]: string, }, - watched_threads: Array, + watched_threads: string[], statistics: { joins: number, leaves: number, }, + scheduler: { + [taskid: string]: { + data: unknown, + expirationTime: number, + repeat: boolean, + delay: number + } + } } diff --git a/src/types/scheduler.ts b/src/types/scheduler.ts new file mode 100644 index 0000000..c2d04e0 --- /dev/null +++ b/src/types/scheduler.ts @@ -0,0 +1,62 @@ +export interface RepeatedSchedulePlan { + + /** + * ms before this task is initially executed + */ + initialDelay: number, + + /** + * ms between task executions + */ + distance: number +} + +export interface SingleSchedulePlan { + /** + * ms before task execution + */ + delay: number +} + +export type SchedulePlan = RepeatedSchedulePlan | SingleSchedulePlan; + +export type Task = ( task: ScheduledTask, data: unknown ) => void + +/** + * Represents a task that has been submitted to the scheduler. + */ +export interface ScheduledTask { + /** + * Unique id assigned to this scheduled task. + */ + readonly id: string, + + /** + * The task that will be executed. + */ + readonly task: Task, + + /** + * The unix timestamp at which this task will run. + */ + readonly date: number, + + /** + * The guild this task was scheduled on. + readonly guild: string, + + /** + * Cancels and removes from persistence this task. + */ + cancel(): void, + + /** + * Whether this scheduled task was cancelled. + */ + cancelled(): boolean, + + /** + * Returns how many ms until this task is executed. + */ + remainingTime(): number, +} diff --git a/src/utils/scheduler.ts b/src/utils/scheduler.ts new file mode 100644 index 0000000..436a051 --- /dev/null +++ b/src/utils/scheduler.ts @@ -0,0 +1,47 @@ +import {SchedulePlan, Task, ScheduledTask} from "../types/scheduler"; +import * as persist from '../utils/persist'; + + +/** + * Resumes a task previously saved to persistence. + * @param id id of the task to get the data from. + * @param task + */ +export function resume( guild: string, id: string, task: Task ): ScheduledTask { + +} + +/** + * Schedules a task for execution. + * @param plan + * @param task + */ +export function schedule( guild: string, plan: SchedulePlan, task: Task ): ScheduledTask { + persist.data(guild).scheduler[] +} + +/** + * Cancels and removes from persistence a scheduled task. + * @param task + */ +export function cancelTask( task: ScheduledTask ): number { + +} + +export function run() { + setInterval( + () => { + + }, + 5 + ); +} + + +/** + * Stops the scheduler and saves all state. + */ +export function shutdown() { + +} +