diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a849eed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to Project Luf 🤖 + +We welcome contributions from the community to make Project Luf even more awesome! Whether you want to report a bug, suggest an enhancement, or contribute code, here's how you can get involved. + +## Reporting Bugs 🐛 + +If you encounter a bug or issue with Project Luf, please follow these steps to report it: + +1. Check the [GitHub Issues](https://github.com/WilardzySenpai/Project-Luf/issues) to see if the issue has already been reported. If it has, feel free to add any additional information or context as a comment on the existing issue. + +2. If the issue hasn't been reported yet, please create a new issue. Be sure to provide a clear and concise description of the problem, including steps to reproduce it if possible. Don't forget to mention the version of Project Luf you are using. + +## Suggesting Enhancements 💡 + +Have an idea to make Project Luf better? We'd love to hear it! Here's how you can suggest enhancements: + +1. Check the [GitHub Issues](https://github.com/WilardzySenpai/Project-Luf/issues) to see if your enhancement idea has already been proposed. If it has, you can show your support by adding a 👍 reaction or adding additional thoughts as comments. + +2. If your enhancement idea is new, create a new issue with a clear and detailed description of the proposed enhancement. Explain why it would be beneficial and provide any relevant details or examples. + +## Contributing Code 🛠️ + +If you're interested in contributing code to Project Luf, please follow these guidelines: + +1. Fork the Project Luf repository to your own GitHub account. + +2. Create a new branch in your forked repository for your changes. + +3. Make your changes and ensure they are well-documented, with clear comments and explanations where necessary. + +4. Run any tests or checks as needed to ensure your code is functioning correctly. + +5. Submit a pull request (PR) to the main Project Luf repository with a clear title and description of your changes. Be sure to reference any related issues. + +6. Your PR will be reviewed by the maintainers, and any necessary feedback or changes will be discussed. + +7. Once your PR is approved, it will be merged into the main project. + +## Code Style and Guidelines 📋 + +Please follow the code style and guidelines outlined in the [CONTRIBUTING.md](CONTRIBUTING.md) file in this repository. Consistency and readability are essential. + +## Licensing 📜 + +By contributing to Project Luf, you agree that your contributions will be licensed under the [MIT License](LICENSE). + +Thank you for your contributions and for helping make Project Luf amazing! + +Happy coding! 🚀 diff --git a/index.js b/index.js new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..093ff18 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "nodejs", + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node ." + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/node": "^18.0.6", + "anime-wallpaper": "^1.1.1", + "axios": "^1.4.0", + "chalk": "^2.4.1", + "cheerio": "^1.0.0-rc.12", + "discord.js": "^14.11.0", + "discord.js-v14-helper": "^1.11.4", + "dotenv": "^16.3.1", + "moment": "^2.29.4", + "mongoose": "^7.3.1", + "ms": "^2.1.3", + "node-fetch": "^3.3.1", + "replicate": "^0.12.3" + } +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..d8b91a2 --- /dev/null +++ b/src/README.md @@ -0,0 +1,45 @@ +# Project Luf 🤖 + +![Project Luf](https://media.discordapp.net/attachments/1123137133331365888/1154435192191193140/project_luf.png) + +Welcome to Project Luf, your ultimate Discord bot experience! With a passion for innovation, Project Luf brings you a feature-packed bot designed to elevate your Discord server to new heights. + +## Features 🚀 + +- **Verification:** Keep your server secure and troll-free with our robust verification system. + +- **Anime Info:** Delve into the world of anime with up-to-date information at your fingertips. + +- **Fun Games:** Banish boredom with a variety of entertaining games that will keep your community engaged for hours. + +- **Interactive Fun:** Interact with Project Luf, your virtual buddy who's always ready for a chat or a good laugh. + +- **Moderation Commands:** Maintain order in your server with powerful moderation tools. + +But that's not all! Project Luf is an evolving project, and we're continuously working on adding more exciting features to make your Discord experience even better. Stay tuned for updates and surprises! + +## Getting Started 🛠️ + +To add Project Luf to your Discord server, follow these simple steps: + +1. Visit the [Project Luf Invite Link](#) and authorize the bot on your server. + +2. Configure the bot's permissions based on your server's needs. + +3. Start using Project Luf's fantastic features and enhance your server's functionality and entertainment value. + +## Contribution 🤝 + +We welcome contributions from the community! Whether you want to report a bug, suggest an enhancement, or even contribute code, check out our [Contribution Guidelines](CONTRIBUTING.md) to get started. + +## Support 📢 + +If you encounter any issues or have questions about Project Luf, join our [Discord Support Server](#) for assistance. Our friendly community and development team are here to help. + +## License 📜 + +Project Luf is released under the [MIT License](LICENSE), so feel free to use, modify, and distribute it as you see fit. + +--- + +We're excited to have you on board with Project Luf! Join us in this journey of innovation and fun. Let's make your Discord server truly remarkable together! 🌟 diff --git a/src/commands/Fun/imagine.js b/src/commands/Fun/imagine.js new file mode 100644 index 0000000..5759ad5 --- /dev/null +++ b/src/commands/Fun/imagine.js @@ -0,0 +1,79 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ApplicationCommandOptionType } = require('discord.js'); +const models = require('../../config/models'); + +module.exports = { + command_data: { + name: 'imagine', + description: 'Generate an image using a prompt.', + type: 1, + options: [ + { + name: 'prompt', + description: 'Enter your prompt', + type: ApplicationCommandOptionType.String, + required: true + }, + { + name: 'model', + description: 'The image model', + type: ApplicationCommandOptionType.String, + choices: models, + required: false + } + ], + }, + role_perms: null, + developers_only: false, + owner_only: false, + cooldown: 5, + logger: true, + category: "Fun", + run: async (client, interaction) => { + try { + await interaction.deferReply(); + + const { default: Replicate } = await import('replicate'); + + const replicate = new Replicate({ + auth: "r8_4CPjfy5UwnlJ3l2lPui8RFdjR2bAXHS2u5yaZ" || process.env.REPLICATE_API_KEY, + }); + + const prompt = interaction.options.getString('prompt'); + const model = interaction.options.getString('model') || models[0].value; + + const output = await replicate.run(model, { input: { prompt } }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel(`Download`) + .setStyle(ButtonStyle.Link) + .setURL(`${output[0]}`) + .setEmoji('1101133529607327764') + ); + + const resultEmbed = new EmbedBuilder() + .setTitle('Image Generated') + .addFields({ name: 'Prompt', value: prompt }) + .setImage(output[0]) + .setColor('#44a3e3') + .setFooter({ + text: `Requested by ${interaction.user.username}`, + iconURL: interaction.user.displayAvatarURL({ dynamic: true }), + }); + + await interaction.editReply({ + embeds: [resultEmbed], + components: [row], + }); + + } catch (e) { + console.log(e) + const errorEmbed = new EmbedBuilder() + .setTitle('An error occurred') + .setDescription('```' + e + '```') + .setColor(0xe32424); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + } +}; \ No newline at end of file diff --git a/src/commands/Information/help.js b/src/commands/Information/help.js new file mode 100644 index 0000000..218b9a5 --- /dev/null +++ b/src/commands/Information/help.js @@ -0,0 +1,47 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + command_data: { + name: 'help', + description: 'Help command', + type: 1, + options: [], + }, + role_perms: null, + developers_only: false, + cooldown: '10s', + category: 'Information', + run: async (client, interaction) => { + await interaction.deferReply(); + + const commandsFetched = await client.application.commands.fetch(); + + const commands = []; + + commandsFetched.forEach((cmd) => { + if (cmd.options?.length > 0) { + for (let option of cmd.options) { + if (option.type !== 1) continue; + + commands.push(``) + }; + } else { + commands.push(``); + }; + }); + + await interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setTitle('Help menu') + .setAuthor({ + name: client.user.username, + iconURL: client.user.displayAvatarURL({ dynamic: true }) + }) + .setDescription(`Hello **${interaction.user.tag}**! Here are the list of my commands, click one of them to use:\n${commands.join(', ')}.`) + .setColor('Blurple') + ] + }); + + } +}; \ No newline at end of file diff --git a/src/commands/Information/owner.js b/src/commands/Information/owner.js new file mode 100644 index 0000000..8c2d58b --- /dev/null +++ b/src/commands/Information/owner.js @@ -0,0 +1,48 @@ +const { EmbedBuilder, ApplicationCommandOptionType } = require('discord.js'); + +module.exports = { + command_data: { + name: 'owners', + description: 'Show the bot owners', + type: 1, + options: [], + }, + role_perms: null, + developers_only: false, + owner_only: true, + cooldown: '5s', + logger: true, + category: '', + run: async (client, interaction, config) => { + try { + await interaction.deferReply() + + const ownersID = config.users.owners; + if (!ownersID) return; + + const ownersARRAY = []; + + ownersID.forEach(Owner => { + const fetchedOWNER = interaction.guild.members.cache.get(Owner); + if (!fetchedOWNER) ownersARRAY.push("*Unknown User#0000*"); + ownersARRAY.push(`${fetchedOWNER}`); + }); + + interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setDescription(`**Only owners command!** \nOwners: **${ownersARRAY.join(", ")}**`) + .setColor("Yellow") + ] + }); + } catch (e) { + console.log(e) + const errorEmbed = new EmbedBuilder() + .setTitle('An error occurred') + .setDescription('```' + e + '```') + .setColor(0xe32424); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + } +}; \ No newline at end of file diff --git a/src/commands/Information/topani.js b/src/commands/Information/topani.js new file mode 100644 index 0000000..24753cb --- /dev/null +++ b/src/commands/Information/topani.js @@ -0,0 +1,136 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ApplicationCommandOptionType } = require('discord.js'); +const { scrapeTopAnime } = require('../../config/topPage'); + +module.exports = { + command_data: { + name: 'top', + description: 'Give you the Top 10 Anime ', + type: 1, + options: [ + { + name: 'page', + description: 'Enter what page to see', + type: ApplicationCommandOptionType.Number, + required: false + } + ], + }, + role_perms: null, + developers_only: false, + owner_only: false, + cooldown: 5, + logger: true, + category: "Information", + run: async (client, interaction) => { + + try { + await interaction.deferReply() + + let page = interaction.options.getNumber("page") || 1; + const topAnimeList = await scrapeTopAnime(page); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('top_previous') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setEmoji('⬅️') + .setDisabled(page == 1), // Disable the button if it's the first page + new ButtonBuilder() + .setCustomId('top_next') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setEmoji('➡️') + .setDisabled(page == 5) // Disable the button if it's the last page + ); + + const topEmbed = new EmbedBuilder() + .setTitle(`Top Anime (MAL) Page ${page}`) + .setColor('#FF0000') + .setDescription('Here are the top 10 anime:') + + topAnimeList.forEach((anime) => { + topEmbed.addFields( + { name: `${anime.rank}. ${anime.title}`, value: `Score: ${anime.score}`, inline: false } + ); + }); + + const message = await interaction.editReply({ embeds: [topEmbed], components: [row] }); + + const collector = message.createMessageComponentCollector({ + filter: (b) => { + if (b.user.id === interaction.user.id) return true; + else { + b.reply({ ephemeral: true, content: `Only **${interaction.user.username}** can use this button!` }); return false; + }; + }, + time: 60000, + idle: 60000 / 2 + }); // Collect for 60 seconds + + collector.on('collect', async (b) => { + if (!b.deferred) await b.deferUpdate() + + console.log('Embed Title:', b.message.embeds[0].title); // Output the full embed title + let titleParts = b.message.embeds[0].title.split(' '); + let currentPage = parseInt(titleParts[titleParts.length - 1]); + console.log('currentPage:', currentPage); + + let newPage; + + if (b.customId === 'top_previous') { + newPage = currentPage - 1; + // console.log(`Previous button clicked, new page is ${newPage}`); + } else if (b.customId === 'top_next') { + newPage = currentPage + 1; + // console.log(`Next button clicked, new page is ${newPage}`); + } + + const topAnimeList = await scrapeTopAnime(newPage); + // console.log(`Scraped ${topAnimeList.length} anime from page ${newPage}`); + const topEmbed = new EmbedBuilder() + .setTitle(`Top Anime (MAL) Page ${newPage}`) + .setColor('#FF0000') + .setDescription('Here are the top 10 anime:'); + + topAnimeList.forEach((anime) => { + topEmbed.addFields( + { name: `${anime.rank}. ${anime.title}`, value: `Score: ${anime.score}`, inline: false } + ); + }); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('top_previous') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setEmoji('⬅️') + .setDisabled(newPage == 1), + new ButtonBuilder() + .setCustomId('top_next') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setEmoji('➡️') + .setDisabled(newPage == 5) + ); + + // Update the message with the new embed and action row + await message.edit({ embeds: [topEmbed], components: [row] }); + }); + collector.on('end', collected => { + console.log(`Collected ${collected.size} interactions`); + // + }); + } catch (e) { + console.log(e) + const errorEmbed = new EmbedBuilder() + .setTitle('An error occurred') + .setDescription('```' + e + '```') + .setColor(0xe32424); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + } +}; \ No newline at end of file diff --git a/src/commands/Information/uptime.js b/src/commands/Information/uptime.js new file mode 100644 index 0000000..608e517 --- /dev/null +++ b/src/commands/Information/uptime.js @@ -0,0 +1,30 @@ +const { EmbedBuilder, codeBlock } = require('discord.js'); + +module.exports = { + command_data: { + name: 'uptime', + description: 'Check the client\'s uptime.', + type: 1, + options: [], + }, + role_perms: null, + developers_only: false, + cooldown: '5s', + category: 'Information', + run: async (client, interaction) => { + const date = new Date().getTime() - (client.uptime); + + return interaction.reply({ + embeds: [ + new EmbedBuilder() + .setAuthor({ + name: client.user.username, + iconURL: client.user.displayAvatarURL({ dynamic: true }) + }) + .setDescription(`Started on: ()`) + .setColor('Green') + ] + }); + + }, +}; diff --git a/src/commands/User/user-info.js b/src/commands/User/user-info.js new file mode 100644 index 0000000..0a9095d --- /dev/null +++ b/src/commands/User/user-info.js @@ -0,0 +1,109 @@ +const { EmbedBuilder, PermissionsBitField, ContextMenuCommandBuilder } = require('discord.js'); +const moment = require('moment'); + +module.exports = { + command_data: { + name: "getInfo", + type: 2 + }, + run: async (client, interaction) => { + + const user = interaction.guild.members.cache.get(interaction.targetId); + + const joinedAgoCalculator = { + fetch: { + user(userInput, type) { + if (!userInput) throw new ReferenceError('You didn\'t provided the user to calculate.'); + + if (type === "discord") { + const joinedDiscordTimestampInNumber = new Date().getTime() - userInput.createdTimestamp; + const joinedDiscordTimestampInString = moment(userInput.user.createdAt).fromNow(); + } else if (type === "server") { + const joinedServerTimestampInNumber = new Date().getTime() - userInput.joinedTimestamp; + const joinedServerTimestampInString = moment(userInput.joinedAt).fromNow(); + + return joinedServerTimestampInString.toString(); + } else throw new ReferenceError('Invalid type. Use "discord" or "server" only.'); + } + } + }; + + const bot = { + true: "Yes", + false: "No" + }; + + const acknowledgements = { + fetch: { + user(userInput) { + let result; + + try { + if (userInput.permissions.has(PermissionsBitField.ViewChannel)) result = "Server Member"; + if (userInput.permissions.has(PermissionsBitField.KickMembers)) result = "Server Moderator"; + if (userInput.permissions.has(PermissionsBitField.ManageServer)) result = "Server Manager"; + if (userInput.permissions.has(PermissionsBitField.Administrator)) result = "Server Administrator"; + if (userInput.id === interaction.guild.ownerId) result = "Server Owner"; + + } catch (e) { + result = "Server Member"; + }; + + return result; + } + } + }; + + return interaction.reply( + { + embeds: [ + new EmbedBuilder() + .setTitle(`${user.user.tag}'s information:`) + .setThumbnail(user.displayAvatarURL( + { + dynamic: true + } + )) + .addFields( + { + name: "Full name", + value: `${user.user.tag}`, + inline: true + }, + { + name: "Identification", + value: `\`${user.id}\``, + inline: true + }, + { + name: `Roles [${user.roles.cache.size - 1}]`, // Use "-1" because we removed the "@everyone" role + value: `${user.roles.cache.map((ROLE) => ROLE).join(' ').replace('@everyone', '') || "[No Roles]"}`, + inline: true + }, + { + name: "Joined server at", + value: `${new Date(user.joinedTimestamp).toLocaleString()}\n(${joinedAgoCalculator.fetch.user(user, "server")})`, + inline: true + }, + { + name: "Joined Discord at", + value: `${new Date(user.user.createdTimestamp).toLocaleString()}\n(${joinedAgoCalculator.fetch.user(user, "discord")})`, + inline: true + }, + { + name: "A Bot?", + value: `${bot[user.user.bot]}`, + inline: true + }, + { + name: "Acknowledgements", + value: `${acknowledgements.fetch.user(user)}` + } + ) + .setColor('Blue') + ], + ephemeral: true + } + ) + } +} \ No newline at end of file diff --git a/src/commands/Utility/aniinfo.js b/src/commands/Utility/aniinfo.js new file mode 100644 index 0000000..015f415 --- /dev/null +++ b/src/commands/Utility/aniinfo.js @@ -0,0 +1,120 @@ +const { EmbedBuilder, ApplicationCommandOptionType, Embed } = require('discord.js'); +const axios = require('axios'); +const cheerio = require('cheerio'); + +module.exports = { + command_data: { + name: 'anifo', + description: 'Get character information', + type: 1, + options: [ + { + name: 'character', + description: 'name of the anime character (MUST BE A FULL NAME)', + type: ApplicationCommandOptionType.String, + required: true + } + ], + }, + role_perms: null, + developers_only: false, + owner_only: false, + cooldown: '5s', + logger: true, + category: 'Utility', + run: async (client, interaction) => { + try { + // define an input + const wik_char = interaction.options.getString("character"); + let aniChar = wik_char; + + // capitalize the first character of the input + aniChar = capitalize(aniChar); + + // check if the input has spaces + if (wik_char.includes(' ')) { + // split the input by spaces, map each word to its capitalized form, join them back together, and replace spaces with underscores + aniChar = aniChar.split(' ').map(capitalize).join(' ').replace(/ /g, '_'); + } + // console.log(aniChar) + const url = `https://characterprofile.fandom.com/wiki/${aniChar}`; + + axios.get(url).then(async response => { + await interaction.deferReply() + // Load the HTML content + const $ = cheerio.load(response.data); + // console.log($); + if (!aniChar === "Monkey_D._Luffy") { + try { + // Extract character information + const name = $('.page-header__title').text(); + const image = $('td.wikia-infobox-image > figure > a > img').attr('src'); + const summary = $('.mw-parser-output > p').first().text(); + const series = $('tr:has(th:contains("Series")) > td').text(); + const sex = $('tr:has(th:contains("Sex")) > td').text(); + const birthday = $('tr:has(th:contains("Birthday")) > td').text(); + const height = $('tr:has(th:contains("Height")) > td').text(); + + // Create a Discord embed + const aniembed = new EmbedBuilder() + .setTitle(name) + .setURL(url) + .setDescription(summary) + .setImage(image) + .setColor(0x0099FF) + .addFields( + { name: "Series: ", value: series, inline: true }, + { name: "Gender: ", value: sex, inline: true }, + { name: "birthday: ", value: birthday, inline: true }, + { name: "height: ", value: height, inline: true } + ) + + await interaction.editReply({ embeds: [aniembed] }); + } catch (e) { + return await interaction.editReply({ embeds: [new EmbedBuilder().setColor(0xe32424).setDescription('Character not found/available to be in here... I guess')] }) + } + } else if (aniChar === "Monkey_D._Luffy") { + // Extract character information + const name = $('.page-header__title').text(); + const image = "https://i.imgur.com/ywG4LqT.png" + const summary = $('.mw-parser-output > p').first().text(); + const series = $('tr:has(th:contains("Series")) > td').text(); + const sex = $('tr:has(th:contains("Sex")) > td').text(); + const birthday = $('tr:has(th:contains("Birthday")) > td').text(); + const height = $('tr:has(th:contains("Height")) > td').text(); + + // Create a Discord embed + const aniembed = new EmbedBuilder() + .setTitle(name) + .setURL(url) + .setDescription(summary, image) + .setImage(image) + .setColor(0x0099FF) + .addFields( + { name: "Series: ", value: series, inline: true }, + { name: "Gender: ", value: sex, inline: true }, + { name: "birthday: ", value: birthday, inline: true }, + { name: "height: ", value: height, inline: true }, + { name: "Reputation: ", value: "The King Of The Pirates", inline: true } + ) + .setFooter({ text: "👑 - The King Of The Pirates" }) + + await interaction.editReply({ embeds: [aniembed] }); + } + }); + } catch (e) { + console.log(e) + const errorEmbed = new EmbedBuilder() + .setTitle('An error occurred') + .setDescription('```' + e + '```') + .setColor(0xe32424); + + await interaction.editReply({ embeds: [errorEmbed] }); + } + } +}; + +// define a function that capitalizes the first letter of a word +function capitalize(word) { + return word.charAt(0).toUpperCase() + word.slice(1); +} \ No newline at end of file diff --git a/src/commands/Utility/hello_world.js b/src/commands/Utility/hello_world.js new file mode 100644 index 0000000..0c2464d --- /dev/null +++ b/src/commands/Utility/hello_world.js @@ -0,0 +1,30 @@ +const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +module.exports = { + command_data: { + name: 'hello-world', + description: 'Replies with hello world!', + type: 1, + options: [], + }, + role_perms: null, + developers_only: false, + cooldown: '5s', + category: 'Utility', + run: async (client, interaction) => { // Even the 'client' parameter is not used, it must be provided. + + return interaction.reply({ + content: 'Click the button below!', + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('hello_reply') + .setLabel('Click me!') + .setStyle(ButtonStyle.Primary) + ) + ] + }); + + } +}; diff --git a/src/commands/Utility/ping.js b/src/commands/Utility/ping.js new file mode 100644 index 0000000..d4260a2 --- /dev/null +++ b/src/commands/Utility/ping.js @@ -0,0 +1,23 @@ +const ms = require('ms'); + +module.exports = { + command_data: { + name: 'ping', + description: 'Replies with pong!', + type: 1, + options: [], + }, + role_perms: null, + developers_only: false, + cooldown: '5s', + category: 'Utility', + run: async (client, interaction) => { + const date = new Date().getTime(); + + await interaction.deferReply(); + + return interaction.editReply({ + content: `\`🏓\` Pong!\nClient latency: **${ms(date - interaction.createdTimestamp, { long: true })}**\nWebsocket latency: **${ms(client.ws.ping, { long: true })}**` + }); + } +}; \ No newline at end of file diff --git a/src/config/main.js b/src/config/main.js new file mode 100644 index 0000000..0e993c9 --- /dev/null +++ b/src/config/main.js @@ -0,0 +1,70 @@ +const { GatewayIntentBits, Partials } = require('discord.js'); +require('dotenv').config(); + +module.exports = { + // Client configuration: + client: { + constructor: { + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildWebhooks, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.GuildScheduledEvents, + GatewayIntentBits.GuildPresences, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMessageTyping, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildInvites, + GatewayIntentBits.GuildIntegrations, + GatewayIntentBits.GuildEmojisAndStickers, + GatewayIntentBits.AutoModerationConfiguration, + GatewayIntentBits.AutoModerationExecution, + GatewayIntentBits.DirectMessageReactions, + GatewayIntentBits.DirectMessageTyping, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.MessageContent + ], + partials: [ + Partials.Channel, + Partials.GuildMember, + Partials.GuildScheduledEvent, + Partials.Message, + Partials.Reaction, + Partials.ThreadMember, + Partials.User + ], + presence: { + activities: [ + { + name: 'Hello world!', + type: 0 + } + ], + status: 'dnd' + } + } + }, + + // Bot info + luf: { + token: "MTEyMzExNDk5NjQ1NzU0OTk2Ng.GBBTjP.X4NTM2bKeCuFM_bn-G0Sq8I1-iXJGxEUrS-h2Q" || process.env.TOKEN, + id: "1123114996457549966" || process.env.BOT_ID + }, + + // Database: + database: { + mongodb_uri: process.env.MONGO + }, + + // Users: + users: { + developers: ["939867069070065714"], + owners: ["939867069070065714"] + }, + + channels: { + logging_channel: "1123137929389293638" || process.env.LOGS + } +}; diff --git a/src/config/models.js b/src/config/models.js new file mode 100644 index 0000000..66d3ee0 --- /dev/null +++ b/src/config/models.js @@ -0,0 +1,24 @@ +module.exports = [ + { + name: 'Stable Diffusion (Default)', + value: + 'stability-ai/stable-diffusion:27b93a2413e7f36cd83da926f3656280b2931564ff050bf9575f1fdf9bcd7478', + }, + { + name: 'Openjourney (Midjourney style)', + value: + 'prompthero/openjourney:9936c2001faa2194a261c01381f90e65261879985476014a0a37a334593a05eb', + }, + { + name: 'Erlich', + value: 'laion-ai/erlich:92fa143ccefeed01534d5d6648bd47796ef06847a6bc55c0e5c5b6975f2dcdfb', + }, + { + name: 'Mini DALL-E', + value: 'kuprel/min-dalle:2af375da21c5b824a84e1c459f45b69a117ec8649c2aa974112d7cf1840fc0ce', + }, + { + name: 'Waifu Diffusion', + value: 'cjwbw/waifu-diffusion:25d2f75ecda0c0bed34c806b7b70319a53a1bccad3ade1a7496524f013f48983', + }, +]; \ No newline at end of file diff --git a/src/config/topPage.js b/src/config/topPage.js new file mode 100644 index 0000000..c4d4a45 --- /dev/null +++ b/src/config/topPage.js @@ -0,0 +1,24 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); + +async function scrapeTopAnime(page) { + const url = `https://myanimelist.net/topanime.php?limit=${(page - 1) * 10}`; + const response = await axios.get(url); + const $ = cheerio.load(response.data); + const topAnimeList = []; + + $('.ranking-list').each((index, element) => { + if (index < 10) { + const rank = $(element).find('.rank').text().trim(); + const title = $(element).find('.title').text().trim(); + const score = $(element).find('.score').text().trim(); + topAnimeList.push({ rank, title, score }); + } + }); + + return topAnimeList; +} + +module.exports = { + scrapeTopAnime, +}; \ No newline at end of file diff --git a/src/error/main.js b/src/error/main.js new file mode 100644 index 0000000..6dffd59 --- /dev/null +++ b/src/error/main.js @@ -0,0 +1,16 @@ +module.exports = () => { + process.on('unhandledRejection', (reason, promise) => { + console.error('[ANTI-CRASH] unhandledRejection'); + console.log(promise, reason); + }); + + process.on("uncaughtException", (err, origin) => { + console.error('[ANTI-CRASH] uncaughtException'); + console.log(err, origin); + }); + + process.on('uncaughtExceptionMonitor', (err, origin) => { + console.error('[ANTI-CRASH] uncaughtExceptionMonitor'); + console.log(err, origin); + }); +}; diff --git a/src/events/Client/ready.js b/src/events/Client/ready.js new file mode 100644 index 0000000..c548c28 --- /dev/null +++ b/src/events/Client/ready.js @@ -0,0 +1,9 @@ +const client = require('../../index'); + +module.exports = { + event: 'ready', + once: true, + run: (client) => { + console.log('> Logged in as ' + client.user.username + '.'); + } +}; \ No newline at end of file diff --git a/src/events/Guild/application_commands.js b/src/events/Guild/application_commands.js new file mode 100644 index 0000000..1dcea2d --- /dev/null +++ b/src/events/Guild/application_commands.js @@ -0,0 +1,164 @@ +// const client = require('../../index'); +const config = require('../../config/main'); +const ms = require('ms'); +const { EmbedBuilder } = require('discord.js'); + +const map_cooldown = new Map(); + +module.exports = { + event: 'interactionCreate', + run: async (client, interaction) => { + if (interaction.isChatInputCommand() || interaction.isMessageContextMenuCommand()) { + const command = await client.commands.get(interaction.commandName); + + if (!command) return interaction.reply({ + content: `\`❌\` Invalid command, please try again later.`, + ephemeral: true + }); + + try { + if (command.owner_only === true && Array.isArray(config.users?.owners) && config.users.developers.length > 0) { + if (!config.users.owners.includes(interaction.user.id)) { + return interaction.reply({ + content: `\`❌\` Sorry but this command is restricted for the bot owner only!`, + ephemeral: true + }); + }; + }; + + if (command.developers_only === true && Array.isArray(config.users?.developers) && config.users.developers.length > 0) { + if (!config.users.developers.includes(interaction.user.id)) { + try { + await interaction.reply({ + content: `\`❌\` Sorry but this command is restricted for developers only!`, + ephemeral: true + }); + } catch (error) { + console.error(`Failed to send interaction reply: ${error}`); + } + return; + } + } + + + if (command.role_perms) { + let boolean = false; + + if (Array.isArray(command.role_perms)) { + if (command.role_perms.length > 0) { + await Promise.all(command.role_perms.map(async (r) => { + const role = interaction.guild.roles.cache.get(r); + + if (role && interaction.member.roles.cache.some((r1) => r1.id === role.id)) { + boolean = true; + } + })); + } + } else if (typeof command.role_perms === 'string') { + const role = interaction.guild.roles.cache.get(command.role_perms); + + if (role && interaction.member.roles.cache.has(role.id)) { + boolean = true; + } + } + + if (!boolean) { + try { + await interaction.reply({ + content: `\`❌\` Sorry but you are not allowed to use this command!`, + ephemeral: true + }); + } catch (error) { + console.error(`Failed to send interaction reply: ${error}`); + } + return; + } + } + + + if (command.cooldown && typeof command.cooldown === 'string') { + const milliseconds = ms(command.cooldown); + + if (map_cooldown.has(interaction.user.id)) { + const date_now = new Date().getTime(); + + const data = map_cooldown.get(interaction.user.id); + + if (data.sent_on < date_now) { + const time = new Date(date_now + milliseconds).getTime(); + + return interaction.reply({ + content: `\`❌\` You are on cooldown! You can use this command again in .`, + ephemeral: true + }); + }; + } else { + const date_now = new Date().getTime(); + + map_cooldown.set(interaction.user.id, { + sent_on: date_now + }); + + setTimeout(async () => { + map_cooldown.delete(interaction.user.id); + }, milliseconds); + }; + }; + + command.run(client, interaction, config); + + if (command.logger && typeof command.logger === 'boolean') { + if (!config.channels?.logging_channel) return; + + const channel = client.channels.cache.get(config.channels.logging_channel); + + if (!channel) return; + + return channel.send({ + embeds: [ + new EmbedBuilder() + .setTitle('Application command used: ' + interaction.commandName) + .setAuthor({ + name: client.user.username, + iconURL: client.user.displayAvatarURL({ dynamic: true }) + }) + .setFields( + { + name: 'User', + value: `${interaction.member} (\`${interaction.user.id}\`)` + }, + { + name: 'Used on', + value: ` ()` + } + ) + .setColor('Blue') + ] + }).catch(() => { }); + }; + } catch (err) { + console.warn(`[WARN] Failed to run the command \'${interaction.commandName}\'.`); + console.log(err); + } finally { + console.log(`[INFO] ${interaction.user.username} has used the SLASHcommand \'${interaction.commandName}\'.`); + }; + } else if (interaction.isUserContextMenuCommand()) { // User + try { + const command = client.user_commands.get(interaction.commandName); + + if (!command) return; + + try { + await command.run(client, interaction, config); + } catch (err) { + console.log(err) + } + } catch (err) { + console.warn(`[WARN] Failed to run the command \'${interaction.commandName}\'.`); + console.log(err) + } finally { + console.log(`[INFO] ${interaction.user.username} has used the USERcommand \'${interaction.commandName}\'.`); + } + } return; + } +} \ No newline at end of file diff --git a/src/events/Guild/interactions.js b/src/events/Guild/interactions.js new file mode 100644 index 0000000..872b2e6 --- /dev/null +++ b/src/events/Guild/interactions.js @@ -0,0 +1,12 @@ +module.exports = { + event: 'interactionCreate', + run: async (client, interaction) => { + if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) { + const interactionModuleCustomId = await client.interactions.get(interaction.customId); + + if (!interactionModuleCustomId) return; + + interactionModuleCustomId.run(client, interaction); + } else return; + } +} \ No newline at end of file diff --git a/src/handlers/application_commands.js b/src/handlers/application_commands.js new file mode 100644 index 0000000..131c27b --- /dev/null +++ b/src/handlers/application_commands.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const chalk = require('chalk'); +const { REST, Routes } = require('discord.js'); + +module.exports = (client, config) => { + let commands = []; + + fs.readdirSync('./src/commands/').forEach((dir) => { + const files = fs.readdirSync('./src/commands/' + dir) + .filter((file) => file.endsWith('.js')) + + for (let file of files) { + let pulled = require('../commands/' + dir + '/' + file); + + if (pulled.command_data && typeof pulled.command_data === 'object') { + console.log(chalk.yellow('Loaded application command: ' + file + '.')) + + commands.push(pulled.command_data); + client.commands.set(pulled.command_data.name, pulled); + } else { + console.log(chalk.yellow('[WARN] Received empty property \'command_data\' invalid type (Object) in ' + file + '.')) + + continue; + }; + }; + }); + + const rest = new REST({ version: '10' }).setToken(config.luf.token); + + (async () => { + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationCommands(config.luf.id), + { body: commands }, + ); + + console.log(chalk.green(`Successfully reloaded ${data.length} application (/) commands.`)); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } + })(); +}; diff --git a/src/handlers/database.js b/src/handlers/database.js new file mode 100644 index 0000000..aa2abc6 --- /dev/null +++ b/src/handlers/database.js @@ -0,0 +1,11 @@ +const { MongoDBConnector } = require('discord.js-v14-helper'); + +module.exports = (client, config) => { + if (require('../config/main').database && require('../config/main').database.mongodb_uri) { + const connector = new MongoDBConnector(require('../config/main').database.mongodb_uri); + + connector.start(); + } else { + console.warn('[WARN] The MongoDB URI couldn\'t be found, database is not connected.'); + }; +}; diff --git a/src/handlers/events.js b/src/handlers/events.js new file mode 100644 index 0000000..467c5a7 --- /dev/null +++ b/src/handlers/events.js @@ -0,0 +1,26 @@ +const fs = require('fs'); +const chalk = require('chalk'); + +module.exports = (client) => { + for (const dir of fs.readdirSync('./src/events/')) { + for (const file of fs.readdirSync('./src/events/' + dir).filter((f) => f.endsWith('.js'))) { + const module = require('../events/' + dir + '/' + file); + + if (!module) continue; + + if (!module.event || !module.run) { + console.log(chalk.red('Unable to load the event ' + file + ' due to missing \'name\' or/and \'run\' properties.', 'warn')); + + continue; + }; + + console.log(chalk.green('Loaded new event: ' + file, 'info')); + + if (module.once) { + client.once(module.event, (...args) => module.run(client, ...args)); + } else { + client.on(module.event, (...args) => module.run(client, ...args)); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/handlers/interactions.js b/src/handlers/interactions.js new file mode 100644 index 0000000..047470c --- /dev/null +++ b/src/handlers/interactions.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const { BetterConsoleLogger, Colors } = require('discord.js-v14-helper'); + +module.exports = (client, config) => { + for (let file of fs.readdirSync('./src/interactions')) { + let module = require('../interactions/' + file); + + if (module.customId && typeof module.customId === 'string') { + client.interactions.set(module.customId, module); + + new BetterConsoleLogger('Loaded interactions: ' + file + '.') + .setTextColor(Colors.Green) + .log(true); + } else { + new BetterConsoleLogger('[WARN] Received empty property \'customId\' or invalid type (String) in ' + file + '.') + .setTextColor(Colors.Red) + .log(true); + + continue; + }; + }; +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cfa9e4c --- /dev/null +++ b/src/index.js @@ -0,0 +1,47 @@ +const { Client, Collection } = require('discord.js'); +const { Colors, BetterConsoleLogger } = require('discord.js-v14-helper'); +const chalk = require('chalk'); +const fs = require('fs'); +const config = require('./config/main'); + +const client = new Client(config.client.constructor); + +client.commands = new Collection(); +client.interactions = new Collection(); +client.user_commands = new Collection(); + +module.exports = client; + +console.log(chalk.hex('#EDE9C6') + ` ▄▄ ▄▄▄▄ + ▀███▀▀▀██▄ ██ ██ ▀████▀ ▄█▀▀▀ + ██ ▀██▄ ██ ██ ██▀ + ██ ▄██▀███▄███ ▄██▀██▄ ▀███ ▄▄█▀██ ▄██▀████████ ██ ▀███ ▀███ █████ + ███████ ██▀ ▀▀ ██▀ ▀██ ██ ▄█▀ ███▀ ██ ██ ██ ██ ██ ██ + ██ ██ ██ ██ ██ ██▀▀▀▀▀▀█ ██ ██ ▄ ██ ██ ██ + ██ ██ ██▄ ▄██ ██ ██▄ ▄█▄ ▄ ██ ██ ▄█ ██ ██ ██ + ▄████▄ ▄████▄ ▀█████▀ ██ ▀█████▀█████▀ ▀████ ██████████ ▀████▀███▄████▄ + ██ ██ + ▀███ + ` +) +fs.readdirSync('./src/handlers').forEach((handler) => { + console.log(chalk.hex('#FFB300')`[INFO] Handler loaded: ${handler}`) + + require('./handlers/' + handler)(client, config); +}); + +require('./error/main')(); + +const AuthenticationToken = process.env.TOKEN || config.luf.token; +if (!AuthenticationToken) { + console.warn(chalk.hex('#A90000')`[CRASH] Authentication Token for Discord bot is required! Use Envrionment Secrets or main.js.`) + process.exit(); +}; + +client.login(AuthenticationToken) + .catch((err) => { + console.error(chalk.hex('#A90000')`[CRASH] Something went wrong while connecting to your bot...`); + console.error(chalk.hex('#A90000')`[CRASH] Error from Discord API: ${err}`); + process.exit(); + }); \ No newline at end of file diff --git a/src/interactions/hello_reply.js b/src/interactions/hello_reply.js new file mode 100644 index 0000000..19ff070 --- /dev/null +++ b/src/interactions/hello_reply.js @@ -0,0 +1,10 @@ +module.exports = { + customId: 'hello_reply', + run: async (client, interaction) => { + + return interaction.reply({ + content: 'Hello world!' + }); + + } +}; \ No newline at end of file diff --git a/src/schemas/jointocreate.js b/src/schemas/jointocreate.js new file mode 100644 index 0000000..3c14769 --- /dev/null +++ b/src/schemas/jointocreate.js @@ -0,0 +1,10 @@ +const { model, Schema } = require('mongoose') + +let jointocreate = new Schema({ + Guild: String, + Channel: String, + Category: String, + VoiceLimit: Number +}); + +module.exports = model('jointocreate', jointocreate); \ No newline at end of file diff --git a/src/schemas/jointocreatechannel.js b/src/schemas/jointocreatechannel.js new file mode 100644 index 0000000..d26ad93 --- /dev/null +++ b/src/schemas/jointocreatechannel.js @@ -0,0 +1,9 @@ +const { model, Schema } = require('mongoose') + +let jointocreatechannel = new Schema({ + Guild: String, + Channel: String, + User: String +}); + +module.exports = model('jointocreatechannel', jointocreatechannel); \ No newline at end of file