├── .gitignore ├── src ├── emotes.js ├── events │ ├── node │ │ ├── nodeConnect.js │ │ ├── nodeReconnect.js │ │ ├── nodeDestroy.js │ │ ├── nodeError.js │ │ └── nodeDisconnect.js │ ├── client │ │ ├── ready.js │ │ ├── InteractionCreate.js │ │ └── messageCreate.js │ └── player │ │ ├── trackEnd.js │ │ ├── queueEnd.js │ │ └── trackStart.js ├── moe.js ├── structures │ ├── Event.js │ ├── Logger.js │ ├── Command.js │ ├── Manager.js │ ├── Canvas.js │ ├── Dispatcher.js │ ├── Client.js │ └── Context.js ├── config.js ├── index.js ├── schemas │ └── guild.js ├── commands │ ├── general │ │ └── ping.js │ ├── settings │ │ └── Prefix.js │ ├── moderation │ │ ├── Banlist.js │ │ ├── Ban.js │ │ └── Kick.js │ └── music │ │ └── play.js ├── utils │ └── function.js └── handlers │ └── functions.js ├── .vscode └── settings.json ├── jsconfig.json ├── README.md ├── package.json ├── .eslintrc └── .github └── workflows └── codeql.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /pnpm-lock.yaml -------------------------------------------------------------------------------- /src/emotes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 1: '1️⃣', 3 | 2: '2️⃣', 4 | 3: '3️⃣', 5 | 4: '4️⃣', 6 | 5: '5️⃣', 7 | }; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 3 | "editor.formatOnPaste": true, 4 | "editor.formatOnSave": true, 5 | "[jsonc]": { 6 | "editor.defaultFormatter": "vscode.json-language-features" 7 | } 8 | } -------------------------------------------------------------------------------- /src/events/node/nodeConnect.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | const { Node } = require('shoukaku'); 3 | 4 | module.exports = class NodeConnect extends Event { 5 | constructor(...args) { 6 | super(...args); 7 | } 8 | /** 9 | * 10 | * @param {Node} node 11 | */ 12 | async run(node) { 13 | this.client.logger.ready(`[Node] - Successfully connected to ${node.name}`); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/events/node/nodeReconnect.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | const { Node } = require('shoukaku'); 3 | 4 | module.exports = class NodeReconnect extends Event { 5 | constructor(...args) { 6 | super(...args); 7 | } 8 | /** 9 | * 10 | * @param {Node} node 11 | */ 12 | async run(node) { 13 | this.client.logger.ready(`[Node] - Successfully reconnected to ${node.name}`); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/events/node/nodeDestroy.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | const { Node } = require('shoukaku'); 3 | 4 | module.exports = class NodeDestroy extends Event { 5 | constructor(...args) { 6 | super(...args); 7 | } 8 | /** 9 | * 10 | * @param {Node} node 11 | */ 12 | async run(node, code, reason) { 13 | this.client.logger.warn(`[Node] - ${node.name} destroyed!`, code, reason); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/moe.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const Client = require('@structures/Client'); 3 | 4 | const client = new Client(); 5 | 6 | client.connect(); 7 | 8 | module.exports = client; 9 | 10 | process.on('uncaughtException', (e) => { 11 | client.logger.error(e); 12 | }); 13 | process.on('unhandledRejection', (e) => { 14 | client.logger.error(e); 15 | }); 16 | process.on('warning', (e) => { 17 | client.logger.warn(e); 18 | }); 19 | -------------------------------------------------------------------------------- /src/events/node/nodeError.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | const { Node } = require('shoukaku'); 3 | 4 | module.exports = class NodeError extends Event { 5 | constructor(...args) { 6 | super(...args); 7 | } 8 | /** 9 | * 10 | * @param {Node} node 11 | */ 12 | async run(node, error) { 13 | this.client.logger.error(`[Node] - Encountered an error on node ${node.name}`, error.name, error.message); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@root/*": ["./*"], 8 | "@schemas/*": ["./src/schemas/*"], 9 | "@src/*": ["./src/*"], 10 | "@utils/*": ["./src/utils/*"], 11 | "@handlers/*": ["./src/handlers/*"], 12 | "@structures/*": ["./src/structures/*"] 13 | } 14 | }, 15 | "exclude": ["node_modules", "**/node_modules/*"] 16 | } -------------------------------------------------------------------------------- /src/events/node/nodeDisconnect.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | const { Node, Player } = require('shoukaku'); 3 | 4 | module.exports = class NodeDisconnect extends Event { 5 | constructor(...args) { 6 | super(...args); 7 | } 8 | /** 9 | * 10 | * @param {Node} node 11 | * @param {Player[]} players 12 | */ 13 | async run(node, players) { 14 | this.client.logger.warn(`[Node] - Connection disconnected from ${node.name}`, `Player size [${players.length}]`); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/events/client/ready.js: -------------------------------------------------------------------------------- 1 | const Event = require('@structures/Event'); 2 | 3 | module.exports = class Ready extends Event { 4 | constructor(...args) { 5 | super(...args); 6 | } 7 | async run() { 8 | 9 | this.client.logger.ready('Logged in as ', this.client.user.tag); 10 | 11 | this.client.user.setPresence({ 12 | activities: [ 13 | { 14 | name: '/play', 15 | type: 2, 16 | }, 17 | ], 18 | status: 'online', 19 | }); 20 | 21 | } 22 | }; -------------------------------------------------------------------------------- /src/structures/Event.js: -------------------------------------------------------------------------------- 1 | module.exports = class Event { 2 | /** 3 | * 4 | * @param {import('@structures/Client')} client 5 | * @param {String} file 6 | * @param {String} options 7 | */ 8 | constructor(client, file, options = {}) { 9 | this.client = client; 10 | this.name = options.name || file.name; 11 | this.file = file; 12 | } 13 | async _execute(...args) { 14 | try { 15 | await this.execute(...args); 16 | } 17 | catch (err) { 18 | this.client.logger.error(err); 19 | } 20 | } 21 | 22 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Alpha 4 |
5 | MoE 6 |
7 |

8 | 9 |

MoE is a multipurpose bot for discord server. It has many features, including but not limited to making commands, emotes, music, moderating chat and much more.

10 | 11 |
12 | 13 | 14 | ## working is progress 15 | 16 | You can add your idea in this repo to finish this repo for everyone 17 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | token: 'your bot token', // Put the token 3 | clientId: 'your bot id', // client id 4 | prefix: '.', // Input Of prefix 5 | owners: ['your id'], // place owner ids in array 6 | color: 'BLURPLE', // You can place any color you want to 7 | database: 'mongodb url', // mongodb link 8 | nodes: [ 9 | { 10 | name: 'Alpha Lavalink', // any name can be given 11 | url: 'lavalink url:lavalink port', // lavalink host and port in this format host:port, ex:- alpha.xyz:6969 12 | auth: 'lavalink pass', // the password for your lavalink server 13 | secure: false, // if ssl set it to true 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { Manager } = require('discord-hybrid-sharding'); 2 | require('module-alias/register'); 3 | const signale = require('signale'); 4 | const { token } = require('@src/config'); 5 | 6 | signale.config({ 7 | displayFilename: true, 8 | displayTimestamp: true, 9 | displayDate: false, 10 | }); 11 | 12 | const manager = new Manager('./src/moe.js', { 13 | mode: 'process', 14 | shardsPerClusters: 0, 15 | token, 16 | totalClusters: 'auto', 17 | totalShards: 'auto', 18 | }); 19 | 20 | manager.on('clusterCreate', cluster => { 21 | signale.info(`[ Launched ] Cluster ${cluster.id}`); 22 | }); 23 | 24 | manager.spawn({ timeout: -1 }).catch((err) => signale.error(err)); 25 | -------------------------------------------------------------------------------- /src/schemas/guild.js: -------------------------------------------------------------------------------- 1 | const { model, Schema } = require('mongoose'); 2 | const { prefix } = require('@src/config'); 3 | const GuildSchema = new Schema({ 4 | _id: { type: String, required: true }, 5 | prefix: { type: String, default: prefix }, 6 | language: { type: String, default: 'en-US' }, 7 | welcomeChannel: { type: String, default: null }, 8 | welcomeMessage: { type: String, default: null }, 9 | leaveChannel: { type: String, default: null }, 10 | leaveMessage: { type: String, default: null }, 11 | modLogChannel: { type: String, default: null }, 12 | eventsLogChannel: { type: String, default: null }, 13 | messageLogChannel: { type: String, default: null }, 14 | autoRole: { type: String, default: null }, 15 | }); 16 | module.exports = model('Guild', GuildSchema); -------------------------------------------------------------------------------- /src/events/player/trackEnd.js: -------------------------------------------------------------------------------- 1 | const Dispatcher = require('@root/src/structures/Dispatcher'); 2 | const Event = require('@structures/Event'); 3 | const { TextChannel } = require('discord.js'); 4 | const { Player } = require('shoukaku'); 5 | 6 | module.exports = class TrackEnd extends Event { 7 | constructor(...args) { 8 | super(...args); 9 | } 10 | /** 11 | * 12 | * @param {Player} player 13 | * @param {import('shoukaku').Track} track 14 | * @param {TextChannel} channel 15 | * @param {Dispatcher} dispatcher 16 | */ 17 | async run(player, track, channel, dispatcher) { 18 | if (dispatcher.loop === 'repeat') dispatcher.queue.unshift(track); 19 | if (dispatcher.loop === 'queue') dispatcher.queue.push(track); 20 | await dispatcher.play(); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/events/player/queueEnd.js: -------------------------------------------------------------------------------- 1 | const Dispatcher = require('@root/src/structures/Dispatcher'); 2 | const Event = require('@structures/Event'); 3 | const { TextChannel } = require('discord.js'); 4 | const { Player } = require('shoukaku'); 5 | 6 | module.exports = class QueueEnd extends Event { 7 | constructor(...args) { 8 | super(...args); 9 | } 10 | /** 11 | * 12 | * @param {Player} player 13 | * @param {import('shoukaku').Track} track 14 | * @param {TextChannel} channel 15 | * @param {Dispatcher} dispatcher 16 | */ 17 | async run(player, track, channel, dispatcher) { 18 | dispatcher.destroy(); 19 | if (!dispatcher.queue.length) return await channel.send({ embeds: [this.client.embed().setDescription('No more tracks have been added in queue so i left the voice channel')] }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/commands/general/ping.js: -------------------------------------------------------------------------------- 1 | const Command = require('@structures/Command'); 2 | 3 | module.exports = class Ping extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'ping', 7 | description: { 8 | content: 'Returns the latency of the bot.', 9 | usage: 'ping', 10 | examples: ['ping'], 11 | }, 12 | aliases: ['pong'], 13 | category: 'general', 14 | cooldown: 3, 15 | player: { 16 | voice: false, 17 | dj: false, 18 | active: false, 19 | djPerm: null, 20 | }, 21 | permissions: { 22 | dev: false, 23 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks'], 24 | user: [], 25 | voteRequired: false, 26 | }, 27 | slashCommand: true, 28 | }); 29 | } 30 | async run(ctx, args) { 31 | const msg = await ctx.sendDeferMessage('Pinging...'); 32 | 33 | return await ctx.editMessage(`Latency: \`${msg.createdTimestamp - ctx.createdTimestamp}ms.\` \nAPI Latency: \`${Math.round(ctx.client.ws.ping)}ms.\``); 34 | } 35 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alpha", 3 | "version": "1.0.0", 4 | "description": "this is a advanced discord multipurpose bot made by blacky", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node src/index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/brblacky/alpha+git" 13 | }, 14 | "keywords": [ 15 | "lavalink", 16 | "music", 17 | "bot", 18 | "multipurpose", 19 | "discord-music", 20 | "discord", 21 | "musicbot", 22 | "music-bot", 23 | "music-bot-discord", 24 | "discord.js-v14" 25 | ], 26 | "bugs": "https://github.com/brblacky/alpha/issues", 27 | "homepage": "https://github.com/brblacky/alpha/#readme", 28 | "author": "blacky", 29 | "license": "ISC", 30 | "dependencies": { 31 | "@napi-rs/canvas": "^0.1.29", 32 | "discord-hybrid-sharding": "^1.7.4", 33 | "discord.js": "^14.5.0", 34 | "module-alias": "^2.2.2", 35 | "moment": "^2.29.4", 36 | "moment-duration-format": "^2.3.2", 37 | "mongoose": "^6.6.4", 38 | "shoukaku": "^3.2.0", 39 | "signale": "^1.4.0" 40 | }, 41 | "_moduleAliases": { 42 | "@root": ".", 43 | "@src": "src/", 44 | "@structures": "src/structures/", 45 | "@handlers": "src/handlers/", 46 | "@schemas": "src/schemas/", 47 | "@utils": "src/utils/" 48 | }, 49 | "devDependencies": { 50 | "eslint": "^8.1.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/structures/Logger.js: -------------------------------------------------------------------------------- 1 | const { Signale } = require('signale'); 2 | 3 | module.exports = class Logger extends Signale { 4 | constructor(config, client) { 5 | super({ 6 | config: config, 7 | types: { 8 | info: { 9 | badge: 'ℹ', 10 | color: 'blue', 11 | label: 'info', 12 | }, 13 | warn: { 14 | badge: '⚠', 15 | color: 'yellow', 16 | label: 'warn', 17 | }, 18 | error: { 19 | badge: '✖', 20 | color: 'red', 21 | label: 'error', 22 | }, 23 | debug: { 24 | badge: '🐛', 25 | color: 'magenta', 26 | label: 'debug', 27 | }, 28 | cmd: { 29 | badge: '⌨️', 30 | color: 'green', 31 | label: 'cmd', 32 | }, 33 | event: { 34 | badge: '🎫', 35 | color: 'cyan', 36 | label: 'event', 37 | }, 38 | ready: { 39 | badge: '✔️', 40 | color: 'green', 41 | label: 'ready', 42 | }, 43 | }, 44 | scope: (client ? `Shard ${('00' + client.shard.ids[0]).slice(-2)}` : 'Manager'), 45 | }); 46 | } 47 | }; -------------------------------------------------------------------------------- /src/commands/settings/Prefix.js: -------------------------------------------------------------------------------- 1 | const Command = require('@structures/Command'); 2 | const guildDb = require('@schemas/guild'); 3 | 4 | module.exports = class Prefix extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'prefix', 8 | description: { 9 | content: 'Sets the prefix for the bot.', 10 | usage: 'prefix ', 11 | examples: ['prefix !'], 12 | }, 13 | aliases: ['setprefix'], 14 | category: 'settings', 15 | cooldown: 3, 16 | player: { 17 | voice: false, 18 | dj: false, 19 | active: false, 20 | djPerm: null, 21 | }, 22 | permissions: { 23 | dev: false, 24 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks'], 25 | user: ['ManageGuild'], 26 | voteRequired: false, 27 | }, 28 | slashCommand: true, 29 | options: [ 30 | { 31 | name: 'prefix', 32 | description: 'The prefix to set.', 33 | type: 3, 34 | maxLength: 20, 35 | required: false, 36 | }, 37 | ], 38 | }); 39 | } 40 | async run(ctx, args) { 41 | const data = await guildDb.findOne({ _id: ctx.guild.id }); 42 | if (!args[0]) { 43 | return await ctx.sendMessage(`The current prefix is \`${data.prefix}\`.`); 44 | } 45 | const pref = args[0].replace(/_/g, ''); 46 | if (pref.length > 20) return await ctx.sendMessage('The prefix can\'t be longer than 20 characters.'); 47 | await data.updateOne({ prefix: pref }); 48 | return await ctx.sendMessage(`successfully set the prefix to \`${pref}\`.`); 49 | } 50 | }; -------------------------------------------------------------------------------- /src/commands/moderation/Banlist.js: -------------------------------------------------------------------------------- 1 | const Command = require('@structures/Command'); 2 | module.exports = class Banlist extends Command { 3 | constructor(client) { 4 | super(client, { 5 | name: 'banlist', 6 | description: { 7 | content: 'Returns the list of banned users in the server.', 8 | usage: 'banlist', 9 | examples: ['banlist'], 10 | }, 11 | category: 'moderation', 12 | cooldown: 3, 13 | player: { 14 | voice: false, 15 | dj: false, 16 | active: false, 17 | djPerm: null, 18 | }, 19 | permissions: { 20 | dev: false, 21 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks', 'BanMembers'], 22 | user: ['BanMembers'], 23 | voteRequired: false, 24 | }, 25 | slashCommand: true, 26 | }); 27 | } 28 | /** 29 | * @param {import('discord.js').Message} ctx 30 | * @param {string[]} args 31 | */ 32 | async run(ctx, args) { 33 | const bans = await ctx.guild.bans.fetch(); 34 | if (!bans.size) return await ctx.sendMessage('There are no banned users in this server.'); 35 | let pagesNum = Math.ceil(bans.size / 10); 36 | if (pagesNum === 0) pagesNum = 1; 37 | const list = bans.map((b) => `**${b.user.tag}** ・**Reason:** ${b.reason || 'No reason'}`); 38 | const pages = []; 39 | for (let i = 0; i < pagesNum; i++) { 40 | const str = list.slice(i * 10, i * 10 + 10).join('\n'); 41 | const embed = this.client.embed() 42 | .setTitle(`Banned Users in ${ctx.guild.name}`) 43 | .setDescription(str) 44 | .setFooter({ text: `Page ${i + 1} of ${pagesNum}` }) 45 | .setColor(this.client.config.color); 46 | pages.push(embed); 47 | } 48 | await ctx.paginate(ctx, pages); 49 | } 50 | }; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2019 9 | }, 10 | 11 | "rules": { 12 | "brace-style": [ 13 | "off", 14 | "stroustrup", 15 | { 16 | "allowSingleLine": true 17 | } 18 | ], 19 | "comma-dangle": [ 20 | "error", 21 | "always-multiline" 22 | ], 23 | "comma-spacing": "error", 24 | "comma-style": "error", 25 | "dot-location": [ 26 | "error", 27 | "property" 28 | ], 29 | "handle-callback-err": "off", 30 | "max-nested-callbacks": [ 31 | "error", 32 | { 33 | "max": 4 34 | } 35 | ], 36 | "max-statements-per-line": [ 37 | "error", 38 | { 39 | "max": 2 40 | } 41 | ], 42 | "no-unused-vars": "off", 43 | "no-console": "off", 44 | "no-empty-function": "error", 45 | "no-floating-decimal": "error", 46 | "no-lonely-if": "error", 47 | "no-multi-spaces": "error", 48 | "no-multiple-empty-lines": [ 49 | "error", 50 | { 51 | "max": 2, 52 | "maxEOF": 1, 53 | "maxBOF": 0 54 | } 55 | ], 56 | "no-shadow": [ 57 | "error", 58 | { 59 | "allow": [ 60 | "err", 61 | "resolve", 62 | "reject" 63 | ] 64 | } 65 | ], 66 | "no-trailing-spaces": [ 67 | "error" 68 | ], 69 | "no-var": "error", 70 | "object-curly-spacing": [ 71 | "error", 72 | "always" 73 | ], 74 | "prefer-const": "error", 75 | "quotes": [ 76 | "error", 77 | "single" 78 | ], 79 | "semi": [ 80 | "error", 81 | "always" 82 | ], 83 | "space-before-blocks": "error", 84 | "space-before-function-paren": [ 85 | "error", 86 | { 87 | "anonymous": "never", 88 | "named": "never", 89 | "asyncArrow": "always" 90 | } 91 | ], 92 | "space-in-parens": "error", 93 | "space-infix-ops": "error", 94 | "space-unary-ops": "error", 95 | "spaced-comment": "error", 96 | "yoda": "error", 97 | "no-case-declarations" : "off", 98 | "no-unsafe-option-chaining": "off" 99 | } 100 | } -------------------------------------------------------------------------------- /src/structures/Command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} CommandOptions Class Properties 3 | * @property {string} name Name for the command 4 | * @property {{ content: string; usage: string; examples: Array}} description A description with three more properties for the command 5 | * @property {?Array} aliases A array of aliases for the command 6 | * @property {?number} cooldown The cooldown for the command 7 | * @property {?{ voice: boolean; dj: boolean; active: boolean; djPerm: any }} player Dispatcher checks 8 | * @property {?{ dev: boolean; client: import('discord.js').PermissionResolvable; user: import('discord.js').PermissionResolvable; voteRequired: boolean; }} permissions Permission Resolves 9 | * @property {?boolean} slashCommand To specify if it's a slash command 10 | * @property {?import('discord.js').ApplicationCommandOption} options Slash Command options 11 | * @property {?string} category The category the command belongs to 12 | */ 13 | 14 | module.exports = class Command { 15 | /** 16 | * 17 | * @param {import('@structures/Client')} client 18 | * @param {CommandOptions} options 19 | */ 20 | constructor(client, options) { 21 | /** 22 | * @type {import('@structures/Client')} Extended Client 23 | */ 24 | this.client = client; 25 | this.name = options.name; 26 | this.description = { 27 | content: options.description ? (options.description.content || 'No description provided') : 'No description provided', 28 | usage: options.description ? (options.description.usage || 'No usage provided') : 'No usage provided', 29 | examples: options.description ? (options.description.examples || 'No examples provided') : 'No examples provided', 30 | }; 31 | this.aliases = options.aliases || 'N/A'; 32 | this.cooldown = options.cooldown || 3; 33 | this.player = { 34 | voice: options.player ? (options.player.voice || false) : false, 35 | dj: options.player ? (options.player.dj || false) : false, 36 | active: options.player ? (options.player.active || false) : false, 37 | djPerm: options.player ? (options.player.djPerm || null) : null, 38 | }; 39 | this.permissions = { 40 | dev: options.permissions ? (options.permissions.dev || false) : false, 41 | client: options.permissions ? (options.permissions.client || []) : ['SendMessages', 'ViewChannel', 'EmbedLinks'], 42 | user: options.permissions ? (options.permissions.user || []) : [], 43 | voteRequired: options.permissions ? (options.permissions.voteRequired || false) : false, 44 | }; 45 | this.slashCommand = options.slashCommand || false; 46 | this.options = options.options || []; 47 | this.category = options.category || 'general'; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/commands/moderation/Ban.js: -------------------------------------------------------------------------------- 1 | const Command = require('@structures/Command'); 2 | 3 | module.exports = class Ban extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'ban', 7 | description: { 8 | content: 'Bans a user from the server.', 9 | usage: 'ban [reason]', 10 | examples: ['ban @user', 'ban @user Spamming'], 11 | }, 12 | aliases: ['banish'], 13 | category: 'moderation', 14 | cooldown: 3, 15 | player: { 16 | voice: false, 17 | dj: false, 18 | active: false, 19 | djPerm: null, 20 | }, 21 | permissions: { 22 | dev: false, 23 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks', 'BanMembers'], 24 | user: ['BanMembers'], 25 | voteRequired: false, 26 | }, 27 | slashCommand: true, 28 | options: [ 29 | { 30 | name: 'user', 31 | description: 'The user to ban.', 32 | type: 6, 33 | required: true, 34 | }, 35 | { 36 | name: 'reason', 37 | description: 'The reason for the ban.', 38 | type: 3, 39 | required: false, 40 | }, 41 | ], 42 | }); 43 | } 44 | /** 45 | * @param {import('@structures/Context')} ctx 46 | * @param {string[]} args 47 | */ 48 | async run(ctx, args) { 49 | let user; 50 | if (ctx.interaction) user = ctx.interaction.options.getUser('user'); 51 | else user = ctx.message.mentions.members.first() || ctx.guild.members.cache.get(args[0]); 52 | 53 | if (!user) return await ctx.sendMessage('Please provide a valid user.'); 54 | if (user.id === ctx.author.id) return await ctx.sendMessage('You can\'t ban yourself.'); 55 | if (user.id === ctx.client.user.id) return await ctx.sendMessage('You can\'t ban me.'); 56 | if (user.id === ctx.guild.ownerId) return await ctx.sendMessage('You can\'t ban the owner.'); 57 | if (ctx.member.roles.highest.position < user.roles.highest.position) return await ctx.sendMessage('You can\'t ban that user! He/She has a higher role than yours.'); 58 | if (user.roles.highest.position >= ctx.guild.members.me.roles.highest.position) return await ctx.sendMessage('I can\'t ban that user! He/She has a higher role than mine.'); 59 | if (user.id === this.client.user.id) return await ctx.sendMessage('Please don\'t ban me!'); 60 | 61 | if (user.bannable) { 62 | const reason = args[1] || 'No reason provided.'; 63 | await user.ban({ reason: reason }); 64 | return await ctx.sendMessage(`Successfully banned \`${user.user.tag}\`.`); 65 | } else { 66 | return await ctx.sendMessage('I can\'t ban that user.'); 67 | } 68 | } 69 | }; -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '20 18 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /src/utils/function.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chunk: function(array, size) { 3 | if (!Array.isArray(array)) throw new Error('array must be an array'); 4 | if (typeof size !== 'number') throw new Error('size must be a number'); 5 | const chunked_arr = []; 6 | for (let i = 0; i < array.length; i += size) { 7 | chunked_arr.push(array.slice(i, i + size)); 8 | } 9 | return chunked_arr; 10 | }, 11 | formatDuration: function formatTime(milliseconds, minimal = false) { 12 | if (typeof milliseconds === 'undefined' || isNaN(milliseconds)) { 13 | throw new RangeError( 14 | 'formatDuration#formatTime() Milliseconds must be a number', 15 | ); 16 | } 17 | if (typeof minimal !== 'boolean') { 18 | throw new RangeError( 19 | 'formatDuration#formatTime() Minimal must be a boolean', 20 | ); 21 | } 22 | if (milliseconds === 0) { 23 | return minimal ? '00:00' : 'N/A'; 24 | } 25 | const times = { 26 | years: 0, 27 | months: 0, 28 | weeks: 0, 29 | days: 0, 30 | hours: 0, 31 | minutes: 0, 32 | seconds: 0, 33 | }; 34 | while (milliseconds > 0) { 35 | if (milliseconds - 31557600000 >= 0) { 36 | milliseconds -= 31557600000; 37 | times.years++; 38 | } else if (milliseconds - 2628000000 >= 0) { 39 | milliseconds -= 2628000000; 40 | times.months++; 41 | } else if (milliseconds - 604800000 >= 0) { 42 | milliseconds -= 604800000; 43 | times.weeks += 7; 44 | } else if (milliseconds - 86400000 >= 0) { 45 | milliseconds -= 86400000; 46 | times.days++; 47 | } else if (milliseconds - 3600000 >= 0) { 48 | milliseconds -= 3600000; 49 | times.hours++; 50 | } else if (milliseconds - 60000 >= 0) { 51 | milliseconds -= 60000; 52 | times.minutes++; 53 | } else { 54 | times.seconds = Math.round(milliseconds / 1000); 55 | milliseconds = 0; 56 | } 57 | } 58 | const finalTime = []; 59 | let first = false; 60 | // eslint-disable-next-line id-length 61 | for (const [k, v] of Object.entries(times)) { 62 | if (minimal) { 63 | if (v === 0 && !first) { 64 | continue; 65 | } 66 | finalTime.push(v < 10 ? `0${v}` : `${v}`); 67 | first = true; 68 | continue; 69 | } 70 | if (v > 0) { 71 | finalTime.push(`${v} ${v > 1 ? k : k.slice(0, -1)}`); 72 | } 73 | } 74 | if (minimal && finalTime.length === 1) { 75 | finalTime.unshift('00'); 76 | } 77 | let time = finalTime.join(minimal ? ':' : ', '); 78 | if (time.includes(',')) { 79 | const pos = time.lastIndexOf(','); 80 | time = `${time.slice(0, pos)} and ${time.slice(pos + 1)}`; 81 | } 82 | return time; 83 | }, 84 | shuffle: function(array) { 85 | let currentIndex = array.length, randomIndex; 86 | 87 | while (currentIndex != 0) { 88 | randomIndex = Math.floor(Math.random() * currentIndex); 89 | currentIndex--; 90 | 91 | [array[currentIndex], array[randomIndex]] = [ 92 | array[randomIndex], array[currentIndex]]; 93 | } 94 | 95 | return array; 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /src/commands/moderation/Kick.js: -------------------------------------------------------------------------------- 1 | const Command = require('@structures/Command'); 2 | 3 | module.exports = class Kick extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'kick', 7 | description: { 8 | content: 'Kicks a user from the server.', 9 | usage: 'Kick [reason]', 10 | examples: ['Kick @user', 'Kick @user Spamming'], 11 | }, 12 | aliases: ['kickout'], 13 | category: 'moderation', 14 | cooldown: 3, 15 | player: { 16 | voice: false, 17 | dj: false, 18 | active: false, 19 | djPerm: null, 20 | }, 21 | permissions: { 22 | dev: false, 23 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks', 'KickMembers'], 24 | user: ['KickMembers'], 25 | voteRequired: false, 26 | }, 27 | slashCommand: true, 28 | options: [ 29 | { 30 | name: 'user', 31 | description: 'The user to kick.', 32 | type: 6, 33 | required: true, 34 | }, 35 | { 36 | name: 'reason', 37 | description: 'The reason for kicking out.', 38 | type: 3, 39 | required: false, 40 | }, 41 | ], 42 | }); 43 | } 44 | /** 45 | * @param {import('@structures/Context')} ctx 46 | * @param {string[]} args 47 | */ 48 | async run(ctx, args) { 49 | let user; 50 | if (ctx.interaction) user = ctx.interaction.options.getUser('user'); 51 | else user = ctx.message.mentions.members.first() || ctx.guild.members.cache.get(args[0]); 52 | let reason; 53 | if(ctx.interaction) reason = ctx.interaction.options.getString("reason") ||'No reason provided.'; 54 | else reason = args.slice(1).join(" ") || 'No reason provided.' ; 55 | 56 | if (!user) return await ctx.sendMessage('Please provide a valid user.'); 57 | if (user.id === ctx.author.id) return await ctx.sendMessage('You can\'t kick yourself.'); 58 | if (user.id === ctx.client.user.id) return await ctx.sendMessage('You can\'t kick me.'); 59 | if (user.id === ctx.guild.ownerId) return await ctx.sendMessage('You can\'t kick the owner.'); 60 | if (ctx.member.roles.highest.position < user.roles.highest.position) return await ctx.sendMessage('You can\'t kick that user! He/She has a higher role than yours.'); 61 | if (user.roles.highest.position >= ctx.guild.members.me.roles.highest.position) return await ctx.sendMessage('I can\'t kick that user! He/She has a higher role than mine.'); 62 | if (user.id === this.client.user.id) return await ctx.sendMessage('Please don\'t kick me!'); 63 | 64 | if (user.kickable) { 65 | await user.kick({ reason: reason }); 66 | return await ctx.sendMessage(`Successfully kicked \`${user.user.tag}\`.`); 67 | } else { 68 | return await ctx.sendMessage('I can\'t kick that user.'); 69 | } 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/structures/Manager.js: -------------------------------------------------------------------------------- 1 | const { Collection, Guild, GuildMember, TextChannel } = require('discord.js'); 2 | const { Shoukaku, Connectors, Node } = require('shoukaku'); 3 | const Moe = require('@structures/Client'); 4 | const Dispatcher = require('@structures/Dispatcher'); 5 | const { EventEmitter } = require('events'); 6 | 7 | class Manager extends EventEmitter { 8 | /** 9 | * 10 | * @param {Moe} client 11 | */ 12 | constructor(client) { 13 | super(); 14 | /** 15 | * @type {Moe} 16 | */ 17 | this.client = client; 18 | /** 19 | * @type {Shoukaku} 20 | */ 21 | this.shoukaku = new Shoukaku( 22 | new Connectors.DiscordJS(this.client), 23 | this.client.config.nodes, 24 | { 25 | moveOnDisconnect: false, 26 | resumable: false, 27 | resumableTimeout: 30, 28 | reconnectTries: 2, 29 | restTimeout: 10000, 30 | }, 31 | ); 32 | 33 | /** 34 | * @type {Collection} 35 | */ 36 | this.players = new Collection(); 37 | 38 | this.shoukaku.on('ready', (name, resumed) => 39 | this.emit( 40 | resumed ? 'nodeReconnect' : 'nodeConnect', 41 | this.shoukaku.getNode(name), 42 | ), 43 | ); 44 | 45 | this.shoukaku.on('error', (name, error) => 46 | this.emit('nodeError', this.shoukaku.getNode(name), error), 47 | ); 48 | 49 | this.shoukaku.on('close', (name, code, reason) => 50 | this.emit('nodeDestroy', this.shoukaku.getNode(name), code, reason), 51 | ); 52 | 53 | this.shoukaku.on('disconnect', (name, players, moved) => { 54 | if (moved) this.emit('playerMove', players); 55 | this.emit('nodeDisconnect', this.shoukaku.getNode(name), players); 56 | }); 57 | 58 | this.shoukaku.on('debug', (name, reason) => 59 | this.emit('nodeRaw', name, reason), 60 | ); 61 | } 62 | /** 63 | * 64 | * @param {string} guildId Guild ID 65 | * @returns {Dispatcher} 66 | */ 67 | getPlayer(guildId) { 68 | return this.players.get(guildId); 69 | } 70 | /** 71 | * 72 | * @param {Guild} guild Guild 73 | * @param {GuildMember} member Member 74 | * @param {TextChannel} channel Channel 75 | * @param {Node} givenNode Node 76 | * @returns {Promise} 77 | */ 78 | async spawn(guild, member, channel, givenNode) { 79 | const existing = this.getPlayer(guild.id); 80 | 81 | if (existing) return existing; 82 | 83 | const node = givenNode || this.shoukaku.getNode(); 84 | 85 | const player = await node.joinChannel({ 86 | guildId: guild.id, 87 | shardId: guild.shardId, 88 | channelId: member.voice.channelId, 89 | deaf: true, 90 | }); 91 | 92 | const dispatcher = new Dispatcher(this.client, guild, channel, player, member.user); 93 | 94 | this.emit('playerCreate', dispatcher.player); 95 | 96 | this.players.set(guild.id, dispatcher); 97 | 98 | return dispatcher; 99 | } 100 | 101 | /** 102 | * 103 | * @param {string} query 104 | * @returns {Promise} 105 | */ 106 | async search(query) { 107 | const node = await this.shoukaku.getNode(); 108 | 109 | /** 110 | * @type {import('shoukaku').LavalinkResponse} 111 | */ 112 | let result; 113 | try { 114 | result = await node.rest.resolve(query); 115 | } catch (err) { 116 | return null; 117 | } 118 | 119 | return result; 120 | } 121 | } 122 | 123 | module.exports = Manager; 124 | -------------------------------------------------------------------------------- /src/structures/Canvas.js: -------------------------------------------------------------------------------- 1 | const nodeCanvas = require('@napi-rs/canvas'); 2 | const moment = require('moment'); 3 | const momentDurationFormatSetup = require('moment-duration-format'); 4 | const Alpha = require('./Client'); 5 | momentDurationFormatSetup(moment); 6 | 7 | module.exports = class Canvas { 8 | /** 9 | * 10 | * @param {Alpha} client 11 | */ 12 | constructor(client) { 13 | /** 14 | * @type {Alpha} 15 | */ 16 | this.client = client; 17 | this.background = { 18 | type: 0, 19 | data: '#2F3136', 20 | }; 21 | } 22 | async buildPlayerCard( 23 | image, 24 | artist, 25 | title, 26 | end, 27 | start, 28 | background = this.background, 29 | ) { 30 | const canvas = nodeCanvas.createCanvas(600, 150); 31 | const ctx = canvas.getContext('2d'); 32 | 33 | const total = end - start; 34 | const progress = Date.now() - this.start; 35 | const progressF = formatTime(progress > total ? total : progress); 36 | const ending = formatTime(total); 37 | 38 | ctx.beginPath(); 39 | if (background.type === 0) { 40 | ctx.rect(0, 0, canvas.width, canvas.height); 41 | ctx.fillStyle = '#2F3136'; 42 | ctx.fillRect(0, 0, canvas.width, canvas.height); 43 | } else { 44 | const img = await nodeCanvas.loadImage(background.data); 45 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 46 | } 47 | 48 | // draw image 49 | const img = await nodeCanvas.loadImage(image); 50 | ctx.drawImage(img, 30, 15, 120, 120); 51 | ctx.arc(125, 125, 100, 0, Math.PI * 2, true); 52 | 53 | // title 54 | ctx.fillStyle = '#FFFFFF'; 55 | ctx.font = 'bold 20px MANROPE_BOLD,NOTO_COLOR_EMOJI'; 56 | await renderEmoji(ctx, shorten(title, 30), 170, 40); 57 | 58 | // artist 59 | ctx.fillStyle = '#F1F1F1'; 60 | ctx.font = '14px MANROPE_REGULAR,NOTO_COLOR_EMOJI'; 61 | await renderEmoji(ctx, `by ${shorten(artist, 40)}`, 170, 70); 62 | 63 | // ending point 64 | ctx.fillStyle = '#B3B3B3'; 65 | ctx.font = '14px MANROPE_REGULAR,NOTO_COLOR_EMOJI'; 66 | await renderEmoji(ctx, ending, 430, 130); 67 | 68 | // progress 69 | ctx.fillStyle = '#B3B3B3'; 70 | ctx.font = '14px MANROPE_REGULAR,NOTO_COLOR_EMOJI'; 71 | await renderEmoji(ctx, progressF, 170, 130); 72 | 73 | // progressbar track 74 | ctx.rect(170, 170, 300, 4); 75 | ctx.fillStyle = '#E8E8E8'; 76 | ctx.fillRect(170, 110, 300, 4); 77 | 78 | // progressbar 79 | ctx.fillStyle = '#1DB954'; 80 | ctx.fillRect(170, 110, this.__calculateProgress(progress, total), 4); 81 | 82 | return canvas.encode('png'); 83 | } 84 | __calculateProgress(progress, total) { 85 | const prg = (progress / total) * 300; 86 | if (isNaN(prg) || prg < 0) return 0; 87 | if (prg > 300) return 300; 88 | return prg; 89 | } 90 | }; 91 | 92 | function shorten(text, len) { 93 | if (typeof text !== 'string') return ''; 94 | if (text.length <= len) return text; 95 | return text.substr(0, len).trim() + '...'; 96 | } 97 | 98 | /** 99 | * 100 | * @param {import('@napi-rs/canvas').SKRSContext2D} ctx 101 | * @param {string} message 102 | * @param {number} x 103 | * @param {number} y 104 | * @param {number} maxWidth 105 | */ 106 | function renderEmoji(ctx, message, x, y) { 107 | return ctx.fillText(message, x, y); 108 | } 109 | 110 | function formatTime(time) { 111 | if (!time) return '00:00'; 112 | const fmt = moment.duration(time).format('dd:hh:mm:ss'); 113 | 114 | const chunk = fmt.split(':'); 115 | if (chunk.length < 2) chunk.unshift('00'); 116 | return chunk.join(':'); 117 | } 118 | -------------------------------------------------------------------------------- /src/structures/Dispatcher.js: -------------------------------------------------------------------------------- 1 | const { Guild, TextChannel, User } = require('discord.js'); 2 | const Moe = require('@structures/Client'); 3 | const { Player } = require('shoukaku'); 4 | const { EventEmitter } = require('events'); 5 | 6 | class Dispatcher extends EventEmitter { 7 | /** 8 | * 9 | * @param {Moe} client 10 | * @param {Guild} guild 11 | * @param {TextChannel} channel 12 | * @param {Player} player 13 | * @param {User} user 14 | */ 15 | constructor(client, guild, channel, player, user) { 16 | super(); 17 | /** 18 | * @type {Moe} 19 | */ 20 | this.client = client; 21 | /** 22 | * @type {Guild} 23 | */ 24 | this.guild = guild; 25 | /** 26 | * @type {TextChannel} 27 | */ 28 | this.channel = channel; 29 | /** 30 | * @type {Player} 31 | */ 32 | this.player = player; 33 | /** 34 | * @type {Array} 35 | */ 36 | this.queue = []; 37 | /** 38 | * @type {boolean} 39 | */ 40 | this.stopped = false; 41 | /** 42 | * @type {import('shoukaku').Track} 43 | */ 44 | this.current = null; 45 | /** 46 | * @type {'off'|'repeat'|'queue'} 47 | */ 48 | this.loop = 'off'; 49 | /** 50 | * @type {import('shoukaku').Track[]} 51 | */ 52 | this.matchedTracks = []; 53 | /** 54 | * @type {User} 55 | */ 56 | this.requester = user; 57 | 58 | this.player 59 | .on('start', (data) => 60 | this.manager.emit('trackStart', this.player, this.current, this.channel, this.matchedTracks, this, data), 61 | ) 62 | .on('end', (data) => { 63 | if (!this.queue.length) this.manager.emit('queueEnd', this.player, this.current, this.channel, this, data); 64 | this.manager.emit('trackEnd', this.player, this.current, this.channel, this, data); 65 | }) 66 | .on('stuck', (data) => 67 | this.manager.emit('trackStuck', this.player, this.current, data), 68 | ) 69 | .on('error', (...arr) => { 70 | this.manager.emit('trackError', this.player, this.current, ...arr); 71 | this._errorHandler(...arr); 72 | }) 73 | .on('closed', (...arr) => { 74 | this.manager.emit('socketClosed', this.player, ...arr); 75 | this._errorHandler(...arr); 76 | }); 77 | } 78 | 79 | get manager() { 80 | return this.client.manager; 81 | } 82 | 83 | /** 84 | * 85 | * @param {Error} data 86 | */ 87 | _errorHandler(data) { 88 | if (data instanceof Error || data instanceof Object) { 89 | this.client.logger.error(data); 90 | } 91 | this.queue.length = 0; 92 | this.destroy('error'); 93 | } 94 | 95 | get exists() { 96 | return this.manager.players.has(this.guild.id); 97 | } 98 | 99 | async play() { 100 | if (!this.exists || (!this.queue.length && !this.current)) { 101 | this.destroy(); 102 | return; 103 | } 104 | this.current = this.queue.length !== 0 ? this.queue.shift() : this.queue[0]; 105 | if (this.matchedTracks.length !== 0) this.matchedTracks = []; 106 | const search = await this.manager.search(`ytsearch:${this.current.info.title}`); 107 | this.matchedTracks.push(...search.tracks.slice(0, 11)); 108 | this.player.playTrack({ track: this.current.track }); 109 | } 110 | 111 | destroy() { 112 | this.queue.length = 0; 113 | this.player.connection.disconnect(); 114 | this.manager.players.delete(this.guild.id); 115 | if (this.stopped) return; 116 | this.manager.emit('playerDestroy', this.player); 117 | } 118 | 119 | /** 120 | * 121 | * @param {import('shoukaku').Track} providedTrack 122 | * @returns {string} 123 | */ 124 | displayThumbnail(providedTrack) { 125 | const track = providedTrack || this.current; 126 | return track.info.uri.includes('youtube') 127 | ? `https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg` 128 | : null; 129 | } 130 | 131 | async check() { 132 | if (this.queue.length && !this.current && !this.player.paused) { 133 | this.play(); 134 | } 135 | } 136 | } 137 | 138 | module.exports = Dispatcher; 139 | -------------------------------------------------------------------------------- /src/events/player/trackStart.js: -------------------------------------------------------------------------------- 1 | const Dispatcher = require('@root/src/structures/Dispatcher'); 2 | const { formatDuration, shuffle } = require('@root/src/utils/function'); 3 | const Event = require('@structures/Event'); 4 | const { TextChannel, AttachmentBuilder, ButtonStyle, Message, User, SelectMenuComponent, ButtonComponent, ActionRow } = require('discord.js'); 5 | const { Player } = require('shoukaku'); 6 | 7 | module.exports = class TrackStart extends Event { 8 | constructor(...args) { 9 | super(...args); 10 | } 11 | /** 12 | * 13 | * @param {Player} player 14 | * @param {import('shoukaku').Track} track 15 | * @param {TextChannel} channel 16 | * @param {import('shoukaku').Track[]} matchedTracks 17 | * @param {Dispatcher} dispatcher 18 | */ 19 | async run(player, track, channel, matchedTracks, dispatcher) { 20 | const embed = this.client 21 | .embed() 22 | .setTitle('Now Playing') 23 | .setDescription( 24 | `${track.info.title} - [\`${formatDuration(track.info.length, true)}\`]`, 25 | ); 26 | 27 | const matchedResults = matchedTracks.filter((v) => !Array.isArray(v) && v.info.uri !== track.info.uri).map((v) => { 28 | return { 29 | label: v.info.title, 30 | value: v.info.uri, 31 | }; 32 | }); 33 | 34 | const image = `https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg`; 35 | 36 | const buffer = await this.client.canvas.buildPlayerCard( 37 | image, 38 | track.info.author, 39 | track.info.title, 40 | track.info.length, 41 | player.position, 42 | ); 43 | 44 | const attachment = new AttachmentBuilder(buffer, { 45 | name: 'nowplaying.png', 46 | }); 47 | 48 | const buttonsRow = this.client.row() 49 | .addComponents([ 50 | this.client.button() 51 | .setEmoji({ name: '🔀' }) 52 | .setCustomId('shuffle') 53 | .setStyle(ButtonStyle.Secondary), 54 | this.client.button() 55 | .setEmoji({ name: '◀️' }) 56 | .setCustomId('backward') 57 | .setStyle(ButtonStyle.Secondary), 58 | this.client.button() 59 | .setEmoji({ name: '⏯️' }) 60 | .setCustomId('pause') 61 | .setStyle(ButtonStyle.Secondary), 62 | this.client.button() 63 | .setEmoji({ name: '▶️' }) 64 | .setCustomId('forward') 65 | .setStyle(ButtonStyle.Secondary), 66 | this.client.button() 67 | .setEmoji({ name: '🔁' }) 68 | .setCustomId('repeat') 69 | .setStyle(ButtonStyle.Secondary), 70 | ]); 71 | 72 | const menuRow = this.client.row() 73 | .addComponents([ 74 | this.client.menu() 75 | .setCustomId('results') 76 | .setPlaceholder('Play Similar songs') 77 | .setMaxValues(1) 78 | .setMinValues(1) 79 | .addOptions(matchedResults), 80 | ]); 81 | 82 | const message = await channel.send({ 83 | embeds: [embed.setImage('attachment://nowplaying.png')], 84 | files: [attachment], 85 | components: [menuRow, buttonsRow], 86 | }); 87 | 88 | this.interacte(message, dispatcher.requester, dispatcher, matchedTracks, player, track); 89 | } 90 | 91 | /** 92 | * 93 | * @param {Message} msg 94 | * @param {User} author 95 | * @param {Dispatcher} dispatcher 96 | * @param {import('shoukaku').Track[]} results 97 | * @param {Player} player 98 | * @param {import('shoukaku').Track} current 99 | */ 100 | interacte(msg, author, dispatcher, results, player, current) { 101 | const collector = msg.createMessageComponentCollector({ 102 | time: current.info.length, 103 | filter: (m) => m.user.id === author.id, 104 | }); 105 | 106 | collector.on('collect', async (i) => { 107 | if (i.user.id !== author.id) 108 | return await i.reply({ 109 | content: `Only **${author.tag}** can operate this buttons, Request one for yourselves.`, 110 | }); 111 | 112 | switch (i.customId) { 113 | case 'results': 114 | const filtered = results.filter((v) => v.info.uri === i.values[0])[0]; 115 | dispatcher.queue.push(filtered); 116 | await i.reply({ embeds: [this.client.embed().setDescription(`Added **${filtered.info.title}** to queue.`).setTitle('Music Queue')], ephemeral: true }); 117 | break; 118 | case 'shuffle': 119 | await i.deferUpdate(); 120 | shuffle(dispatcher.queue); 121 | break; 122 | case 'backward': 123 | await i.deferUpdate(); 124 | player.seekTo(0 * 1000); 125 | break; 126 | case 'forward': 127 | await i.deferUpdate(); 128 | player.stopTrack(); 129 | break; 130 | case 'pause': 131 | await i.deferUpdate(); 132 | player.setPaused(player.paused ? false : true); 133 | break; 134 | case 'repeat': 135 | await i.deferUpdate(); 136 | switch (dispatcher.loop) { 137 | case 'repeat': 138 | dispatcher.loop = 'queue'; 139 | break; 140 | case 'queue': 141 | dispatcher.loop = 'off'; 142 | break; 143 | case 'off': 144 | dispatcher.loop = 'repeat'; 145 | break; 146 | } 147 | } 148 | }); 149 | 150 | collector.on('end', async () => { 151 | await msg.delete(); 152 | }); 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /src/handlers/functions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-const-assign */ 2 | /* eslint-disable no-shadow */ 3 | const guildDb = require('@schemas/guild'); 4 | const { CommandInteraction, ButtonBuilder, ActionRowBuilder, ButtonStyle } = require('discord.js'); 5 | 6 | module.exports = class Functions { 7 | /** 8 | * 9 | * @param {import('discord.js').Guild} guildId 10 | * @returns {Promise} 11 | */ 12 | static async createGuildDb(guildId) { 13 | let guild = await guildDb.findById({ _id: guildId }); 14 | if (guild) return; 15 | guild = new guildDb({ _id: guildId }); 16 | 17 | await guild.save(); 18 | return guild; 19 | } 20 | /** 21 | * 22 | * @param {import('discord.js').Guild} guildId 23 | * @returns {Promise} 24 | */ 25 | static async getPrefix(guildId) { 26 | const guild = await guildDb.findOne({ _id: guildId }); 27 | 28 | return guild.prefix; 29 | } 30 | static async paginate(ctx, embed) { 31 | if (embed.length < 2) { 32 | if (ctx instanceof CommandInteraction) { 33 | ctx.deferred ? ctx.followUp({ embeds: embed }) : ctx.reply({ embeds: embed }); 34 | return; 35 | } else { 36 | ctx.channel.send({ embeds: embed }); 37 | return; 38 | } 39 | } 40 | const page = 0; 41 | const getButton = (page) => { 42 | const firstEmbed = page === 0; 43 | const lastEmbed = page === embed.length - 1; 44 | const pageEmbed = embed[page]; 45 | const first = new ButtonBuilder() 46 | .setCustomId('first') 47 | .setEmoji('⏪') 48 | .setStyle(ButtonStyle.Primary); 49 | if (firstEmbed) first.setDisabled(true); 50 | const back = new ButtonBuilder() 51 | .setCustomId('back') 52 | .setEmoji('◀️') 53 | .setStyle(ButtonStyle.Primary); 54 | if (firstEmbed) back.setDisabled(true); 55 | const next = new ButtonBuilder() 56 | .setCustomId('next') 57 | .setEmoji('▶️') 58 | .setStyle(ButtonStyle.Primary); 59 | if (lastEmbed) next.setDisabled(true); 60 | const last = new ButtonBuilder() 61 | .setCustomId('last') 62 | .setEmoji('⏩') 63 | .setStyle(ButtonStyle.Primary); 64 | if (lastEmbed) last.setDisabled(true); 65 | const stop = new ButtonBuilder() 66 | .setCustomId('stop') 67 | .setEmoji('⏹️') 68 | .setStyle(ButtonStyle.Danger); 69 | const row = new ActionRowBuilder() 70 | .addComponents(first, back, stop, next, last); 71 | return { embeds: [pageEmbed], components: [row] }; 72 | }; 73 | const msgOptions = getButton(0); 74 | let msg; 75 | if (ctx instanceof CommandInteraction) { 76 | msg = await ctx.deferred ? ctx.followUp({ ...msgOptions, fetchReply: true }) : ctx.reply({ ...msgOptions, fetchReply: true }); 77 | } else { 78 | msg = await ctx.channel.send({ ...msgOptions }); 79 | } 80 | let author; 81 | if (ctx instanceof CommandInteraction) { 82 | author = ctx.user; 83 | } else { 84 | author = ctx.author; 85 | } 86 | const filter = (interaction) => interaction.user.id === author.id; 87 | const collector = msg.createMessageComponentCollector({ filter, time: 60000 }); 88 | collector.on('collect', async (interaction) => { 89 | if (interaction.user.id === author.id) { 90 | await interaction.deferUpdate(); 91 | if (interaction.customId === 'fast') { 92 | if (page !== 0) { 93 | page = 0; 94 | const newEmbed = getButton(page); 95 | await interaction.editReply(newEmbed); 96 | } 97 | } 98 | if (interaction.customId === 'back') { 99 | if (page !== 0) { 100 | page--; 101 | const newEmbed = getButton(page); 102 | await interaction.editReply(newEmbed); 103 | } 104 | } 105 | if (interaction.customId === 'stop') { 106 | collector.stop(); 107 | await interaction.editReply({ embeds: [embed[page]], components: [] }); 108 | } 109 | if (interaction.customId === 'next') { 110 | if (page !== embed.length - 1) { 111 | page++; 112 | const newEmbed = getButton(page); 113 | await interaction.editReply(newEmbed); 114 | } 115 | } 116 | if (interaction.customId === 'last') { 117 | if (page !== embed.length - 1) { 118 | page = embed.length - 1; 119 | const newEmbed = getButton(page); 120 | await interaction.editReply(newEmbed); 121 | } 122 | 123 | } 124 | } else { 125 | await interaction.reply({ content: 'You can\'t use this button', ephemeral: true }); 126 | } 127 | }); 128 | 129 | collector.on('end', async () => { 130 | await msg.edit({ embeds: [embed[page]], components: [] }); 131 | }); 132 | } 133 | }; -------------------------------------------------------------------------------- /src/events/client/InteractionCreate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty-function */ 2 | const { InteractionType, Collection, PermissionFlagsBits, ActionRowBuilder, EmbedBuilder, ChannelType } = require('discord.js'); 3 | const Event = require('@structures/Event'); 4 | const Context = require('@structures/Context'); 5 | 6 | module.exports = class InteractionCreate extends Event { 7 | constructor(...args) { 8 | super(...args); 9 | } 10 | async run(interaction) { 11 | const color = this.client.config.color ? this.client.config.color : '#59D893'; 12 | 13 | if (interaction.type === InteractionType.ApplicationCommand) { 14 | const { commandName } = interaction; 15 | if (!commandName) return await interaction.reply({ content: 'Unknow interaction!' }).catch(() => { }); 16 | const prefix = this.client.config.prefix; 17 | const cmd = this.client.commands.get(interaction.commandName); 18 | if (!cmd || !cmd.slashCommand) return; 19 | const command = cmd.name.toLowerCase(); 20 | 21 | const ctx = new Context(interaction, interaction.options.data); 22 | 23 | this.client.logger.cmd('%s used by %s from %s', command, ctx.author.id, ctx.guild.id); 24 | if (!interaction.inGuild() || !interaction.channel.permissionsFor(interaction.guild.members.me).has(PermissionFlagsBits.ViewChannel)) return; 25 | 26 | if (!interaction.guild.members.me.permissions.has(PermissionFlagsBits.SendMessages)) return await interaction.author.send({ content: `I don't have **\`SEND_MESSAGES\`** permission in \`${interaction.guild.name}\`\nchannel: <#${interaction.channelId}>` }).catch(() => { }); 27 | 28 | if (!interaction.guild.members.me.permissions.has(PermissionFlagsBits.EmbedLinks)) return await interaction.channel.send({ content: 'I don\'t have **`EMBED_LINKS`** permission.' }).catch(() => { }); 29 | 30 | if (!interaction.guild.members.me.permissions.has([PermissionFlagsBits.EmbedLinks, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.UseApplicationCommands])) return await interaction.followUp({ content: 'I don\'t have enough permission to execute this cmd.', ephemeral: true }).catch(() => { }); 31 | 32 | if (cmd.permissions) { 33 | if (cmd.permissions.client) { 34 | if (!interaction.guild.members.me.permissions.has(cmd.permissions.client)) return await interaction.reply({ content: 'I don\'t have enough permissions to execute this cmd.', ephemeral: true }).catch(() => { }); 35 | } 36 | 37 | if (cmd.permissions.user) { 38 | if (!interaction.member.permissions.has(cmd.permissions.user)) return await interaction.reply({ content: 'You don\'t have enough permissions to execute this cmd.', ephemeral: true }).catch(() => { }); 39 | } 40 | if (cmd.permissions.dev) { 41 | if (this.client.config.owners) { 42 | const findDev = this.client.config.owners.find((x) => x === interaction.user.id); 43 | if (!findDev) return; 44 | } 45 | 46 | } 47 | } 48 | 49 | if (cmd.player) { 50 | if (cmd.player.voice) { 51 | if (!interaction.member.voice.channel) return await interaction.reply({ content: `You must be connected to a voice channel to use this \`${cmd.name}\` cmd.`, ephemeral: true }).catch(() => { }); 52 | 53 | if (!interaction.guild.members.me.permissions.has(PermissionFlagsBits.Speak)) return await interaction.reply({ content: `I don't have \`CONNECT\` permissions to execute this \`${cmd.name}\` cmd.`, ephemeral: true }).catch(() => { }); 54 | 55 | if (!interaction.guild.members.me.permissions.has(PermissionFlagsBits.Speak)) return await interaction.reply({ content: `I don't have \`SPEAK\` permissions to execute this \`${cmd.name}\` cmd.`, ephemeral: true }).catch(() => { }); 56 | 57 | if (interaction.member.voice.channel.type === ChannelType.GuildStageVoice && !interaction.guild.members.me.permissions.has(PermissionFlagsBits.RequestToSpeak)) return await interaction.reply({ content: `I don't have \`REQUEST TO SPEAK\` permission to execute this \`${cmd.name}\` cmd.`, ephemeral: true }).catch(() => { }); 58 | 59 | if (interaction.guild.members.me.voice.channel) { 60 | if (interaction.guild.members.me.voice.channelId !== interaction.member.voice.channelId) return await interaction.reply({ content: `You are not connected to ${interaction.guild.members.me.voice.channel} to use this \`${cmd.name}\` cmd.`, ephemeral: true }).catch(() => { }); 61 | } 62 | } 63 | 64 | if (cmd.player.active) { 65 | if (!this.client.player.get(interaction.guildId)) return await interaction.reply({ content: 'Nothing is playing right now.', ephemeral: true }).catch(() => { }); 66 | if (!this.client.player.get(interaction.guildId).queue) return await interaction.reply({ content: 'Nothing is playing right now.', ephemeral: true }).catch(() => { }); 67 | if (!this.client.player.get(interaction.guildId).queue.current) return await interaction.reply({ content: 'Nothing is playing right now.', ephemeral: true }).catch(() => { }); 68 | } 69 | 70 | } 71 | 72 | 73 | try { 74 | 75 | return await cmd.run(ctx, ctx.args); 76 | 77 | } catch (error) { 78 | console.error(error); 79 | await interaction.reply({ 80 | ephemeral: true, 81 | content: 'An unexpected error occured, the developers have been notified.', 82 | }).catch(() => { }); 83 | } 84 | } 85 | 86 | 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/events/client/messageCreate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty-function */ 2 | const { Message, Collection, PermissionFlagsBits, ActionRowBuilder, EmbedBuilder, ChannelType } = require('discord.js'); 3 | const Event = require('@structures/Event'); 4 | const Context = require('@root/src/structures/Context'); 5 | const { getPrefix, createGuildDb } = require('@handlers/functions'); 6 | 7 | module.exports = class MessageCreate extends Event { 8 | constructor(...args) { 9 | super(...args); 10 | } 11 | /** 12 | * 13 | * @param {Message} message 14 | * @returns 15 | */ 16 | async run(message) { 17 | 18 | if (message.author.bot || message.channel.type === ChannelType.DM) return; 19 | if (message.partial) await message.fetch(); 20 | await createGuildDb(message.guild.id); 21 | const prefix = await getPrefix(message.guild.id); 22 | const color = this.client.config.color ? this.client.config.color : '#59D893'; 23 | 24 | const ctx = new Context(message); 25 | 26 | const mention = new RegExp(`^<@!?${this.client.user.id}>( |)$`); 27 | if (message.content.match(mention)) { 28 | if (message.channel.permissionsFor(this.client.user).has([PermissionFlagsBits.SendMessages, PermissionFlagsBits.EmbedLinks, PermissionFlagsBits.ViewChannel])) { 29 | return await message.reply({ content: `Hey, my prefix for this server is \`${prefix}\` Want more info? then do \`${prefix}help\`\nStay Safe, Stay Awesome!` }).catch(() => { }); 30 | } 31 | } 32 | 33 | const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 34 | const prefixRegex = new RegExp(`^(<@!?${this.client.user.id}>|${escapeRegex(prefix)})\\s*`); 35 | if (!prefixRegex.test(message.content)) return; 36 | const [matchedPrefix] = message.content.match(prefixRegex); 37 | 38 | const args = message.content.slice(matchedPrefix.length).trim().split(/ +/g); 39 | const commandName = args.shift().toLowerCase(); 40 | const command = this.client.commands.get(commandName) || this.client.commands.get(this.client.aliases.get(commandName)); 41 | 42 | ctx.setArgs(args); 43 | 44 | if (!command) return; 45 | this.client.logger.cmd('%s used by %s from %s', commandName, ctx.author.id, ctx.guild.id); 46 | let dm = message.author.dmChannel; 47 | if (typeof dm === 'undefined') dm = await message.author.createDM(); 48 | 49 | if (!message.inGuild() || !message.channel.permissionsFor(message.guild.members.me).has(PermissionFlagsBits.ViewChannel)) return; 50 | 51 | if (!message.guild.members.me.permissions.has(PermissionFlagsBits.SendMessages)) return await message.author.send({ content: `I don't have **\`SEND_MESSAGES\`** permission in \`${message.guild.name}\`\nchannel: <#${message.channelId}>` }).catch(() => { }); 52 | 53 | if (!message.guild.members.me.permissions.has(PermissionFlagsBits.EmbedLinks)) return await message.channel.send({ content: 'I don\'t have **`EMBED_LINKS`** permission.' }).catch(() => { }); 54 | 55 | if (command.permissions) { 56 | if (command.permissions.client) { 57 | if (!message.guild.members.me.permissions.has(command.permissions.client)) return await message.reply({ content: 'I don\'t have enough permissions to execute this command.' }); 58 | } 59 | 60 | if (command.permissions.user) { 61 | if (!message.member.permissions.has(command.permissions.user)) return await message.reply({ content: 'You don\'t have enough permissions to use this command.' }); 62 | 63 | } 64 | if (command.permissions.dev) { 65 | if (this.client.config.owners) { 66 | const findDev = this.client.config.owners.find((x) => x === message.author.id); 67 | if (!findDev) return; 68 | } 69 | 70 | } 71 | } 72 | 73 | if (command.player) { 74 | if (command.player.voice) { 75 | if (!message.member.voice.channel) return await message.reply({ content: `You must be connected to a voice channel to use this \`${command.name}\` command.` }); 76 | 77 | if (!message.guild.members.me.permissions.has(PermissionFlagsBits.Speak)) return await message.reply({ content: `I don't have \`CONNECT\` permissions to execute this \`${command.name}\` command.` }); 78 | 79 | if (!message.guild.members.me.permissions.has(PermissionFlagsBits.Speak)) return await message.reply({ content: `I don't have \`SPEAK\` permissions to execute this \`${command.name}\` command.` }); 80 | 81 | if (message.member.voice.channel.type === ChannelType.GuildStageVoice && !message.guild.members.me.permissions.has(PermissionFlagsBits.RequestToSpeak)) return await message.reply({ content: `I don't have \`REQUEST TO SPEAK\` permission to execute this \`${command.name}\` command.` }); 82 | 83 | if (message.guild.members.me.voice.channel) { 84 | if (message.guild.members.me.voice.channelId !== message.member.voice.channelId) return await message.reply({ content: `You are not connected to ${message.guild.members.me.voice.channel} to use this \`${command.name}\` command.` }); 85 | } 86 | } 87 | 88 | if (command.player.active) { 89 | if (!this.client.player.get(message.guildId)) return await message.reply({ content: 'Nothing is playing right now.' }); 90 | if (!this.client.player.get(message.guildId).queue) return await message.reply({ content: 'Nothing is playing right now.' }); 91 | if (!this.client.player.get(message.guildId).queue.current) return await message.reply({ content: 'Nothing is playing right now.' }); 92 | } 93 | } 94 | if (command.args) { 95 | if (!args.length) return await message.reply({ content: `Please provide the required arguments. \`${command.description.examples}\`` }); 96 | } 97 | 98 | 99 | try { 100 | 101 | return await command.run(ctx, ctx.args); 102 | 103 | } catch (error) { 104 | await message.channel.send({ content: 'An unexpected error occured, the developers have been notified!' }).catch(() => { }); 105 | console.error(error); 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /src/structures/Client.js: -------------------------------------------------------------------------------- 1 | const { Client, Routes, REST, ActionRowBuilder, PermissionsBitField, ApplicationCommandType, GatewayIntentBits, Partials, Collection, EmbedBuilder, ButtonBuilder, SelectMenuBuilder, Colors } = require('discord.js'); 2 | const { connect, connection } = require('mongoose'); 3 | const { readdirSync } = require('node:fs'); 4 | const Logger = require('@structures/Logger'); 5 | const Cluster = require('discord-hybrid-sharding'); 6 | const Manager = require('@structures/Manager'); 7 | const Canvas = require('./Canvas'); 8 | 9 | module.exports = class Alpha extends Client { 10 | constructor() { 11 | super({ 12 | allowedMentions: { 13 | parse: ['users', 'roles', 'everyone'], 14 | repliedUser: false, 15 | }, 16 | shards: Cluster.data.SHARD_LIST, 17 | shardCount: Cluster.data.TOTAL_SHARDS, 18 | intents: [ 19 | GatewayIntentBits.Guilds, 20 | GatewayIntentBits.GuildMessages, 21 | GatewayIntentBits.MessageContent, 22 | GatewayIntentBits.GuildInvites, 23 | GatewayIntentBits.GuildMembers, 24 | GatewayIntentBits.GuildPresences, 25 | GatewayIntentBits.GuildMessageReactions, 26 | GatewayIntentBits.GuildVoiceStates, 27 | GatewayIntentBits.GuildMessageTyping, 28 | GatewayIntentBits.GuildBans, 29 | GatewayIntentBits.GuildWebhooks, 30 | GatewayIntentBits.DirectMessages, 31 | GatewayIntentBits.DirectMessageReactions, 32 | GatewayIntentBits.GuildEmojisAndStickers, 33 | ], 34 | partials: [ 35 | Partials.Channel, 36 | Partials.GuildMember, 37 | Partials.Message, 38 | Partials.User, 39 | Partials.Reaction, 40 | ], 41 | restTimeOffset: 0, 42 | restRequestTimeout: 20000, 43 | }); 44 | 45 | this.commands = new Collection(); 46 | this.config = require('@src/config'); 47 | this.emotes = require('@src/emotes'); 48 | this.aliases = new Collection(); 49 | this.contextMenus = new Collection(); 50 | this.cooldowns = new Collection(); 51 | this.cluster = new Cluster.Client(this); 52 | this.logger = new Logger({ 53 | displayTimestamp: true, 54 | displayDate: true, 55 | }); 56 | this._connectMongodb(); 57 | this.manager = new Manager(this); 58 | this.canvas = new Canvas(); 59 | } 60 | /** 61 | * @param {import('discord.js').APIEmbed} data 62 | * @returns {EmbedBuilder} 63 | */ 64 | embed(data) { 65 | return new EmbedBuilder(data).setColor('Blurple'); 66 | } 67 | /** 68 | * @param {import('discord.js').APIButtonComponent} data 69 | * @returns {ButtonBuilder} 70 | */ 71 | button(data) { 72 | return new ButtonBuilder(data); 73 | } 74 | /** 75 | * @param {import('discord.js').APISelectMenuComponent} data 76 | * @returns {SelectMenuBuilder} 77 | */ 78 | menu(data) { 79 | return new SelectMenuBuilder(data); 80 | } 81 | /** 82 | * @param {import('discord.js').APIActionRowComponent} data 83 | * @returns {ActionRowBuilder} 84 | */ 85 | row(data) { 86 | return new ActionRowBuilder(data); 87 | } 88 | 89 | loadCommands() { 90 | if (this.cluster.id !== 0) return; 91 | const data = []; 92 | let i = 0; 93 | const commandsFolder = readdirSync('./src/commands/'); 94 | commandsFolder.forEach((category) => { 95 | const categories = readdirSync(`./src/commands/${category}/`).filter( 96 | (file) => file.endsWith('.js'), 97 | ); 98 | categories.forEach((command) => { 99 | const f = require(`../commands/${category}/${command}`); 100 | const cmd = new f(this, f); 101 | cmd.category = category; 102 | cmd.file = f; 103 | cmd.fileName = f.name; 104 | this.commands.set(cmd.name, cmd); 105 | if (cmd.aliases && Array.isArray(cmd.aliases)) { 106 | for (const alias of cmd.aliases) { 107 | this.aliases.set(alias, cmd); 108 | } 109 | } 110 | if (cmd.slashCommand) { 111 | data.push({ 112 | name: cmd.name, 113 | description: cmd.description.content, 114 | options: cmd.options, 115 | type: ApplicationCommandType.ChatInput, 116 | }); 117 | if (cmd.permissions.user.length > 0) 118 | data.default_member_permissions = cmd.permissions.user 119 | ? PermissionsBitField.resolve(cmd.permissions.user).toString() 120 | : 0; 121 | ++i; 122 | } 123 | this.logger.event(`Successfully loaded [/] command ${i}.`); 124 | }); 125 | }); 126 | 127 | const rest = new REST({ version: '10' }).setToken( 128 | this.user ? this.token : require('@src/config').token, 129 | ); 130 | 131 | rest 132 | .put( 133 | Routes.applicationCommands( 134 | this.user ? this.user.id : require('@src/config').clientId, 135 | ), 136 | { body: data }, 137 | ) 138 | .then(() => 139 | this.logger.info('Successfully reloaded application (/) commands.'), 140 | ) 141 | .catch((e) => this.logger.error(e.name, e.message)); 142 | } 143 | loadEvents() { 144 | if (this.cluster.id !== 0) return; 145 | const EventsFolder = readdirSync('./src/events'); 146 | let i = 0; 147 | EventsFolder.forEach(async (eventFolder) => { 148 | const events = readdirSync(`./src/events/${eventFolder}`).filter( 149 | (c) => c.split('.').pop() === 'js', 150 | ); 151 | events.forEach(async (eventStr) => { 152 | if (!events.length) throw Error('No event files found!'); 153 | const file = require(`../events/${eventFolder}/${eventStr}`); 154 | const event = new file(this, file); 155 | const eventName = 156 | eventStr.split('.')[0].charAt(0).toLowerCase() + 157 | eventStr.split('.')[0].slice(1); 158 | 159 | switch (eventFolder) { 160 | case 'player': 161 | this.manager.on(eventName, (...args) => event.run(...args)); 162 | ++i; 163 | break; 164 | case 'node': 165 | this.manager.on(eventName, (...args) => event.run(...args)); 166 | ++i; 167 | break; 168 | default: 169 | this.on(eventName, (...args) => event.run(...args)); 170 | ++i; 171 | break; 172 | } 173 | }); 174 | }); 175 | this.logger.event(`Successfully loaded event ${i}.`); 176 | } 177 | async _connectMongodb() { 178 | if ([1, 2, 99].includes(connection.readyState)) return; 179 | await connect(this.config.database); 180 | this.logger.ready('Successfully connected to MongoDB.'); 181 | } 182 | async connect() { 183 | super.login(this.config.token); 184 | this.loadEvents(); 185 | this.loadCommands(); 186 | } 187 | }; 188 | -------------------------------------------------------------------------------- /src/structures/Context.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty-function */ 2 | const { Message, CommandInteraction, EmbedBuilder, User, Guild, GuildMember, CommandInteractionOptionResolver, TextChannel, VoiceChannel, ThreadChannel, DMChannel } = require('discord.js'); 3 | const { paginate } = require('@handlers/functions'); 4 | module.exports = class Context { 5 | /** 6 | * 7 | * @param {Message | CommandInteraction} ctx 8 | * @param {string[] | CommandInteractionOptionResolver} args 9 | */ 10 | constructor(ctx, args) { 11 | /** 12 | * @type {boolean} 13 | */ 14 | this.isInteraction = ctx instanceof CommandInteraction; 15 | /** 16 | * @type {Message | CommandInteraction} 17 | */ 18 | this.ctx = ctx; 19 | /** 20 | * @type {void} 21 | */ 22 | this.setArgs(args); 23 | /** 24 | * @type {CommandInteraction} 25 | */ 26 | this.interaction = this.isInteraction ? ctx : null; 27 | /** 28 | * @type {Message} 29 | */ 30 | this.message = this.isInteraction ? null : ctx; 31 | /** 32 | * @type {String} 33 | */ 34 | this.id = ctx.id; 35 | /** 36 | * @type {String} 37 | */ 38 | this.applicationId = ctx.applicationId; 39 | /** 40 | * @type {String} 41 | */ 42 | this.channelId = ctx.channelId; 43 | /** 44 | * @type {String} 45 | */ 46 | this.guildId = ctx.guildId; 47 | /** 48 | * @type {import('@structures/Client')} 49 | */ 50 | this.client = ctx.client; 51 | /** 52 | * @type {User} 53 | */ 54 | this.author = ctx instanceof Message ? ctx.author : ctx.user; 55 | /** 56 | * @type {TextChannel | VoiceChannel | ThreadChannel | DMChannel} 57 | */ 58 | this.channel = ctx.channel; 59 | /** 60 | * @type {Guild} 61 | */ 62 | this.guild = ctx.guild; 63 | /** 64 | * @type {GuildMember} 65 | */ 66 | this.member = ctx.member; 67 | /** 68 | * @type {Date} 69 | */ 70 | this.createdAt = ctx.createdAt; 71 | /** 72 | * @type {Number} 73 | */ 74 | this.createdTimestamp = ctx.createdTimestamp; 75 | /** 76 | * @type {Collection} 77 | */ 78 | this.attachments = ctx.attachments; 79 | /** 80 | * @type {import('discord.js').MessageEmbed[]} 81 | */ 82 | this.paginates = paginate; 83 | } 84 | /** 85 | * 86 | * @param {any} args 87 | * @returns {void} 88 | */ 89 | setArgs(args) { 90 | if (this.isInteraction) { 91 | this.args = args.map(arg => arg.value); 92 | } 93 | else { 94 | this.args = args; 95 | } 96 | } 97 | /** 98 | * 99 | * @param {String} content 100 | * @returns {Promise} 101 | */ 102 | async sendMessage(content) { 103 | if (this.isInteraction) { 104 | this.msg = this.interaction.deferred ? await this.followUp(content) : await this.reply(content); 105 | return this.msg; 106 | } else { 107 | this.msg = this.message.channel.send(content); 108 | return this.msg; 109 | } 110 | } 111 | /** 112 | * 113 | * @param {String} content 114 | * @returns {Promise} 115 | */ 116 | async sendDeferMessage(content) { 117 | if (this.isInteraction) { 118 | this.msg = await this.interaction.deferReply({ fetchReply: true }); 119 | return this.msg; 120 | } 121 | else { 122 | this.msg = await this.message.channel.send(content); 123 | return this.msg; 124 | } 125 | } 126 | /** 127 | * 128 | * @param {String} content 129 | * @returns {Promise} 130 | */ 131 | async sendFollowUp(content) { 132 | if (this.isInteraction) { 133 | await this.followUp(content); 134 | } 135 | else { 136 | this.channel.send(content); 137 | } 138 | } 139 | /** 140 | * 141 | * @param {String} content 142 | * @returns {Promise} 143 | */ 144 | async editMessage(content) { 145 | if (this.isInteraction) { 146 | return this.interaction.editReply(content); 147 | } 148 | else { 149 | return this.msg.edit(content); 150 | } 151 | } 152 | /** 153 | * 154 | * @returns {Promise} 155 | */ 156 | deleteMessage() { 157 | if (this.isInteraction) { 158 | return this.interaction.deleteReply(); 159 | } 160 | else { 161 | return this.msg.delete(); 162 | } 163 | } 164 | paginate(ctx, embed) { 165 | return this.paginates(ctx, embed); 166 | } 167 | /** 168 | * @param {string} commandName 169 | * @param {Message} message 170 | * @param {string[]} args 171 | * @param {import('@structures/Client')} client 172 | * @returns {Promise} 173 | */ 174 | async invalidArgs(commandName, message, args, client) { 175 | try { 176 | const color = client.config.color ? client.config.color : '#59D893'; 177 | const prefix = client.config.prefix; 178 | const command = client.commands.get(commandName) || client.commands.get(client.aliases.get(commandName)); 179 | if (!command) return await message.edit({ 180 | embeds: [new EmbedBuilder().setColor(color).setDescription(args)], allowedMentions: { 181 | repliedUser: false, 182 | }, 183 | }).catch(() => { }); 184 | const embed = new EmbedBuilder() 185 | .setColor(color) 186 | .setAuthor({ name: message.author.tag.toString(), iconURL: message.author.displayAvatarURL({ dynamic: true }).toString() }) 187 | .setDescription(`**${args}**`) 188 | .setTitle(`__${command.name}__`) 189 | .addFields([ 190 | { 191 | name: 'Usage', 192 | value: `\`${command.description.usage ? `${prefix}${command.name} ${command.description.usage}` : `${prefix}${command.name}`}\``, 193 | inline: false, 194 | }, { 195 | name: 'Example(s)', 196 | value: `${command.description.examples ? `\`${prefix}${command.description.examples.join(`\`\n\`${prefix}`)}\`` : '`' + prefix + command.name + '`'}`, 197 | }, 198 | ]); 199 | 200 | await this.msg.edit({ 201 | content: null, 202 | embeds: [embed], 203 | allowedMentions: { repliedUser: false }, 204 | }); 205 | } 206 | catch (e) { 207 | console.error(e); 208 | } 209 | } 210 | 211 | }; -------------------------------------------------------------------------------- /src/commands/music/play.js: -------------------------------------------------------------------------------- 1 | const Context = require('@root/src/structures/Context'); 2 | const Dispatcher = require('@root/src/structures/Dispatcher'); 3 | const { formatDuration } = require('@root/src/utils/function'); 4 | const Command = require('@structures/Command'); 5 | const { 6 | CommandInteractionOptionResolver, 7 | ApplicationCommandOptionType, 8 | Message, 9 | User, 10 | Colors, 11 | } = require('discord.js'); 12 | 13 | module.exports = class Play extends Command { 14 | constructor(client) { 15 | super(client, { 16 | name: 'play', 17 | description: { 18 | content: 'Plays audio from any supported source.', 19 | usage: '', 20 | examples: ['play alone'], 21 | }, 22 | aliases: ['p'], 23 | category: 'Music', 24 | cooldown: 3, 25 | player: { 26 | voice: true, 27 | dj: false, 28 | active: false, 29 | djPerm: null, 30 | }, 31 | permissions: { 32 | dev: false, 33 | client: ['SendMessages', 'ViewChannels', 'EmbedLinks', 'Connect'], 34 | user: ['SendMessages'], 35 | voteRequired: false, 36 | }, 37 | slashCommand: true, 38 | options: [ 39 | { 40 | name: 'query', 41 | description: 'Song name or URL to play.', 42 | required: true, 43 | type: ApplicationCommandOptionType.String, 44 | autocomplete: true, 45 | }, 46 | ], 47 | }); 48 | } 49 | 50 | /** 51 | * 52 | * @param {Context} ctx 53 | * @param {string[] | CommandInteractionOptionResolver} args 54 | */ 55 | async run(ctx, args) { 56 | if (ctx.isInteraction) { 57 | await ctx.sendDeferMessage(); 58 | await ctx.deleteMessage(); 59 | } 60 | if (!args.length) 61 | return await ctx.channel.send({ 62 | embeds: [ 63 | this.client.embed() 64 | .setDescription('Please provide an URL or search query'), 65 | ], 66 | }); 67 | 68 | /** 69 | * @type {string} 70 | */ 71 | const query = args.length > 1 ? args.join(' ') : args[0]; 72 | const isURL = this.checkURL(query); 73 | const dispatcher = await this.client.manager.spawn(ctx.guild, ctx.member, ctx.channel); 74 | const result = await this.client.manager.search(isURL ? query : `ytsearch:${query}`); 75 | const embed = this.client.embed(); 76 | const row = this.client.row; 77 | 78 | // LoadType checking 79 | switch (result.loadType) { 80 | case 'LOAD_FAILED': 81 | await ctx.channel.send({ 82 | embeds: [embed.setDescription('Failed to load track try again')], 83 | }); 84 | break; 85 | case 'NO_MATCHES': 86 | await ctx.channel.send({ 87 | embeds: [embed.setDescription('No matches found for given query')], 88 | }); 89 | break; 90 | case 'SEARCH_RESULT': 91 | const buttons = result.tracks.slice(0, 5).map((value, index) => { 92 | return { 93 | type: 2, 94 | emoji: { 95 | name: `${this.client.emotes[++index]}`, 96 | id: null, 97 | animated: false, 98 | }, 99 | custom_id: `${value.info.uri}`, 100 | style: 2, 101 | }; 102 | }); 103 | const intialDescription = result.tracks 104 | .slice(0, 5) 105 | .map( 106 | (value, index) => 107 | `${++index} - [${value.info.title}](${value.info.uri 108 | }) [\`${formatDuration(value.info.length, true)}\`]`, 109 | ) 110 | .join('\n'); 111 | embed.setDescription( 112 | `Multiple tracks found. Please choose one of the following\n\n${intialDescription}`, 113 | ); 114 | const msg = await ctx.channel.send({ 115 | embeds: [ 116 | embed.setAuthor({ 117 | name: ctx.author.tag, 118 | iconURL: ctx.author.displayAvatarURL(), 119 | }), 120 | ], 121 | components: [ 122 | row({ 123 | type: 1, 124 | components: buttons, 125 | }), 126 | this.client.row({ 127 | type: 1, 128 | components: [ 129 | { 130 | type: 2, 131 | emoji: { 132 | name: '⏹️', 133 | id: null, 134 | animated: false, 135 | }, 136 | custom_id: 'cancel', 137 | style: 2, 138 | }, 139 | ], 140 | }), 141 | ], 142 | }); 143 | await this.interacte(msg, ctx.author, dispatcher, result); 144 | break; 145 | case 'TRACK_LOADED': 146 | dispatcher.queue.push(result.tracks[0]); 147 | await dispatcher.check(); 148 | await ctx.channel.send({ 149 | embeds: [ 150 | embed 151 | .setTitle('Music Queue') 152 | .setDescription( 153 | `Added **${result.tracks[0].info.title}** to queue.`, 154 | ), 155 | ], 156 | }); 157 | break; 158 | case 'PLAYLIST_LOADED': 159 | dispatcher.queue.push(...result.tracks); 160 | await dispatcher.check(); 161 | await ctx.channel.send({ 162 | embeds: [ 163 | embed 164 | .setDescription( 165 | `Added **${result.playlistInfo.name}** with **${result.tracks.length} Songs!**`, 166 | ) 167 | .setTitle('Music Queue'), 168 | ], 169 | }); 170 | break; 171 | } 172 | } 173 | 174 | /** 175 | * 176 | * @param {string} string 177 | * @returns {boolean} 178 | */ 179 | checkURL(string) { 180 | try { 181 | new URL(string); 182 | return true; 183 | } catch (error) { 184 | return false; 185 | } 186 | } 187 | 188 | /** 189 | * 190 | * @param {Message} msg 191 | * @param {User} author 192 | * @param {Dispatcher} dispatcher 193 | * @param {import('shoukaku').LavalinkResponse} result 194 | */ 195 | async interacte(msg, author, dispatcher, result) { 196 | const collector = msg.createMessageComponentCollector({ 197 | max: 1, 198 | time: 60 * 1000, 199 | filter: (m) => m.user.id === author.id, 200 | }); 201 | 202 | collector.on('collect', async (i) => { 203 | await i.deferUpdate(); 204 | if (i.user.id !== author.id) 205 | return await i.reply({ 206 | content: `Only **${author.tag}** can operate this buttons, Request one for yourselves.`, 207 | }); 208 | switch (i.customId) { 209 | case 'cancel': 210 | collector.stop(); 211 | break; 212 | default: 213 | const find = result.tracks 214 | .slice(0, 5) 215 | .filter((value) => value.info.uri === i.customId)[0]; 216 | dispatcher.queue.push(find); 217 | await dispatcher.check(); 218 | await msg.edit({ 219 | embeds: [ 220 | { 221 | description: `Added **${find.info.title}** to queue.`, 222 | title: 'Music Queue', 223 | color: Colors.Blurple, 224 | }, 225 | ], 226 | components: [], 227 | }); 228 | break; 229 | } 230 | }); 231 | 232 | collector.on('end', async () => { 233 | await msg 234 | .edit({ 235 | components: [ 236 | { 237 | type: 1, 238 | components: [ 239 | ...msg.components[0].components.map((v) => { 240 | return { 241 | type: v.data.type, 242 | emoji: v.data.emoji, 243 | style: v.data.style, 244 | custom_id: v.data.custom_id, 245 | disabled: true, 246 | }; 247 | }), 248 | ], 249 | }, 250 | { 251 | type: 1, 252 | components: [ 253 | ...msg.components[1].components.map((v) => { 254 | return { 255 | type: v.data.type, 256 | emoji: v.data.emoji, 257 | style: v.data.style, 258 | custom_id: v.data.custom_id, 259 | disabled: true, 260 | }; 261 | }), 262 | ], 263 | }, 264 | ], 265 | }).catch((err) => this.client.logger.error(err)); 266 | }); 267 | } 268 | }; 269 | --------------------------------------------------------------------------------