diff --git a/assets/images/404.png b/assets/images/404.png new file mode 100644 index 0000000..5246766 Binary files /dev/null and b/assets/images/404.png differ diff --git a/assets/images/whatsapp-bot.png b/assets/images/whatsapp-bot.png new file mode 100644 index 0000000..45d8cbb Binary files /dev/null and b/assets/images/whatsapp-bot.png differ diff --git a/public/bgimg.jpg b/public/bgimg.jpg new file mode 100644 index 0000000..26ad5f3 Binary files /dev/null and b/public/bgimg.jpg differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..d8592db Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..e4049f4 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + WhatsApp-bot + + + + +
+
+
+
+ +

Hellow There!

+

Please Enter Your Session ID to Authenticate

+
+
+ +
+
+

Authenticate

+ + +
+
+ +
+
+ + + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..e20d243 --- /dev/null +++ b/public/style.css @@ -0,0 +1,92 @@ +body { + margin: 0; + padding: 0; +} +form { + display: flex; + align-items: center; + justify-self: center; + flex-direction: column; +} +form > input { + padding: 10px; + border: none; + margin: 8px 0; + width: 100%; + border-radius: 3px; + background: rgba( 255, 255, 255, 0.45 ); +box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 ); +backdrop-filter: blur( 4.5px ); +-webkit-backdrop-filter: blur( 4.5px ); +border-radius: 10px; +border: 1px none rgba( 255, 255, 255, 0.18 ); +} + +button { + color: aliceblue; + padding: 13px; + margin-top: 5px; + text-transform: uppercase; + background: rgba( 246, 197, 92, 0.45 );; +box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 ); +backdrop-filter: blur( 17px ); +-webkit-backdrop-filter: blur( 17px ); +border-radius: 10px; +border: 1px none rgba( 255, 255, 255, 0.18 ); +} +.greets { + background-color: #f6c55c; + height: inherit; + width: inherit; + text-align: center; + display: flex; + align-items: center; + justify-self: center; + background: rgba( 246, 197, 92, 0.45 ); +box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.37); +border-radius: 10px; +border: 1px solid rgba( 255, 255, 255, 0.18 ); +} +.outer-form { + height: 480px; + width: 550px; + display: flex; + flex-direction: row; + background: rgba( 255, 255, 255, 0.2 ); +box-shadow: 0 0 32px 0 rgba(2, 2, 2, 0.37); +border-radius: 10px; +border: 1px solid rgba( 255, 255, 255, 0.18 ); +} +.form { + display: flex; + align-items: center; + justify-content: center; + width: inherit; +} +section { + height: 100vh; + width: auto; + display: flex; + background-image: url('bgimg.jpg'); + background-size: cover; + align-items: center; + justify-content: center; +} +p { + font-family: 'Ubuntu','Courier New', Courier, monospace; + font-weight: 400; +} +h1 { + font-family: 'Ubuntu','Courier New', Courier, monospace; + font-weight: 700; + text-align: center; +} +@media screen and (max-width:524px) { + h1 { + font-size: x-large; + } + .outer-form { + height: 300px; + width: 450px; + } + } diff --git a/src/Commands/Dev/Ban.ts b/src/Commands/Dev/Ban.ts new file mode 100644 index 0000000..3ae7fed --- /dev/null +++ b/src/Commands/Dev/Ban.ts @@ -0,0 +1,55 @@ +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('ban', { + description: 'Bans/unban users', + category: 'dev', + cooldown: 5, + usage: 'ban --action=[ban/unban] [tag/quote users]', + exp: 15 +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags }: IArgs): Promise => { + const users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + if (users.length < 1) return void M.reply('Tag or quote a user to use this command') + flags = flags.filter((flag) => flag.startsWith('--action=')) + if (flags.length < 1) + return void M.reply( + `Provide the action of the ban. Example: *${this.client.config.prefix}ban --action=ban*` + ) + const actions = ['ban', 'unban'] + const action = flags[0].split('=')[1] + if (action === '') + return void M.reply( + `Provide the action of the ban. Example: *${this.client.config.prefix}ban --action=ban*` + ) + if (!actions.includes(action.toLowerCase())) return void M.reply('Invalid action') + let text = `🚦 *State: ${action.toLowerCase() === 'ban' ? 'BANNED' : 'UNBANNED'}*\n⚗ *Users:*\n` + let Text = '🚦 *State: SKIPPED*\n⚗ *Users:*\n\n' + let resultText = '' + let skippedFlag = false + for (const user of users) { + const info = await this.client.DB.getUser(user) + if ( + ((this.client.config.mods.includes(user) || info.banned) && action.toLowerCase() === 'ban') || + (!info.banned && action.toLowerCase() === 'unban') + ) { + skippedFlag = true + Text += `*@${user.split('@')[0]}* (Skipped as this user is ${ + this.client.config.mods.includes(user) + ? 'a moderator' + : action.toLowerCase() === 'ban' + ? 'already banned' + : 'already unbanned' + })\n` + continue + } + text += `\n*@${user.split('@')[0]}*` + await this.client.DB.updateBanStatus(user, action.toLowerCase() as 'ban' | 'unban') + } + if (skippedFlag) resultText += `${Text}\n` + resultText += text + return void (await M.reply(resultText, 'text', undefined, undefined, undefined, users)) + } +} diff --git a/src/Commands/Dev/Eval.ts b/src/Commands/Dev/Eval.ts new file mode 100644 index 0000000..e2a7f39 --- /dev/null +++ b/src/Commands/Dev/Eval.ts @@ -0,0 +1,21 @@ +import { Command, Message, BaseCommand } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('eval', { + description: 'Evaluates JavaScript', + category: 'dev', + usage: 'eval [JavaScript code]', + dm: true +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + let out!: string + try { + const result = eval(context) + out = JSON.stringify(result, null, '\t') || 'Evaluated JavaScript' + } catch (error) { + out = (error as any).message + } + return void M.reply(out) + } +} diff --git a/src/Commands/Dev/Toggle.ts b/src/Commands/Dev/Toggle.ts new file mode 100644 index 0000000..d6d8993 --- /dev/null +++ b/src/Commands/Dev/Toggle.ts @@ -0,0 +1,64 @@ +import moment from 'moment-timezone' +import { BaseCommand, Command, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('toggle', { + description: 'Toggles a command state', + usage: 'toggle --command=[command_name] --state=[disable/enable] | ', + exp: 10, + category: 'dev', + cooldown: 10 +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags, context }: IArgs): Promise => { + flags.forEach((flag) => (context = context.replace(flag, ''))) + const commandFlag = flags.filter((flag) => flag.startsWith('--command=')) + const stateFlag = flags.filter((flag) => flag.startsWith('--state=')) + if (commandFlag.length < 1 || stateFlag.length < 1) + return void M.reply( + `Provide the command and the state (disable/enable) of the command that you wanna to. Example: *${this.client.config.prefix}toggle --command=hi --state=disable | Well...*` + ) + const cmd = commandFlag[0].toLowerCase().split('=') + const state = stateFlag[0].toLowerCase().split('=') + if (state[1] === '' || cmd[1] === '') + return void M.reply( + `Provide the command and the state (disable/enable) of the command that you wanna to. Example: *${this.client.config.prefix}toggle --command=hi --state=disable | Well...*` + ) + const command = this.handler.commands.get(cmd[1].trim()) || this.handler.aliases.get(cmd[1].trim()) + if (!command) return void M.reply(`No command found | *"${this.client.utils.capitalize(cmd[1])}"*`) + const actions = ['disable', 'enable'] + if (!actions.includes(state[1])) return void M.reply('Invalid command state') + const disabledCommands = await this.client.DB.getDisabledCommands() + const index = disabledCommands.findIndex((cmd) => cmd.command === command.name) + let text = '' + if (state[1] === 'disable') { + if (index >= 0) + return void M.reply( + `🟨 *${this.client.utils.capitalize(cmd[1])}* is already disabled by *${ + disabledCommands[index].disabledBy + }* in *${disabledCommands[index].time} (GMT)*. ❓ *Reason:* ${disabledCommands[index].reason}` + ) + if (!context || !context.split('|')[1]) + return void M.reply( + `Provide the reason for disabling this command. Example: *${ + this.client.config.prefix + }toggle --command=${this.client.utils.capitalize(cmd[1])} --state=disable | Well...*` + ) + disabledCommands.push({ + command: command.name, + disabledBy: M.sender.username, + reason: context.split('|')[1].trim(), + time: moment.tz('Etc/GMT').format('MMM D, YYYY HH:mm:ss') + }) + text += `*${this.client.utils.capitalize(cmd[1])}* has been disabled. ❓ *Reason:* ${context + .split('|')[1] + .trim()}` + } else { + if (index < 0) return void M.reply(`🟨 *${this.client.utils.capitalize(cmd[1])}* is already enabled`) + disabledCommands.splice(index, 1) + text += `*${this.client.utils.capitalize(cmd[1])}* has been enabled.` + } + await this.client.DB.updateDisabledCommands(disabledCommands) + return void M.reply(text) + } +} diff --git a/src/Commands/Fun/Friendship.ts b/src/Commands/Fun/Friendship.ts new file mode 100644 index 0000000..c97bd33 --- /dev/null +++ b/src/Commands/Fun/Friendship.ts @@ -0,0 +1,48 @@ +import { Friendship, IFriendShip } from '@shineiichijo/canvas-chan' +import { Command, Message, BaseCommand } from '../../Structures' + +@Command('friendship', { + description: 'Calculates the level of a friendship', + usage: 'friendship [tag/quote users]', + cooldown: 10, + exp: 50, + category: 'fun' +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + const friendshipArray: IFriendShip[] = [] + let users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + while (users.length < 2) users.push(M.sender.jid) + if (users.includes(M.sender.jid)) users = users.reverse() + for (const user of users) { + const name = this.client.contact.getContact(user).username + let image!: string + try { + image = + (await this.client.profilePictureUrl(user, 'image')) || + 'https://upload.wikimedia.org/wikipedia/commons/a/ac/Default_pfp.jpg' + } catch (error) { + image = 'https://upload.wikimedia.org/wikipedia/commons/a/ac/Default_pfp.jpg' + } + friendshipArray.push({ name, image }) + } + const percentage = Math.floor(Math.random() * 101) + let text = '' + if (percentage >= 0 && percentage < 10) text = 'Fake friends' + else if (percentage >= 10 && percentage < 25) text = 'Awful' + else if (percentage >= 25 && percentage < 40) text = 'Very Bad' + else if (percentage >= 40 && percentage < 50) text = 'Average' + else if (percentage >= 50 && percentage < 75) text = 'Nice' + else if (percentage >= 75 && percentage < 90) text = 'Besties' + else if (percentage >= 90) text = 'Soulmates' + const image = new Friendship(friendshipArray, percentage, text) + let caption = `\t🍁 *Calculating...* 🍁 \n` + caption += `\t\t---------------------------------\n` + caption += `@${users[0].split('@')[0]} & @${users[1].split('@')[0]}\n` + caption += `\t\t---------------------------------\n` + caption += `\t\t\t\t\t${percentage < 40 ? '📉' : percentage < 75 ? '📈' : '💫'} *Percentage: ${percentage}%*\n` + caption += text + return void (await M.reply(await image.build(), 'image', undefined, undefined, caption, [users[0], users[1]])) + } +} diff --git a/src/Commands/Fun/Reaction.ts b/src/Commands/Fun/Reaction.ts new file mode 100644 index 0000000..6263bed --- /dev/null +++ b/src/Commands/Fun/Reaction.ts @@ -0,0 +1,49 @@ +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' +import { Reaction, Reactions, reaction } from '../../lib' + +const reactions = Object.keys(Reactions) + +@Command('reaction', { + description: 'React via anime gifs with the tagged or quoted user', + category: 'fun', + cooldown: 10, + exp: 20, + usage: 'reaction (reaction) [tag/quote user] || (reaction) [tag/quote user]', + aliases: ['r', ...reactions] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + const command = M.content.split(' ')[0].toLowerCase().slice(this.client.config.prefix.length).trim() + let flag = true + if (command === 'r' || command === 'reaction') flag = false + if (!flag && !context) + return void M.reply( + `💫 *Available Reactions:*\n\n- ${reactions + .sort((x, y) => (x < y ? -1 : x > y ? 1 : 0)) + .map((reaction) => this.client.utils.capitalize(reaction)) + .join('\n- ')}\n\n🔗 *Usage:* ${this.client.config.prefix}reaction (reaction) [tag/quote user] | ${ + this.client.config.prefix + }(reaction) [tag/quote user]\nExample: ${this.client.config.prefix}pat` + ) + const reaction = (flag ? command : context.split(' ')[0].trim().toLowerCase()) as reaction + if (!flag && !reactions.includes(reaction)) + return void M.reply( + `Invalid reaction. Use *${this.client.config.prefix}react* to see all of the available reactions` + ) + const users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + while (users.length < 1) users.push(M.sender.jid) + const reactant = users[0] + const single = reactant === M.sender.jid + const { url, words } = await new Reaction().getReaction(reaction, single) + return void (await M.reply( + await this.client.utils.gifToMp4(await this.client.utils.getBuffer(url)), + 'video', + true, + undefined, + `*@${M.sender.jid.split('@')[0]} ${words} ${single ? 'Themselves' : `@${reactant.split('@')[0]}`}*`, + [M.sender.jid, reactant] + )) + } +} diff --git a/src/Commands/Fun/Ship.ts b/src/Commands/Fun/Ship.ts new file mode 100644 index 0000000..412b4e2 --- /dev/null +++ b/src/Commands/Fun/Ship.ts @@ -0,0 +1,48 @@ +import { Ship, IShipOptions } from '@shineiichijo/canvas-chan' +import { Message, Command, BaseCommand } from '../../Structures' + +@Command('ship', { + description: 'Ship People! ♥', + cooldown: 15, + exp: 50, + category: 'fun', + usage: 'ship [tag users]' +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + const shipArray: IShipOptions[] = [] + let users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + while (users.length < 2) users.push(M.sender.jid) + if (users.includes(M.sender.jid)) users = users.reverse() + for (const user of users) { + const name = this.client.contact.getContact(user).username + let image!: string + try { + image = + (await this.client.profilePictureUrl(user, 'image')) || + 'https://upload.wikimedia.org/wikipedia/commons/a/ac/Default_pfp.jpg' + } catch (error) { + image = 'https://upload.wikimedia.org/wikipedia/commons/a/ac/Default_pfp.jpg' + } + shipArray.push({ name, image }) + } + const percentage = Math.floor(Math.random() * 101) + let text = '' + if (percentage >= 0 && percentage < 10) text = 'Awful' + else if (percentage >= 10 && percentage < 25) text = 'Very Bad' + else if (percentage >= 25 && percentage < 40) text = 'Poor' + else if (percentage >= 40 && percentage < 55) text = 'Average' + else if (percentage >= 55 && percentage < 75) text = 'Good' + else if (percentage >= 75 && percentage < 90) text = 'Great' + else if (percentage >= 90) text = 'Amazing' + const image = new Ship(shipArray, percentage, text) + let caption = `\t❣️ *Matchmaking...* ❣️ \n` + caption += `\t\t---------------------------------\n` + caption += `@${users[0].split('@')[0]} x @${users[1].split('@')[0]}\n` + caption += `\t\t---------------------------------\n` + caption += `\t\t\t\t\t${percentage < 40 ? '💔' : percentage < 75 ? '❤' : '💗'} *ShipCent: ${percentage}%*\n` + caption += text + return void (await M.reply(await image.build(), 'image', undefined, undefined, caption, [users[0], users[1]])) + } +} diff --git a/src/Commands/Fun/Simp.ts b/src/Commands/Fun/Simp.ts new file mode 100644 index 0000000..0b7528b --- /dev/null +++ b/src/Commands/Fun/Simp.ts @@ -0,0 +1,46 @@ +import { Simp } from '@shineiichijo/canvas-chan' +import { MessageType, proto } from '@adiwajshing/baileys' +import { Command, BaseCommand, Message } from '../../Structures' + +@Command('simp', { + description: 'Makes a person simp', + cooldown: 15, + usage: 'simp [tag/quote user || quote/caption image]', + category: 'fun', + exp: 10 +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + let buffer!: Buffer + if (M.hasSupportedMediaMessage && (Object.keys(M.message) as MessageType[]).includes('imageMessage')) + buffer = await M.downloadMediaMessage(M.message.message as proto.IMessage) + else if (M.mentioned.length) { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.mentioned[0], 'image') + } catch (error) { + return void M.reply("Can't access to the tagged user's profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } else if (M.quoted) { + if (!M.quoted.hasSupportedMediaMessage) { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.quoted.sender.jid, 'image') + } catch (error) { + return void M.reply("Can't access to the quoted user's profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } else buffer = await M.downloadMediaMessage(M.quoted.message) + } else { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.sender.jid, 'image') + } catch (error) { + return void M.reply("Can't access to the your profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } + return void (await M.reply(await new Simp(buffer).build(), 'image')) + } +} diff --git a/src/Commands/Fun/Triggered.ts b/src/Commands/Fun/Triggered.ts new file mode 100644 index 0000000..8908c09 --- /dev/null +++ b/src/Commands/Fun/Triggered.ts @@ -0,0 +1,51 @@ +import { Triggered } from '@shineiichijo/canvas-chan' +import { MessageType, proto } from '@adiwajshing/baileys' +import { Command, Message, BaseCommand } from '../../Structures' + +@Command('triggered', { + description: 'Makes a triggered gif of the tagged/quoted user or the provided/quoted image', + cooldown: 10, + usage: 'triggered [tag/quote user or caption/quote image]', + category: 'fun', + exp: 25, + aliases: ['trigger'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + let buffer!: Buffer + if (M.hasSupportedMediaMessage && (Object.keys(M.message) as MessageType[]).includes('imageMessage')) + buffer = await M.downloadMediaMessage(M.message.message as proto.IMessage) + else if (M.mentioned.length) { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.mentioned[0], 'image') + } catch (error) { + return void M.reply("Can't access to the tagged user's profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } else if (M.quoted) { + if (!M.quoted.hasSupportedMediaMessage) { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.quoted.sender.jid, 'image') + } catch (error) { + return void M.reply("Can't access to the quoted user's profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } else buffer = await M.downloadMediaMessage(M.quoted.message) + } else { + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(M.sender.jid, 'image') + } catch (error) { + return void M.reply("Can't access to the your profile picture") + } + buffer = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + } + return void (await M.reply( + await this.client.utils.gifToMp4(await new Triggered(buffer).build()), + 'video', + true + )) + } +} diff --git a/src/Commands/General/Help.ts b/src/Commands/General/Help.ts new file mode 100644 index 0000000..e731c69 --- /dev/null +++ b/src/Commands/General/Help.ts @@ -0,0 +1,56 @@ +import { BaseCommand, Command, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('help', { + description: "Displays the bot's usable commands", + aliases: ['h'], + cooldown: 10, + exp: 20, + usage: 'help || help ', + category: 'general' +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) { + let commands = Array.from(this.handler.commands, ([command, data]) => ({ + command, + data + })).filter((command) => command.data.config.category !== 'dev') + const { nsfw } = await this.client.DB.getGroup(M.from) + if (!nsfw) commands = commands.filter(({ data }) => data.config.category !== 'nsfw') + let text = `👋🏻 (💙ω💙) Konichiwa! *@${M.sender.jid.split('@')[0]}*, I'm ${ + this.client.config.name + }\nMy prefix is - "${this.client.config.prefix}"\n\nThe usable commands are listed below.` + const categories: string[] = [] + for (const command of commands) { + if (categories.includes(command.data.config.category)) continue + categories.push(command.data.config.category) + } + for (const category of categories) { + const categoryCommands: string[] = [] + const filteredCommands = commands.filter((command) => command.data.config.category === category) + text += `\n\n*━━━❰ ${this.client.utils.capitalize(category)} ❱━━━*\n\n` + filteredCommands.forEach((command) => categoryCommands.push(command.data.name)) + text += `\`\`\`${categoryCommands.join(', ')}\`\`\`` + } + text += `\n\n📕 *Note:* Use ${this.client.config.prefix}help for more info of a specific command. Example: *${this.client.config.prefix}help hello*` + return void (await M.reply(text, 'text', undefined, undefined, undefined, [M.sender.jid])) + } else { + const cmd = context.trim().toLowerCase() + const command = this.handler.commands.get(cmd) || this.handler.aliases.get(cmd) + if (!command) return void M.reply(`No command found | *"${context.trim()}"*`) + return void M.reply( + `🎐 *Command:* ${this.client.utils.capitalize(command.name)}\n🎴 *Aliases:* ${ + !command.config.aliases + ? '' + : command.config.aliases.map((alias) => this.client.utils.capitalize(alias)).join(', ') + }\n🔗 *Category:* ${this.client.utils.capitalize(command.config.category)}\n⏰ *Cooldown:* ${ + command.config.cooldown ?? 3 + }s\n🎗 *Usage:* ${command.config.usage + .split('||') + .map((usage) => `${this.client.config.prefix}${usage.trim()}`) + .join(' | ')}\n🧧 *Description:* ${command.config.description}` + ) + } + } +} diff --git a/src/Commands/General/Hi.ts b/src/Commands/General/Hi.ts new file mode 100644 index 0000000..e0762eb --- /dev/null +++ b/src/Commands/General/Hi.ts @@ -0,0 +1,14 @@ +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('hi', { + description: 'Says hello to the bot', + category: 'general', + usage: 'hi', + aliases: ['hello'], + exp: 25, + cooldown: 5 +}) +export default class extends BaseCommand { + public override execute = async ({ sender, reply }: Message): Promise => + void (await reply(`Hello! *${sender.username}*`)) +} diff --git a/src/Commands/General/Info.ts b/src/Commands/General/Info.ts new file mode 100644 index 0000000..03a85e9 --- /dev/null +++ b/src/Commands/General/Info.ts @@ -0,0 +1,28 @@ +import { join } from 'path' +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('info', { + description: "Displays bot's info", + usage: 'info', + category: 'general', + cooldown: 10, + exp: 100 +}) +export default class extends BaseCommand { + public override execute = async ({ reply }: Message): Promise => { + const { description, name, homepage } = require(join(__dirname, '..', '..', '..', 'package.json')) as { + description: string + homepage: string + name: string + } + const image = this.client.assets.get('whatsapp-bot') as Buffer + const uptime = this.client.utils.formatSeconds(process.uptime()) + const text = `🌟 *WhatsApp-bot* 🌟\n\n📙 *Description: ${description}*\n\n🔗 *Commands:* ${this.handler.commands.size}\n\n🚦 *Uptime:* ${uptime}` + return void (await reply(image, 'image', undefined, undefined, text, undefined, { + title: this.client.utils.capitalize(name), + thumbnail: image, + mediaType: 1, + sourceUrl: homepage + })) + } +} diff --git a/src/Commands/General/Mods.ts b/src/Commands/General/Mods.ts new file mode 100644 index 0000000..dbc997c --- /dev/null +++ b/src/Commands/General/Mods.ts @@ -0,0 +1,22 @@ +import { Message, Command, BaseCommand } from '../../Structures' + +@Command('mods', { + description: "Displays the bot's moderators", + exp: 20, + cooldown: 5, + dm: true, + category: 'general', + usage: 'mods', + aliases: ['mod', 'owner', 'moderators'] +}) +export default class extends BaseCommand { + public override execute = async ({ reply }: Message): Promise => { + if (!this.client.config.mods.length) return void reply('*[UNMODERATED]*') + let text = `🤍 *${this.client.config.name} Moderators* 🖤\n` + for (let i = 0; i < this.client.config.mods.length; i++) + text += `\n*#${i + 1}*\n🎐 *Username:* ${ + this.client.contact.getContact(this.client.config.mods[i]).username + }\n🔗 *Contact: https://wa.me/+${this.client.config.mods[i].split('@')[0]}*` + return void (await reply(text)) + } +} diff --git a/src/Commands/General/Profile.ts b/src/Commands/General/Profile.ts new file mode 100644 index 0000000..4ea6733 --- /dev/null +++ b/src/Commands/General/Profile.ts @@ -0,0 +1,45 @@ +import { BaseCommand, Command, Message } from '../../Structures' +import { getStats } from '../../lib' + +@Command('profile', { + description: "Displays user's profile", + category: 'general', + aliases: ['p'], + cooldown: 15, + exp: 30, + usage: 'profile [tag/quote users]' +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + const users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + while (users.length < 1) users.push(M.sender.jid) + const user = users[0] + const username = user === M.sender.jid ? M.sender.username : this.client.contact.getContact(user).username + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(user, 'image') + } catch { + pfpUrl = undefined + } + const pfp = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + let bio!: string + try { + bio = (await this.client.fetchStatus(user))?.status || '' + } catch (error) { + bio = '' + } + const { banned, experience, level, tag } = await this.client.DB.getUser(user) + const admin = this.client.utils.capitalize(`${M.groupMetadata?.admins?.includes(user) || false}`) + const { rank } = getStats(level) + return void M.reply( + pfp, + 'image', + undefined, + undefined, + `🏮 *Username:* ${username}#${tag}\n\n🎫 *Bio:* ${bio}\n\n🌟 *Experience:* ${experience}\n\n🥇 *Rank:* ${rank}\n\n🍀 *Level:* ${level}\n\n👑 *Admin:* ${admin}\n\n🟥 *Banned:* ${this.client.utils.capitalize( + `${banned || false}` + )}` + ) + } +} diff --git a/src/Commands/General/Rank.ts b/src/Commands/General/Rank.ts new file mode 100644 index 0000000..08f20dc --- /dev/null +++ b/src/Commands/General/Rank.ts @@ -0,0 +1,49 @@ +import { Rank } from 'canvacord' +import { getStats } from '../../lib' +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('rank', { + description: "Displays user's rank", + category: 'general', + exp: 20, + cooldown: 10, + aliases: ['card'], + usage: 'rank [tag/quote user]' +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + const users = M.mentioned + if (M.quoted && !users.includes(M.quoted.sender.jid)) users.push(M.quoted.sender.jid) + while (users.length < 1) users.push(M.sender.jid) + const user = users[0] + const username = user === M.sender.jid ? M.sender.username : this.client.contact.getContact(user).username + let pfpUrl: string | undefined + try { + pfpUrl = await this.client.profilePictureUrl(user, 'image') + } catch { + pfpUrl = undefined + } + const pfp = pfpUrl ? await this.client.utils.getBuffer(pfpUrl) : (this.client.assets.get('404') as Buffer) + const { experience, level, tag } = await this.client.DB.getUser(user) + const { requiredXpToLevelUp, rank } = getStats(level) + const card = await new Rank() + .setAvatar(pfp) + .setLevel(1, '', false) + .setCurrentXP(experience) + .setRequiredXP(requiredXpToLevelUp) + .setProgressBar(this.client.utils.generateRandomHex()) + .setDiscriminator(tag, this.client.utils.generateRandomHex()) + .setUsername(username, this.client.utils.generateRandomHex()) + .setBackground('COLOR', this.client.utils.generateRandomHex()) + .setRank(1, '', false) + .renderEmojis(true) + .build({ fontX: 'arial', fontY: 'arial' }) + return void (await M.reply( + card, + 'image', + undefined, + undefined, + `🏮 *Username:* ${username}#${tag}\n\n🌟 *Experience: ${experience} / ${requiredXpToLevelUp}*\n\n🥇 *Rank:* ${rank}\n\n🍀 *Level:* ${level}` + )) + } +} diff --git a/src/Commands/Media/Lyrics.ts b/src/Commands/Media/Lyrics.ts new file mode 100644 index 0000000..4c49432 --- /dev/null +++ b/src/Commands/Media/Lyrics.ts @@ -0,0 +1,31 @@ +import { Lyrics } from '../../lib' +import { Message, Command, BaseCommand } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('lyrics', { + description: 'Sends the lyrics of a given song', + usage: 'lyrics [song]', + cooldown: 10, + exp: 20, + category: 'media' +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void (await M.reply('Provide the name of the song to search the lyrics')) + const term = context.trim() + const lyrics = new Lyrics() + const data = await lyrics.search(term) + if (!data.length) return void (await M.reply(`Couldn't find any lyrics | *"${term}"*`)) + const buffer = await this.client.utils.getBuffer(data[0].image) + let text = `🌿 *Title:* ${data[0].title} *(${data[0].fullTitle})*\n🍥 *Artist:* ${data[0].artist}` + text += `\n\n${await lyrics.parseLyrics(data[0].url)}` + return void (await M.reply(buffer, 'image', undefined, undefined, text, undefined, { + title: data[0].title, + body: data[0].fullTitle, + thumbnail: buffer, + sourceUrl: data[0].url, + mediaType: 1, + mediaUrl: data[0].url + })) + } +} diff --git a/src/Commands/Media/Play.ts b/src/Commands/Media/Play.ts new file mode 100644 index 0000000..c08c82b --- /dev/null +++ b/src/Commands/Media/Play.ts @@ -0,0 +1,28 @@ +import yts from 'yt-search' +import { YT } from '../../lib' +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('play', { + description: 'Plays a song of the given term from YouTube', + cooldown: 15, + exp: 35, + category: 'media', + usage: 'play [term]' +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void M.reply('Provide a term to play, Baka!') + const term = context.trim() + const { videos } = await yts(term) + if (!videos || !videos.length) return void M.reply(`No matching songs found | *"${term}"*`) + const buffer = await new YT(videos[0].url, 'audio').download() + return void (await M.reply(buffer, 'audio', undefined, undefined, undefined, undefined, { + title: videos[0].title, + thumbnail: await this.client.utils.getBuffer(videos[0].thumbnail), + mediaType: 2, + body: videos[0].description, + mediaUrl: videos[0].url + })) + } +} diff --git a/src/Commands/Media/Spotify.ts b/src/Commands/Media/Spotify.ts new file mode 100644 index 0000000..9825c7a --- /dev/null +++ b/src/Commands/Media/Spotify.ts @@ -0,0 +1,28 @@ +import TrackDetails from 'spotifydl-core/dist/lib/details/Track' +import { Spotify } from '../../lib' +import { Command, BaseCommand, Message } from '../../Structures' + +@Command('spotify', { + description: 'Downloads and sends the track of thr given spotify track URL', + aliases: ['sp'], + usage: 'spotify [track_url]', + cooldown: 10, + category: 'media', + exp: 25 +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + M.urls = M.urls.filter((url) => url.includes('open.spotify.com')) + if (!M.urls.length) return void M.reply('Provide a spotify track URL to download, Baka!') + const spotify = new Spotify(M.urls[0]) + const info = await spotify.getInfo() + if ((info as { error: string }).error) return void M.reply('Provide a valid spotify track URL, Baka!') + const { name, artists, album_name, release_date, cover_url } = info as TrackDetails + const text = `🎧 *Title:* ${name || ''}\n🎤 *Artists:* ${(artists || []).join( + ',' + )}\n💽 *Album:* ${album_name}\n📆 *Release Date:* ${release_date || ''}` + await M.reply(await this.client.utils.getBuffer(cover_url), 'image', undefined, undefined, text) + const buffer = await spotify.download() + return void (await M.reply(buffer, 'audio')) + } +} diff --git a/src/Commands/Media/YTAudio.ts b/src/Commands/Media/YTAudio.ts new file mode 100644 index 0000000..d1e5c5c --- /dev/null +++ b/src/Commands/Media/YTAudio.ts @@ -0,0 +1,22 @@ +import { YT } from '../../lib' +import { Message, Command, BaseCommand } from '../../Structures' + +@Command('yta', { + description: 'Downloads and sends the video as an audio of the provided YouTube video link', + cooldown: 10, + category: 'media', + exp: 25, + usage: 'yta [link]', + aliases: ['ytaudio'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + const urls = M.urls.filter((url) => url.includes('youtube.com') || url.includes('youtu.be')) + if (!urls.length) return void M.reply('Provide a YouTube video URL to download, Baka!') + const url = urls[0] + const { validate, download } = new YT(url, 'audio') + if (!validate()) return void M.reply('Provide a valid YouTube video URL, Baka!') + const audio = await download() + return void (await M.reply(audio, 'audio')) + } +} diff --git a/src/Commands/Media/YTSearch.ts b/src/Commands/Media/YTSearch.ts new file mode 100644 index 0000000..ee93c55 --- /dev/null +++ b/src/Commands/Media/YTSearch.ts @@ -0,0 +1,33 @@ +import yts from 'yt-search' +import { Message, Command, BaseCommand } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('yts', { + description: 'Searches the video of the given query in YouTube', + category: 'media', + cooldown: 10, + exp: 10, + usage: 'yts [query]', + aliases: ['ytsearch'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void M.reply('Provide a query, Baka!') + const query = context.trim() + const { videos } = await yts(query) + if (!videos || !videos.length) return void M.reply(`No videos found | *"${query}"*`) + let text = '' + const length = videos.length >= 10 ? 10 : videos.length + for (let i = 0; i < length; i++) + text += `*#${i + 1}*\n📗 *Title: ${videos[i].title}*\n📕 *Channel: ${ + videos[i].author.name + }*\n📙 *Duration: ${videos[i].seconds}s*\n🔗 *URL: ${videos[i].url}*\n\n` + return void (await M.reply(text, 'text', undefined, undefined, undefined, undefined, { + title: videos[0].title, + thumbnail: await this.client.utils.getBuffer(videos[0].thumbnail), + mediaType: 2, + body: videos[0].description, + mediaUrl: videos[0].url + })) + } +} diff --git a/src/Commands/Media/YTVideo.ts b/src/Commands/Media/YTVideo.ts new file mode 100644 index 0000000..156938d --- /dev/null +++ b/src/Commands/Media/YTVideo.ts @@ -0,0 +1,55 @@ +import { YT } from '../../lib' +import { Message, Command, BaseCommand } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('ytv', { + description: 'Downloads and sends the video of the provided YouTube video link', + cooldown: 10, + category: 'media', + exp: 25, + usage: 'ytv [link] --quality=[low/medium/high]', + aliases: ['ytvideo'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags }: IArgs): Promise => { + const urls = M.urls.filter((url) => url.includes('youtube.com') || url.includes('youtu.be')) + if (!urls.length) return void M.reply('Provide a YouTube video URL to download, Baka!') + const url = urls[0] + const { validate, download, getInfo } = new YT(url) + if (!validate()) return void M.reply('Provide a valid YouTube video URL, Baka!') + flags = flags.filter((flag) => flag.startsWith('--quality=')) + const qualities = ['low', 'medium', 'high'] + const quality = ( + flags[0] && flags[0].split('=')[1] !== '' && qualities.includes(flags[0].split('=')[1].toLowerCase()) + ? flags[0].split('=')[1].toLowerCase() + : 'medium' + ) as 'low' | 'medium' | 'high' + const { videoDetails } = await getInfo() + if (Number(videoDetails.lengthSeconds) > 1800) return void M.reply('The video is too long to send') + const video = await download(quality) + const text = `📗 *Title: ${videoDetails.title}*\n📕 *Channel: ${videoDetails.author.name}*\n📙 *Duration: ${videoDetails.lengthSeconds}s*` + return void (await M.reply(video, 'video', undefined, undefined, text).catch(async () => { + await M.reply("Sending the video as Document as the video's too big") + setTimeout(async () => { + await M.reply( + await this.client.utils.getBuffer(videoDetails.thumbnails[0].url), + 'image', + undefined, + undefined, + text + ) + return void (await M.reply( + video, + 'document', + undefined, + 'video/mp4', + undefined, + undefined, + undefined, + await this.client.utils.getBuffer(videoDetails.thumbnails[0].url), + `${videoDetails.title}.mp4` + )) + }, 3000) + })) + } +} diff --git a/src/Commands/Moderation/Ping.ts b/src/Commands/Moderation/Ping.ts new file mode 100644 index 0000000..a2f4afa --- /dev/null +++ b/src/Commands/Moderation/Ping.ts @@ -0,0 +1,63 @@ +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('ping', { + description: 'Tags all of the members in a group', + usage: 'ping (--tags=hidden)', + category: 'moderation', + exp: 35, + cooldown: 15, + aliases: ['all', 'tagall', 'everyone'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags, context }: IArgs): Promise => { + if (!M.groupMetadata) return void M.reply('*Try Again!*') + const hidden = this.getPingOptions(flags) + flags.forEach((flag) => (context = context.replace(flag, ''))) + const message = context ? context.trim() : M.quoted ? M.quoted.content : '' + let text = `${message !== '' ? `🧧 *Message: ${message}*\n\n` : ''}🍀 *Group:* ${ + M.groupMetadata.subject + }\n🎈 *Members:* ${M.groupMetadata.participants.length}\n📣 *Tagger: @${ + M.sender.jid.split('@')[0] + }*\n🔖 *Tags:* ${hidden ? '*[HIDDEN]*' : '\n'}` + const botJid = this.client.correctJid(this.client.user?.id || '') + if (!hidden) { + text += `\n🍁 *@${botJid.split('@')[0]}*` + const mods: string[] = [] + const admins: string[] = [] + const members: string[] = [] + for (const jid of M.groupMetadata.participants.map((x) => x.id)) { + if (jid === M.sender.jid || jid === botJid) continue + if (this.client.config.mods.includes(jid)) { + mods.push(jid) + continue + } + if (M.groupMetadata.admins?.includes(jid)) { + admins.push(jid) + continue + } + members.push(jid) + } + for (let i = 0; i < mods.length; i++) text += `${i === 0 ? '\n\n' : '\n'}🌟 *@${mods[i].split('@')[0]}*` + for (let i = 0; i < admins.length; i++) text += `${i === 0 ? '\n\n' : '\n'}💈 *@${admins[i].split('@')[0]}*` + for (let i = 0; i < members.length; i++) + text += `${i === 0 ? '\n\n' : '\n'}🎗 *@${members[i].split('@')[0]}*` + } + return void (await M.reply( + text, + 'text', + undefined, + undefined, + undefined, + M.groupMetadata.participants.map((x) => x.id) + )) + } + + private getPingOptions = (flags: string[]): boolean => { + if (!flags.length) return false + const taggingFlags = flags.filter((flag) => flag.startsWith('--tags=')) + let hidden = false + if (taggingFlags.length && taggingFlags[0].split('=')[1].toLowerCase().includes('hidden')) hidden = true + return hidden + } +} diff --git a/src/Commands/Moderation/Set.ts b/src/Commands/Moderation/Set.ts new file mode 100644 index 0000000..8cb53a8 --- /dev/null +++ b/src/Commands/Moderation/Set.ts @@ -0,0 +1,85 @@ +import { proto } from '@adiwajshing/baileys' +import { Message, BaseCommand, Command } from '../../Structures' +import { IArgs, GroupFeatures } from '../../Types' + +@Command('set', { + description: 'Enables/Disables a certain group feature', + usage: 'set', + cooldown: 5, + category: 'moderation', + exp: 25, + aliases: ['feature'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags }: IArgs): Promise => { + const features = Object.keys(GroupFeatures) as (keyof typeof GroupFeatures)[] + if (!flags.length) { + const sections: proto.ISection[] = [] + let text = '🍁 *Available Features*' + for (const feature of features) { + const rows: proto.IRow[] = [] + rows.push( + { + title: `Enable ${this.client.utils.capitalize(feature)}`, + rowId: `${this.client.config.prefix}set --${feature}=true` + }, + { + title: `Disable ${this.client.utils.capitalize(feature)}`, + rowId: `${this.client.config.prefix}set --${feature}=false` + } + ) + sections.push({ title: this.client.utils.capitalize(feature), rows }) + text += `\n\n☘ *Feature:* ${this.client.utils.capitalize(feature)}\n📄 *Description:* ${ + GroupFeatures[feature] + }` + } + return void M.reply( + text, + 'text', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + sections, + buttonText: 'Group Features' + } + ) + } else { + const options = flags[0].trim().toLowerCase().split('=') + const feature = options[0].replace('--', '') as keyof typeof GroupFeatures + const actions = ['true', 'false'] + if (!features.includes(feature)) + return void M.reply( + `Invalid feature. Use *${this.client.config.prefix}set* to see all of the available features` + ) + const action = options[1] + if (!action || !actions.includes(action)) + return void M.reply( + `${ + action + ? `Invalid option. It should be one of them: *${actions + .map(this.client.utils.capitalize) + .join(', ')}*.` + : `Provide the option to be set of this feature.` + } Example: *${this.client.config.prefix}set --${feature}=true*` + ) + const data = await this.client.DB.getGroup(M.from) + if ((action === 'true' && data[feature]) || (action === 'false' && !data[feature])) + return void M.reply( + `🟨 *${this.client.utils.capitalize(feature)} is already ${ + action === 'true' ? 'Enabled' : 'Disabled' + }*` + ) + await this.client.DB.updateGroup(M.from, feature, action === 'true') + return void M.reply( + `${action === 'true' ? '🟩' : '🟥'} *${this.client.utils.capitalize(feature)} is now ${ + action === 'true' ? 'Enabled' : 'Disabled' + }*` + ) + } + } +} diff --git a/src/Commands/Nsfw/NHentai.ts b/src/Commands/Nsfw/NHentai.ts new file mode 100644 index 0000000..09d0420 --- /dev/null +++ b/src/Commands/Nsfw/NHentai.ts @@ -0,0 +1,186 @@ +import { NHentai } from '@shineiichijo/nhentai-ts' +import { delay, proto } from '@adiwajshing/baileys' +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('nhentai', { + description: 'Search or download a doujin from nhentai', + cooldown: 15, + exp: 40, + category: 'nsfw', + aliases: ['doujin', 'doujinshi'], + usage: 'nhentai [query]' +}) +export default class command extends BaseCommand { + public override execute = async (M: Message, args: IArgs): Promise => { + args.flags.forEach((flag) => (args.context = args.context.replace(flag, ''))) + args.flags = args.flags.filter( + (flag) => + flag.startsWith('--type=') || + flag.startsWith('--get=') || + flag.startsWith('--id=') || + flag.startsWith('--page=') + ) + const options = this.getOptions(args.flags) + const nhentai = new NHentai() + switch (options.type) { + case 'search': + return await this.handleSearch(M, args, options, nhentai) + case 'get': + return await this.handleDownload(M, options, nhentai) + } + } + + private handleSearch = async ( + M: Message, + { context }: IArgs, + { page }: TOption, + nhentai: NHentai + ): Promise => { + if (!context) return void M.reply('Provide a query for the search') + return await nhentai + .search(context.trim(), { page }) + .then(async ({ data, pagination }) => { + const sections: proto.ISection[] = [] + const paginationRows: proto.IRow[] = [] + if (pagination.currentPage > 1) + paginationRows.push({ + title: 'Previous Page', + rowId: `${this.client.config.prefix}nhentai ${context.trim()} --type=search --page=${ + pagination.currentPage - 1 + }`, + description: 'Returns to the previous page of the search' + }) + if (pagination.hasNextPage) + paginationRows.push({ + title: 'Next Page', + rowId: `${this.client.config.prefix}nhentai ${context.trim()} --type=search --page=${ + pagination.currentPage + 1 + }`, + description: 'Goes to the next page of the search' + }) + if (paginationRows.length) sections.push({ title: 'Pagination', rows: paginationRows }) + let text = '' + data.forEach((content, i) => { + const rows: proto.IRow[] = [] + rows.push( + { + title: 'Get PDF', + rowId: `${this.client.config.prefix}nhentai --type=get --id=${content.id} --get=pdf` + }, + { + title: 'Get ZIP', + rowId: `${this.client.config.prefix}nhentai --type=get --id=${content.id} --get=zip` + } + ) + text += `${i === 0 ? '' : '\n\n'}*#${i + 1}*\n📕 *Title:* ${ + content.title + }\n🌐 *URL: ${content.url.replace('to', 'net')}*` + sections.push({ title: content.title, rows }) + }) + return void (await M.reply( + text, + 'text', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + sections, + buttonText: 'Search Results' + } + )) + }) + .catch(async () => void (await M.reply(`Couldn't find any doujin | *"${context.trim()}"*`))) + } + + private handleDownload = async (M: Message, { id, output }: TOption, nhentai: NHentai): Promise => { + if (id === '') return void M.reply('Provide the id of the doujin that you want to download') + if (output === '') output = 'pdf' + const valid = await nhentai.validate(id) + if (!valid) return void M.reply(`Invaid Doujin ID | *"${id}"*`) + await delay(1500) + return await nhentai + .getDoujin(id) + .then(async (res) => { + const { title, originalTitle, cover, url, tags, images, artists } = res + const thumbnail = await this.client.utils.getBuffer(cover || 'https://i.imgur.com/uLAimaY.png') + await M.reply( + thumbnail, + 'image', + undefined, + undefined, + `📕 *Title:* ${title} *(${originalTitle})*\n✍ *Artists:* ${artists}\n🔖 *Tags:* ${tags + .map(this.client.utils.capitalize) + .join(', ')}\n📚 *Pages:* ${images.pages.length}`, + undefined, + { + title, + body: originalTitle, + thumbnail, + mediaType: 1, + sourceUrl: url.replace('to', 'net') + } + ) + const buffer = await images[output === 'zip' ? 'zip' : 'PDF']() + return void (await M.reply( + buffer, + 'document', + undefined, + `application/${output}`, + undefined, + undefined, + undefined, + thumbnail, + `${title}.${output}` + )) + }) + .catch(async () => void (await M.reply(`*Try Again!*`))) + } + + private getOptions = ( + flags: string[] + ): { type: 'search' | 'get'; page: number; id: string; output: '' | 'pdf' | 'zip' } => { + return { + type: this.getType(flags), + page: this.getPage(flags), + id: this.getID(flags), + output: this.getOutput(flags) + } + } + private getType = (flags: string[]): 'search' | 'get' => { + const index = this.getIndex(flags, '--type=') + if (index < 0 || !['search', 'get'].includes(flags[index].split('=')[1].toLowerCase())) return 'search' + return flags[index].split('=')[1].toLowerCase() as 'search' | 'get' + } + + private getPage = (flags: string[]): number => { + const index = this.getIndex(flags, '--page=') + if ( + index < 0 || + isNaN(Number(flags[index].split('--page=')[1])) || + Number(flags[index].split('--page=')[1]) < 1 + ) + return 1 + return Number(flags[index].split('--page=')[1]) + } + + private getID = (flags: string[]): string => { + const index = this.getIndex(flags, '--id=') + if (index < 0 || flags[index].split('--id=')[1] === '') return '' + return flags[index].split('--id=')[1] + } + + private getOutput = (flags: string[]): '' | 'zip' | 'pdf' => { + const index = this.getIndex(flags, '--get=') + if (index < 0 || !['zip', 'pdf'].includes(flags[index].split('--get=')[1].toLowerCase())) return '' + return flags[index].split('--get=')[1].toLowerCase() as 'zip' | 'pdf' + } + + private getIndex = (array: string[], search: string): number => array.findIndex((el) => el.startsWith(search)) +} + +type TOption = ReturnType diff --git a/src/Commands/Utils/Prettier.ts b/src/Commands/Utils/Prettier.ts new file mode 100644 index 0000000..b519ea8 --- /dev/null +++ b/src/Commands/Utils/Prettier.ts @@ -0,0 +1,72 @@ +import { format, LiteralUnion, BuiltInParserName } from 'prettier' +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +const supportedLang = ['json', 'ts', 'js', 'css', 'md', 'yaml', 'html'] + +@Command('prettier', { + description: 'Runs prettier of the given code', + category: 'utils', + exp: 50, + cooldown: 15, + usage: `prettier --lang[${supportedLang.join(', ')}] [provide/quote the message containing the code]`, + aliases: ['format'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags, context }: IArgs): Promise => { + flags.forEach((flag) => (context = context.replace(flag, ''))) + if (!context && (!M.quoted || M.quoted.content === '')) + return void M.reply( + `Provide or quote a message containing the code that you want to run prettier along with the language and options. Example: *${this.client.config.prefix}prettier --lang=ts --no-semi --single-quote *[quotes a message containing the code]**` + ) + const langFlag = flags.filter((flag) => flag.startsWith('--lang=') || flag.startsWith('--language='))[0] + let lang = 'js' + if (langFlag) lang = langFlag.split('=')[1] + const parser = this.getParserFromLanguage(lang) + try { + const formattedCode = format(context || (M.quoted?.content as string), { + parser, + semi: parser !== 'babel' && parser !== 'babel-ts', + singleQuote: parser !== 'babel' && parser !== 'babel-ts' + }) + return void (await M.reply(`\`\`\`${formattedCode}\`\`\``)) + } catch (error) { + await M.reply(`${(error as any).message}`) + return void (await M.reply( + `If the code's not wrong, try changing the languages to: \`\`\`${supportedLang.join(', ')}\`\`\`` + )) + } + } + + private getParserFromLanguage = (lang: string): LiteralUnion => { + let parser + switch (lang.toLowerCase().trim()) { + default: + case 'js': + case 'javascript': + parser = 'babel' + break + case 'css': + parser = 'css' + break + case 'html': + parser = 'html' + break + case 'json': + parser = 'json' + break + case 'ts': + case 'typescript': + parser = 'babel-ts' + break + case 'md': + case 'markdown': + parser = 'markdown' + break + case 'yaml': + parser = 'markdown' + break + } + return parser + } +} diff --git a/src/Commands/Utils/React.ts b/src/Commands/Utils/React.ts new file mode 100644 index 0000000..3692224 --- /dev/null +++ b/src/Commands/Utils/React.ts @@ -0,0 +1,16 @@ +import { Message, Command, BaseCommand } from '../../Structures' + +@Command('react', { + category: 'utils', + description: 'Reacts a message with the given emoji', + usage: 'react [emoji] || react [emoji] [quote a message]', + cooldown: 5, + exp: 10 +}) +export default class extends BaseCommand { + public override execute = async ({ react, reply, quoted, emojis, message }: Message): Promise => { + if (!emojis.length) return void reply('Provide an emoji to react') + const key = quoted ? quoted.key : message.key + return void (await react(emojis[0], key)) + } +} diff --git a/src/Commands/Utils/Retrieve.ts b/src/Commands/Utils/Retrieve.ts new file mode 100644 index 0000000..4626fdf --- /dev/null +++ b/src/Commands/Utils/Retrieve.ts @@ -0,0 +1,20 @@ +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('retrieve', { + description: 'Retrieves view once message', + category: 'utils', + usage: 'retrieve [quote view once message]', + cooldown: 10, + exp: 40 +}) +export default class extends BaseCommand { + public override execute = async (M: Message): Promise => { + if (!M.quoted || M.quoted.type !== 'viewOnceMessage') + return void M.reply('Quote a view once message to retrieve, Baka!') + const buffer = await M.downloadMediaMessage(M.quoted.message) + const type = Object.keys(M.quoted.message.viewOnceMessage?.message || {})[0].replace('Message', '') as + | 'image' + | 'video' + return void (await M.reply(buffer, type)) + } +} diff --git a/src/Commands/Utils/Sticker.ts b/src/Commands/Utils/Sticker.ts new file mode 100644 index 0000000..171dc1a --- /dev/null +++ b/src/Commands/Utils/Sticker.ts @@ -0,0 +1,95 @@ +import { proto } from '@adiwajshing/baileys' +import { Sticker, Categories } from 'wa-sticker-formatter' +import { Command, Message, BaseCommand } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('sticker', { + description: 'Converts image/video/gif to sticker', + category: 'utils', + exp: 15, + cooldown: 10, + usage: 'sticker [caption/quote message containing media] [options] | | ', + aliases: ['s'] +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { flags, context }: IArgs): Promise => { + if (!M.hasSupportedMediaMessage && !M.quoted?.hasSupportedMediaMessage) + return void M.reply('Provide an image/gif/video by captioning it as a message or by quoting it') + let buffer!: Buffer + if (M.hasSupportedMediaMessage) buffer = await M.downloadMediaMessage(M.message.message as proto.IMessage) + else if (M.quoted && M.quoted.hasSupportedMediaMessage) buffer = await M.downloadMediaMessage(M.quoted.message) + flags.forEach((flag) => (context = context.replace(flag, ''))) + const numbersFlag = this.client.utils + .extractNumbers(flags.join(' ').replace(/\--/g, '')) + .filter((number) => number > 0 && number <= 100) + const quality = + numbersFlag[0] || this.getQualityFromType(flags.filter((flag) => this.qualityTypes.includes(flag))) || 50 + const categories = this.getStickerEmojisFromCategories(flags) + const pack = context.split('|') + const sticker = new Sticker(buffer, { + categories, + pack: pack[1] ? pack[1].trim() : '🤍 Made for you', + author: pack[2] ? pack[2].trim() : `${this.client.config.name} 🖤`, + quality, + type: + flags.includes('--c') || flags.includes('--crop') || flags.includes('--cropped') + ? 'crop' + : flags.includes('--s') || flags.includes('--stretch') || flags.includes('--stretched') + ? 'default' + : flags.includes('--circle') || + flags.includes('--r') || + flags.includes('--round') || + flags.includes('--rounded') + ? 'circle' + : 'full' + }) + return void (await M.reply(await sticker.build(), 'sticker')) + } + + private qualityTypes = ['--low', '--broke', '--medium', '--high'] + + private getQualityFromType = (types: string[]): number | undefined => { + if (!types[0]) return + for (const type of types) { + switch (type) { + case '--broke': + return 1 + case '--low': + return 10 + case '--medium': + return 50 + case '--high': + return 100 + } + } + } + + private getStickerEmojisFromCategories = (flags: string[]): Categories[] => { + const categories: Categories[] = [] + for (const flag of flags) { + if (categories.length >= 3) return categories + switch (flag) { + case '--angry': + categories.push('💢') + break + case '--happy': + categories.push('😄') + break + case '--sad': + categories.push('😭') + break + case '--love': + categories.push('❤️') + break + case '--celebrate': + categories.push('🎉') + break + case '--greet': + categories.push('👋') + break + } + } + if (categories.length < 1) categories.push('✨', '💗') + return categories + } +} diff --git a/src/Commands/Weeb/Anime.ts b/src/Commands/Weeb/Anime.ts new file mode 100644 index 0000000..330d928 --- /dev/null +++ b/src/Commands/Weeb/Anime.ts @@ -0,0 +1,54 @@ +import { Anime } from '@shineiichijo/marika' +import { BaseCommand, Command, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('anime', { + description: 'Searches an anime of the given query in MyAnimeList', + aliases: ['ani'], + category: 'weeb', + usage: 'anime [query]', + exp: 20, + cooldown: 20 +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void M.reply('Provide a query for the search, Baka!') + const query = context.trim() + await new Anime() + .searchAnime(query) + .then(async ({ data }) => { + const result = data[0] + let text = `🎀 *Title:* ${result.title}\n🎋 *Format:* ${ + result.type + }\n📈 *Status:* ${this.client.utils.capitalize( + result.status.toLowerCase().replace(/\_/g, ' ') + )}\n🍥 *Total episodes:* ${result.episodes}\n🎈 *Duration:* ${ + result.duration + }\n🧧 *Genres:* ${result.genres + .map((genre) => genre.name) + .join(', ')}\n✨ *Based on:* ${this.client.utils.capitalize( + result.source.toLowerCase() + )}\n📍 *Studios:* ${result.studios + .map((studio) => studio.name) + .join(', ')}\n🎴 *Producers:* ${result.producers + .map((producer) => producer.name) + .join(', ')}\n💫 *Premiered on:* ${result.aired.from}\n🎗 *Ended on:* ${ + result.aired.to + }\n🎐 *Popularity:* ${result.popularity}\n🎏 *Favorites:* ${result.favorites}\n🎇 *Rating:* ${ + result.rating + }\n🏅 *Rank:* ${result.rank}\n\n` + if (result.background !== null) text += `🎆 *Background:* ${result.background}*\n\n` + text += `❄ *Description:* ${result.synopsis}` + const image = await this.client.utils.getBuffer(result.images.jpg.large_image_url) + return void (await M.reply(image, 'image', undefined, undefined, text, undefined, { + title: result.title, + mediaType: 1, + thumbnail: image, + sourceUrl: result.url + })) + }) + .catch(() => { + return void M.reply(`Couldn't find any anime | *"${query}"*`) + }) + } +} diff --git a/src/Commands/Weeb/Character.ts b/src/Commands/Weeb/Character.ts new file mode 100644 index 0000000..1b7a24a --- /dev/null +++ b/src/Commands/Weeb/Character.ts @@ -0,0 +1,47 @@ +import { Character } from '@shineiichijo/marika' +import { Command, BaseCommand, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('character', { + description: 'Searches a character of the given query in MyAnimeList', + usage: 'character [query]', + category: 'weeb', + aliases: ['chara'], + exp: 20, + cooldown: 15 +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void M.reply('Provide a query for the search, Baka!') + const query = context.trim() + await new Character() + .searchCharacter(query) + .then(async ({ data }) => { + const chara = data[0] + let source!: string + await new Character() + .getCharacterAnime(chara.mal_id) + .then((res) => (source = res.data[0].anime.title)) + .catch(async () => { + await new Character() + .getCharacterManga(chara.mal_id.toString()) + .then((res) => (source = res.data[0].manga.title)) + .catch(() => (source = '')) + }) + let text = `💙 *Name:* ${chara.name}\n💚 *Nicknames:* ${chara.nicknames.join( + ', ' + )}\n💛 *Source:* ${source}` + if (chara.about !== null) text += `\n\n❤ *Description:* ${chara.about}` + const image = await this.client.utils.getBuffer(chara.images.jpg.image_url) + return void (await M.reply(image, 'image', undefined, undefined, text, undefined, { + title: chara.name, + mediaType: 1, + thumbnail: image, + sourceUrl: chara.url + })) + }) + .catch(() => { + return void M.reply(`No character found | *"${query}"*`) + }) + } +} diff --git a/src/Commands/Weeb/Kitsune.ts b/src/Commands/Weeb/Kitsune.ts new file mode 100644 index 0000000..6eb271b --- /dev/null +++ b/src/Commands/Weeb/Kitsune.ts @@ -0,0 +1,17 @@ +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('kitsune', { + description: 'Sends random kitsune image', + category: 'weeb', + usage: 'kitsune', + exp: 20, + cooldown: 5 +}) +export default class extends BaseCommand { + public override execute = async ({ reply }: Message): Promise => { + const { results } = await this.client.utils.fetch<{ + results: { artist_href: string; artist_name: string; source_url: string; url: string }[] + }>('https://nekos.best/api/v2/kitsune') + return void (await reply(await this.client.utils.getBuffer(results[0].url), 'image')) + } +} diff --git a/src/Commands/Weeb/Manga.ts b/src/Commands/Weeb/Manga.ts new file mode 100644 index 0000000..9ed2af3 --- /dev/null +++ b/src/Commands/Weeb/Manga.ts @@ -0,0 +1,45 @@ +import { Manga } from '@shineiichijo/marika' +import { BaseCommand, Command, Message } from '../../Structures' +import { IArgs } from '../../Types' + +@Command('manga', { + description: 'Searches a manga of the given query in MyAnimeList', + category: 'weeb', + exp: 10, + usage: 'manga [query]', + cooldown: 20 +}) +export default class extends BaseCommand { + public override execute = async (M: Message, { context }: IArgs): Promise => { + if (!context) return void M.reply('Provide a query for the search, Baka!') + const query = context.trim() + await new Manga() + .searchManga(query) + .then(async ({ data }) => { + const result = data[0] + let text = `🎀 *Title:* ${result.title}\n🎋 *Format:* ${ + result.type + }\n📈 *Status:* ${this.client.utils.capitalize( + result.status.toLowerCase().replace(/\_/g, ' ') + )}\n🍥 *Total chapters:* ${result.chapters}\n🎈 *Total volumes:* ${ + result.volumes + }\n🧧 *Genres:* ${result.genres.map((genre) => genre.name).join(', ')}\n💫 *Published on:* ${ + result.published.from + }\n🎗 *Ended on:* ${result.published.to}\n🎐 *Popularity:* ${result.popularity}\n🎏 *Favorites:* ${ + result.favorites + }\n🏅 *Rank:* ${result.rank}\n\n` + if (result.background !== null) text += `🎆 *Background:* ${result.background}*\n\n` + text += `❄ *Description:* ${result.synopsis}` + const image = await this.client.utils.getBuffer(result.images.jpg.large_image_url) + return void (await M.reply(image, 'image', undefined, undefined, text, undefined, { + title: result.title, + mediaType: 1, + thumbnail: image, + sourceUrl: result.url + })) + }) + .catch(() => { + return void M.reply(`No manga found | *"${query}"*`) + }) + } +} diff --git a/src/Commands/Weeb/Neko.ts b/src/Commands/Weeb/Neko.ts new file mode 100644 index 0000000..1dad596 --- /dev/null +++ b/src/Commands/Weeb/Neko.ts @@ -0,0 +1,15 @@ +import { BaseCommand, Command, Message } from '../../Structures' + +@Command('neko', { + description: 'Sends a random neko image', + category: 'weeb', + usage: 'neko', + exp: 20, + cooldown: 5 +}) +export default class extends BaseCommand { + public override execute = async ({ reply }: Message): Promise => { + const { url } = await this.client.utils.fetch<{ url: string }>('https://nekos.life/api/v2/img/neko') + return void (await reply(await this.client.utils.getBuffer(url), 'image')) + } +} diff --git a/src/Commands/Weeb/Waifu.ts b/src/Commands/Weeb/Waifu.ts new file mode 100644 index 0000000..4e784bc --- /dev/null +++ b/src/Commands/Weeb/Waifu.ts @@ -0,0 +1,17 @@ +import { Command, BaseCommand, Message } from '../../Structures' + +@Command('waifu', { + description: 'Sends a random waifu image', + category: 'weeb', + usage: 'waifu', + exp: 10, + cooldown: 5 +}) +export default class extends BaseCommand { + public override execute = async ({ reply }: Message): Promise => { + const { images } = await this.client.utils.fetch<{ images: { url: string }[] }>( + 'https://api.waifu.im/random/?selected_tags=waifu' + ) + return void (await reply(await this.client.utils.getBuffer(images[0].url), 'image')) + } +} diff --git a/src/Commands/_Command_Example_.ts b/src/Commands/_Command_Example_.ts new file mode 100644 index 0000000..5478e16 --- /dev/null +++ b/src/Commands/_Command_Example_.ts @@ -0,0 +1,16 @@ +import { BaseCommand, Command, Message } from '../Structures' +import { IArgs } from '../Types' + +@Command('command_name', { + description: 'Description of the command', + category: 'category', + usage: 'example on using the command', + cooldown: 5, + exp: 10, + dm: false +}) +export default class extends BaseCommand { + public override execute = async (M: Message, args: IArgs): Promise => { + //do something + } +} diff --git a/src/Database/Models/Command.ts b/src/Database/Models/Command.ts new file mode 100644 index 0000000..af702e1 --- /dev/null +++ b/src/Database/Models/Command.ts @@ -0,0 +1,28 @@ +import { prop, getModelForClass } from '@typegoose/typegoose' +import { Document } from 'mongoose' + +export class DisabledCommandsSchema { + @prop({ type: String, required: true, unique: true }) + public title!: string + + @prop({ type: () => DisabledCommand, required: true, default: [] }) + public disabledCommands!: DisabledCommand[] +} + +class DisabledCommand { + @prop({ type: String, required: true }) + public command!: string + + @prop({ type: String, required: true }) + public reason!: string + + @prop({ type: String, required: true }) + public disabledBy!: string + + @prop({ type: String, required: true }) + public time!: string +} + +export type TCommandModel = DisabledCommandsSchema & Document + +export const disabledCommandsSchema = getModelForClass(DisabledCommandsSchema) diff --git a/src/Database/Models/Contact.ts b/src/Database/Models/Contact.ts new file mode 100644 index 0000000..f692eed --- /dev/null +++ b/src/Database/Models/Contact.ts @@ -0,0 +1,34 @@ +import { prop, getModelForClass } from '@typegoose/typegoose' +import { Document } from 'mongoose' + +export class Contact { + @prop({ type: String, required: true, unique: true }) + public ID!: string + + @prop({ type: () => contact, required: true, default: [] }) + public data!: contact[] +} + +class contact { + @prop({ type: String, required: true }) + public id!: string + + @prop({ type: String }) + public notify?: string + + @prop({ type: String }) + public name?: string + + @prop({ type: String }) + public verifiedName?: string + + @prop({ type: String }) + public status?: string + + @prop({ type: String }) + public imgUrl?: string +} + +export type TContactModel = Contact & Document + +export const contactSchema = getModelForClass(Contact) diff --git a/src/Database/Models/Group.ts b/src/Database/Models/Group.ts new file mode 100644 index 0000000..61cf38b --- /dev/null +++ b/src/Database/Models/Group.ts @@ -0,0 +1,20 @@ +import { prop, getModelForClass } from '@typegoose/typegoose' +import { Document } from 'mongoose' + +export class GroupSchema { + @prop({ type: String, unique: true, required: true }) + public jid!: string + + @prop({ type: Boolean, required: true, default: false }) + public events!: boolean + + @prop({ type: Boolean, required: true, default: false }) + public mods!: boolean + + @prop({ type: Boolean, required: true, default: false }) + public nsfw!: boolean +} + +export type TGroupModel = GroupSchema & Document + +export const groupSchema = getModelForClass(GroupSchema) diff --git a/src/Database/Models/Session.ts b/src/Database/Models/Session.ts new file mode 100644 index 0000000..a44713f --- /dev/null +++ b/src/Database/Models/Session.ts @@ -0,0 +1,14 @@ +import { prop, getModelForClass } from '@typegoose/typegoose' +import { Document } from 'mongoose' + +export class SessionsSchema { + @prop({ type: String, required: true, unique: true }) + public sessionId!: string + + @prop({ type: String }) + public session?: string +} + +export type TSessionModel = SessionsSchema & Document + +export const sessionSchema = getModelForClass(SessionsSchema) diff --git a/src/Database/Models/User.ts b/src/Database/Models/User.ts new file mode 100644 index 0000000..c36dc98 --- /dev/null +++ b/src/Database/Models/User.ts @@ -0,0 +1,23 @@ +import { prop, getModelForClass } from '@typegoose/typegoose' +import { Document } from 'mongoose' + +export class UserSchema { + @prop({ type: String, required: true, unique: true }) + public jid!: string + + @prop({ type: Number, required: true, default: 0 }) + public experience!: number + + @prop({ type: Boolean, required: true, default: false }) + public banned!: boolean + + @prop({ type: Number, required: true, default: 1 }) + public level!: number + + @prop({ type: String, required: true }) + public tag!: string +} + +export type TUserModel = UserSchema & Document + +export const userSchema = getModelForClass(UserSchema) diff --git a/src/Database/Models/index.ts b/src/Database/Models/index.ts new file mode 100644 index 0000000..50d5b05 --- /dev/null +++ b/src/Database/Models/index.ts @@ -0,0 +1,5 @@ +export * from './Command' +export * from './Contact' +export * from './Group' +export * from './Session' +export * from './User' diff --git a/src/Database/index.ts b/src/Database/index.ts new file mode 100644 index 0000000..2618531 --- /dev/null +++ b/src/Database/index.ts @@ -0,0 +1 @@ +export * from './Models' diff --git a/src/Handlers/Asset.ts b/src/Handlers/Asset.ts new file mode 100644 index 0000000..44eda18 --- /dev/null +++ b/src/Handlers/Asset.ts @@ -0,0 +1,28 @@ +import { readdirSync, readFileSync } from 'fs-extra' +import { join } from 'path' +import chalk from 'chalk' +import { Client } from '../Structures' + +export class AssetHandler { + constructor(private client: Client) {} + + public loadAssets = (): void => { + this.client.log('Loading Assets...') + const folders = readdirSync(join(...this.path)) + for (const folder of folders) { + this.path.push(folder) + const assets = readdirSync(join(...this.path)) + for (const asset of assets) { + this.path.push(asset) + const buffer = readFileSync(join(...this.path)) + this.client.assets.set(asset.split('.')[0], buffer) + this.client.log(`Loaded: ${chalk.redBright(asset.split('.')[0])} from ${chalk.blueBright(folder)}`) + this.path.splice(this.path.indexOf(asset), 1) + } + this.path.splice(this.path.indexOf(folder), 1) + } + return this.client.log(`Successfully loaded ${chalk.cyanBright(this.client.assets.size)} assets`) + } + + private path = [__dirname, '..', '..', 'assets'] +} diff --git a/src/Handlers/Call.ts b/src/Handlers/Call.ts new file mode 100644 index 0000000..706f1fe --- /dev/null +++ b/src/Handlers/Call.ts @@ -0,0 +1,16 @@ +import chalk from 'chalk' +import { WACallEvent } from '@adiwajshing/baileys' +import { Client } from '../Structures' + +export class CallHandler { + constructor(private client: Client) {} + + public handleCall = async (call: WACallEvent): Promise => { + const caller = call.from + const { username } = this.client.contact.getContact(caller) + this.client.log(`${chalk.cyanBright('Call')} from ${chalk.blueBright(username)}`) + await this.client.sendMessage(caller, { text: 'You are now banned' }) + await this.client.DB.updateBanStatus(caller) + return void (await this.client.updateBlockStatus(caller, 'block')) + } +} diff --git a/src/Handlers/Event.ts b/src/Handlers/Event.ts new file mode 100644 index 0000000..84fff5f --- /dev/null +++ b/src/Handlers/Event.ts @@ -0,0 +1,81 @@ +import chalk from 'chalk' +import { delay } from '@adiwajshing/baileys' +import { Client } from '../Structures' +import { IEvent } from '../Types' + +export class EventHandler { + constructor(private client: Client) {} + + public handleEvents = async (event: IEvent): Promise => { + let group: { subject: string; description: string } = { + subject: '', + description: '' + } + await delay(1500) + await this.client + .groupMetadata(event.jid) + .then((res) => { + group.subject = res.subject + group.description = res.desc || 'No Description' + }) + .catch(() => { + group.subject = '__' + group.description = '' + }) + this.client.log( + `${chalk.blueBright('EVENT')} ${chalk.green( + `${this.client.utils.capitalize(event.action)}[${event.participants.length}]` + )} in ${chalk.cyanBright(`$`)}` + ) + const { events } = await this.client.DB.getGroup(event.jid) + if ( + !events || + (event.action === 'remove' && + event.participants.includes( + `${(this.client.user?.id || '').split('@')[0].split(':')[0]}@s.whatsapp.net` + )) + ) + return void null + const text = + event.action === 'add' + ? `- ${group.subject} -\n\n💈 *Group Description:*\n${ + group.description + }\n\nHope you follow the rules and have fun!\n\n*‣ ${event.participants + .map((jid) => `@${jid.split('@')[0]}`) + .join(' ')}*` + : event.action === 'remove' + ? `Goodbye *${event.participants + .map((jid) => `@${jid.split('@')[0]}`) + .join(', ')}* 👋🏻, we're probably not gonna miss you.` + : event.action === 'demote' + ? `Ara Ara, looks like *@${event.participants[0].split('@')[0]}* got Demoted` + : `Congratulations *@${event.participants[0].split('@')[0]}*, you're now an admin` + if (event.action === 'add') { + let imageUrl: string | undefined + try { + imageUrl = await this.client.profilePictureUrl(event.jid) + } catch (error) { + imageUrl = undefined + } + const image = imageUrl + ? await this.client.utils.getBuffer(imageUrl) + : (this.client.assets.get('404') as Buffer) + return void (await this.client.sendMessage(event.jid, { + image: image, + mentions: event.participants, + caption: text + })) + } + return void (await this.client.sendMessage(event.jid, { + text, + mentions: event.participants + })) + } + + public sendMessageOnJoiningGroup = async (group: { subject: string; jid: string }): Promise => { + this.client.log(`${chalk.blueBright('JOINED')} ${chalk.cyanBright(group.subject)}`) + return void (await this.client.sendMessage(group.jid, { + text: `Thanks for adding me in this group. Please use *${this.client.config.prefix}help* to get started.` + })) + } +} diff --git a/src/Handlers/Message.ts b/src/Handlers/Message.ts new file mode 100644 index 0000000..807146e --- /dev/null +++ b/src/Handlers/Message.ts @@ -0,0 +1,146 @@ +import { join } from 'path' +import { readdirSync } from 'fs-extra' +import chalk from 'chalk' +import { Message, Client, BaseCommand } from '../Structures' +import { getStats } from '../lib' +import { ICommand, IArgs } from '../Types' + +export class MessageHandler { + constructor(private client: Client) {} + + public handleMessage = async (M: Message): Promise => { + const { prefix } = this.client.config + const args = M.content.split(' ') + const title = M.chat === 'group' ? M.groupMetadata?.subject || 'Group' : 'DM' + await this.moderate(M) + if (!args[0] || !args[0].startsWith(prefix)) + return void this.client.log( + `${chalk.cyanBright('Message')} from ${chalk.yellowBright(M.sender.username)} in ${chalk.blueBright( + title + )}` + ) + this.client.log( + `${chalk.cyanBright(`Command ${args[0]}[${args.length - 1}]`)} from ${chalk.yellowBright( + M.sender.username + )} in ${chalk.blueBright(`${title}`)}` + ) + const { banned, tag } = await this.client.DB.getUser(M.sender.jid) + if (banned) return void M.reply('You are banned from using commands') + if (!tag) + await this.client.DB.updateUser(M.sender.jid, 'tag', 'set', this.client.utils.generateRandomUniqueTag()) + const cmd = args[0].toLowerCase().slice(prefix.length) + const command = this.commands.get(cmd) || this.aliases.get(cmd) + if (!command) return void M.reply('No such command, Baka!') + const disabledCommands = await this.client.DB.getDisabledCommands() + const index = disabledCommands.findIndex((CMD) => CMD.command === command.name) + if (index >= 0) + return void M.reply( + `*${this.client.utils.capitalize(cmd)}* is currently disabled by *${ + disabledCommands[index].disabledBy + }* in *${disabledCommands[index].time} (GMT)*. ❓ *Reason:* ${disabledCommands[index].reason}` + ) + if (command.config.category === 'dev' && !this.client.config.mods.includes(M.sender.jid)) + return void M.reply('This command can only be used by the MODS') + if (M.chat === 'dm' && !command.config.dm) return void M.reply('This command can only be used in groups') + if (command.config.category === 'moderation' && !M.sender.isAdmin) + return void M.reply('This command can only be used by the group admins') + const { nsfw } = await this.client.DB.getGroup(M.from) + if (command.config.category === 'nsfw' && !nsfw) + return void M.reply('This command can only be used in NSFW enabled groups') + const cooldownAmount = (command.config.cooldown ?? 3) * 1000 + const time = cooldownAmount + Date.now() + if (this.cooldowns.has(`${M.sender.jid}${command.name}`)) { + const cd = this.cooldowns.get(`${M.sender.jid}${command.name}`) + const remainingTime = this.client.utils.convertMs((cd as number) - Date.now()) + return void M.reply( + `You are on a cooldown. Wait *${remainingTime}* ${ + remainingTime > 1 ? 'seconds' : 'second' + } before using this command again` + ) + } else this.cooldowns.set(`${M.sender.jid}${command.name}`, time) + setTimeout(() => this.cooldowns.delete(`${M.sender.jid}${command.name}`), cooldownAmount) + await this.client.DB.setExp(M.sender.jid, command.config.exp || 10) + await this.handleUserStats(M) + try { + await command.execute(M, this.formatArgs(args)) + } catch (error) { + this.client.log((error as any).message, true) + } + } + + private moderate = async (M: Message): Promise => { + if (M.chat !== 'group') return void null + const { mods } = await this.client.DB.getGroup(M.from) + const isAdmin = M.groupMetadata?.admins?.includes(this.client.correctJid(this.client.user?.id || '')) + if (!mods || M.sender.isAdmin || !isAdmin) return void null + const urls = this.client.utils.extractUrls(M.content) + if (urls.length > 0) { + const groupinvites = urls.filter((url) => url.includes('chat.whatsapp.com')) + if (groupinvites.length > 0) { + groupinvites.forEach(async (invite) => { + const code = await this.client.groupInviteCode(M.from) + const inviteSplit = invite.split('/') + if (inviteSplit[inviteSplit.length - 1] !== code) { + this.client.log( + `${chalk.blueBright('MOD')} ${chalk.green('Group Invite')} by ${chalk.yellow( + M.sender.username + )} in ${chalk.cyanBright(M.groupMetadata?.subject || 'Group')}` + ) + return void (await this.client.groupParticipantsUpdate(M.from, [M.sender.jid], 'remove')) + } + }) + } + } + } + + private formatArgs = (args: string[]): IArgs => { + args.splice(0, 1) + return { + args, + context: args.join(' ').trim(), + flags: args.filter((arg) => arg.startsWith('--')) + } + } + + public loadCommands = (): void => { + this.client.log('Loading Commands...') + const files = readdirSync(join(...this.path)).filter((file) => !file.startsWith('_')) + for (const file of files) { + this.path.push(file) + const Commands = readdirSync(join(...this.path)) + for (const Command of Commands) { + this.path.push(Command) + const command: BaseCommand = new (require(join(...this.path)).default)() + command.client = this.client + command.handler = this + this.commands.set(command.name, command) + if (command.config.aliases) command.config.aliases.forEach((alias) => this.aliases.set(alias, command)) + this.client.log( + `Loaded: ${chalk.yellowBright(command.name)} from ${chalk.cyanBright(command.config.category)}` + ) + this.path.splice(this.path.indexOf(Command), 1) + } + this.path.splice(this.path.indexOf(file), 1) + } + return this.client.log( + `Successfully loaded ${chalk.cyanBright(this.commands.size)} ${ + this.commands.size > 1 ? 'commands' : 'command' + } with ${chalk.yellowBright(this.aliases.size)} ${this.aliases.size > 1 ? 'aliases' : 'alias'}` + ) + } + + private handleUserStats = async (M: Message): Promise => { + const { experience, level } = await this.client.DB.getUser(M.sender.jid) + const { requiredXpToLevelUp } = getStats(level) + if (requiredXpToLevelUp > experience) return void null + await this.client.DB.updateUser(M.sender.jid, 'level', 'inc', 1) + } + + public commands = new Map() + + public aliases = new Map() + + private cooldowns = new Map() + + private path = [__dirname, '..', 'Commands'] +} diff --git a/src/Handlers/index.ts b/src/Handlers/index.ts new file mode 100644 index 0000000..193df5d --- /dev/null +++ b/src/Handlers/index.ts @@ -0,0 +1,4 @@ +export * from './Call' +export * from './Asset' +export * from './Message' +export * from './Event' diff --git a/src/Structures/Auth.ts b/src/Structures/Auth.ts new file mode 100644 index 0000000..5cadabf --- /dev/null +++ b/src/Structures/Auth.ts @@ -0,0 +1,77 @@ +import { + proto, + BufferJSON, + initAuthCreds, + AuthenticationCreds, + SignalDataTypeMap, + AuthenticationState +} from '@adiwajshing/baileys' +import { Database } from '.' + +export class AuthenticationFromDatabase { + constructor(private sessionId: string) {} + + public useDatabaseAuth = async (): Promise<{ + state: AuthenticationState + saveState: () => Promise + clearState: () => Promise + }> => { + let creds: AuthenticationCreds + let keys: any = {} + const storedCreds = await this.DB.getSession(this.sessionId) + if (storedCreds !== null && storedCreds.session) { + const parsedCreds = JSON.parse(storedCreds.session, BufferJSON.reviver) + creds = parsedCreds.creds + keys = parsedCreds.keys + } else { + if (!storedCreds) await this.DB.saveNewSession(this.sessionId) + creds = initAuthCreds() + } + const saveState = async (): Promise => { + const session = JSON.stringify({ creds, keys }, BufferJSON.replacer, 2) + await this.DB.updateSession(this.sessionId, session) + } + const clearState = async (): Promise => { + await this.DB.removeSession(this.sessionId) + } + return { + state: { + creds, + keys: { + get: (type, ids) => { + const key = this.KEY_MAP[type] + return ids.reduce((dict: any, id) => { + let value = keys[key]?.[id] + if (value) { + if (type === 'app-state-sync-key') value = proto.AppStateSyncKeyData.fromObject(value) + dict[id] = value + } + return dict + }, {}) + }, + set: (data: any) => { + for (const _key in data) { + const key = this.KEY_MAP[_key as keyof SignalDataTypeMap] + keys[key] = keys[key] || {} + Object.assign(keys[key], data[_key]) + } + saveState() + } + } + }, + saveState, + clearState + } + } + + private KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = { + 'pre-key': 'preKeys', + session: 'sessions', + 'sender-key': 'senderKeys', + 'app-state-sync-key': 'appStateSyncKeys', + 'app-state-sync-version': 'appStateVersions', + 'sender-key-memory': 'senderKeyMemory' + } + + private DB = new Database() +} diff --git a/src/Structures/Client.ts b/src/Structures/Client.ts new file mode 100644 index 0000000..12caba2 --- /dev/null +++ b/src/Structures/Client.ts @@ -0,0 +1,221 @@ +import chalk from 'chalk' +import { config as Config } from 'dotenv' +import EventEmitter from 'events' +import TypedEventEmitter from 'typed-emitter' +import Baileys, { + DisconnectReason, + fetchLatestBaileysVersion, + ParticipantAction, + proto, + WACallEvent +} from '@adiwajshing/baileys' +import P from 'pino' +import { connect } from 'mongoose' +import { Boom } from '@hapi/boom' +import qr from 'qr-image' +import { Utils } from '../lib' +import { Database, Contact, Message, AuthenticationFromDatabase, Server } from '.' +import { IConfig, client, IEvent } from '../Types' + +export class Client extends (EventEmitter as new () => TypedEventEmitter) implements client { + private client!: client + constructor() { + super() + Config() + this.config = { + name: process.env.BOT_NAME || 'Bot', + session: process.env.SESSION || 'SESSION', + prefix: process.env.PREFIX || ':', + mods: (process.env.MODS || '').split(', ').map((user) => `${user}@s.whatsapp.net`), + PORT: Number(process.env.PORT || 3000) + } + new Server(this) + } + + public start = async (): Promise => { + if (!process.env.MONGO_URI) { + throw new Error('No MongoDB URI provided') + } + await connect(process.env.MONGO_URI) + this.log('Connected to the Database') + const { useDatabaseAuth } = new AuthenticationFromDatabase(this.config.session) + const { saveState, state, clearState } = await useDatabaseAuth() + const { version } = await fetchLatestBaileysVersion() + this.client = Baileys({ + version, + printQRInTerminal: true, + auth: state, + logger: P({ level: 'fatal' }), + browser: ['WhatsApp-bot', 'fatal', '4.0.0'], + getMessage: async (key) => { + return { + conversation: '' + } + }, + msgRetryCounterMap: {}, + markOnlineOnConnect: false + }) + for (const method of Object.keys(this.client)) + this[method as keyof Client] = this.client[method as keyof client] + this.ev.on('call', (call) => this.emit('new_call', call[0])) + this.ev.on('contacts.update', async (contacts) => await this.contact.saveContacts(contacts)) + this.ev.on('messages.upsert', async ({ messages }) => { + const M = new Message(messages[0], this) + if (M.type === 'protocolMessage' || M.type === 'senderKeyDistributionMessage') return void null + if (M.stubType && M.stubParameters) { + const emitParticipantsUpdate = (action: ParticipantAction): boolean => + this.emit('participants_update', { + jid: M.from, + participants: M.stubParameters as string[], + action + }) + switch (M.stubType) { + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_CREATE: + return void this.emit('new_group_joined', { + jid: M.from, + subject: M.stubParameters[0] + }) + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_ADD: + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_INVITE: + return void emitParticipantsUpdate('add') + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_LEAVE: + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_REMOVE: + return void emitParticipantsUpdate('remove') + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_DEMOTE: + return void emitParticipantsUpdate('demote') + case proto.WebMessageInfo.WebMessageInfoStubType.GROUP_PARTICIPANT_PROMOTE: + return void emitParticipantsUpdate('promote') + } + } + return void this.emit('new_message', await M.simplify()) + }) + this.ev.on('connection.update', (update) => { + if (update.qr) { + this.log( + `QR code generated. Scan it to continue | You can also authenicate in http://localhost:${this.config.PORT}` + ) + this.QR = qr.imageSync(update.qr) + } + const { connection, lastDisconnect } = update + if (connection === 'close') { + if ((lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut) { + this.log('Reconnecting...') + setTimeout(() => this.start(), 3000) + } else { + this.log('Disconnected.', true) + this.log('Deleting session and restarting') + clearState() + this.log('Session deleted') + this.log('Starting...') + setTimeout(() => this.start(), 3000) + } + } + if (connection === 'connecting') { + this.condition = 'connecting' + this.log('Connecting to WhatsApp...') + } + if (connection === 'open') { + this.condition = 'connected' + this.log('Connected to WhatsApp') + } + }) + this.ev.on('creds.update', saveState) + return this.client + } + + public utils = new Utils() + + public DB = new Database() + + public config: IConfig + + public contact = new Contact(this) + + public correctJid = (jid: string): string => `${jid.split('@')[0].split(':')[0]}@s.whatsapp.net` + + public assets = new Map() + + public log = (text: string, error: boolean = false): void => + console.log( + chalk[error ? 'red' : 'blue'](`[${this.config.name.toUpperCase()}]`), + chalk[error ? 'redBright' : 'greenBright'](text) + ) + + public QR!: Buffer + + public condition!: 'connected' | 'connecting' | 'logged_out' + + public end!: client['end'] + public ev!: client['ev'] + public fetchBlocklist!: client['fetchBlocklist'] + public fetchPrivacySettings!: client['fetchPrivacySettings'] + public fetchStatus!: client['fetchStatus'] + public generateMessageTag!: client['generateMessageTag'] + public getBusinessProfile!: client['getBusinessProfile'] + public getCatalog!: client['getCatalog'] + public getCollections!: client['getCollections'] + public getOrderDetails!: client['getOrderDetails'] + public groupAcceptInvite!: client['groupAcceptInvite'] + public groupAcceptInviteV4!: client['groupAcceptInviteV4'] + public groupInviteCode!: client['groupInviteCode'] + public groupLeave!: client['groupLeave'] + public groupMetadata!: client['groupMetadata'] + public groupCreate!: client['groupCreate'] + public groupFetchAllParticipating!: client['groupFetchAllParticipating'] + public groupGetInviteInfo!: client['groupGetInviteInfo'] + public groupRevokeInvite!: client['groupRevokeInvite'] + public groupSettingUpdate!: client['groupSettingUpdate'] + public groupToggleEphemeral!: client['groupToggleEphemeral'] + public groupUpdateDescription!: client['groupUpdateDescription'] + public groupUpdateSubject!: client['groupUpdateSubject'] + public groupParticipantsUpdate!: client['groupParticipantsUpdate'] + public logout!: client['logout'] + public presenceSubscribe!: client['presenceSubscribe'] + public productDelete!: client['productDelete'] + public productCreate!: client['productCreate'] + public productUpdate!: client['productUpdate'] + public profilePictureUrl!: client['profilePictureUrl'] + public updateMediaMessage!: client['updateMediaMessage'] + public query!: client['query'] + public readMessages!: client['readMessages'] + public refreshMediaConn!: client['refreshMediaConn'] + public relayMessage!: client['relayMessage'] + public resyncAppState!: client['resyncAppState'] + public resyncMainAppState!: client['resyncMainAppState'] + public sendMessageAck!: client['sendMessageAck'] + public sendNode!: client['sendNode'] + public sendRawMessage!: client['sendRawMessage'] + public sendReadReceipt!: client['sendReadReceipt'] + public sendRetryRequest!: client['sendRetryRequest'] + public sendMessage!: client['sendMessage'] + public sendPresenceUpdate!: client['sendPresenceUpdate'] + public sendReceipt!: client['sendReceipt'] + public type!: client['type'] + public updateBlockStatus!: client['updateBlockStatus'] + public onUnexpectedError!: client['onUnexpectedError'] + public onWhatsApp!: client['onWhatsApp'] + public uploadPreKeys!: client['uploadPreKeys'] + public updateProfilePicture!: client['updateProfilePicture'] + public user!: client['user'] + public ws!: client['ws'] + public waitForMessage!: client['waitForMessage'] + public waitForSocketOpen!: client['waitForSocketOpen'] + public waitForConnectionUpdate!: client['waitForConnectionUpdate'] + public waUploadToServer!: client['waUploadToServer'] + public getPrivacyTokens!: client['getPrivacyTokens'] + public assertSessions!: client['assertSessions'] + public processingMutex!: client['processingMutex'] + public appPatch!: client['appPatch'] + public authState!: client['authState'] + public upsertMessage!: client['upsertMessage'] + public updateProfileStatus!: client['updateProfileStatus'] + public chatModify!: client['chatModify'] +} + +type Events = { + new_call: (call: WACallEvent) => void + new_message: (M: Message) => void + participants_update: (event: IEvent) => void + new_group_joined: (group: { jid: string; subject: string }) => void +} diff --git a/src/Structures/Command/Base.ts b/src/Structures/Command/Base.ts new file mode 100644 index 0000000..26ccb88 --- /dev/null +++ b/src/Structures/Command/Base.ts @@ -0,0 +1,15 @@ +import { IArgs, ICommand } from '../../Types' +import { Client, Message } from '../' +import { MessageHandler } from '../../Handlers' + +export class BaseCommand { + constructor(public name: string, public config: ICommand['config']) {} + + public execute = async (M: Message, args: IArgs): Promise => { + throw new Error('Command method not implemented') + } + + public client!: Client + + public handler!: MessageHandler +} diff --git a/src/Structures/Command/Command.ts b/src/Structures/Command/Command.ts new file mode 100644 index 0000000..9a92912 --- /dev/null +++ b/src/Structures/Command/Command.ts @@ -0,0 +1,10 @@ +import { BaseCommand } from '.' +import { ICommand } from '../../Types' + +export const Command = (name: string, config: ICommand['config']): ClassDecorator => + () => T>(target: T): T => + //@ts-ignore + class extends target { + name = name + config = config + }) as ClassDecorator diff --git a/src/Structures/Command/index.ts b/src/Structures/Command/index.ts new file mode 100644 index 0000000..4c246b3 --- /dev/null +++ b/src/Structures/Command/index.ts @@ -0,0 +1,2 @@ +export * from './Base' +export * from './Command' diff --git a/src/Structures/Contact.ts b/src/Structures/Contact.ts new file mode 100644 index 0000000..f4e8093 --- /dev/null +++ b/src/Structures/Contact.ts @@ -0,0 +1,61 @@ +import { IContact } from '../Types' +import { Contact as contact } from '@adiwajshing/baileys' +import { Database, Client } from '.' + +export class Contact { + constructor(private client: Client) {} + public saveContacts = async (contacts: Partial[]): Promise => { + if (!this.contacts.has('contacts')) { + const data = await this.DB.getContacts() + this.contacts.set('contacts', data) + } + const data = this.contacts.get('contacts') as contact[] + for (const contact of contacts) { + if (contact.id) { + const index = data.findIndex(({ id }) => id === contact.id) + if (index >= 0) { + if (contact.notify !== data[index].notify) data[index].notify = contact.notify + continue + } + data.push({ + id: contact.id, + notify: contact.notify, + status: contact.status, + imgUrl: contact.imgUrl, + name: contact.name, + verifiedName: contact.verifiedName + }) + } + } + this.contacts.set('contacts', data) + await this.DB.contact.updateOne({ ID: 'contacts' }, { $set: { data } }) + } + + public getContact = (jid: string): IContact => { + const contact = this.contacts.get('contacts') + const isMod = this.client.config.mods.includes(jid) + if (!contact) + return { + username: 'User', + jid, + isMod + } + const index = contact.findIndex(({ id }) => id === jid) + if (index < 0) + return { + username: 'User', + jid, + isMod + } + const { notify, verifiedName, name } = contact[index] + return { + username: notify || verifiedName || name || 'User', + jid, + isMod + } + } + + private DB = new Database() + + private contacts = new Map<'contacts', contact[]>() +} diff --git a/src/Structures/Database.ts b/src/Structures/Database.ts new file mode 100644 index 0000000..a5aa15d --- /dev/null +++ b/src/Structures/Database.ts @@ -0,0 +1,94 @@ +import { Contact } from '@adiwajshing/baileys' +import { + userSchema, + groupSchema, + contactSchema, + sessionSchema, + disabledCommandsSchema, + TCommandModel, + TGroupModel, + TSessionModel, + TUserModel, + UserSchema, + GroupSchema +} from '../Database' +import { Utils } from '../lib' + +export class Database { + public getUser = async (jid: string): Promise => + (await this.user.findOne({ jid })) || + (await new this.user({ jid, tag: this.utils.generateRandomUniqueTag() }).save()) + + public setExp = async (jid: string, experience: number): Promise => { + experience = experience + Math.floor(Math.random() * 25) + await this.updateUser(jid, 'experience', 'inc', experience) + } + + public updateBanStatus = async (jid: string, action: 'ban' | 'unban' = 'ban'): Promise => { + await this.updateUser(jid, 'banned', 'set', action === 'ban') + } + + public updateUser = async ( + jid: string, + field: keyof UserSchema, + method: 'inc' | 'set', + update: UserSchema[typeof field] + ): Promise => { + await this.getUser(jid) + await this.user.updateOne({ jid }, { [`$${method}`]: { [field]: update } }) + } + + public getGroup = async (jid: string): Promise => + (await this.group.findOne({ jid })) || (await new this.group({ jid }).save()) + + public updateGroup = async (jid: string, field: keyof GroupSchema, update: boolean): Promise => { + await this.getGroup(jid) + await this.group.updateOne({ jid }, { $set: { [field]: update } }) + } + + public getSession = async (sessionId: string): Promise => + await this.session.findOne({ sessionId }) + + public saveNewSession = async (sessionId: string): Promise => { + await new this.session({ sessionId }).save() + } + + public updateSession = async (sessionId: string, session: string): Promise => { + await this.session.updateOne({ sessionId }, { $set: { session } }) + } + + public removeSession = async (sessionId: string): Promise => { + await this.session.deleteOne({ sessionId }) + } + + public getContacts = async (): Promise => { + let result = await this.contact.findOne({ ID: 'contacts' }) + if (!result) result = await new this.contact({ ID: 'contacts' }).save() + return result.data + } + + public getDisabledCommands = async (): Promise => { + let result = await this.disabledCommands.findOne({ title: 'commands' }) + if (!result) result = await new this.disabledCommands({ title: 'commands' }).save() + return result.disabledCommands + } + + public updateDisabledCommands = async (update: TCommandModel['disabledCommands']): Promise => { + await this.getDisabledCommands() + await this.disabledCommands.updateOne({ title: 'commands' }, { $set: { disabledCommands: update } }) + } + + private utils = new Utils() + + public user = userSchema + + public group = groupSchema + + public contact = contactSchema + + public session = sessionSchema + + public disabledCommands = disabledCommandsSchema +} + +type valueof = T[keyof T] diff --git a/src/Structures/Message.ts b/src/Structures/Message.ts new file mode 100644 index 0000000..0fe0142 --- /dev/null +++ b/src/Structures/Message.ts @@ -0,0 +1,222 @@ +import { proto, MessageType, MediaType, AnyMessageContent, downloadContentFromMessage } from '@adiwajshing/baileys' +import { Client } from '.' +import { ISender, DownloadableMessage, IGroup } from '../Types' + +export class Message { + constructor(private M: proto.IWebMessageInfo, private client: Client) { + this.message = this.M + this.from = M.key.remoteJid || '' + this.chat = this.from.endsWith('@s.whatsapp.net') ? 'dm' : 'group' + const { jid, username, isMod } = this.client.contact.getContact( + this.chat === 'dm' && this.M.key.fromMe + ? this.client.correctJid(this.client.user?.id || '') + : this.chat === 'group' + ? this.client.correctJid(M.key.participant || '') + : this.client.correctJid(this.from) + ) + this.sender = { + jid, + username, + isMod, + isAdmin: false + } + this.type = (Object.keys(M.message || {})[0] as MessageType) || 'conversation' + if (this.M.pushName) this.sender.username = this.M.pushName + const supportedMediaType = ['videoMessage', 'imageMessage'] + this.hasSupportedMediaMessage = + this.type === 'buttonsMessage' + ? supportedMediaType.includes(Object.keys(M.message?.buttonsMessage || {})[0]) + : supportedMediaType.includes(this.type) + const getContent = (): string => { + if (M.message?.buttonsResponseMessage) return M.message?.buttonsResponseMessage?.selectedButtonId || '' + if (M.message?.listResponseMessage) + return M.message?.listResponseMessage?.singleSelectReply?.selectedRowId || '' + return M.message?.conversation + ? M.message.conversation + : this.hasSupportedMediaMessage + ? supportedMediaType + .map((type) => M.message?.[type as 'imageMessage']?.caption) + .filter((caption) => caption)[0] || '' + : M.message?.extendedTextMessage?.text + ? M.message?.extendedTextMessage.text + : '' + } + this.content = getContent() + this.urls = this.client.utils.extractUrls(this.content) + const mentions = (M.message?.[this.type as 'extendedTextMessage']?.contextInfo?.mentionedJid || []).filter( + (x) => x !== null && x !== undefined + ) + for (const mentioned of mentions) this.mentioned.push(mentioned) + let text = this.content + for (const mentioned of this.mentioned) text = text.replace(mentioned.split('@')[0], '') + this.numbers = this.client.utils.extractNumbers(text) + if (M.message?.[this.type as 'extendedTextMessage']?.contextInfo?.quotedMessage) { + const { quotedMessage, participant, stanzaId } = + M.message?.[this.type as 'extendedTextMessage']?.contextInfo || {} + if (quotedMessage && participant && stanzaId) { + const Type = Object.keys(quotedMessage)[0] as MessageType + const getQuotedContent = (): string => { + if (quotedMessage?.buttonsResponseMessage) + return quotedMessage?.buttonsResponseMessage?.selectedDisplayText || '' + if (quotedMessage?.listResponseMessage) + return quotedMessage?.listResponseMessage?.singleSelectReply?.selectedRowId || '' + return quotedMessage?.conversation + ? quotedMessage.conversation + : supportedMediaType.includes(Type) + ? supportedMediaType + .map((type) => quotedMessage?.[type as 'imageMessage']?.caption) + .filter((caption) => caption)[0] || '' + : quotedMessage?.extendedTextMessage?.text + ? quotedMessage?.extendedTextMessage.text + : '' + } + const { username, jid, isMod } = this.client.contact.getContact(this.client.correctJid(participant)) + this.quoted = { + sender: { + jid, + username, + isMod, + isAdmin: false + } || { + username: 'User', + jid: this.client.correctJid(participant), + isMod: this.client.config.mods.includes(this.client.correctJid(participant)), + isAdmin: false + }, + content: getQuotedContent(), + message: quotedMessage, + type: Type, + hasSupportedMediaMessage: + Type !== 'buttonsMessage' + ? supportedMediaType.includes(Type) + : supportedMediaType.includes(Object.keys(quotedMessage?.buttonsMessage || {})[1]), + key: { + remoteJid: this.from, + fromMe: + this.client.correctJid(participant) === this.client.correctJid(this.client.user?.id || ''), + id: stanzaId, + participant + } + } + } + } + this.emojis = this.client.utils.extractEmojis(this.content) + } + + public simplify = async (): Promise => { + if (this.chat === 'dm') return this + return await this.client + .groupMetadata(this.from) + .then((res) => { + const result: IGroup = res + result.admins = result.participants + .filter((x) => x.admin !== null && x.admin !== undefined) + .map((x) => x.id) + this.groupMetadata = result + this.sender.isAdmin = result.admins.includes(this.sender.jid) + if (this.quoted) this.quoted.sender.isAdmin = result.admins.includes(this.quoted.sender.jid) + return this + }) + .catch(() => this) + } + + get stubType(): proto.WebMessageInfo.WebMessageInfoStubType | undefined | null { + return this.M.messageStubType + } + + get stubParameters(): string[] | undefined | null { + return this.M.messageStubParameters + } + + public reply = async ( + content: string | Buffer, + type: 'text' | 'image' | 'video' | 'audio' | 'sticker' | 'document' = 'text', + gif?: boolean, + mimetype?: string, + caption?: string, + mentions?: string[], + externalAdReply?: proto.IContextInfo['externalAdReply'], + thumbnail?: Buffer, + fileName?: string, + options: { sections?: proto.ISection[]; buttonText?: string; title?: string } = {} + ): Promise> => { + if (type === 'text' && Buffer.isBuffer(content)) throw new Error('Cannot send Buffer as a text message') + return this.client.sendMessage( + this.from, + { + [type]: content, + gifPlayback: gif, + caption, + mimetype, + mentions, + fileName, + jpegThumbnail: thumbnail ? thumbnail.toString('base64') : undefined, + contextInfo: externalAdReply + ? { + externalAdReply + } + : undefined, + footer: options.sections?.length ? `🤍 ${this.client.config.name} 🖤` : undefined, + sections: options.sections, + title: options.title, + buttonText: options.buttonText + } as unknown as AnyMessageContent, + { + quoted: this.M + } + ) + } + + public react = async ( + emoji: string, + key: proto.IMessageKey = this.M.key + ): Promise> => + await this.client.sendMessage(this.from, { + react: { + text: emoji, + key + } + }) + + public downloadMediaMessage = async (message: proto.IMessage): Promise => { + let type = Object.keys(message)[0] as MessageType + let msg = message[type as keyof typeof message] + if (type === 'buttonsMessage' || type === 'viewOnceMessage') { + if (type === 'viewOnceMessage') { + msg = message.viewOnceMessage?.message + type = Object.keys(msg || {})[0] as MessageType + } else type = Object.keys(msg || {})[1] as MessageType + msg = (msg as any)[type] + } + const stream = await downloadContentFromMessage( + msg as DownloadableMessage, + type.replace('Message', '') as MediaType + ) + let buffer = Buffer.from([]) + for await (const chunk of stream) { + buffer = Buffer.concat([buffer, chunk]) + } + return buffer + } + + public from: string + public sender: ISender + public content: string + public numbers: number[] + public hasSupportedMediaMessage: boolean + public type: MessageType + public message: proto.IWebMessageInfo + public chat: 'dm' | 'group' + public mentioned: string[] = [] + public quoted?: { + content: string + sender: ISender + type: MessageType + message: proto.IMessage + hasSupportedMediaMessage: boolean + key: proto.IMessageKey + } + public emojis: string[] + public urls: string[] + public groupMetadata?: IGroup +} diff --git a/src/Structures/Server.ts b/src/Structures/Server.ts new file mode 100644 index 0000000..ca5bcfe --- /dev/null +++ b/src/Structures/Server.ts @@ -0,0 +1,34 @@ +import express from 'express' +import { join } from 'path' +import { Client } from '.' + +export class Server { + constructor(private client: Client) { + this.app.use('/', express.static(this.path)) + + this.app.get('/wa/qr', async (req, res) => { + const { session } = req.query + if (!session || !this.client || this.client.config.session !== (req.query.session as string)) + return void res.status(404).setHeader('Content-Type', 'text/plain').send('Invalid Session').end() + if (!this.client || !this.client.QR) + return void res + .status(404) + .setHeader('Content-Type', 'text/plain') + .send( + this.client.condition === 'connected' + ? 'You are already connected to WhatsApp' + : 'QR not generated' + ) + .end() + res.status(200).contentType('image/png').send(this.client.QR) + }) + + this.app.all('*', (req, res) => res.sendStatus(404)) + + this.app.listen(client.config.PORT, () => client.log(`Server started on PORT : ${client.config.PORT}`)) + } + + private path = join(__dirname, '..', '..', 'public') + + private app = express() +} diff --git a/src/Structures/index.ts b/src/Structures/index.ts new file mode 100644 index 0000000..4f6f206 --- /dev/null +++ b/src/Structures/index.ts @@ -0,0 +1,7 @@ +export * from './Database' +export * from './Client' +export * from './Contact' +export * from './Auth' +export * from './Server' +export * from './Message' +export * from './Command' diff --git a/src/Types/Command.ts b/src/Types/Command.ts new file mode 100644 index 0000000..3ea5e98 --- /dev/null +++ b/src/Types/Command.ts @@ -0,0 +1,35 @@ +import { IArgs } from '.' +import { Client, Message } from '../Structures' +import { MessageHandler } from '../Handlers' + +export interface ICommand { + /**Name of the command */ + name: string + /**The client of WhatsApp */ + client: Client + /**Handler of message */ + handler: MessageHandler + /**Configuration of the command */ + config: ICommandConfig + /**Method for executing the command */ + execute(M: Message, args: IArgs): Promise +} + +interface ICommandConfig { + /**Description of the command */ + description: string + /**An example on how the command should be used */ + usage: string + /**Category of the command */ + category: TCategory + /**Aliases of the command */ + aliases?: string[] + /**Experience to be gained by using this command */ + exp?: number + /**Can be used in dm? */ + dm?: boolean + /**Cooldown of the command */ + cooldown?: number +} + +export type TCategory = 'dev' | 'general' | 'weeb' | 'utils' | 'fun' | 'moderation' | 'media' | 'category' | 'nsfw' diff --git a/src/Types/Config.ts b/src/Types/Config.ts new file mode 100644 index 0000000..261be85 --- /dev/null +++ b/src/Types/Config.ts @@ -0,0 +1,12 @@ +export interface IConfig { + /**name of your bot */ + name: string + /**prefix of your bot */ + prefix: string + /**session of the bot */ + session: string + /**number of the users who's the bot admins of the bot */ + mods: string[] + /**port number where the server will be started */ + PORT: number +} diff --git a/src/Types/Message.ts b/src/Types/Message.ts new file mode 100644 index 0000000..aeb18b7 --- /dev/null +++ b/src/Types/Message.ts @@ -0,0 +1,9 @@ +import { downloadContentFromMessage } from '@adiwajshing/baileys' + +export interface IArgs { + context: string + args: string[] + flags: string[] +} + +export type DownloadableMessage = Parameters[0] diff --git a/src/Types/index.ts b/src/Types/index.ts new file mode 100644 index 0000000..4d75c57 --- /dev/null +++ b/src/Types/index.ts @@ -0,0 +1,33 @@ +import Baileys, { GroupMetadata, ParticipantAction } from '@adiwajshing/baileys' + +export * from './Config' +export * from './Command' +export * from './Message' + +export interface IContact { + jid: string + username: string + isMod: boolean +} + +export interface ISender extends IContact { + isAdmin: boolean +} + +export interface IEvent { + jid: string + participants: string[] + action: ParticipantAction +} + +export enum GroupFeatures { + 'events' = 'By enabling this feature, the bot will welcome new members, gives farewell to the members who left the group or were removed and reacts when a member is promoted or demoted', + 'mods' = "By enabling this feature, it enables the bot to remove the member (except for admins) which sent an invite link of other groups. This will work if and only if the bot's an admin", + 'nsfw' = 'By enabling this feature, it enables the bot to send *NSFW* contents' +} + +export interface IGroup extends GroupMetadata { + admins?: string[] +} + +export type client = ReturnType diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..955a0cd --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,25 @@ +import { Client } from './Structures' +import { MessageHandler, AssetHandler, CallHandler, EventHandler } from './Handlers' +;(async (): Promise => { + const client = new Client() + + await client.start() + + new AssetHandler(client).loadAssets() + + const { handleMessage, loadCommands } = new MessageHandler(client) + + const { handleEvents, sendMessageOnJoiningGroup } = new EventHandler(client) + + const { handleCall } = new CallHandler(client) + + loadCommands() + + client.on('new_message', async (M) => await handleMessage(M)) + + client.on('participants_update', async (event) => await handleEvents(event)) + + client.on('new_group_joined', async (group) => await sendMessageOnJoiningGroup(group)) + + client.on('new_call', async (call) => await handleCall(call)) +})() diff --git a/src/lib/Lyrics.ts b/src/lib/Lyrics.ts new file mode 100644 index 0000000..ef1b68d --- /dev/null +++ b/src/lib/Lyrics.ts @@ -0,0 +1,47 @@ +import axios from 'axios' +import { load } from 'cheerio' + +export class Lyrics { + public search = async (query: string, page: number = 1): Promise => + await axios.get(`https://genius.com/api/search/song?q=${query}&page=${page}`).then((res) => { + //@ts-ignore + const results = res.data.response.sections[0].hits.map((x) => x.result) + const data: ILyrics[] = [] + for (const result of results) + data.push({ + title: result.title as string, + fullTitle: result.full_title as string, + artist: result.artist_names as string, + image: result.header_image_url as string, + url: result.url as string + }) + return data + }) + + public parseLyrics = async (url: string): Promise => + await axios.get(url).then(({ data }) => { + const $ = load(data) + let text = '' + $('.Lyrics__Container-sc-1ynbvzw-6.YYrds').each((i, el) => { + const t = ($(el).html() || '').replace(/<(?:.|\n)*?>/gm, '\n') + const $$ = load(t) + text += $$.text().replace(/\(\n/g, '(').replace(/\n[)]/g, ')') + }) + return text + .split('\n\n\n\n\n\n\n\n\n\n\n\n') + .join('\n\n') + .split('\n\n\n\n\n\n\n\n\n\n') + .join('\n\n') + .split('\n\n\n') + .join('\n') + .trim() + }) +} + +export interface ILyrics { + title: string + fullTitle: string + artist: string + image: string + url: string +} diff --git a/src/lib/Reaction.ts b/src/lib/Reaction.ts new file mode 100644 index 0000000..eb9f181 --- /dev/null +++ b/src/lib/Reaction.ts @@ -0,0 +1,139 @@ +import { Utils } from '.' + +enum baseUrls { + 'waifu.pics' = 'https://api.waifu.pics/sfw/', + 'nekos.life' = 'https://nekos.life/api/v2/img/', + 'nekos.best' = 'https://nekos.best/api/v2/' +} + +export enum Reactions { + bully = baseUrls['waifu.pics'], + cuddle = baseUrls['nekos.life'], + cry = baseUrls['waifu.pics'], + hug = baseUrls['nekos.life'], + kiss = baseUrls['waifu.pics'], + lick = baseUrls['waifu.pics'], + pat = baseUrls['nekos.life'], + smug = baseUrls['nekos.life'], + yeet = baseUrls['waifu.pics'], + blush = baseUrls['waifu.pics'], + bonk = baseUrls['waifu.pics'], + smile = baseUrls['waifu.pics'], + wave = baseUrls['waifu.pics'], + highfive = baseUrls['waifu.pics'], + bite = baseUrls['waifu.pics'], + handhold = baseUrls['waifu.pics'], + nom = baseUrls['waifu.pics'], + glomp = baseUrls['waifu.pics'], + kill = baseUrls['waifu.pics'], + kick = baseUrls['waifu.pics'], + slap = baseUrls['nekos.life'], + happy = baseUrls['waifu.pics'], + wink = baseUrls['waifu.pics'], + poke = baseUrls['waifu.pics'], + dance = baseUrls['waifu.pics'], + cringe = baseUrls['waifu.pics'], + tickle = baseUrls['nekos.life'], + baka = baseUrls['nekos.best'], + bored = baseUrls['nekos.best'], + laugh = baseUrls['nekos.best'], + punch = baseUrls['nekos.best'], + pout = baseUrls['nekos.best'], + stare = baseUrls['nekos.best'], + thumbsup = baseUrls['nekos.best'] +} + +export type reaction = keyof typeof Reactions + +export class Reaction { + public getReaction = async (reaction: reaction, single: boolean = true) => { + const { url } = await this.fetch(`${Reactions[reaction]}${reaction}`) + const words = this.getSuitableWords(reaction, single) + return { + url, + words + } + } + + private getSuitableWords = (reaction: reaction, single: boolean = true): string => { + switch (reaction) { + case 'bite': + return 'Bit' + case 'blush': + return 'Blushed at' + case 'bonk': + return 'Bonked' + case 'bully': + return 'Bullied' + case 'cringe': + return 'Cringed at' + case 'cry': + return single ? 'Cried by' : 'Cried in front of' + case 'cuddle': + return 'Cuddled' + case 'dance': + return 'Danced with' + case 'glomp': + return 'Glomped at' + case 'handhold': + return 'Held the hands of' + case 'happy': + return single ? 'is Happied by' : 'is Happied with' + case 'highfive': + return 'High-fived' + case 'hug': + return 'Hugged' + case 'kick': + return 'Kicked' + case 'kill': + return 'Killed' + case 'kiss': + return 'Kissed' + case 'lick': + return 'Licked' + case 'nom': + return 'Nomed' + case 'pat': + return 'Patted' + case 'poke': + return 'Poked' + case 'slap': + return 'Slapped' + case 'smile': + return 'Smiled at' + case 'smug': + return 'Smugged' + case 'tickle': + return 'Tickled' + case 'wave': + return 'Waved at' + case 'wink': + return 'Winked at' + case 'yeet': + return 'Yeeted at' + case 'baka': + return 'Yelled BAKA at' + case 'bored': + return 'is Bored of' + case 'laugh': + return 'Laughed at' + case 'punch': + return 'Punched' + case 'pout': + return 'Pouted' + case 'stare': + return 'Stared at' + case 'thumbsup': + return 'Thumbs-upped at' + } + } + + private fetch = async (url: string): Promise<{ url: string }> => { + const data = await this.utils.fetch<{ url: string } | { results: { anime_name: string; url: string }[] }>(url) + const res = data as { results: { anime_name: string; url: string }[] } + if (res.results) return res.results[0] + return data as { url: string } + } + + private utils = new Utils() +} diff --git a/src/lib/Spotify.ts b/src/lib/Spotify.ts new file mode 100644 index 0000000..6ce0d3b --- /dev/null +++ b/src/lib/Spotify.ts @@ -0,0 +1,22 @@ +import spotify from 'spotifydl-core' +import TrackDetails from 'spotifydl-core/dist/lib/details/Track' +import { Utils } from '.' + +export class Spotify extends spotify { + constructor(public url: string) { + super({ + clientId: 'acc6302297e040aeb6e4ac1fbdfd62c3', + clientSecret: '0e8439a1280a43aba9a5bc0a16f3f009' + }) + } + + public getInfo = async (): Promise => + await this.getTrack(this.url).catch(() => { + return { error: 'Failed' } + }) + + public download = async (): Promise => + await this.utils.mp3ToOpus((await this.downloadTrack(this.url)) as Buffer) + + private readonly utils = new Utils() +} diff --git a/src/lib/Stats.ts b/src/lib/Stats.ts new file mode 100644 index 0000000..c3d6e52 --- /dev/null +++ b/src/lib/Stats.ts @@ -0,0 +1,56 @@ +export const ranks = [ + '🌸 Citizen', + '🔎 Cleric', + '🔮 Wizard', + '♦️ Mage', + '🎯 Noble', + '🎯 Noble II', + '✨ Elite', + '✨ Elite II', + '✨ Elite III', + '🔶️ Ace', + '🔶️ Ace II', + '🔶️ Ace III', + '🔶️ Ace IV', + '☣ Knight', + '☣ Knight II', + '☣ Knight III', + '☣ Knight IV', + '☣ Knight V', + '🌀 Hero', + '🌀 Hero II', + '🌀 Hero III', + '🌀 Hero IV', + '🌀 Hero V', + '💎 Supreme', + '💎 Supreme II', + '💎 Supreme III', + '💎 Supreme IV', + '💎 Supreme V', + '❄️ Mystic', + '❄️ Mystic II', + '❄️ Mystic III', + '❄️ Mystic IV', + '❄️ Mystic V', + '🔆 Legendary', + '🔆 Legendary II', + '🔆 Legendary III', + '🔆 Legendary IV', + '🔆 Legendary V', + '🛡 Guardian', + '🛡 Guardian II', + '🛡 Guardian III', + '🛡 Guardian IV', + '🛡 Guardian V', + '♨ Valor' +] + +export const getStats = (level: number): { requiredXpToLevelUp: number; rank: string } => { + let required = 100 + for (let i = 1; i <= level; i++) required += 5 * (i * 50) + 100 * i * (i * (i + 1)) + 300 + const rank = level > ranks.length ? ranks[ranks.length - 1] : ranks[level - 1] + return { + requiredXpToLevelUp: required, + rank + } +} diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts new file mode 100644 index 0000000..443dff4 --- /dev/null +++ b/src/lib/Utils.ts @@ -0,0 +1,82 @@ +import axios from 'axios' +import { tmpdir } from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' +import { readFile, unlink, writeFile } from 'fs-extra' +import regex from 'emoji-regex' +import getUrls from 'get-urls' + +export class Utils { + public generateRandomHex = (): string => `#${(~~(Math.random() * (1 << 24))).toString(16)}` + + public capitalize = (content: string): string => `${content.charAt(0).toUpperCase()}${content.slice(1)}` + + public generateRandomUniqueTag = (n: number = 4): string => { + let max = 11 + if (n > max) return `${this.generateRandomUniqueTag(max)}${this.generateRandomUniqueTag(n - max)}` + max = Math.pow(10, n + 1) + const min = max / 10 + return (Math.floor(Math.random() * (max - min + 1)) + min).toString().substring(1) + } + + public extractNumbers = (content: string): number[] => { + const search = content.match(/(-\d+|\d+)/g) + if (search !== null) return search.map((string) => parseInt(string)) + return [] + } + + public extractUrls = (content: string): string[] => Array.from(getUrls(content)) + + public extractEmojis = (content: string): string[] => content.match(regex()) || [] + + public formatSeconds = (seconds: number): string => new Date(seconds * 1000).toISOString().substr(11, 8) + + public convertMs = (ms: number, to: 'seconds' | 'minutes' | 'hours' = 'seconds'): number => { + const seconds = parseInt((ms / 1000).toString().split('.')[0]) + const minutes = parseInt((seconds / 60).toString().split('.')[0]) + const hours = parseInt((minutes / 60).toString().split('.')[0]) + if (to === 'hours') return hours + if (to === 'minutes') return minutes + return seconds + } + + public webpToPng = async (webp: Buffer): Promise => { + const filename = `${tmpdir()}/${Math.random().toString(36)}` + await writeFile(`${filename}.webp`, webp) + await this.exec(`dwebp "${filename}.webp" -o "${filename}.png"`) + const buffer = await readFile(`${filename}.png`) + Promise.all([unlink(`${filename}.png`), unlink(`${filename}.webp`)]) + return buffer + } + + public mp3ToOpus = async (mp3: Buffer): Promise => { + const filename = `${tmpdir()}/${Math.random().toString(36)}` + await writeFile(`${filename}.mp3`, mp3) + await this.exec(`ffmpeg -i ${filename}.mp3 -c:a libopus ${filename}.opus`) + const buffer = await readFile(`${filename}.opus`) + Promise.all([unlink(`${filename}.mp3`), unlink(`${filename}.opus`)]) + return buffer + } + + public gifToMp4 = async (gif: Buffer): Promise => { + const filename = `${tmpdir()}/${Math.random().toString(36)}` + await writeFile(`${filename}.gif`, gif) + await this.exec( + `ffmpeg -f gif -i ${filename}.gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ${filename}.mp4` + ) + const buffer = await readFile(`${filename}.mp4`) + Promise.all([unlink(`${filename}.gif`), unlink(`${filename}.mp4`)]) + return buffer + } + + public fetch = async (url: string): Promise => (await axios.get(url)).data + + public getBuffer = async (url: string): Promise => + ( + await axios.get(url, { + responseType: 'arraybuffer' + }) + ).data + + public exec = promisify(exec) +} diff --git a/src/lib/YT.ts b/src/lib/YT.ts new file mode 100644 index 0000000..1a58824 --- /dev/null +++ b/src/lib/YT.ts @@ -0,0 +1,54 @@ +import ytdl, { validateURL, getInfo } from 'ytdl-core' +import { createWriteStream, readFile, unlink } from 'fs-extra' +import { tmpdir } from 'os' +import { Utils } from '.' + +export class YT { + constructor(private url: string, private type: 'video' | 'audio' = 'video') {} + + public validate = (): boolean => validateURL(this.url) + + public getInfo = async (): Promise => await getInfo(this.url) + + public download = async (quality: 'high' | 'medium' | 'low' = 'medium'): Promise => { + if (this.type === 'audio' || quality === 'medium') { + let filename = `${tmpdir()}/${Math.random().toString(36)}.${this.type === 'audio' ? 'mp3' : 'mp4'}` + const stream = createWriteStream(filename) + ytdl(this.url, { + quality: this.type === 'audio' ? 'highestaudio' : 'highest' + }).pipe(stream) + filename = await new Promise((resolve, reject) => { + stream.on('finish', () => resolve(filename)) + stream.on('error', (error) => reject(error && console.log(error))) + }) + const buffer = await readFile(filename) + await unlink(filename) + return buffer + } + let audioFilename = `${tmpdir()}/${Math.random().toString(36)}.mp3` + let videoFilename = `${tmpdir()}/${Math.random().toString(36)}.mp4` + const filename = `${tmpdir()}/${Math.random().toString(36)}.mp4` + const audioStream = createWriteStream(audioFilename) + ytdl(this.url, { + quality: 'highestaudio' + }).pipe(audioStream) + audioFilename = await new Promise((resolve, reject) => { + audioStream.on('finish', () => resolve(audioFilename)) + audioStream.on('error', (error) => reject(error && console.log(error))) + }) + const stream = createWriteStream(videoFilename) + ytdl(this.url, { + quality: quality === 'high' ? 'highestvideo' : 'lowestvideo' + }).pipe(stream) + videoFilename = await new Promise((resolve, reject) => { + stream.on('finish', () => resolve(videoFilename)) + stream.on('error', (error) => reject(error && console.log(error))) + }) + await this.utils.exec(`ffmpeg -i ${videoFilename} -i ${audioFilename} -c:v copy -c:a aac ${filename}`) + const buffer = await readFile(filename) + Promise.all([unlink(videoFilename), unlink(audioFilename), unlink(filename)]) + return buffer + } + + private utils = new Utils() +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..66342a1 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,6 @@ +export * from './Utils' +export * from './Stats' +export * from './Reaction' +export * from './YT' +export * from './Spotify' +export * from './Lyrics'