diff --git a/.prettierrc.json b/.prettierrc.json index 479993c13..45a0c08e5 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,10 @@ { "trailingComma": "es5", "tabWidth": 2, + "useTabs": false, "semi": true, "singleQuote": false, - "printWidth": 120 + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "always" } diff --git a/config-sample.js b/config-sample.js index 76d3e23e5..888de7efd 100644 --- a/config-sample.js +++ b/config-sample.js @@ -28,6 +28,9 @@ module.exports = { IMAGE_API: "https://discord-js-image-manipulation.herokuapp.com", // Image commands won't work without this WEATHERSTACK_KEY: "", // https://weatherstack.com/ }, + MISCELLANEOUS: { + DAILY_COINS: 100, // coins to be received by daily command + }, /* Bot Embed Colors */ EMBED_COLORS: { BOT_EMBED: "#068ADD", diff --git a/package.json b/package.json index b612a8562..e2a16c994 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "url": "saiteja-madha/discord-js-bot" }, "dependencies": { + "@discordjs/opus": "^0.5.3", "ascii-table": "0.0.9", "axios": "^0.21.1", "btoa": "^1.2.1", "country-language": "^0.1.7", + "discord-player": "^5.1.0", "discord.js": "^13.1.0", "ejs": "^3.1.6", "express": "^4.17.1", diff --git a/src/commands/admin/maxwarn.js b/src/commands/admin/maxwarn.js new file mode 100644 index 000000000..c403330e4 --- /dev/null +++ b/src/commands/admin/maxwarn.js @@ -0,0 +1,77 @@ +const { Command } = require("@src/structures"); +const { maxWarnings, maxWarnAction } = require("@schemas/guild-schema"); +const { Message } = require("discord.js"); +const { getRoleByName } = require("@utils/guildUtils"); + +module.exports = class MaxWarn extends Command { + constructor(client) { + super(client, { + name: "maxwarn", + description: "set max warnings configuration", + command: { + enabled: true, + minArgsCount: 1, + subcommands: [ + { + trigger: "limit ", + description: "set max warnings a member can receive before taking an action", + }, + { + trigger: "action ", + description: "set action to performed after receiving maximum warnings", + }, + ], + category: "ADMIN", + userPermissions: ["ADMINISTRATOR"], + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args, invoke, prefix) { + const input = args[0].toUpperCase(); + + // Limit configuration + if (input === "LIMIT") { + const max = args[1]; + if (isNaN(max) || Number.parseInt(max) < 1) + return message.reply("Max Warnings must be a valid number greater than 0"); + + await maxWarnings(message.guildId, max); + message.channel.send(`Configuration saved! Maximum warnings is set to ${max}`); + } + + // Action + else if (input === "ACTION") { + const action = args[1]?.toUpperCase(); + + if (!action) message.reply("Please choose an action. Action can be `Mute`/`Kick`/`Ban`"); + if (!["MUTE", "KICK", "BAN"].includes(action)) + return message.reply("Not a valid action. Action can be `Mute`/`Kick`/`Ban`"); + + if (action === "MUTE") { + let mutedRole = getRoleByName(message.guild, "muted"); + if (!mutedRole) { + return message.reply(`Muted role doesn't exist in this guild. Use \`${prefix}mute setup\` to create one`); + } + + if (!mutedRole.editable) { + return message.reply( + "I do not have permission to move members to `Muted` role. Is that role below my highest role?" + ); + } + } + + await maxWarnAction(message.guildId, action); + message.channel.send(`Configuration saved! Max Warnings action is set to ${action}`); + } + + // send usage + else { + return this.sendUsage(message.channel, prefix, invoke, "Incorrect Arguments"); + } + } +}; diff --git a/src/commands/admin/xpsystem.js b/src/commands/admin/xpsystem.js index 3085c9dbf..9b4c56e51 100644 --- a/src/commands/admin/xpsystem.js +++ b/src/commands/admin/xpsystem.js @@ -11,6 +11,7 @@ module.exports = class XPSystem extends Command { enabled: true, usage: "", minArgsCount: 1, + aliases: ["xptracking"], category: "ADMIN", userPermissions: ["ADMINISTRATOR"], }, diff --git a/src/commands/automod/automod.js b/src/commands/automod/automod.js index d79e410e4..4e3c42e17 100644 --- a/src/commands/automod/automod.js +++ b/src/commands/automod/automod.js @@ -24,7 +24,7 @@ module.exports = class Automod extends Command { }, { trigger: "action ", - description: "set automod action to performed after maximum strikes", + description: "set action to be performed after receiving maximum strikes", }, { trigger: "debug ", @@ -107,7 +107,7 @@ async function setAction(message, args, prefix) { if (action === "MUTE") { let mutedRole = getRoleByName(message.guild, "muted"); if (!mutedRole) { - return await message.reply(`Muted role doesn't exist in this guild. Use \`${prefix}mute setup\` to create one`); + return message.reply(`Muted role doesn't exist in this guild. Use \`${prefix}mute setup\` to create one`); } if (!mutedRole.editable) { diff --git a/src/commands/economy/daily.js b/src/commands/economy/daily.js index 4209ba4d6..c1a97c813 100644 --- a/src/commands/economy/daily.js +++ b/src/commands/economy/daily.js @@ -1,7 +1,7 @@ const { Command } = require("@src/structures"); const { MessageEmbed, Message } = require("discord.js"); const { getUser, updateDailyStreak } = require("@schemas/user-schema"); -const { EMBED_COLORS, EMOJIS } = require("@root/config.js"); +const { EMBED_COLORS, EMOJIS, MISCELLANEOUS } = require("@root/config.js"); const { diffHours, getRemainingTime } = require("@utils/miscUtils"); module.exports = class DailyCommand extends Command { @@ -39,13 +39,13 @@ module.exports = class DailyCommand extends Command { else streak = 0; } - const updated = await updateDailyStreak(member.id, 1000, streak); + const updated = await updateDailyStreak(member.id, MISCELLANEOUS.DAILY_COINS, streak); const embed = new MessageEmbed() .setColor(EMBED_COLORS.BOT_EMBED) .setAuthor(member.displayName, member.user.displayAvatarURL()) .setDescription( - `You got 1000${EMOJIS.CURRENCY} as your daily reward\n` + + `You got ${MISCELLANEOUS.DAILY_COINS}${EMOJIS.CURRENCY} as your daily reward\n` + `**Updated Balance:** ${updated?.coins || 0}${EMOJIS.CURRENCY}` ); diff --git a/src/commands/info/avatar.js b/src/commands/info/avatar.js index 759bec4af..fd8d211b6 100644 --- a/src/commands/info/avatar.js +++ b/src/commands/info/avatar.js @@ -1,5 +1,11 @@ const { Command } = require("@src/structures"); -const { MessageEmbed, Message, CommandInteraction, CommandInteractionOptionResolver } = require("discord.js"); +const { + MessageEmbed, + Message, + CommandInteraction, + CommandInteractionOptionResolver, + ContextMenuInteraction, +} = require("discord.js"); const { EMOJIS, EMBED_COLORS } = require("@root/config.js"); const { resolveMember } = require("@utils/guildUtils"); @@ -8,6 +14,7 @@ module.exports = class AvatarCommand extends Command { super(client, { name: "avatar", description: "displays avatar information about the user", + cooldown: 5, command: { enabled: true, usage: "[@member|id]", @@ -25,6 +32,10 @@ module.exports = class AvatarCommand extends Command { }, ], }, + contextMenu: { + enabled: true, + type: "USER", + }, }); } @@ -48,6 +59,15 @@ module.exports = class AvatarCommand extends Command { const embed = buildEmbed(target); interaction.followUp({ embeds: [embed] }); } + + /** + * @param {ContextMenuInteraction} interaction + */ + async contextRun(interaction) { + const user = await interaction.client.users.fetch(interaction.targetId); + const embed = buildEmbed(user); + interaction.followUp({ embeds: [embed] }); + } }; const buildEmbed = (user) => { diff --git a/src/commands/info/emoji-info.js b/src/commands/info/emoji-info.js new file mode 100644 index 000000000..39bf71210 --- /dev/null +++ b/src/commands/info/emoji-info.js @@ -0,0 +1,43 @@ +const { Command } = require("@src/structures"); +const { Message, Util, MessageEmbed } = require("discord.js"); + +module.exports = class EmojiInfo extends Command { + constructor(client) { + super(client, { + name: "emojiinfo", + description: "shows info about an emoji", + command: { + enabled: true, + usage: "", + minArgsCount: 1, + aliases: ["emoji"], + category: "INFORMATION", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const emoji = args[0]; + let custom = Util.parseEmoji(emoji); + if (!custom.id) return message.channel.send("This is not a valid guild emoji"); + + let url = `https://cdn.discordapp.com/emojis/${custom.id}.${custom.animated ? "gif?v=1" : "png"}`; + + const embed = new MessageEmbed() + .setColor(this.client.config.EMBED_COLORS.BOT_EMBED) + .setAuthor("Emoji Info") + .setDescription( + `**Id:** ${custom.id}\n` + `**Name:** ${custom.name}\n` + `**Animated:** ${custom.animated ? "Yes" : "No"}` + ) + .setImage(url); + + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/info/profile.js b/src/commands/info/profile.js new file mode 100644 index 000000000..571fcb0cf --- /dev/null +++ b/src/commands/info/profile.js @@ -0,0 +1,69 @@ +const { Command } = require("@src/structures"); +const { Message, MessageEmbed, ContextMenuInteraction } = require("discord.js"); +const { EMBED_COLORS, EMOJIS } = require("@root/config"); +const { getProfile } = require("@schemas/profile-schema"); +const { getSettings } = require("@schemas/guild-schema"); +const { resolveMember } = require("@utils/guildUtils"); +const { getUser } = require("@schemas/user-schema"); + +module.exports = class Profile extends Command { + constructor(client) { + super(client, { + name: "profile", + description: "shows members profile", + cooldown: 5, + command: { + enabled: true, + category: "INFORMATION", + }, + contextMenu: { + enabled: true, + type: "USER", + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const target = (await resolveMember(message, args[0])) || message.member; + const embed = await buildEmbed(message.guild, target); + message.channel.send({ embeds: [embed] }); + } + + /** + * @param {ContextMenuInteraction} interaction + */ + async contextRun(interaction) { + const target = (await interaction.guild.members.fetch(interaction.targetId)) || interaction.member; + const embed = await buildEmbed(interaction.guild, target); + interaction.followUp({ embeds: [embed] }); + } +}; + +const buildEmbed = async (guild, target) => { + const { user } = target; + const settings = await getSettings(guild); + const profile = await getProfile(guild.id, user.id); + const userData = await getUser(user.id); + + return new MessageEmbed() + .setThumbnail(user.displayAvatarURL()) + .setColor(EMBED_COLORS.BOT_EMBED) + .addField("User Tag", user.tag, true) + .addField("ID", user.id, true) + .addField("Discord Registered", user.createdAt.toDateString(), false) + .addField("Cash", `${userData?.coins || 0} ${EMOJIS.CURRENCY}`, true) + .addField("Bank", `${userData?.bank || 0} ${EMOJIS.CURRENCY}`, true) + .addField("Net Worth", `${(userData?.coins || 0) + (userData?.bank || 0)}${EMOJIS.CURRENCY}`, true) + .addField("Messages*", `${settings.ranking.enabled ? (profile?.messages || 0) + " " : "Not Tracked"}`, true) + .addField("XP*", `${settings.ranking.enabled ? (profile?.xp || 0) + " " : "Not Tracked"}`, true) + .addField("Level*", `${settings.ranking.enabled ? (profile?.level || 0) + " " : "Not Tracked"}`, true) + .addField("Strikes*", (profile?.strikes || 0) + " ", true) + .addField("Warnings*", (profile?.warnings || 0) + " ", true) + .addField("Reputation", `${userData?.reputation?.received || 0}`, true) + .addField("Avatar-URL", user.displayAvatarURL({ format: "png" })) + .setFooter("Fields marked (*) are guild specific"); +}; diff --git a/src/commands/info/rank.js b/src/commands/info/rank.js new file mode 100644 index 000000000..e6d5a366f --- /dev/null +++ b/src/commands/info/rank.js @@ -0,0 +1,63 @@ +const { Command } = require("@src/structures"); +const { Message, MessageAttachment } = require("discord.js"); +const { API, EMBED_COLORS } = require("@root/config"); +const { getProfile, getTop100 } = require("@schemas/profile-schema"); +const { downloadImage } = require("@utils/httpUtils"); +const { getSettings } = require("@schemas/guild-schema"); +const { resolveMember } = require("@utils/guildUtils"); + +module.exports = class Rank extends Command { + constructor(client) { + super(client, { + name: "rank", + description: "shows members rank in this server", + cooldown: 5, + command: { + enabled: true, + category: "INFORMATION", + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const target = (await resolveMember(message, args[0])) || message.member; + const { user } = target; + + const settings = await getSettings(message.guild); + if (!settings.ranking.enabled) return message.channel.send("Ranking is disabled on this server"); + + const profile = await getProfile(message.guildId, user.id); + if (!profile) return message.channel.send(`${user.tag} is not ranked yet!`); + + const lb = await getTop100(message.guildId); + let pos = -1; + lb.forEach((doc, i) => { + if (doc.member_id == target.id) { + pos = i + 1; + } + }); + + const xpNeeded = profile.level * profile.level * 100; + + const url = new URL(`${API.IMAGE_API}/utils/rank-card`); + url.searchParams.append("name", user.username); + url.searchParams.append("discriminator", user.discriminator); + url.searchParams.append("avatar", user.displayAvatarURL({ format: "png", size: 128 })); + url.searchParams.append("currentxp", profile.xp); + url.searchParams.append("reqxp", xpNeeded); + url.searchParams.append("level", profile.level); + url.searchParams.append("barcolor", EMBED_COLORS.BOT_EMBED); + url.searchParams.append("status", message.member.presence.status.toString()); + if (pos !== -1) url.searchParams.append("rank", pos); + + const rankCard = await downloadImage(url.href); + if (!rankCard) return message.reply("Failed to generate rank-card"); + + const attachment = new MessageAttachment(rankCard, "rank.png"); + message.channel.send({ files: [attachment] }); + } +}; diff --git a/src/commands/info/userinfo.js b/src/commands/info/userinfo.js index 1519db781..1997039e3 100644 --- a/src/commands/info/userinfo.js +++ b/src/commands/info/userinfo.js @@ -33,7 +33,7 @@ module.exports = class UserInfo extends Command { ], }, contextMenu: { - enabled: true, + enabled: false, type: "USER", }, }); diff --git a/src/commands/moderation/ban.js b/src/commands/moderation/ban.js index 1cff62276..c47e8cdf6 100644 --- a/src/commands/moderation/ban.js +++ b/src/commands/moderation/ban.js @@ -1,6 +1,6 @@ const { resolveMember } = require("@root/src/utils/guildUtils"); const { Command } = require("@src/structures"); -const { canInteract, banTarget } = require("@utils/modUtils"); +const { canInteract, addModAction } = require("@utils/modUtils"); const { Message } = require("discord.js"); module.exports = class BanCommand extends Command { @@ -47,7 +47,7 @@ module.exports = class BanCommand extends Command { async function ban(message, target, reason) { if (!canInteract(message.member, target, "ban", message.channel)) return; - const status = await banTarget(message.member, target, reason); + const status = await addModAction(message.member, target, reason, "BAN"); if (status) message.channel.send(`${target.user.tag} is banned from this server`); else message.channel.send(`Failed to ban ${target.user.tag}`); } diff --git a/src/commands/moderation/kick.js b/src/commands/moderation/kick.js index cdcfbd813..ef9315be8 100644 --- a/src/commands/moderation/kick.js +++ b/src/commands/moderation/kick.js @@ -1,6 +1,6 @@ const { resolveMember } = require("@root/src/utils/guildUtils"); const { Command } = require("@src/structures"); -const { canInteract, kickTarget } = require("@utils/modUtils"); +const { canInteract, addModAction } = require("@utils/modUtils"); const { Message } = require("discord.js"); module.exports = class KickCommand extends Command { @@ -47,7 +47,7 @@ module.exports = class KickCommand extends Command { async function kick(message, target, reason) { if (!canInteract(message.member, target, "kick", message.channel)) return; - let status = await kickTarget(message.member, target, reason); + let status = await addModAction(message.member, target, reason, "KICK"); if (status) message.channel.send(`${target.user.tag} is kicked from this server`); else message.channel.send(`Failed to kick ${target.user.tag}`); } diff --git a/src/commands/moderation/mute.js b/src/commands/moderation/mute.js index 0417d8acf..c49a5f9ca 100644 --- a/src/commands/moderation/mute.js +++ b/src/commands/moderation/mute.js @@ -1,5 +1,5 @@ const { Command } = require("@src/structures"); -const { setupMutedRole, canInteract, muteTarget } = require("@utils/modUtils"); +const { setupMutedRole, canInteract, addModAction } = require("@utils/modUtils"); const { getRoleByName, resolveMember } = require("@utils/guildUtils"); const { Message } = require("discord.js"); @@ -81,7 +81,7 @@ async function muteSetup(message) { async function mute(message, target, reason) { if (!canInteract(message.member, target, "mute", message.channel)) return; - const status = await muteTarget(message.member, target, reason); + const status = await addModAction(message.member, target, reason, "MUTE"); if (status === "ALREADY_MUTED") return message.channel.send(`${target.user.tag} is already muted`); if (status) message.channel.send(`${target.user.tag} is now muted on this server`); else message.channel.send(`Failed to add muted role to ${target.user.tag}`); diff --git a/src/commands/moderation/softban.js b/src/commands/moderation/softban.js index 1f8151c21..611862a22 100644 --- a/src/commands/moderation/softban.js +++ b/src/commands/moderation/softban.js @@ -1,6 +1,6 @@ const { resolveMember } = require("@root/src/utils/guildUtils"); const { Command } = require("@src/structures"); -const { canInteract, softbanTarget } = require("@utils/modUtils"); +const { canInteract, addModAction } = require("@utils/modUtils"); const { Message } = require("discord.js"); module.exports = class SoftBan extends Command { @@ -47,7 +47,7 @@ module.exports = class SoftBan extends Command { async function softban(message, target, reason) { if (!canInteract(message.member, target, "softban", message.channel)) return; - const status = await softbanTarget(message.member, target, reason); + const status = await addModAction(message.member, target, reason, "SOFTBAN"); if (status) message.channel.send(`${target.user.tag} is soft-banned from this server`); else message.channel.send(`Failed to softban ${target.user.tag}`); } diff --git a/src/commands/moderation/unmute.js b/src/commands/moderation/unmute.js index 3e695c4cd..8bfc62246 100644 --- a/src/commands/moderation/unmute.js +++ b/src/commands/moderation/unmute.js @@ -1,5 +1,5 @@ const { Command } = require("@src/structures"); -const { canInteract, unmuteTarget } = require("@utils/modUtils"); +const { canInteract, addModAction } = require("@utils/modUtils"); const { getRoleByName, resolveMember } = require("@utils/guildUtils"); const { Message } = require("discord.js"); @@ -54,7 +54,7 @@ module.exports = class UnmuteCommand extends Command { async function unmute(message, target, reason) { if (!canInteract(message.member, target, "unmute", message.channel)) return; - const status = await unmuteTarget(message.member, target, reason); + const status = await addModAction(message.member, target, reason, "UNMUTE"); if (status === "NOT_MUTED") return message.channel.send(`${target.user.tag} is not muted`); if (status) message.channel.send(`${target.user.tag} is unmuted`); else message.channel.send(`Failed to unmute ${target.user.tag}`); diff --git a/src/commands/moderation/warn.js b/src/commands/moderation/warn.js new file mode 100644 index 000000000..1fc328b67 --- /dev/null +++ b/src/commands/moderation/warn.js @@ -0,0 +1,69 @@ +const { resolveMember } = require("@root/src/utils/guildUtils"); +const { Command } = require("@src/structures"); +const { canInteract, addModAction } = require("@utils/modUtils"); +const { Message, ContextMenuInteraction } = require("discord.js"); + +module.exports = class Warn extends Command { + constructor(client) { + super(client, { + name: "warn", + description: "warns the specified member(s)", + command: { + enabled: true, + usage: " [reason]", + minArgsCount: 1, + category: "MODERATION", + userPermissions: ["KICK_MEMBERS"], + }, + contextMenu: { + enabled: true, + type: "USER", + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const { content } = message; + const mentions = message.mentions.members; + + // !warn ID + if (mentions.size === 0) { + const target = await resolveMember(message, args[0], true); + if (!target) return message.reply(`No user found matching ${args[0]}`); + const reason = content.split(args[0])[1].trim(); + return warn(message, target, reason); + } + + // !kick @m1 @m2 ... + const regex = /<@!?(\d+)>/g; + const matches = content.match(regex); + const lastMatch = matches[matches.length - 1]; + const reason = content.split(lastMatch)[1].trim(); + + mentions.forEach(async (target) => await warn(message, target, reason)); + } + + /** + * @param {ContextMenuInteraction} interaction + */ + async contextRun(interaction) { + const target = (await interaction.guild.members.fetch(interaction.targetId)) || interaction.member; + if (!canInteract(interaction.member, target, "warn")) { + interaction.followUp("Missing permission to warn this member"); + } + let status = await addModAction(interaction.member, target, "", "WARN"); + if (status) interaction.followUp(`${target.user.tag} is warned by ${interaction.member.user.tag}`); + else interaction.followUp(`Failed to warn ${target.user.tag}`); + } +}; + +async function warn(message, target, reason) { + if (!canInteract(message.member, target, "warn", message.channel)) return; + let status = await addModAction(message.member, target, reason, "WARN"); + if (status) message.channel.send(`${target.user.tag} is warned by ${message.author.tag}`); + else message.channel.send(`Failed to warn ${target.user.tag}`); +} diff --git a/src/commands/music/bassboost.js b/src/commands/music/bassboost.js new file mode 100644 index 000000000..6e1b312c6 --- /dev/null +++ b/src/commands/music/bassboost.js @@ -0,0 +1,37 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Bassboost extends Command { + constructor(client) { + super(client, { + name: "bassboost", + description: "Toggles bassboost", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + + await queue.setFilters({ + bassboost: !queue.getFiltersEnabled().includes("bassboost"), + normalizer2: !queue.getFiltersEnabled().includes("bassboost"), + }); + + const embed = new MessageEmbed().setDescription( + `šŸŽµ | Bassboost ${queue.getFiltersEnabled().includes("bassboost") ? "Enabled | āœ…" : "Disabled | āŒ"}` + ); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/np.js b/src/commands/music/np.js new file mode 100644 index 000000000..259509fe2 --- /dev/null +++ b/src/commands/music/np.js @@ -0,0 +1,35 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Skip extends Command { + constructor(client) { + super(client, { + name: "np", + description: "SHow what is playing currently", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + + const progress = queue.createProgressBar(); + const perc = queue.getPlayerTimestamp(); + + const embed = new MessageEmbed() + .setTitle("Currently Playing") + .setDescription(`šŸŽ¶ | **${queue.current.title}**! (\`${perc.progress == 'Infinity' ? 'Live' : perc.progress + '%'}\`)\n\n${progress.replace(/ 0:00/g, ' ā—‰ LIVE')}`) + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/pause.js b/src/commands/music/pause.js new file mode 100644 index 000000000..aed643506 --- /dev/null +++ b/src/commands/music/pause.js @@ -0,0 +1,32 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Pause extends Command { + constructor(client) { + super(client, { + name: "pause", + description: "Pause the current song", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + + const paused = queue.setPaused(true); + + const embed = new MessageEmbed().setDescription(paused ? "šŸŽµ | Music Paused | āø" : "šŸŽµ | Music Already Paused"); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/play.js b/src/commands/music/play.js new file mode 100644 index 000000000..494ff6c72 --- /dev/null +++ b/src/commands/music/play.js @@ -0,0 +1,58 @@ +const { Command } = require("@src/structures"); +const { QueryType } = require("discord-player"); +const { Message } = require("discord.js"); + +module.exports = class Play extends Command { + constructor(client) { + super(client, { + name: "play", + description: "play a song from youtube", + command: { + enabled: true, + usage: "", + minArgsCount: 1, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const { guild, channel, member } = message; + const query = args.join(" "); + + if (!member.voice.channel) return message.reply("You need to join a voice channel first"); + + let searchResult; + try { + searchResult = await this.client.player.search(query, { + requestedBy: message.author, + searchEngine: QueryType.AUTO, + }); + } catch (ex) { + console.log(ex); + message.channel.send("Failed to fetch results"); + } + + const queue = this.client.player.createQueue(guild, { + metadata: channel, + }); + + try { + if (!queue.connection) await queue.connect(member.voice.channel); + } catch { + this.client.player.deleteQueue(message.guildId); + return message.channel.send("Could not join your voice channel!"); + } + + await message.channel.send(`ā± | Loading your ${searchResult.playlist ? "playlist" : "track"}...`); + searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]); + if (!queue.playing) await queue.play(); + } +}; diff --git a/src/commands/music/queue.js b/src/commands/music/queue.js new file mode 100644 index 000000000..1dc848fbb --- /dev/null +++ b/src/commands/music/queue.js @@ -0,0 +1,42 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Skip extends Command { + constructor(client) { + super(client, { + name: "queue", + description: "Shows the current music queue", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + if (!args[0] || isNaN(args[0])) args[0] = 1; + + const pageStart = 10 * (args[0] - 1); + const pageEnd = pageStart + 10; + const currentTrack = queue.current; + + const tracks = queue.tracks.slice(pageStart, pageEnd).map((m, i) => { + return `${i + pageStart + 1}. **${m.title}** ([link](${m.url}))`; + }); + + const embed = new MessageEmbed() + .setTitle(`Music Queue`) + .setDescription(`${tracks.join('\n')}${queue.tracks.length > pageEnd ? `\n...${queue.tracks.length - pageEnd} more track(s)` : ''}`) + .addField('Now Playing', `šŸŽ¶ | **${currentTrack.title}** ([link](${currentTrack.url}))`); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/resume.js b/src/commands/music/resume.js new file mode 100644 index 000000000..dd0220ebb --- /dev/null +++ b/src/commands/music/resume.js @@ -0,0 +1,32 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Resume extends Command { + constructor(client) { + super(client, { + name: "resume", + description: "Resumes the paused song", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + + const paused = queue.setPaused(false); + + const embed = new MessageEmbed().setDescription(paused ? "šŸŽµ | Music Resumed | ā–¶" : "šŸŽµ | Music not Paused"); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/seek.js b/src/commands/music/seek.js new file mode 100644 index 000000000..f082bbd57 --- /dev/null +++ b/src/commands/music/seek.js @@ -0,0 +1,36 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Seek extends Command { + constructor(client) { + super(client, { + name: "seek", + description: "Seeks to the given time in seconds", + command: { + enabled: true, + usage: "", + minArgsCount: 1, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + if (isNaN(args[0])) return message.channel.send("Input must be in seconds"); + + const time = args[0] * 1000; + await queue.seek(time); + + const embed = new MessageEmbed().setDescription("ā© | Seeked to " + time / 1000 + " seconds"); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/skip.js b/src/commands/music/skip.js new file mode 100644 index 000000000..77f4b069f --- /dev/null +++ b/src/commands/music/skip.js @@ -0,0 +1,35 @@ +const { Command } = require("@src/structures"); +const { MessageEmbed, Message } = require("discord.js"); + +module.exports = class Skip extends Command { + constructor(client) { + super(client, { + name: "skip", + description: "Skip the current song", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = message.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + + const currentTrack = queue.current; + const success = queue.skip(); + + const embed = new MessageEmbed().setDescription( + success ? `ā­ļø | Skipped **${currentTrack}**` : "āŒ | Something went wrong!" + ); + return message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/commands/music/stop.js b/src/commands/music/stop.js new file mode 100644 index 000000000..204a8b22b --- /dev/null +++ b/src/commands/music/stop.js @@ -0,0 +1,29 @@ +const { Command } = require("@src/structures"); +const { Message } = require("discord.js"); + +module.exports = class Stop extends Command { + constructor(client) { + super(client, { + name: "stop", + description: "stop the music player", + command: { + enabled: true, + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const queue = this.client.player.getQueue(message.guildId); + if (!queue || !queue.playing) return message.channel.send("No music is being played!"); + queue.destroy(); + return message.channel.send("Stopped the player!"); + } +}; diff --git a/src/commands/music/volume.js b/src/commands/music/volume.js new file mode 100644 index 000000000..1c4e6ab9a --- /dev/null +++ b/src/commands/music/volume.js @@ -0,0 +1,43 @@ +const { Command } = require("@src/structures"); +const { Message } = require("discord.js"); + +module.exports = class Volume extends Command { + constructor(client) { + super(client, { + name: "volume", + description: "set the music player volume", + command: { + enabled: true, + usage: "<1-100>", + category: "MUSIC", + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + async messageRun(message, args) { + const { channel } = message; + const volume = args[0]; + + const queue = this.client.player.getQueue(message.guild); + if (!queue || !queue.playing) return channel.send("No music currently playing !"); + + if (!volume) return channel.send(`Current volume is \`${queue.volume}\`%!`); + if (isNaN(volume) || volume < 1 || volume > 100) return channel.send("Volume must be a number between 1 and 100!"); + + if (!message.member.voice.channel) return channel.send("You're not in a voice channel !"); + + if (message.guild.me.voice.channel && message.member.voice.channel.id !== message.guild.me.voice.channel.id) + return channel.send("We are not in the same voice channel!"); + + const success = queue.setVolume(volume); + if (success) channel.send(`Volume set to \`${parseInt(volume)}%\` !`); + else channel.send("Failed to set volume"); + } +}; diff --git a/src/commands/utility/help.js b/src/commands/utility/help.js index 0b84322ce..366bf23ac 100644 --- a/src/commands/utility/help.js +++ b/src/commands/utility/help.js @@ -43,6 +43,11 @@ const CMD_CATEGORIES = { image: "https://icons.iconarchive.com/icons/lawyerwordpress/law/128/Gavel-Law-icon.png", emoji: "\uD83D\uDD28", }, + MUSIC: { + name: "Music", + image: "https://icons.iconarchive.com/icons/wwalczyszyn/iwindows/256/Music-Library-icon.png", + emoji: "šŸŽµ", + }, SOCIAL: { name: "Social", image: "https://icons.iconarchive.com/icons/dryicons/aesthetica-2/128/community-users-icon.png", diff --git a/src/commands/utility/paste.js b/src/commands/utility/paste.js new file mode 100644 index 000000000..49ffffd79 --- /dev/null +++ b/src/commands/utility/paste.js @@ -0,0 +1,46 @@ +const fetch = require('node-fetch'); +const { Command } = require("@src/structures"); +const { MessageEmbed } = require("discord.js"); + +module.exports = class CatCommand extends Command { + constructor(client) { + super(client, { + name: "paste", + description: "Paste something in sourceb.in", + cooldown: 5, + command: { + enabled: true, + category: "UTILITY", + botPermissions: ["EMBED_LINKS"], + }, + slashCommand: { + enabled: false, + }, + }); + } + + /** + * @param {Message} message + * @param {string[]} args + */ + + async messageRun(message, args) { + + if (!args || args.length < 3) return message.channel.send("You need to add title, description and content"); + + + const { key } = await fetch("https://sourceb.in/api/bins", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ "title": args[0], "description": args[1], "files": [{ "content": args.splice(2).join(' ') }] }) + }).then(res => res.json()); + + + if (!key) return message.channel.send("āŒ Something went wrong"); + + const embed = new MessageEmbed() + .setTitle("Paste links") + .setDescription("šŸ”ø Normal: https://sourceb.in/" + key + "\nšŸ”¹ Raw: https://cdn.sourceb.in/bins/" + key + "/0") + message.channel.send({ embeds: [embed] }); + } +}; diff --git a/src/events/interactions/interactionCreate.js b/src/events/interactions/interactionCreate.js index 995ad2b93..fcd27742b 100644 --- a/src/events/interactions/interactionCreate.js +++ b/src/events/interactions/interactionCreate.js @@ -32,8 +32,12 @@ module.exports = async (client, interaction) => { await interaction.deferReply({ ephemeral: command.slashCommand.ephemeral }).catch(() => {}); // Run the event - await command.interactionRun(interaction, interaction.options); - command.applyCooldown(interaction.user.id); + try { + await command.interactionRun(interaction, interaction.options); + command.applyCooldown(interaction.user.id); + } catch { + interaction.deferReply("Oops! An error occurred while running the command"); + } } // Context Menu @@ -61,7 +65,11 @@ module.exports = async (client, interaction) => { await interaction.deferReply({ ephemeral: command.contextMenu.ephemeral }).catch(() => {}); // Run the event - await command.contextRun(interaction, interaction.options); - command.applyCooldown(interaction.user.id); + try { + await command.contextRun(interaction, interaction.options); + command.applyCooldown(interaction.user.id); + } catch { + interaction.deferReply("Oops! An error occurred while running the command"); + } } }; diff --git a/src/events/message/messageCreate.js b/src/events/message/messageCreate.js index 19e327754..222c13a56 100644 --- a/src/events/message/messageCreate.js +++ b/src/events/message/messageCreate.js @@ -41,11 +41,13 @@ module.exports = async (client, message) => { * @param {Message} message */ async function xpHandler(message) { - const key = `${message.guild.id}|${message.member.id}`; + const key = `${message.guildId}|${message.member.id}`; + + // Ignore possible bot commands // Cooldown check to prevent Message Spamming if (message.client.xpCooldownCache.has(key)) { - const difference = Date.now() - message.client.xpCooldownCache.get(key) / 0.001; + const difference = Date.now() - message.client.xpCooldownCache.get(key) * 0.001; if (difference < message.client.config.XP_SYSTEM.COOLDOWN) { return incrementMessages(message.guildId, message.member.id); // if on cooldown only increment messages } diff --git a/src/events/ready.js b/src/events/ready.js index d597a89b5..cfac18698 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -1,5 +1,5 @@ const { BotClient } = require("@src/structures"); -const { counterHandler, inviteHandler } = require("@src/handlers"); +const { counterHandler, inviteHandler, musicHandler } = require("@src/handlers"); const { loadReactionRoles } = require("@schemas/reactionrole-schema"); const { getSettings } = require("@schemas/guild-schema"); @@ -9,10 +9,17 @@ const { getSettings } = require("@schemas/guild-schema"); module.exports = async (client) => { console.log(`Logged in as ${client.user.tag}! (${client.user.id})`); + // Update Bot Presence + updatePresence(client); + setInterval(() => updatePresence(client), 10 * 60 * 1000); + // Register Interactions if (client.config.INTERACTIONS.GLOBAL) await client.registerInteractions(); else await client.registerInteractions(client.config.INTERACTIONS.TEST_GUILD_ID); + // register player events + musicHandler.registerPlayerEvents(client); + // Load reaction roles to cache await loadReactionRoles(); @@ -25,3 +32,21 @@ module.exports = async (client) => { if (settings.invite.tracking) inviteHandler.cacheGuildInvites(guild); }); }; + +/** + * @param {BotClient} client + */ +const updatePresence = (client) => { + const guilds = client.guilds.cache; + const members = guilds.map((g) => g.memberCount).reduce((partial_sum, a) => partial_sum + a, 0); + + client.user.setPresence({ + status: "online", + activities: [ + { + name: `${members} members in ${guilds.size} servers`, + type: "WATCHING", + }, + ], + }); +}; diff --git a/src/handlers/automod-handler.js b/src/handlers/automod-handler.js index d96558206..6291fed1e 100644 --- a/src/handlers/automod-handler.js +++ b/src/handlers/automod-handler.js @@ -2,7 +2,7 @@ const { Message, MessageEmbed } = require("discord.js"); const { sendMessage, safeDM } = require("@utils/botUtils"); const { containsLink, containsDiscordInvite } = require("@utils/miscUtils"); const { addStrikes } = require("@schemas/profile-schema"); -const { muteTarget, kickTarget, banTarget } = require("@utils/modUtils"); +const { addModAction } = require("@utils/modUtils"); const { EMBED_COLORS } = require("@root/config"); const Ascii = require("ascii-table"); @@ -140,21 +140,8 @@ async function performAutomod(message, settings) { // check if max strikes are received if (profile.strikes >= automod.strikes) { - const reason = "Automod: Max strikes received"; - - switch (automod.action) { - case "MUTE": - await muteTarget(message.guild.me, message.member, reason); - break; - - case "KICK": - await kickTarget(message.guild.me, message.member, reason); - break; - - case "BAN": - await banTarget(message.guild.me, message.member, reason); - break; - } + // Add Moderation + await addModAction(message.guild.me, message.member, "Automod: Max strikes received", automod.action); // Reset Strikes await addStrikes(message.guildId, message.member.id, -profile.strikes); diff --git a/src/handlers/index.js b/src/handlers/index.js index 0a5ebd756..dea78e2e9 100644 --- a/src/handlers/index.js +++ b/src/handlers/index.js @@ -3,5 +3,6 @@ module.exports = { counterHandler: require("./counter-handler"), greetingHandler: require("./greeting-handler"), inviteHandler: require("./invite-handler"), + musicHandler: require("./music-handler"), reactionHandler: require("./reaction-handler"), }; diff --git a/src/handlers/music-handler.js b/src/handlers/music-handler.js new file mode 100644 index 000000000..2c67cec6b --- /dev/null +++ b/src/handlers/music-handler.js @@ -0,0 +1,38 @@ +const { BotClient } = require("@src/structures"); + +/** + * @param {BotClient} client + */ +function registerPlayerEvents(client) { + client.player.on("error", (queue, error) => { + console.log(`[${queue.guild.name}] Error emitted from the queue: ${error.message}`); + }); + + client.player.on("connectionError", (queue, error) => { + console.log(`[${queue.guild.name}] Error emitted from the connection: ${error.message}`); + }); + + client.player.on("trackStart", (queue, track) => { + queue.metadata.send(`šŸŽ¶ | Started playing: **${track.title}** in **${queue.connection.channel.name}**!`); + }); + + client.player.on("trackAdd", (queue, track) => { + queue.metadata.send(`šŸŽ¶ | Track **${track.title}** queued!`); + }); + + client.player.on("botDisconnect", (queue) => { + queue.metadata.send("āŒ | I was manually disconnected from the voice channel, clearing queue!"); + }); + + client.player.on("channelEmpty", (queue) => { + queue.metadata.send("āŒ | Nobody is in the voice channel, leaving..."); + }); + + client.player.on("queueEnd", (queue) => { + queue.metadata.send("āœ… | Queue finished!"); + }); +} + +module.exports = { + registerPlayerEvents, +}; diff --git a/src/handlers/reaction-handler.js b/src/handlers/reaction-handler.js index 54d89f539..a453d42fc 100644 --- a/src/handlers/reaction-handler.js +++ b/src/handlers/reaction-handler.js @@ -94,13 +94,17 @@ async function handleFlagReaction(emoji, message, user) { let src; let desc = ""; + let translated = 0; for (const tc of targetCodes) { const response = await translate(message.content, tc); - if (!response.success) continue; + if (!response) continue; src = response.inputLang; desc += `**${response.outputLang}:**\n${response.output}\n\n`; + translated += 1; } + if (translated === 0) return; + const head = `Original Message: [here](${message.url})\nSource Language: ${src}\n\n`; const embed = new MessageEmbed() .setColor(message.client.config.EMBED_COLORS.BOT_EMBED) diff --git a/src/schemas/counter-schema.js b/src/schemas/counter-schema.js index 440094d58..fd1f590dd 100644 --- a/src/schemas/counter-schema.js +++ b/src/schemas/counter-schema.js @@ -31,54 +31,73 @@ module.exports = { return config; }, - setTotalCountChannel: async (guildId, channelId, name) => - Model.updateOne( + getCounterGuilds: async () => Model.find().select("_id").lean({ defaults: true }), + + setTotalCountChannel: async (guildId, channelId, name) => { + await Model.updateOne( { _id: guildId }, { - _id: guildId, tc_channel: channelId, tc_name: name, }, { upsert: true } - ).then(cache.remove(guildId)), + ); - setMemberCountChannel: async (guildId, channelId, name) => - Model.updateOne( + if (cache.contains(guildId)) { + const config = cache.get(guildId); + config.tc_channel = channelId; + config.tc_name = name; + } + }, + + setMemberCountChannel: async (guildId, channelId, name) => { + await Model.updateOne( { _id: guildId }, { - _id: guildId, mc_channel: channelId, mc_name: name, }, { upsert: true } - ).then(cache.remove(guildId)), + ); + + if (cache.contains(guildId)) { + const config = cache.get(guildId); + config.mc_channel = channelId; + config.mc_name = name; + } + }, - setBotCountChannel: async (guildId, channelId, name) => - Model.updateOne( + setBotCountChannel: async (guildId, channelId, name) => { + await Model.updateOne( { _id: guildId }, { - _id: guildId, bc_channel: channelId, bc_name: name, }, { upsert: true } - ).then(cache.remove(guildId)), + ); + + if (cache.contains(guildId)) { + const config = cache.get(guildId); + config.bc_channel = channelId; + config.bc_name = name; + } + }, updateBotCount: async (guildId, count, isIncrement = false) => { + // Increment Count if (isIncrement) { - return Model.updateOne( - { _id: guildId }, - { - _id: guildId, - $inc: { bot_count: count }, - }, - { upsert: true } - ).then(cache.remove(guildId)); + await Model.updateOne({ _id: guildId }, { $inc: { bot_count: count } }, { upsert: true }); + if (cache.contains(guildId)) { + cache.get(guildId).bot_count += count; + } + } + // Update Count + else { + await Model.updateOne({ _id: guildId }, { bot_count: count }, { upsert: true }); + if (cache.contains(guildId)) { + cache.get(guildId).bot_count = count; + } } - return Model.updateOne({ _id: guildId }, { _id: guildId, bot_count: count }, { upsert: true }).then( - cache.remove(guildId) - ); }, - - getCounterGuilds: async () => Model.find().select("_id").lean({ defaults: true }), }; diff --git a/src/schemas/guild-schema.js b/src/schemas/guild-schema.js index 3bf972e84..cccbf801a 100644 --- a/src/schemas/guild-schema.js +++ b/src/schemas/guild-schema.js @@ -75,6 +75,14 @@ const Schema = mongoose.Schema({ ], }, modlog_channel: String, + max_warnings: { + type: Number, + default: 5, + }, + max_warn_action: { + type: String, + default: "BAN", + }, }); const Model = mongoose.model("guild", Schema); @@ -111,80 +119,165 @@ module.exports = { return guildData; }, + registerGuild, + + updateGuildLeft: async (guild) => + Model.updateOne({ _id: guild.id }, { "data.leftAt": new Date() }).then(cache.remove(guild.id)), + setPrefix: async (_id, prefix) => { - await Model.updateOne({ _id }, { prefix }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { prefix }); + if (cache.contains(_id)) { + cache.get(_id).prefix = prefix; + } }, xpSystem: async (_id, status) => { - await Model.updateOne({ _id }, { "ranking.enabled": status }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "ranking.enabled": status }); + if (cache.contains(_id)) { + cache.get(_id).ranking.enabled = status; + } }, setTicketLogChannel: async (_id, channelId) => { - await Model.updateOne({ _id }, { "ticket.log_channel": channelId }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "ticket.log_channel": channelId }); + if (cache.contains(_id)) { + cache.get(_id).ticket.log_channel = channelId; + } }, setTicketLimit: async (_id, limit) => { - await Model.updateOne({ _id }, { "ticket.limit": limit }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "ticket.limit": limit }); + if (cache.contains(_id)) { + cache.get(_id).ticket.limit = limit; + } }, - maxStrikes: async (_id, strikes) => Model.updateOne({ _id }, { "automod.strikes": strikes }).then(cache.remove(_id)), + maxStrikes: async (_id, strikes) => { + await Model.updateOne({ _id }, { "automod.strikes": strikes }); + if (cache.contains(_id)) { + cache.get(_id).automod.strikes = strikes; + } + }, - automodAction: async (_id, action) => Model.updateOne({ _id }, { "automod.action": action }).then(cache.remove(_id)), + automodAction: async (_id, action) => { + await Model.updateOne({ _id }, { "automod.action": action }); + if (cache.contains(_id)) { + cache.get(_id).automod.action = action; + } + }, - automodDebug: async (_id, status) => Model.updateOne({ _id }, { "automod.debug": status }).then(cache.remove(_id)), + automodDebug: async (_id, status) => { + await Model.updateOne({ _id }, { "automod.debug": status }); + if (cache.contains(_id)) { + cache.get(_id).automod.debug = status; + } + }, - antiLinks: async (_id, status) => Model.updateOne({ _id }, { "automod.anti_links": status }).then(cache.remove(_id)), + antiLinks: async (_id, status) => { + await Model.updateOne({ _id }, { "automod.anti_links": status }); + if (cache.contains(_id)) { + cache.get(_id).automod.anti_links = status; + } + }, - antiScam: async (_id, status) => Model.updateOne({ _id }, { "automod.anti_scam": status }).then(cache.remove(_id)), + antiScam: async (_id, status) => { + await Model.updateOne({ _id }, { "automod.anti_scam": status }); + if (cache.contains(_id)) { + cache.get(_id).automod.anti_scam = status; + } + }, - antiInvites: async (_id, status) => - Model.updateOne({ _id }, { "automod.anti_invites": status }).then(cache.remove(_id)), + antiInvites: async (_id, status) => { + await Model.updateOne({ _id }, { "automod.anti_invites": status }); + if (cache.contains(_id)) { + cache.get(_id).automod.anti_invites = status; + } + }, - antiGhostPing: async (_id, status) => - Model.updateOne({ _id }, { "automod.anti_ghostping": status }).then(cache.remove(_id)), + antiGhostPing: async (_id, status) => { + await Model.updateOne({ _id }, { "automod.anti_ghostping": status }); + if (cache.contains(_id)) { + cache.get(_id).automod.anti_ghostping = status; + } + }, - maxMentions: async (_id, amount) => - Model.updateOne({ _id }, { "automod.max_mentions": amount }).then(cache.remove(_id)), + maxMentions: async (_id, amount) => { + await Model.updateOne({ _id }, { "automod.max_mentions": amount }); + if (cache.contains(_id)) { + cache.get(_id).automod.max_mentions = amount; + } + }, - maxRoleMentions: async (_id, amount) => - Model.updateOne({ _id }, { "automod.max_role_mentions": amount }).then(cache.remove(_id)), + maxRoleMentions: async (_id, amount) => { + await Model.updateOne({ _id }, { "automod.max_role_mentions": amount }); + if (cache.contains(_id)) { + cache.get(_id).automod.max_role_mentions = amount; + } + }, - maxLines: async (_id, amount) => Model.updateOne({ _id }, { "automod.max_lines": amount }).then(cache.remove(_id)), + maxLines: async (_id, amount) => { + await Model.updateOne({ _id }, { "automod.max_lines": amount }); + if (cache.contains(_id)) { + cache.get(_id).automod.max_lines = amount; + } + }, inviteTracking: async (_id, status) => { - Model.updateOne({ _id }, { $set: { "invite.tracking": status } }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "invite.tracking": status }); + if (cache.contains(_id)) { + cache.get(_id).invite.tracking = status; + } }, - addInviteRank: async (_id, roleId, invites) => - Model.updateOne( - { _id }, - { - $push: { - "invite.ranks": { - _id: roleId, - invites, - }, - }, - } - ).then(cache.remove(_id)), - - removeInviteRank: async (_id, roleId) => - Model.updateOne({ _id }, { $pull: { "invite.ranks": { _id: roleId } } }).then(cache.remove(_id)), + addInviteRank: async (_id, roleId, invites) => { + const toPush = { + _id: roleId, + invites, + }; - registerGuild, + await Model.updateOne({ _id }, { $push: { "invite.ranks": toPush } }); + if (cache.contains(_id)) { + cache.get(_id).invite.ranks.push(toPush); + } + }, - updateGuildLeft: async (guild) => { - await Model.updateOne({ _id: guild.id }, { "data.leftAt": new Date() }).then(cache.remove(guild.id)); + removeInviteRank: async (_id, roleId) => { + await Model.updateOne({ _id }, { $pull: { "invite.ranks": { _id: roleId } } }); + cache.remove(_id); }, flagTranslation: async (_id, status) => { - await Model.updateOne({ _id }, { $set: { "flag_translation.enabled": status } }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "flag_translation.enabled": status }); + if (cache.contains(_id)) { + cache.get(_id).flag_translation.enabled = status; + } }, setFlagTrChannels: async (_id, channels) => { - await Model.updateOne({ _id }, { "flag_translation.channels": channels }).then(cache.remove(_id)); + await Model.updateOne({ _id }, { "flag_translation.channels": channels }); + if (cache.contains(_id)) { + cache.get(_id).flag_translation.channels = channels; + } + }, + + modLogChannel: async (_id, channelId) => { + await Model.updateOne({ _id }, { modlog_channel: channelId }); + if (cache.contains(_id)) { + cache.get(_id).modlog_channel = channelId; + } }, - modLogChannel: async (_id, channelId) => - Model.updateOne({ _id }, { modlog_channel: channelId }).then(cache.remove(_id)), + maxWarnings: async (_id, amount) => { + await Model.updateOne({ _id }, { max_warnings: amount }); + if (cache.contains(_id)) { + cache.get(_id).max_warnings = amount; + } + }, + + maxWarnAction: async (_id, action) => { + await Model.updateOne({ _id }, { max_warn_action: action }); + if (cache.contains(_id)) { + cache.get(_id).max_warn_action = action; + } + }, }; diff --git a/src/schemas/modlog-schema.js b/src/schemas/modlog-schema.js index 43774efa4..4c20079fc 100644 --- a/src/schemas/modlog-schema.js +++ b/src/schemas/modlog-schema.js @@ -82,4 +82,11 @@ module.exports = { }, { "data.current": false } ), + + getWarnings: async (guildId, targetId) => + Model.find({ + guild_id: guildId, + member_id: targetId, + type: "WARN", + }), }; diff --git a/src/schemas/profile-schema.js b/src/schemas/profile-schema.js index cce54815f..f82780a14 100644 --- a/src/schemas/profile-schema.js +++ b/src/schemas/profile-schema.js @@ -24,11 +24,29 @@ const Schema = mongoose.Schema({ type: Number, default: 0, }, + warnings: { + type: Number, + default: 0, + }, }); const Model = mongoose.model("profile", Schema); module.exports = { + getProfile: async (guildId, memberId) => + Model.findOne({ + guild_id: guildId, + member_id: memberId, + }).lean({ defaults: true }), + + getTop100: async (guildId) => + Model.find({ + guild_id: guildId, + }) + .limit(100) + .sort({ level: -1, xp: -1 }) + .lean({ defaults: true }), + incrementXP: async (guildId, memberId, xp) => Model.findOneAndUpdate( { @@ -82,5 +100,18 @@ module.exports = { upsert: true, new: true, } - ), + ).lean({ defaults: true }), + + addWarnings: async (guildId, memberId, warnings) => + Model.findOneAndUpdate( + { + guild_id: guildId, + member_id: memberId, + }, + { $inc: { warnings } }, + { + upsert: true, + new: true, + } + ).lean({ defaults: true }), }; diff --git a/src/schemas/reactionrole-schema.js b/src/schemas/reactionrole-schema.js index adf2c878f..62db2e48e 100644 --- a/src/schemas/reactionrole-schema.js +++ b/src/schemas/reactionrole-schema.js @@ -35,6 +35,8 @@ module.exports = { }); }, + getReactionRole: (guildId, channelId, messageId) => cache.get(getKey(guildId, channelId, messageId)), + addReactionRole: async (guildId, channelId, messageId, emote, roleId) => { const filter = { guild_id: guildId, @@ -70,6 +72,4 @@ module.exports = { await Model.deleteOne(filter); cache.delete(getKey(guildId, channelId, messageId)); }, - - getReactionRole: (guildId, channelId, messageId) => cache.get(getKey(guildId, channelId, messageId)), }; diff --git a/src/structures/BotClient.js b/src/structures/BotClient.js index 9fa16caca..9d6ebb7ae 100644 --- a/src/structures/BotClient.js +++ b/src/structures/BotClient.js @@ -5,6 +5,7 @@ const Ascii = require("ascii-table"); const mongoose = require("mongoose"); const Command = require("./command"); mongoose.plugin(require("mongoose-lean-defaults").default); +const { Player } = require("discord-player"); module.exports = class BotClient extends Client { constructor() { @@ -16,6 +17,7 @@ module.exports = class BotClient extends Client { Intents.FLAGS.GUILD_MEMBERS, Intents.FLAGS.GUILD_PRESENCES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, + Intents.FLAGS.GUILD_VOICE_STATES, ], partials: ["USER", "MESSAGE", "REACTION"], }); @@ -38,6 +40,9 @@ module.exports = class BotClient extends Client { this.joinLeaveWebhook = this.config.JOIN_LEAVE_WEBHOOK ? new WebhookClient({ url: this.config.JOIN_LEAVE_WEBHOOK }) : undefined; + + // Music Player + this.player = new Player(this); } /** diff --git a/src/structures/command.js b/src/structures/command.js index 3f5ea3314..3ab41539f 100644 --- a/src/structures/command.js +++ b/src/structures/command.js @@ -23,7 +23,7 @@ class Command { */ /** - * @typedef {"ADMIN" | "AUTOMOD" | "ECONOMY" | "FUN" | "IMAGE" | "INFORMATION" | "INVITE" | "MODERATION" | "NONE" | "OWNER" | "SOCIAL" | "TICKET" | "UTILITY" } CommandCategory + * @typedef {"ADMIN" | "AUTOMOD" | "ECONOMY" | "FUN" | "IMAGE" | "INFORMATION" | "INVITE" | "MODERATION" | "MUSIC" |"NONE" | "OWNER" | "SOCIAL" | "TICKET" | "UTILITY" } CommandCategory */ /** diff --git a/src/utils/modUtils.js b/src/utils/modUtils.js index a0483a8ba..3270f9b83 100644 --- a/src/utils/modUtils.js +++ b/src/utils/modUtils.js @@ -3,6 +3,7 @@ const { sendMessage } = require("@utils/botUtils"); const { containsLink, timeformat } = require("@utils/miscUtils"); const { addModLogToDb, removeMutes, getMuteInfo } = require("@schemas/modlog-schema"); const { getSettings } = require("@schemas/guild-schema"); +const { addWarnings } = require("@schemas/profile-schema"); const { EMOJIS, EMBED_COLORS } = require("@root/config"); const { getRoleByName } = require("./guildUtils"); @@ -148,6 +149,33 @@ async function purgeMessages(message, type, amount, argument) { } } +/** + * warns the target and logs to the database, channel + * @param {GuildMember} issuer + * @param {GuildMember} target + * @param {string} reason + */ +async function warnTarget(issuer, target, reason) { + const { guild } = issuer; + if (!memberInteract(guild.me, target)) return; + + try { + await logModeration(issuer, target, reason, "Warn"); + const profile = await addWarnings(guild.id, target.id, 1); + const settings = await getSettings(guild); + + // check if max warnings are reached + if (profile.warnings > settings.max_warnings) { + await addModAction(guild.me, target, "Max warnings reached", settings.max_warn_action); // moderate + await addWarnings(guild.id, target.id, -profile.warnings); // reset warnings + } + return true; + } catch (ex) { + console.log(ex); + return false; + } +} + /** * Checks if the target has the muted role * @param {GuildMember} target @@ -341,15 +369,47 @@ async function logModeration(issuer, target, reason, type, data = {}) { sendMessage(logChannel, { embeds: [embed] }); } +/** + * + * @param {GuildMember} issuer + * @param {GuildMember} target + * @param {string} reason + * @param {"WARN"|"MUTE"|"UNMUTE"|"KICK"|"SOFTBAN"|"BAN"} action + */ +async function addModAction(issuer, target, reason, action) { + switch (action) { + case "WARN": + await warnTarget(issuer, target, reason); + break; + + case "MUTE": + await muteTarget(issuer, target, reason); + break; + + case "UNMUTE": + await unmuteTarget(issuer, target, reason); + break; + + case "KICK": + await kickTarget(issuer, target, reason); + break; + + case "SOFTBAN": + await softbanTarget(issuer, target, reason); + break; + + case "BAN": + await banTarget(issuer, target, reason); + break; + } + + return true; +} + module.exports = { memberInteract, canInteract, setupMutedRole, purgeMessages, - muteTarget, - unmuteTarget, - kickTarget, - softbanTarget, - banTarget, - logModeration, + addModAction, };