From 05b3cffb3efc8ee5742729e658c639e58d6befef Mon Sep 17 00:00:00 2001 From: yowsef <70838519+aboreda12@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:31:11 +0200 Subject: [PATCH 1/5] Update README.md --- open-ticket/ot-moderation/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/open-ticket/ot-moderation/README.md b/open-ticket/ot-moderation/README.md index 7db8a5e..35e2aa7 100644 --- a/open-ticket/ot-moderation/README.md +++ b/open-ticket/ot-moderation/README.md @@ -1,7 +1,10 @@ # OT Moderation A clean and minimal moderation plugin for **Open Discord** — helping server admins keep their communities safe and respectful with essential tools. -> Currently supports `ban`, `kick`, `unban`, and `warn`. More features coming soon! +> Currently supports `ban`, `kick`, `unban`, and `warn`, +>NEW features: +> +> `timeout``untimeout``mute``unmute` --- @@ -11,3 +14,7 @@ A clean and minimal moderation plugin for **Open Discord** — helping server ad - `/kick @user` – Kicks a member from the server. - `/unban userID` – Unbans a user by ID. - `/warn @user` – Issues a warning to a user (logs coming soon). +- `/unban userID` – Unbans a user by ID. +- `/timeout @user period` – timeout a user. +- `/untimeout` – remove a user timeout. +- `/mute/unmute @user` – mute/unmute a user. From 91e85adc6b6bc9974a908fd2c1a596e38d139bb8 Mon Sep 17 00:00:00 2001 From: yowsef <70838519+aboreda12@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:32:04 +0200 Subject: [PATCH 2/5] Update index.ts new features --- open-ticket/ot-moderation/index.ts | 1038 ++++++++++++++++++++++++---- 1 file changed, 906 insertions(+), 132 deletions(-) diff --git a/open-ticket/ot-moderation/index.ts b/open-ticket/ot-moderation/index.ts index e6e673e..cdef1bf 100644 --- a/open-ticket/ot-moderation/index.ts +++ b/open-ticket/ot-moderation/index.ts @@ -1,6 +1,9 @@ import { api, opendiscord, utilities } from "#opendiscord"; import * as discord from "discord.js"; +// Load plugin config +const pluginConfig = opendiscord.configs.get("ot-moderation"); + //DECLARATION declare module "#opendiscord-types" { export interface ODPluginManagerIds_Default { @@ -11,31 +14,51 @@ declare module "#opendiscord-types" { "ot-moderation:unban": api.ODSlashCommand; "ot-moderation:kick": api.ODSlashCommand; "ot-moderation:warn": api.ODSlashCommand; + "ot-moderation:timeout": api.ODSlashCommand; + "ot-moderation:untimeout": api.ODSlashCommand; + "ot-moderation:mute": api.ODSlashCommand; + "ot-moderation:unmute": api.ODSlashCommand; } export interface ODTextCommandManagerIds_Default { "ot-moderation:ban": api.ODTextCommand; "ot-moderation:unban": api.ODTextCommand; "ot-moderation:kick": api.ODTextCommand; "ot-moderation:warn": api.ODTextCommand; + "ot-moderation:timeout": api.ODTextCommand; + "ot-moderation:untimeout": api.ODTextCommand; + "ot-moderation:mute": api.ODTextCommand; + "ot-moderation:unmute": api.ODTextCommand; } export interface ODCommandResponderManagerIds_Default { "ot-moderation:ban": { source: "slash" | "text", params: {}, workers: "ot-moderation:ban" | "ot-moderation:logs" }; "ot-moderation:unban": { source: "slash" | "text", params: {}, workers: "ot-moderation:unban" | "ot-moderation:logs" }; "ot-moderation:kick": { source: "slash" | "text", params: {}, workers: "ot-moderation:kick" | "ot-moderation:logs" }; "ot-moderation:warn": { source: "slash" | "text", params: {}, workers: "ot-moderation:warn" | "ot-moderation:logs" }; + "ot-moderation:timeout": { source: "slash" | "text", params: {}, workers: "ot-moderation:timeout" | "ot-moderation:logs" }; + "ot-moderation:untimeout": { source: "slash" | "text", params: {}, workers: "ot-moderation:untimeout" | "ot-moderation:logs" }; + "ot-moderation:mute": { source: "slash" | "text", params: {}, workers: "ot-moderation:mute" | "ot-moderation:logs" }; + "ot-moderation:unmute": { source: "slash" | "text", params: {}, workers: "ot-moderation:unmute" | "ot-moderation:logs" }; } export interface ODMessageManagerIds_Default { "ot-moderation:ban-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:ban-message" }; "ot-moderation:unban-message": { source: "slash" | "text" | "other", params: {author:discord.User,userId:string,reason:string|null}, workers: "ot-moderation:unban-message" }; "ot-moderation:kick-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:kick-message" }; "ot-moderation:warn-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:warn-message" }; - } + "ot-moderation:timeout-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,duration:string,reason:string|null}, workers: "ot-moderation:timeout-message" }; + "ot-moderation:untimeout-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:untimeout-message" }; + "ot-moderation:mute-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:mute-message" }; + "ot-moderation:unmute-message": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:unmute-message" }; + } export interface ODEmbedManagerIds_Default { "ot-moderation:ban-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:ban-embed" }; "ot-moderation:unban-embed": { source: "slash" | "text" | "other", params: {author:discord.User,userId:string,reason:string|null}, workers: "ot-moderation:unban-embed" }; "ot-moderation:kick-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:kick-embed" }; "ot-moderation:warn-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:warn-embed" }; - } + "ot-moderation:timeout-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,duration:string,reason:string|null}, workers: "ot-moderation:timeout-embed" }; + "ot-moderation:untimeout-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:untimeout-embed" }; + "ot-moderation:mute-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:mute-embed" }; + "ot-moderation:unmute-embed": { source: "slash" | "text" | "other", params: {author:discord.User,user:discord.User,reason:string|null}, workers: "ot-moderation:unmute-embed" }; + } } // REGISTER SLASH COMMANDS @@ -66,7 +89,7 @@ opendiscord.events.get("onSlashCommandLoad").listen((slash) => { // Unban Command slash.add(new api.ODSlashCommand("ot-moderation:unban", { name: "unban", - description: "Unban a user from the server.", + description: "✅ Unban a user from the server.", type: discord.ApplicationCommandType.ChatInput, contexts: [discord.InteractionContextType.Guild], integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], @@ -89,7 +112,7 @@ opendiscord.events.get("onSlashCommandLoad").listen((slash) => { // Kick Command slash.add(new api.ODSlashCommand("ot-moderation:kick", { name: "kick", - description: "Kick a user from the server.", + description: "👋 Kick a user from the server.", type: discord.ApplicationCommandType.ChatInput, contexts: [discord.InteractionContextType.Guild], integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], @@ -112,7 +135,7 @@ opendiscord.events.get("onSlashCommandLoad").listen((slash) => { // Warn Command slash.add(new api.ODSlashCommand("ot-moderation:warn", { name: "warn", - description: "Warn a user in the server.", + description: "⚠️ Warn a user in the server.", type: discord.ApplicationCommandType.ChatInput, contexts: [discord.InteractionContextType.Guild], integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], @@ -131,6 +154,104 @@ opendiscord.events.get("onSlashCommandLoad").listen((slash) => { } ] })); + + // Timeout Command + slash.add(new api.ODSlashCommand("ot-moderation:timeout", { + name: "timeout", + description: "⏱️ Timeout a user in the server.", + type: discord.ApplicationCommandType.ChatInput, + contexts: [discord.InteractionContextType.Guild], + integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], + options: [ + { + name: "user", + description: "The user to timeout.", + type: discord.ApplicationCommandOptionType.User, + required: true, + }, + { + name: "duration", + description: "Duration (e.g., 10m, 1h, 1d, 1w).", + type: discord.ApplicationCommandOptionType.String, + required: true, + }, + { + name: "reason", + description: "The reason for the timeout.", + type: discord.ApplicationCommandOptionType.String, + required: false, + } + ] + })); + + // Untimeout Command + slash.add(new api.ODSlashCommand("ot-moderation:untimeout", { + name: "untimeout", + description: "⏰ Remove timeout from a user.", + type: discord.ApplicationCommandType.ChatInput, + contexts: [discord.InteractionContextType.Guild], + integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], + options: [ + { + name: "user", + description: "The user to remove timeout from.", + type: discord.ApplicationCommandOptionType.User, + required: true, + }, + { + name: "reason", + description: "The reason for removing timeout.", + type: discord.ApplicationCommandOptionType.String, + required: false, + } + ] + })); + + // Mute Command + slash.add(new api.ODSlashCommand("ot-moderation:mute", { + name: "mute", + description: "🔇 Mute a user in the server.", + type: discord.ApplicationCommandType.ChatInput, + contexts: [discord.InteractionContextType.Guild], + integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], + options: [ + { + name: "user", + description: "The user to mute.", + type: discord.ApplicationCommandOptionType.User, + required: true, + }, + { + name: "reason", + description: "The reason for the mute.", + type: discord.ApplicationCommandOptionType.String, + required: false, + } + ] + })); + + // Unmute Command + slash.add(new api.ODSlashCommand("ot-moderation:unmute", { + name: "unmute", + description: "🔊 Unmute a user in the server.", + type: discord.ApplicationCommandType.ChatInput, + contexts: [discord.InteractionContextType.Guild], + integrationTypes: [discord.ApplicationIntegrationType.GuildInstall], + options: [ + { + name: "user", + description: "The user to unmute.", + type: discord.ApplicationCommandOptionType.User, + required: true, + }, + { + name: "reason", + description: "The reason for the unmute.", + type: discord.ApplicationCommandOptionType.String, + required: false, + } + ] + })); }); // REGISTER TEXT COMMANDS @@ -221,6 +342,96 @@ opendiscord.events.get("onTextCommandLoad").listen((text) => { } ] })); + + // Timeout Command + text.add(new api.ODTextCommand("ot-moderation:timeout", { + name: "timeout", + prefix: generalConfig.data.prefix, + dmPermission: false, + guildPermission: true, + options:[ + { + type:"user", + name:"user", + required:true + }, + { + type:"string", + name:"duration", + required:true, + regex:/^\d+[smhdw]$/ + }, + { + type:"string", + name:"reason", + required:false, + allowSpaces:true + } + ] + })); + + // Untimeout Command + text.add(new api.ODTextCommand("ot-moderation:untimeout", { + name: "untimeout", + prefix: generalConfig.data.prefix, + dmPermission: false, + guildPermission: true, + options:[ + { + type:"user", + name:"user", + required:true + }, + { + type:"string", + name:"reason", + required:false, + allowSpaces:true + } + ] + })); + + // Mute Command + text.add(new api.ODTextCommand("ot-moderation:mute", { + name: "mute", + prefix: generalConfig.data.prefix, + dmPermission: false, + guildPermission: true, + options:[ + { + type:"user", + name:"user", + required:true + }, + { + type:"string", + name:"reason", + required:false, + allowSpaces:true + } + ] + })); + + // Unmute Command + text.add(new api.ODTextCommand("ot-moderation:unmute", { + name: "unmute", + prefix: generalConfig.data.prefix, + dmPermission: false, + guildPermission: true, + options:[ + { + type:"user", + name:"user", + required:true + }, + { + type:"string", + name:"reason", + required:false, + allowSpaces:true + } + ] + })); }); // REGISTER HELP MENU @@ -256,21 +467,115 @@ opendiscord.events.get("onHelpMenuComponentLoad").listen((menu) => { slashDescription: "Warn a user in the server.", textDescription: "Warn a user in the server.", })); + + // Timeout Command + menu.get("opendiscord:extra").add(new api.ODHelpMenuCommandComponent("ot-moderation:timeout", 0, { + slashName: "timeout", + textName: "timeout", + slashDescription: "Timeout a user in the server.", + textDescription: "Timeout a user in the server.", + })); + + // Untimeout Command + menu.get("opendiscord:extra").add(new api.ODHelpMenuCommandComponent("ot-moderation:untimeout", 0, { + slashName: "untimeout", + textName: "untimeout", + slashDescription: "Remove timeout from a user.", + textDescription: "Remove timeout from a user.", + })); + + // Mute Command + menu.get("opendiscord:extra").add(new api.ODHelpMenuCommandComponent("ot-moderation:mute", 0, { + slashName: "mute", + textName: "mute", + slashDescription: "Mute a user in the server.", + textDescription: "Mute a user in the server.", + })); + + // Unmute Command + menu.get("opendiscord:extra").add(new api.ODHelpMenuCommandComponent("ot-moderation:unmute", 0, { + slashName: "unmute", + textName: "unmute", + slashDescription: "Unmute a user in the server.", + textDescription: "Unmute a user in the server.", + })); }); +// HELPER FUNCTION: Check if user has admin/moderator roles +function hasRequiredRole(member: discord.GuildMember, action: 'admin' | 'moderator'): boolean { + const config = opendiscord.configs.get("ot-moderation"); + + // If no roles configured, return true (fall back to Discord permissions) + if (action === 'admin') { + const adminRoles: string[] = config?.data?.adminRoles || []; + if (adminRoles.length === 0) return true; + return adminRoles.some(roleId => member.roles.cache.has(roleId)); + } else { + const moderatorRoles: string[] = config?.data?.moderatorRoles || []; + const adminRoles: string[] = config?.data?.adminRoles || []; + + + // If no roles configured, return true (fall back to Discord permissions) + if (moderatorRoles.length === 0 && adminRoles.length === 0) return true; + + // Check if user has moderator or admin role + return moderatorRoles.some(roleId => member.roles.cache.has(roleId)) || + adminRoles.some(roleId => member.roles.cache.has(roleId)); + } +} + +// HELPER FUNCTION: Parse Duration +function parseDuration(duration: string): number | null { + const match = duration.match(/^(\d+)([smhdw])$/); + if (!match) return null; + + const value = parseInt(match[1]); + const unit = match[2]; + + const multipliers: { [key: string]: number } = { + 's': 1000, + 'm': 60 * 1000, + 'h': 60 * 60 * 1000, + 'd': 24 * 60 * 60 * 1000, + 'w': 7 * 24 * 60 * 60 * 1000 + }; + + return value * multipliers[unit]; +} + +// HELPER FUNCTION: Format Duration +function formatDuration(duration: string): string { + const match = duration.match(/^(\d+)([smhdw])$/); + if (!match) return duration; + + const value = match[1]; + const unit = match[2]; + + const units: { [key: string]: string } = { + 's': 'second(s)', + 'm': 'minute(s)', + 'h': 'hour(s)', + 'd': 'day(s)', + 'w': 'week(s)' + }; + + return `${value} ${units[unit]}`; +} + // REGISTER EMBED BUILDERS opendiscord.events.get("onEmbedBuilderLoad").listen((embeds) => { const generalConfig = opendiscord.configs.get("opendiscord:general"); + // Ban Embed embeds.add(new api.ODEmbed("ot-moderation:ban-embed")); embeds.get("ot-moderation:ban-embed").workers.add( new api.ODWorker("ot-moderation:ban-embed", 0, (instance, params, source, cancel) => { - instance.setTitle(utilities.emojiTitle("🚫","User Banned")) + instance.setTitle(utilities.emojiTitle("🚫","User Banned")); instance.setColor(generalConfig.data.mainColor); instance.setDescription(discord.userMention(params.user.id)+" has been banned from the server."); - instance.addFields({name:"Reason:",value:"```"+params.reason+"```"}) - instance.setThumbnail(params.user.displayAvatarURL()) - instance.setAuthor(params.author.displayName,params.author.displayAvatarURL()) + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); }) ); @@ -278,12 +583,12 @@ opendiscord.events.get("onEmbedBuilderLoad").listen((embeds) => { embeds.add(new api.ODEmbed("ot-moderation:unban-embed")); embeds.get("ot-moderation:unban-embed").workers.add( new api.ODWorker("ot-moderation:unban-embed", 0, (instance, params, source, cancel) => { - instance.setTitle(utilities.emojiTitle("🚫","User Unbanned")) + instance.setTitle(utilities.emojiTitle("✅","User Unbanned")); instance.setColor(generalConfig.data.mainColor); instance.setDescription(discord.userMention(params.userId)+" has been unbanned from the server."); - instance.addFields({name:"Reason:",value:"```"+params.reason+"```"}) - instance.setThumbnail(params.author.displayAvatarURL()) - instance.setAuthor(params.author.displayName,params.author.displayAvatarURL()) + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.author.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); }) ); @@ -291,12 +596,12 @@ opendiscord.events.get("onEmbedBuilderLoad").listen((embeds) => { embeds.add(new api.ODEmbed("ot-moderation:kick-embed")); embeds.get("ot-moderation:kick-embed").workers.add( new api.ODWorker("ot-moderation:kick-embed", 0, (instance, params, source, cancel) => { - instance.setTitle(utilities.emojiTitle("👋","User Kicked")) + instance.setTitle(utilities.emojiTitle("👋","User Kicked")); instance.setColor(generalConfig.data.mainColor); instance.setDescription(discord.userMention(params.user.id)+" has been kicked from the server."); - instance.addFields({name:"Reason:",value:"```"+params.reason+"```"}) - instance.setThumbnail(params.user.displayAvatarURL()) - instance.setAuthor(params.author.displayName,params.author.displayAvatarURL()) + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); }) ); @@ -304,12 +609,64 @@ opendiscord.events.get("onEmbedBuilderLoad").listen((embeds) => { embeds.add(new api.ODEmbed("ot-moderation:warn-embed")); embeds.get("ot-moderation:warn-embed").workers.add( new api.ODWorker("ot-moderation:warn-embed", 0, (instance, params, source, cancel) => { - instance.setTitle(utilities.emojiTitle("⚠️","User Warned")) + instance.setTitle(utilities.emojiTitle("⚠️","User Warned")); instance.setColor(generalConfig.data.mainColor); instance.setDescription(discord.userMention(params.user.id)+" has been warned."); - instance.addFields({name:"Reason:",value:"```"+params.reason+"```"}) - instance.setThumbnail(params.user.displayAvatarURL()) - instance.setAuthor(params.author.displayName,params.author.displayAvatarURL()) + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); + }) + ); + + // Timeout Embed + embeds.add(new api.ODEmbed("ot-moderation:timeout-embed")); + embeds.get("ot-moderation:timeout-embed").workers.add( + new api.ODWorker("ot-moderation:timeout-embed", 0, (instance, params, source, cancel) => { + instance.setTitle(utilities.emojiTitle("⏱️","User Timed Out")); + instance.setColor(generalConfig.data.mainColor); + instance.setDescription(discord.userMention(params.user.id)+" has been timed out for **"+formatDuration(params.duration)+"**."); + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); + }) + ); + + // Untimeout Embed + embeds.add(new api.ODEmbed("ot-moderation:untimeout-embed")); + embeds.get("ot-moderation:untimeout-embed").workers.add( + new api.ODWorker("ot-moderation:untimeout-embed", 0, (instance, params, source, cancel) => { + instance.setTitle(utilities.emojiTitle("⏰","Timeout Removed")); + instance.setColor(generalConfig.data.mainColor); + instance.setDescription(discord.userMention(params.user.id)+"'s timeout has been removed."); + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); + }) + ); + + // Mute Embed + embeds.add(new api.ODEmbed("ot-moderation:mute-embed")); + embeds.get("ot-moderation:mute-embed").workers.add( + new api.ODWorker("ot-moderation:mute-embed", 0, (instance, params, source, cancel) => { + instance.setTitle(utilities.emojiTitle("🔇","User Muted")); + instance.setColor(generalConfig.data.mainColor); + instance.setDescription(discord.userMention(params.user.id)+" has been muted."); + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); + }) + ); + + // Unmute Embed + embeds.add(new api.ODEmbed("ot-moderation:unmute-embed")); + embeds.get("ot-moderation:unmute-embed").workers.add( + new api.ODWorker("ot-moderation:unmute-embed", 0, (instance, params, source, cancel) => { + instance.setTitle(utilities.emojiTitle("🔊","User Unmuted")); + instance.setColor(generalConfig.data.mainColor); + instance.setDescription(discord.userMention(params.user.id)+" has been unmuted."); + instance.addFields({name:"Reason:",value:"```"+(params.reason || "No reason provided")+"```"}); + instance.setThumbnail(params.user.displayAvatarURL()); + instance.setAuthor(params.author.displayName, params.author.displayAvatarURL()); }) ); }); @@ -317,38 +674,74 @@ opendiscord.events.get("onEmbedBuilderLoad").listen((embeds) => { // REGISTER MESSAGE BUILDERS opendiscord.events.get("onMessageBuilderLoad").listen((messages) => { // Ban Message Builder - messages.add(new api.ODMessage("ot-moderation:ban-message")) + messages.add(new api.ODMessage("ot-moderation:ban-message")); messages.get("ot-moderation:ban-message").workers.add( new api.ODWorker("ot-moderation:ban-message", 0, async (instance, params, source, cancel) => { - const {user,reason,author} = params - instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:ban-embed").build(source,{user,reason,author})); + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:ban-embed").build(source, {user, reason, author})); }) ); // Unban Message Builder - messages.add(new api.ODMessage("ot-moderation:unban-message")) + messages.add(new api.ODMessage("ot-moderation:unban-message")); messages.get("ot-moderation:unban-message").workers.add( new api.ODWorker("ot-moderation:unban-message", 0, async (instance, params, source, cancel) => { - const {userId,reason,author} = params - instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:unban-embed").build(source,{userId,reason,author})); + const {userId, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:unban-embed").build(source, {userId, reason, author})); }) ); // Kick Message Builder - messages.add(new api.ODMessage("ot-moderation:kick-message")) + messages.add(new api.ODMessage("ot-moderation:kick-message")); messages.get("ot-moderation:kick-message").workers.add( new api.ODWorker("ot-moderation:kick-message", 0, async (instance, params, source, cancel) => { - const {user,reason,author} = params - instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:kick-embed").build(source,{user,reason,author})); + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:kick-embed").build(source, {user, reason, author})); }) ); // Warn Message Builder - messages.add(new api.ODMessage("ot-moderation:warn-message")) + messages.add(new api.ODMessage("ot-moderation:warn-message")); messages.get("ot-moderation:warn-message").workers.add( new api.ODWorker("ot-moderation:warn-message", 0, async (instance, params, source, cancel) => { - const {user,reason,author} = params - instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:warn-embed").build(source,{user,reason,author})); + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:warn-embed").build(source, {user, reason, author})); + }) + ); + + // Timeout Message Builder + messages.add(new api.ODMessage("ot-moderation:timeout-message")); + messages.get("ot-moderation:timeout-message").workers.add( + new api.ODWorker("ot-moderation:timeout-message", 0, async (instance, params, source, cancel) => { + const {user, duration, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:timeout-embed").build(source, {user, duration, reason, author})); + }) + ); + + // Untimeout Message Builder + messages.add(new api.ODMessage("ot-moderation:untimeout-message")); + messages.get("ot-moderation:untimeout-message").workers.add( + new api.ODWorker("ot-moderation:untimeout-message", 0, async (instance, params, source, cancel) => { + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:untimeout-embed").build(source, {user, reason, author})); + }) + ); + + // Mute Message Builder + messages.add(new api.ODMessage("ot-moderation:mute-message")); + messages.get("ot-moderation:mute-message").workers.add( + new api.ODWorker("ot-moderation:mute-message", 0, async (instance, params, source, cancel) => { + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:mute-embed").build(source, {user, reason, author})); + }) + ); + + // Unmute Message Builder + messages.add(new api.ODMessage("ot-moderation:unmute-message")); + messages.get("ot-moderation:unmute-message").workers.add( + new api.ODWorker("ot-moderation:unmute-message", 0, async (instance, params, source, cancel) => { + const {user, reason, author} = params; + instance.addEmbed(await opendiscord.builders.embeds.getSafe("ot-moderation:unmute-embed").build(source, {user, reason, author})); }) ); }); @@ -358,115 +751,126 @@ opendiscord.events.get("onCommandResponderLoad").listen((commands) => { const generalConfig = opendiscord.configs.get("opendiscord:general"); // Ban Command Responder - commands.add(new api.ODCommandResponder("ot-moderation:ban",generalConfig.data.prefix,"ban")); + commands.add(new api.ODCommandResponder("ot-moderation:ban", generalConfig.data.prefix, "ban")); commands.get("ot-moderation:ban").workers.add([ new api.ODWorker("ot-moderation:ban", 0, async (instance, params, source, cancel) => { const { guild, channel, user, member } = instance; - if (!guild || !member){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source,{channel,user})); + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); return cancel(); } // Check if the bot has permission to ban members if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.BanMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"The bot doesn't have permissions to ban people in this server."})); + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to ban people in this server."})); return cancel(); } // Check if the user has permission to ban members - if (!member.permissions.has(discord.PermissionsBitField.Flags.BanMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild,channel,user,permissions:["admin","discord-administrator"]})); + if (!member.permissions.has(discord.PermissionsBitField.Flags.BanMembers) && !hasRequiredRole(member, 'admin')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); return cancel(); } // Get the user to ban and the reason - const targetUser = instance.options.getUser("user",true); - const reason = instance.options.getString("reason",false) - const targetMember = await opendiscord.client.fetchGuildMember(guild,targetUser.id) + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); - if (!targetMember){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"Unable to find target member in server."})); + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); return cancel(); } // Check if the target user has a higher or equal role than the user executing the command if (targetMember.roles.highest.position >= member.roles.highest.position) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"You do not have permissions to ban a user with a higher role."})); + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"You do not have permissions to ban a user with a higher or equal role."})); return cancel(); } - await opendiscord.client.sendUserDm(targetUser,{ephemeral:false,id:new api.ODId("ot-moderation:banned-dm"),message:{ - content:"You have been banned from **"+guild.name+"** for the following reason:\n```"+(reason ?? "/")+"```" - }}) + // Check if the bot can ban this user + if (guild.members.me && targetMember.roles.highest.position >= guild.members.me.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"I cannot ban a user with a higher or equal role than me."})); + return cancel(); + } + + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:banned-dm"), + message: { + content: "You have been banned from **"+guild.name+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); // Ban the user - try{ - await guild.members.ban(targetUser,{reason:(reason ?? "/")}) - await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:ban-message").build(source,{author:user,user:targetUser,reason})); - }catch(err){ - process.emit("uncaughtException",err) + try { + await guild.members.ban(targetUser, {reason: (reason || "No reason provided")}); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:ban-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); } }), new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { - const targetUser = instance.options.getUser("user",true); - const reason = instance.options.getString("reason",false) ?? "/" + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; opendiscord.log(`${instance.user.displayName} has banned ${targetUser.displayName} from the server!`, "plugin", [ - {key:"user",value:instance.user.username}, - {key:"userid",value:instance.user.id,hidden:true}, - {key:"target",value:targetUser.username}, - {key:"targetid",value:targetUser.id,hidden:true}, - {key:"method",value:source}, - {key:"reason",value:reason}, - ]) + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); }) ]); - // Unban Command Responder + // Unban Command Responder commands.add(new api.ODCommandResponder("ot-moderation:unban", generalConfig.data.prefix, "unban")); commands.get("ot-moderation:unban").workers.add([ new api.ODWorker("ot-moderation:unban", 0, async (instance, params, source, cancel) => { const { guild, channel, user, member } = instance; - if (!guild || !member){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source,{channel,user})); + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); return cancel(); } // Check if the bot has permission to ban members if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.BanMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"The bot doesn't have permissions to unban people in this server."})); + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to unban people in this server."})); return cancel(); } // Check if the user has permission to ban members - if (!member.permissions.has(discord.PermissionsBitField.Flags.BanMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild,channel,user,permissions:["admin","discord-administrator"]})); + if (!member.permissions.has(discord.PermissionsBitField.Flags.BanMembers) && !hasRequiredRole(member, 'admin')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); return cancel(); } // Get the user ID to unban and the reason - const userId = instance.options.getString("userid", true) - const reason = instance.options.getString("reason", false) + const userId = instance.options.getString("userid", true); + const reason = instance.options.getString("reason", false); // Unban the user try { - await guild.members.unban(userId, reason ?? "/"); - await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:unban-message").build(source,{author:user,userId,reason})); - }catch(err){ - process.emit("uncaughtException",err) + await guild.members.unban(userId, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:unban-message").build(source, {author: user, userId, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); } }), new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { - const targetUserId = instance.options.getString("userid", true) - const reason = instance.options.getString("reason",false) ?? "/" + const targetUserId = instance.options.getString("userid", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; opendiscord.log(`${instance.user.displayName} has unbanned ${targetUserId} in the server!`, "plugin", [ - {key:"user",value:instance.user.username}, - {key:"userid",value:instance.user.id,hidden:true}, - {key:"targetid",value:targetUserId}, - {key:"method",value:source}, - {key:"reason",value:reason}, - ]) + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "targetid", value: targetUserId}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); }) ]); @@ -476,62 +880,73 @@ opendiscord.events.get("onCommandResponderLoad").listen((commands) => { new api.ODWorker("ot-moderation:kick", 0, async (instance, params, source, cancel) => { const { guild, channel, user, member } = instance; - if (!guild || !member){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source,{channel,user})); + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); return cancel(); } // Check if the bot has permission to kick members if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.KickMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"The bot doesn't have permissions to kick people in this server."})); + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to kick people in this server."})); return cancel(); } // Check if the user has permission to kick members - if (!member.permissions.has(discord.PermissionsBitField.Flags.KickMembers)) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild,channel,user,permissions:["admin","discord-administrator"]})); + if (!member.permissions.has(discord.PermissionsBitField.Flags.KickMembers) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); return cancel(); } // Get the user to kick and the reason - const targetUser = instance.options.getUser("user", true) - const reason = instance.options.getString("reason", false) - const targetMember = await opendiscord.client.fetchGuildMember(guild,targetUser.id) + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); - if (!targetMember){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"Unable to find target member in server."})); + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); return cancel(); } // Check if the target user has a higher or equal role than the user executing the command if (targetMember.roles.highest.position >= member.roles.highest.position) { - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild,channel,user,layout:"simple",error:"You do not have permissions to ban a user with a higher role."})); + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"You do not have permissions to kick a user with a higher or equal role."})); + return cancel(); + } + + // Check if the bot can kick this user + if (guild.members.me && targetMember.roles.highest.position >= guild.members.me.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"I cannot kick a user with a higher or equal role than me."})); return cancel(); } - await opendiscord.client.sendUserDm(targetUser,{ephemeral:false,id:new api.ODId("ot-moderation:kicked-dm"),message:{ - content:"You have been kicked from **"+guild.name+"** for the following reason:\n```"+(reason ?? "/")+"```" - }}) + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:kicked-dm"), + message: { + content: "You have been kicked from **"+guild.name+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); // Kick the user - try{ - await guild.members.kick(targetUser,reason ?? "/") - await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:kick-message").build(source,{author:user,user:targetUser,reason})); - }catch(err){ - process.emit("uncaughtException",err) + try { + await guild.members.kick(targetUser, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:kick-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); } }), new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { - const targetUser = instance.options.getUser("user",true); - const reason = instance.options.getString("reason",false) ?? "/" + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; opendiscord.log(`${instance.user.displayName} has kicked ${targetUser.displayName} from the server!`, "plugin", [ - {key:"user",value:instance.user.username}, - {key:"userid",value:instance.user.id,hidden:true}, - {key:"target",value:targetUser.username}, - {key:"targetid",value:targetUser.id,hidden:true}, - {key:"method",value:source}, - {key:"reason",value:reason}, - ]) + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); }) ]); @@ -541,44 +956,403 @@ opendiscord.events.get("onCommandResponderLoad").listen((commands) => { new api.ODWorker("ot-moderation:warn", 0, async (instance, params, source, cancel) => { const { guild, channel, user, member } = instance; - if (!guild || !member){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source,{channel,user})); + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); return cancel(); } // Check if the user has permission to warn members - if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(user,channel,guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild,channel,user,permissions:["admin","discord-administrator"]})); + if (!opendiscord.permissions.hasPermissions("support", await opendiscord.permissions.getPermissions(user, channel, guild, {allowChannelRoleScope: false, allowChannelUserScope: false, allowGlobalRoleScope: true, allowGlobalUserScope: true})) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); return cancel(); } // Get the user to warn and the reason - const targetUser = instance.options?.getUser("user", true) - const reason = instance.options?.getString("reason", false) + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); - await opendiscord.client.sendUserDm(targetUser,{ephemeral:false,id:new api.ODId("ot-moderation:warned-dm"),message:{ - content:"You have been warned in **"+guild.name+"** for the following reason:\n```"+(reason ?? "/")+"```" - }}) + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:warned-dm"), + message: { + content: "You have been warned in **"+guild.name+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); // Warn the user - try{ + try { //TODO: PRESERVE WARNINGS :) - await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:warn-message").build(source,{author:user,user:targetUser,reason})); - }catch(err){ - process.emit("uncaughtException",err) + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:warn-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); } }), new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { - const targetUser = instance.options.getUser("user",true); - const reason = instance.options.getString("reason",false) ?? "/" + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; opendiscord.log(`${instance.user.displayName} has warned ${targetUser.displayName} in the server!`, "plugin", [ - {key:"user",value:instance.user.username}, - {key:"userid",value:instance.user.id,hidden:true}, - {key:"target",value:targetUser.username}, - {key:"targetid",value:targetUser.id,hidden:true}, - {key:"method",value:source}, - {key:"reason",value:reason}, - ]) + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); + }) + ]); + + // Timeout Command Responder + commands.add(new api.ODCommandResponder("ot-moderation:timeout", generalConfig.data.prefix, "timeout")); + commands.get("ot-moderation:timeout").workers.add([ + new api.ODWorker("ot-moderation:timeout", 0, async (instance, params, source, cancel) => { + const { guild, channel, user, member } = instance; + + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); + return cancel(); + } + + // Check if the bot has permission to timeout members + if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.ModerateMembers)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to timeout members in this server."})); + return cancel(); + } + + // Check if the user has permission to timeout members + if (!member.permissions.has(discord.PermissionsBitField.Flags.ModerateMembers) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); + return cancel(); + } + + // Get the user to timeout, duration, and the reason + const targetUser = instance.options.getUser("user", true); + const durationStr = instance.options.getString("duration", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); + + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); + return cancel(); + } + + // Parse duration + const duration = parseDuration(durationStr); + if (!duration) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Invalid duration format. Use format like: 10m, 1h, 1d, 1w"})); + return cancel(); + } + + // Discord timeout limit is 28 days + const maxTimeout = 28 * 24 * 60 * 60 * 1000; // 28 days in milliseconds + if (duration > maxTimeout) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Timeout duration cannot exceed 28 days."})); + return cancel(); + } + + // Check if the target user has a higher or equal role than the user executing the command + if (targetMember.roles.highest.position >= member.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"You do not have permissions to timeout a user with a higher or equal role."})); + return cancel(); + } + + // Check if the bot can timeout this user + if (guild.members.me && targetMember.roles.highest.position >= guild.members.me.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"I cannot timeout a user with a higher or equal role than me."})); + return cancel(); + } + + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:timeout-dm"), + message: { + content: "You have been timed out in **"+guild.name+"** for **"+formatDuration(durationStr)+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); + + // Timeout the user + try { + await targetMember.timeout(duration, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:timeout-message").build(source, {author: user, user: targetUser, duration: durationStr, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); + } + }), + new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { + const targetUser = instance.options.getUser("user", true); + const durationStr = instance.options.getString("duration", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; + opendiscord.log(`${instance.user.displayName} has timed out ${targetUser.displayName} for ${formatDuration(durationStr)}!`, "plugin", [ + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "duration", value: formatDuration(durationStr)}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); + }) + ]); + + // Untimeout Command Responder + commands.add(new api.ODCommandResponder("ot-moderation:untimeout", generalConfig.data.prefix, "untimeout")); + commands.get("ot-moderation:untimeout").workers.add([ + new api.ODWorker("ot-moderation:untimeout", 0, async (instance, params, source, cancel) => { + const { guild, channel, user, member } = instance; + + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); + return cancel(); + } + + // Check if the bot has permission to timeout members + if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.ModerateMembers)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to remove timeouts in this server."})); + return cancel(); + } + + // Check if the user has permission to timeout members + if (!member.permissions.has(discord.PermissionsBitField.Flags.ModerateMembers) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); + return cancel(); + } + + // Get the user to remove timeout from and the reason + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); + + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); + return cancel(); + } + + // Check if user is actually timed out + if (!targetMember.isCommunicationDisabled()) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"This user is not currently timed out."})); + return cancel(); + } + + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:untimeout-dm"), + message: { + content: "Your timeout in **"+guild.name+"** has been removed for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); + + // Remove timeout + try { + await targetMember.timeout(null, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:untimeout-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); + } + }), + new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; + opendiscord.log(`${instance.user.displayName} has removed timeout from ${targetUser.displayName}!`, "plugin", [ + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); + }) + ]); + + // Mute Command Responder + commands.add(new api.ODCommandResponder("ot-moderation:mute", generalConfig.data.prefix, "mute")); + commands.get("ot-moderation:mute").workers.add([ + new api.ODWorker("ot-moderation:mute", 0, async (instance, params, source, cancel) => { + const { guild, channel, user, member } = instance; + + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); + return cancel(); + } + + // Check if the bot has permission to manage roles + if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.ManageRoles)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to manage roles in this server."})); + return cancel(); + } + + // Check if the user has permission to mute members + if (!member.permissions.has(discord.PermissionsBitField.Flags.ManageRoles) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); + return cancel(); + } + + // Get the user to mute and the reason + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); + + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); + return cancel(); + } + + // Find or create mute role + const muteRoleName = pluginConfig?.data?.muteRoleName || "Muted"; + let muteRole = guild.roles.cache.find(role => role.name === muteRoleName); + + if (!muteRole) { + try { + muteRole = await guild.roles.create({ + name: muteRoleName, + color: discord.Colors.DarkGrey, + permissions: [], + reason: "Auto-created mute role" + }); + + // Configure permissions for all channels (excluding threads) + for (const [, channel] of guild.channels.cache) { + if ((channel.isTextBased() || channel.isVoiceBased()) && !channel.isThread()) { + await channel.permissionOverwrites.create(muteRole, { + SendMessages: false, + Speak: false, + AddReactions: false + }); + } + } + } catch(err) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Failed to create Muted role. Please create it manually."})); + return cancel(); + } + } + + // Check if the target user has a higher or equal role than the user executing the command + if (targetMember.roles.highest.position >= member.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"You do not have permissions to mute a user with a higher or equal role."})); + return cancel(); + } + + // Check if the bot can mute this user + if (guild.members.me && targetMember.roles.highest.position >= guild.members.me.roles.highest.position) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"I cannot mute a user with a higher or equal role than me."})); + return cancel(); + } + + // Check if user already has mute role + if (targetMember.roles.cache.has(muteRole.id)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"This user is already muted."})); + return cancel(); + } + + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:muted-dm"), + message: { + content: "You have been muted in **"+guild.name+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); + + // Mute the user + try { + await targetMember.roles.add(muteRole, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:mute-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); + } + }), + new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; + opendiscord.log(`${instance.user.displayName} has muted ${targetUser.displayName} in the server!`, "plugin", [ + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); + }) + ]); + + // Unmute Command Responder + commands.add(new api.ODCommandResponder("ot-moderation:unmute", generalConfig.data.prefix, "unmute")); + commands.get("ot-moderation:unmute").workers.add([ + new api.ODWorker("ot-moderation:unmute", 0, async (instance, params, source, cancel) => { + const { guild, channel, user, member } = instance; + + if (!guild || !member) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build(source, {channel, user})); + return cancel(); + } + + // Check if the bot has permission to manage roles + if (!guild.members.me || !guild.members.me.permissions.has(discord.PermissionsBitField.Flags.ManageRoles)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"The bot doesn't have permissions to manage roles in this server."})); + return cancel(); + } + + // Check if the user has permission to unmute members + if (!member.permissions.has(discord.PermissionsBitField.Flags.ManageRoles) && !hasRequiredRole(member, 'moderator')) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source, {guild, channel, user, permissions:["admin","discord-administrator"]})); + return cancel(); + } + + // Get the user to unmute and the reason + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false); + const targetMember = await opendiscord.client.fetchGuildMember(guild, targetUser.id); + + if (!targetMember) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"Unable to find target member in server."})); + return cancel(); + } + + // Find mute role + const muteRoleName = pluginConfig?.data?.muteRoleName || "Muted"; + + const muteRole = guild.roles.cache.find(role => role.name === muteRoleName); + + if (!muteRole) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:`${muteRoleName} role not found. Please create it manually.`})); + return cancel(); + } + + // Check if user has mute role + if (!targetMember.roles.cache.has(muteRole.id)) { + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source, {guild, channel, user, layout:"simple", error:"This user is not currently muted."})); + return cancel(); + } + + // Send DM to user + await opendiscord.client.sendUserDm(targetUser, { + ephemeral: false, + id: new api.ODId("ot-moderation:unmuted-dm"), + message: { + content: "You have been unmuted in **"+guild.name+"** for the following reason:\n```"+(reason || "No reason provided")+"```" + } + }); + + // Unmute the user + try { + await targetMember.roles.remove(muteRole, reason || "No reason provided"); + await instance.reply(await opendiscord.builders.messages.getSafe("ot-moderation:unmute-message").build(source, {author: user, user: targetUser, reason})); + } catch(err) { + process.emit("uncaughtException", err as Error); + } + }), + new api.ODWorker("ot-moderation:logs", -1, (instance, params, source, cancel) => { + const targetUser = instance.options.getUser("user", true); + const reason = instance.options.getString("reason", false) || "No reason provided"; + opendiscord.log(`${instance.user.displayName} has unmuted ${targetUser.displayName} in the server!`, "plugin", [ + {key: "user", value: instance.user.username}, + {key: "userid", value: instance.user.id, hidden: true}, + {key: "target", value: targetUser.username}, + {key: "targetid", value: targetUser.id, hidden: true}, + {key: "method", value: source}, + {key: "reason", value: reason}, + ]); }) ]); }); From 07f3830393313333bbf1429597416089b9d13acc Mon Sep 17 00:00:00 2001 From: yowsef <70838519+aboreda12@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:33:29 +0200 Subject: [PATCH 3/5] Update plugin.json --- open-ticket/ot-moderation/plugin.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/open-ticket/ot-moderation/plugin.json b/open-ticket/ot-moderation/plugin.json index 06e8852..3ffb055 100644 --- a/open-ticket/ot-moderation/plugin.json +++ b/open-ticket/ot-moderation/plugin.json @@ -1,7 +1,7 @@ { "name":"OT Moderation", "id":"ot-moderation", - "version":"1.0.0", + "version":"2.0.0", "startFile":"index.ts", "enabled":true, @@ -13,11 +13,11 @@ "incompatiblePlugins":[], "details":{ - "author":"NotMukundOP", + "author":"NotMukundOP,Updated by:aboreda12", "shortDescription":"A simple moderation plugin for Open Discord/Ticket.", "longDescription":"A simple moderation plugin for Open Discord/Ticket.", "imageUrl":"", "projectUrl":"", - "tags":["moderation","ban","kick","warn"] + "tags":["moderation","ban","kick","warn","mute","unmute","unban","timeout","untimeout"] } -} \ No newline at end of file +} From d0346d3e534f3f82e7aa6cede369dc42a11acd79 Mon Sep 17 00:00:00 2001 From: yowsef <70838519+aboreda12@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:36:11 +0200 Subject: [PATCH 4/5] Delete open-ticket/ot-moderation/ot-moderation.zip --- open-ticket/ot-moderation/ot-moderation.zip | Bin 4345 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 open-ticket/ot-moderation/ot-moderation.zip diff --git a/open-ticket/ot-moderation/ot-moderation.zip b/open-ticket/ot-moderation/ot-moderation.zip deleted file mode 100644 index 7de7c137382193736c3198471796704fa256bef4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4345 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 From 24b7e7a06d3f17a63b0bab34bb816f924e2485eb Mon Sep 17 00:00:00 2001 From: yowsef <70838519+aboreda12@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:37:14 +0200 Subject: [PATCH 5/5] Create config.json --- open-ticket/ot-moderation/config.json | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 open-ticket/ot-moderation/config.json diff --git a/open-ticket/ot-moderation/config.json b/open-ticket/ot-moderation/config.json new file mode 100644 index 0000000..4e4d4d8 --- /dev/null +++ b/open-ticket/ot-moderation/config.json @@ -0,0 +1,38 @@ +{ + "adminRoles": { + "description": "Role IDs that can use moderation commands. Leave empty to use Discord permissions.", + "type": "array", + "default": [], + "value": [ROLEID] + }, + "moderatorRoles": { + "description": "Role IDs that can use kick, warn, timeout, and mute commands (less severe actions).", + "type": "array", + "default": [], + "value": [ROLEID] + }, + "muteRoleName": { + "description": "Name of the mute role to create/use.", + "type": "string", + "default": "Muted", + "value": "Muted" + }, + "sendDMNotifications": { + "description": "Send DM notifications to users when they are moderated.", + "type": "boolean", + "default": true, + "value": true + }, + "logChannel": { + "description": "Channel ID for moderation logs. Leave empty to disable.(Have some issues)", + "type": "string", + "default": "", + "value": "" + }, + "requireReason": { + "description": "Require a reason for all moderation actions.", + "type": "boolean", + "default": false, + "value": false + } +}