├── .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 |
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 |
--------------------------------------------------------------------------------