diff --git a/Main.js b/Main.js index 27f31d1..fd459e1 100644 --- a/Main.js +++ b/Main.js @@ -16,7 +16,7 @@ if (process.argv.includes("-d")) { const sqlite = require('sqlite'); const configFile = require('./config.json'); -const { errLog, trySend, noPerm, getUTCComparison, defaultEventLogEmbed, getChannel } = require('./resources/functions'); +const { errLog, trySend, noPerm, getUTCComparison, defaultEventLogEmbed, getChannel, getUser } = require('./resources/functions'); const { join } = require('path'); const getColor = require("./resources/getColor"); const { timestampAt } = require("./resources/debug"); @@ -199,4 +199,22 @@ process.on("uncaughtException", e => errLog(e, null, client)); process.on("unhandledRejection", e => errLog(e, null, client)); process.on("warning", e => errLog(e, null, client)); +async function execPunishmentSchedule([guildID, userID, type]) { + if (!guildID || !userID || !type) throw new TypeError("Undefined param!"); + let USER = client.users.resolve(userID); + if (!USER) USER = await client.users.fetch(userID); + if (!USER) throw new Error("Unknown user"); + const GUILD = client.guilds.resolve(guildID); + if (!GUILD) throw new Error("Unknown guild"); + if (!GUILD.DB) GUILD.dbLoad(); + const CL = GUILD.member(client.user); + let ret; + if (type === "mute") { + ret = await USER.unmute(GUILD, CL, "Punishment expired"); + } else { + ret = await USER.unban(GUILD, CL, "Punishment expired"); + } + return ret; +} + client.login(configFile.token); \ No newline at end of file diff --git a/cmds/moderation/ban.js b/cmds/moderation/ban.js index 5d08464..50f53ca 100644 --- a/cmds/moderation/ban.js +++ b/cmds/moderation/ban.js @@ -31,7 +31,7 @@ module.exports = class ban extends commando.Command { async run(msg, arg) { const args = parseDoubleDash(arg), target = args?.shift(); - let reason = "No reason provided", pDuration = {}, execTarget = [], resultMsg = "", daysToDeleteMessage = 0; + let reason = "No reason provided", pDuration = {}, execTarget = [], resultMsg = "", daysToDeleteMessages = 0; if (!target || target.length < 1) return trySend(msg.client, msg, this.description); else { const ET = await targetUser(msg, target); @@ -44,7 +44,7 @@ module.exports = class ban extends commando.Command { if (ARG === "--" || ARG.trim().length < 1) continue; if (ARG.startsWith("d ")) { const U = ARG.slice(2).trim(); - if (U.length > 0 && !/\D/.test(U)) daysToDeleteMessage = parseInt(U, 10); else return trySend(msg.client, msg, "Invalid number of days to delete messages!"); + if (U.length > 0 && !/\D/.test(U)) daysToDeleteMessages = parseInt(U, 10); else return trySend(msg.client, msg, "Invalid number of days to delete messages!"); continue; } else if (CHECK_FOR_DURATION_REGEXP.test(ARG.trim())) diff --git a/cmds/moderation/mute.js b/cmds/moderation/mute.js index 4ffddbe..c71d862 100644 --- a/cmds/moderation/mute.js +++ b/cmds/moderation/mute.js @@ -126,15 +126,6 @@ module.exports = class mute extends commando.Command { if (/Missing Permissions|someone with higher position/.test(e.message)) cant.push(EXEC.id); else if (/already muted/.test(e.message)) already.push(EXEC.id); else console.log(e); continue; } - if (!EXEC.bot) { - const emb = defaultEventLogEmbed(msg.guild); - emb.setTitle("You have been muted") - .setDescription("**Reason**\n" + reason) - .addField("At", defaultDateFormat(duration.invoked), true) - .addField("Until", duration.until ? defaultDateFormat(duration.until) : "Never", true) - .addField("For", duration.duration?.strings.join(" ") || "Indefinite", true); - EXEC.createDM().then(r => trySend(msg.client, r, emb)); - } } infractionToDoc.executed = muted; @@ -143,23 +134,16 @@ module.exports = class mute extends commando.Command { if (muted.length > 0) await msg.guild.addInfraction(infractionToDoc); - const NAME = msg.guild.id + "/" + infractionToDoc.infraction, - newUnmuteSchedule = { - name: NAME, - path: "./scheduler/unmute.js", - worker: { - argv: [NAME] - }, - date: duration.until?.toJSDate() - }; - - let emb = defaultImageEmbed(msg, null, "Infraction #" + infractionToDoc.infraction); + const emb = defaultImageEmbed(msg, null, "Infraction #" + infractionToDoc.infraction); let mutedStr = "", mutedArr = []; + if (muted.length > 0) for (const U of muted) { const tU = "<@" + U + ">\n"; if ((mutedStr + tU).length < 1000) mutedStr += tU; else mutedArr.push(U); } + if (mutedArr.length > 0) mutedStr += `and ${mutedArr.length} more...`; + emb.setDescription("**Reason**\n" + reason) .addField("Muted", mutedStr || "`[NONE]`") .addField("At", defaultDateFormat(duration.invoked), true) diff --git a/cmds/moderation/src/createSchedule.js b/cmds/moderation/src/createSchedule.js new file mode 100644 index 0000000..2f9712b --- /dev/null +++ b/cmds/moderation/src/createSchedule.js @@ -0,0 +1,52 @@ +'use strict'; + +const Bree = require("bree"); +const { errLog } = require("../../../resources/functions"); + +const { join } = require("path"), + scheduler = require("../../../resources/scheduler"), + { database } = require("../../../database/mongo"), + col = database.collection("Schedule"); + +/** + * @type {Bree} + */ +let jobManager; + +async function createSchedule(client, { guildID, userID, type, until }) { + if (!client || !guildID || !userID || !type || !until) throw new TypeError("Undefined params!"); + if (!jobManager) await init(client); + let path; + if (type === "mute") path = "./unmuteSc.js"; + else if (type === "ban") path = "./unbanSc.js"; + else throw new TypeError("Invalid type: " + type); + if (typeof until === "string") until = new Date(until); + const NAME = guildID + "/" + userID + "/" + type, + SC = { + name: NAME, + path: join(__dirname, path), + + /** + * @type {import("worker_threads").WorkerOptions} + */ + worker: { + argv: [NAME] + }, + date: until + }; + + try { + await col.updateOne({ document: NAME }, { $set: SC, $setOnInsert: { document: NAME } }, { upsert: true }); + await jobManager.remove(NAME).catch(() => { }) + jobManager.add(SC); + jobManager.start(NAME); + } catch (e) { + return errLog(e, null, client); + } +} + +async function init(client) { + const jobs = await col.find({}).toArray(); + jobManager = scheduler(client, jobs); + jobManager.start(); +} \ No newline at end of file diff --git a/cmds/moderation/src/unmuteSc.js b/cmds/moderation/src/unmuteSc.js new file mode 100644 index 0000000..dcd2b45 --- /dev/null +++ b/cmds/moderation/src/unmuteSc.js @@ -0,0 +1,4 @@ +'use strict'; + +const NAME = process.argv[2]?.split(/\//); + diff --git a/cmds/moderation/unmute.js b/cmds/moderation/unmute.js index 6d296ba..3171278 100644 --- a/cmds/moderation/unmute.js +++ b/cmds/moderation/unmute.js @@ -43,23 +43,15 @@ module.exports = class unmute extends commando.Command { await USER.unmute(msg.guild, msg.member, reason) .then(() => { success.push(USER.id); - if (!USER.bot) { - const emb = defaultEventLogEmbed(msg.guild); - - emb.setTitle("You have been unmuted") - .setDescription("**Reason**\n" + reason); - - USER.createDM().then(r => trySend(msg.client, r, emb)); - } }) .catch((e) => { console.log(e); if (/isn't muted in/.test(e.message)) return notMuted.push(USER.id); - cant.push(USER.id) + cant.push(USER.id); }); } - let emb = defaultImageEmbed(msg, null, "Unmute"); + const emb = defaultImageEmbed(msg, null, "Unmute"); emb.setDescription("**Reason**\n" + reason) .addField("Unmuted", (success.length > 0 ? "<@" + success.join(">, <@") + ">" : "`[NONE]`")); if (cant.length > 0) emb.addField("Can't unmute", "<@" + cant.join(">, <@") + ">"); diff --git a/cmds/profile/profile.js b/cmds/profile/profile.js index 3d785f2..4387a46 100644 --- a/cmds/profile/profile.js +++ b/cmds/profile/profile.js @@ -4,6 +4,7 @@ const commando = require("@iceprod/discord.js-commando"); const { Message } = require("discord.js"); const { DateTime, Interval } = require("luxon"); const { trySend, getUser, defaultImageEmbed, splitOnLength, defaultDateFormat } = require("../../resources/functions"); +const getColor = require("../../resources/getColor"); const { intervalToDuration } = require("../moderation/src/duration"); module.exports = class profile extends commando.Command { @@ -42,7 +43,8 @@ module.exports = class profile extends commando.Command { RFS = splitOnLength(RI, 1010, ">, <@&"), INT = Interval.fromDateTimes(DateTime.fromJSDate(MEM.joinedAt), DateTime.now()); emb.addField("Joined", defaultDateFormat(MEM.joinedAt) + `\n(${intervalToDuration(INT).strings.join(" ")} ago)`) - .addField("Nick", `\`${MEM.displayName}\``); + .addField("Nick", `\`${MEM.displayName}\``) + .setColor(getColor(MEM.displayColor)); if (RFS[0]?.length > 0) { for (const p of RFS) { diff --git a/config_copy.json b/config_copy.json index 7c8e75d..d5cf4be 100644 --- a/config_copy.json +++ b/config_copy.json @@ -26,5 +26,6 @@ "chatChannel": "837178237322919966", "guildLog": "840154722434154496", "shardChannel": "851361670533218324", - "home": "772073587792281600" + "home": "772073587792281600", + "schedulerLog": "868682302111248394" } \ No newline at end of file diff --git a/package.json b/package.json index 21b51d9..34929ce 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "zlib-sync": "^0.1.7" }, "devDependencies": { + "@types/bree": "^6.2.1", "@types/luxon": "^1.27.1" } } diff --git a/resources/eventsLogger/guildMemberAdd.js b/resources/eventsLogger/guildMemberAdd.js index 3b70481..7e3d867 100644 --- a/resources/eventsLogger/guildMemberAdd.js +++ b/resources/eventsLogger/guildMemberAdd.js @@ -1,10 +1,10 @@ 'use strict'; const { GuildMember } = require("discord.js"), - { DateTime } = require("luxon"), + { DateTime, Interval } = require("luxon"), { getChannel, defaultEventLogEmbed, trySend, defaultDateFormat } = require("../functions"), getColor = require("../getColor"), - { DT_PRINT_FORMAT } = require("../../cmds/moderation/src/duration"); + { DT_PRINT_FORMAT, intervalToDuration } = require("../../cmds/moderation/src/duration"); /** * Log newly joined Guild Member @@ -20,7 +20,8 @@ module.exports = (member) => { .setTitle("`" + member.user.tag + "` joined") .setThumbnail(member.user.displayAvatarURL({ format: "png", size: 4096, dynamic: true })) .setColor(getColor("cyan")) - .addField("Registered", defaultDateFormat(member.user.createdAt)) + .addField("Registered", defaultDateFormat(member.user.createdAt) + + `\n(${intervalToDuration(Interval.fromDateTimes(DateTime.fromJSDate(member.user.createdAt), DateTime.now())).strings.join(" ")} ago)`) .setDescription(`<@!${member.id}> (${member.id}) just joined.\nWe have ${member.guild.memberCount} total members now.`); return trySend(member.client, log, emb); } diff --git a/resources/eventsLogger/guildMemberRemove.js b/resources/eventsLogger/guildMemberRemove.js index de2939c..9ad4715 100644 --- a/resources/eventsLogger/guildMemberRemove.js +++ b/resources/eventsLogger/guildMemberRemove.js @@ -24,12 +24,11 @@ module.exports = (member) => { .setTitle("`" + member.user.tag + "` left") .setThumbnail(member.user.displayAvatarURL({ format: "png", size: 4096, dynamic: true })) .setColor(getColor("yellow")) - .addField("Registered", defaultDateFormat(member.user.createdAt)) - .addField("Joined", defaultDateFormat(member.joinedAt) + `\n(${intervalToDuration(INT).strings.join(" ")} ago)`) .addField("Nick", "`" + member.displayName + "`") + .addField("Joined", defaultDateFormat(member.joinedAt) + `\n(${intervalToDuration(INT).strings.join(" ")} ago)`) .setDescription(`<@!${member.id}> (${member.id}) just left.\nWe have ${member.guild.memberCount} total members now.`); for (const U of RU) { - emb.addField(emb.fields.length === 3 ? "Roles" : "", U.length > 0 ? "<@&" + U.join(">, <@&") + ">" : "`[NONE]`"); + emb.addField(emb.fields.length === 2 ? "Roles" : "​", U.length > 0 ? "<@&" + U.join(">, <@&") + ">" : "`[NONE]`"); } return trySend(member.client, log, emb); } diff --git a/resources/scheduler.js b/resources/scheduler.js index cb90d09..e72ba6a 100644 --- a/resources/scheduler.js +++ b/resources/scheduler.js @@ -1,11 +1,22 @@ 'use strict'; -const bree = require("bree"); +const Bree = require("bree"); const cabin = require("cabin"); +const { Client } = require("discord.js"); +const { errLog, trySend } = require("./functions"), + { schedulerLog } = require("../config.json"); -module.exports.scheduler = new bree({ - // logger: new cabin(), - root: false, - workerMessageHandler: () => console.log, - errorHandler: () => console.error -}); \ No newline at end of file +/** + * @param {Client} client + * @param {object[]} jobs + * @returns {Bree} + */ +module.exports = (client, jobs = []) => { + return new Bree({ + // logger: new cabin(), + root: false, + jobs: jobs, + workerMessageHandler: (a) => trySend(client, schedulerLog, a), + errorHandler: (e, m) => errLog(e, null, client, false, `\`${m?.threadId}\` \`${m?.name}\``) + }); +} \ No newline at end of file diff --git a/resources/scheduler/unmute.js b/resources/scheduler/unmute.js deleted file mode 100644 index 0fedcb2..0000000 --- a/resources/scheduler/unmute.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -require("@iceprod/discord.js-commando"); - -module.exports.unmuteExec = async function () {} \ No newline at end of file diff --git a/resources/structures.js b/resources/structures.js index f65d677..623bb4d 100644 --- a/resources/structures.js +++ b/resources/structures.js @@ -2,7 +2,7 @@ const { Structures, Guild, GuildMember, BanOptions } = require("discord.js"), { database } = require("../database/mongo"), - { errLog } = require("./functions"); + { errLog, defaultEventLogEmbed, defaultDateFormat, trySend } = require("./functions"); const { TimedPunishment } = require("./classes"); Structures.extend("Guild", u => { @@ -193,30 +193,70 @@ Structures.extend("User", u => { /** * @param {Guild} guild * @param {string} reason - * @param {{duration: object, saveTakenRoles: boolean, infraction: number, moderator: User}} data + * @param {{duration: object, saveTakenRoles: boolean, infraction: number, moderator: GuildMember}} data */ async mute(guild, data, reason) { - if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is " + typeof guild); + if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is: " + guild); if (!data?.infraction) throw new Error("Missing infraction id"); - if (!guild.DB) await guild.dbLoad(); + const MEM = guild.member(this); + const CL = guild.member(this.client.user); + + if (!CL.hasPermission("MANAGE_ROLES") || + !data.moderator.hasPermission("MANAGE_ROLES")) throw new Error("Missing Permissions"); + if (MEM) { if (data.moderator.roles.highest.position < MEM.roles.highest.position || MEM.roles.highest.position > guild.member(this.client.user).roles.highest.position) throw new Error("You can't mute someone with higher position than you <:nekokekLife:852865942530949160>"); await MEM.mute(data, reason); } + + if (!guild.DB) await guild.dbLoad(); + + if (!this.bot) { + const emb = defaultEventLogEmbed(guild); + emb.setTitle("You have been muted") + .setDescription("**Reason**\n" + reason) + .addField("At", defaultDateFormat(data.duration.invoked), true) + .addField("Until", data.duration.until ? defaultDateFormat(data.duration.until) : "Never", true) + .addField("For", data.duration.duration?.strings.join(" ") || "Indefinite"); + this.createDM().then(r => trySend(this.client, r, emb)); + } + const MC = guild.getTimedPunishment(this.id, "mute"), TP = new TimedPunishment({ userID: this.id, duration: data.duration, infraction: data.infraction, type: "mute" }); + return { set: await guild.setTimedPunishment(TP), existing: MC } } + /** + * @param {Guild} guild + * @param {GuildMember} moderator + * @param {string} reason + * @returns + */ async unmute(guild, moderator, reason) { - if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is " + typeof guild); + if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is: " + guild); + const MEM = guild.member(this); + const CL = guild.member(this.client.user); + if (!CL.hasPermission("MANAGE_ROLES") || + !moderator.hasPermission("MANAGE_ROLES")) throw new Error("Missing Permissions"); if (!guild.DB) await guild.dbLoad(); + + if (!this.bot) { + const emb = defaultEventLogEmbed(guild); + + emb.setTitle("You have been unmuted") + .setDescription("**Reason**\n" + reason); + + this.createDM().then(r => trySend(this.client, r, emb)); + } + const MC = guild.getTimedPunishment(this.id, "mute"); if (!MC) throw new Error(this.tag + " isn't muted in " + guild.name); - const MEM = guild.member(this); if (MEM) { - if (moderator.roles.highest.position < MEM.roles.highest.position || MEM.roles.highest.position > guild.member(this.client.user).roles.highest.position) throw new Error("You can't mute someone with higher position than you <:nekokekLife:852865942530949160>"); + if (moderator.roles.highest.position < MEM.roles.highest.position || + MEM.roles.highest.position > CL.roles.highest.position) + throw new Error("You can't mute someone with higher position than you <:nekokekLife:852865942530949160>"); await MEM.unmute(reason); } return guild.removeTimedPunishment(this.id, "mute"); @@ -224,10 +264,56 @@ Structures.extend("User", u => { /** * @param {Guild} guild + * @param {{duration: object, infraction: number, moderator: GuildMember}} data * @param {BanOptions} option */ - async ban(guild, option) { - guild.members.ban(this, option); + async ban(guild, data, option) { + if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is: " + guild); + if (!data?.infraction) throw new Error("Missing infraction id"); + const MEM = guild.member(this); + const CL = guild.member(this.client.user); + if (!CL.hasPermission("BAN_MEMBERS") || + !data.moderator.hasPermission("BAN_MEMBERS")) throw new Error("Missing Permissions"); + if (MEM) { + if (moderator.roles.highest.position < MEM.roles.highest.position || + MEM.roles.highest.position > CL.roles.highest.position) + throw new Error("You can't mute someone with higher position than you <:nekokekLife:852865942530949160>"); + } + await guild.members.ban(this, option); + if (!guild.DB) await guild.dbLoad(); + + if (!this.bot) { + const emb = defaultEventLogEmbed(guild); + emb.setTitle("You have been banned") + .setDescription("**Reason**\n" + option.reason) + .addField("At", defaultDateFormat(data.duration.invoked), true) + .addField("Until", data.duration.until ? defaultDateFormat(data.duration.until) : "Never", true) + .addField("For", data.duration.duration?.strings.join(" ") || "Indefinite"); + this.createDM().then(r => trySend(this.client, r, emb)); + } + + const MC = guild.getTimedPunishment(this.id, "ban"), + TP = new TimedPunishment({ userID: this.id, duration: data.duration, infraction: data.infraction, type: "ban" }); + return { set: await guild.setTimedPunishment(TP), existing: MC } + } + + async unban(guild, moderator, reason) { + if (!guild || !(guild instanceof Guild)) throw new TypeError("Guild is: " + guild); + const CL = guild.member(this.client.user); + if (!moderator.isAdmin || !CL.isAdmin) throw new Error("Missing permissions"); + await guild.members.unban(this, reason); + if (!guild.DB) await guild.DB.dbLoad(); + + if (!this.bot) { + const emb = defaultEventLogEmbed(guild); + + emb.setTitle("You have been unbanned") + .setDescription("**Reason**\n" + reason); + + this.createDM().then(r => trySend(this.client, r, emb)); + } + + return guild.removeTimedPunishment(this.id, "ban"); } } });