├── .env-example ├── .gitignore ├── graphql.config.yml ├── crowdin.yml ├── src ├── banlist │ └── banstruc.txt ├── language │ ├── language_setup.js │ └── language │ │ ├── en.json │ │ └── vi.json ├── queries │ ├── staff.graphql │ ├── popular.graphql │ ├── trending.graphql │ ├── user.graphql │ ├── studio.graphql │ ├── characters.graphql │ ├── manga.graphql │ ├── schedule.graphql │ ├── random.graphql │ ├── search.graphql │ └── anime.graphql ├── status.js ├── commands │ ├── others │ │ ├── switch_language.js │ │ ├── ban_check.js │ │ ├── avatar.js │ │ ├── help.js │ │ ├── botstats.js │ │ └── weather.js │ ├── anime_commands │ │ ├── staff.js │ │ ├── charactersearch.js │ │ ├── characters.js │ │ ├── user.js │ │ ├── anime.js │ │ ├── trending.js │ │ ├── manga.js │ │ ├── random.js │ │ ├── popular.js │ │ ├── schedule.js │ │ ├── studio.js │ │ └── search.js │ └── minecraft_commands │ │ └── mcuser.js ├── bancheck.js ├── utli │ └── unregister_slash_commands.js └── index.js ├── package.json ├── LICENSE └── README.md /.env-example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN=Your bot token here -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | .idea 4 | .vscode -------------------------------------------------------------------------------- /graphql.config.yml: -------------------------------------------------------------------------------- 1 | schema: schema.graphql 2 | documents: '**/*.graphql' 3 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: src/language/language/en.json 3 | translation: /src/language/language/%two_letters_code%.%file_extension% 4 | -------------------------------------------------------------------------------- /src/banlist/banstruc.txt: -------------------------------------------------------------------------------- 1 | # Follow this ban structure "Time, date, reason" 2 | # For example: 3 | # UUID discord users are 12345678 4 | # Create 1 txt file with the name "12345678" and the file content as below 5 | # 1H11, 1/1/1111, Exemplary ban -------------------------------------------------------------------------------- /src/language/language_setup.js: -------------------------------------------------------------------------------- 1 | const language = require('i18n'); 2 | 3 | language.configure({ 4 | locales: ['vi', 'en'], 5 | directory: __dirname + '/language', 6 | defaultLocale: 'vi', 7 | objectNotation: true, 8 | }); 9 | 10 | module.exports = language; -------------------------------------------------------------------------------- /src/queries/staff.graphql: -------------------------------------------------------------------------------- 1 | query ($search: String) { 2 | Staff(search: $search) { 3 | id 4 | siteUrl 5 | name { 6 | first 7 | last 8 | } 9 | image { 10 | large 11 | } 12 | description 13 | } 14 | } -------------------------------------------------------------------------------- /src/queries/popular.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | Page (perPage: 10) { 3 | media (sort: POPULARITY_DESC, type: ANIME) { 4 | id 5 | siteUrl 6 | title { 7 | romaji 8 | } 9 | description 10 | coverImage { 11 | large 12 | } 13 | averageScore 14 | meanScore 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/queries/trending.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | Page (perPage: 10) { 3 | media (sort: TRENDING_DESC, type: ANIME) { 4 | id 5 | siteUrl 6 | title { 7 | romaji 8 | } 9 | description 10 | coverImage { 11 | large 12 | } 13 | averageScore 14 | meanScore 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/queries/user.graphql: -------------------------------------------------------------------------------- 1 | query ($username: String) { 2 | User(name: $username) { 3 | id 4 | name 5 | about 6 | siteUrl 7 | avatar { 8 | large 9 | } 10 | statistics { 11 | anime { 12 | count 13 | minutesWatched 14 | } 15 | manga { 16 | count 17 | chaptersRead 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/queries/studio.graphql: -------------------------------------------------------------------------------- 1 | query ($search: String) { 2 | Studio(search: $search) { 3 | id 4 | name 5 | siteUrl 6 | media(isMain: true, sort: POPULARITY_DESC) { 7 | nodes { 8 | id 9 | siteUrl 10 | title { 11 | romaji 12 | } 13 | startDate { 14 | year 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/queries/characters.graphql: -------------------------------------------------------------------------------- 1 | query Characters($search: String) { 2 | Character(search: $search) { 3 | id 4 | siteUrl 5 | name { 6 | full 7 | } 8 | image { 9 | large 10 | } 11 | description 12 | media { 13 | nodes { 14 | id 15 | title { 16 | romaji 17 | } 18 | coverImage { 19 | large 20 | } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/queries/manga.graphql: -------------------------------------------------------------------------------- 1 | query ($name: String) { 2 | Media(search: $name, type: MANGA) { 3 | id 4 | siteUrl 5 | title { 6 | romaji 7 | } 8 | description 9 | coverImage { 10 | large 11 | } 12 | chapters 13 | genres 14 | averageScore 15 | meanScore 16 | studios(isMain: true) { 17 | edges { 18 | node { 19 | name 20 | } 21 | } 22 | } 23 | genres 24 | } 25 | } -------------------------------------------------------------------------------- /src/queries/schedule.graphql: -------------------------------------------------------------------------------- 1 | query ($page: Int, $perPage: Int, $airingAtGreater: Int) { 2 | Page(page: $page, perPage: $perPage) { 3 | pageInfo { 4 | total 5 | currentPage 6 | lastPage 7 | hasNextPage 8 | } 9 | airingSchedules(airingAt_greater: $airingAtGreater) { 10 | id 11 | mediaId 12 | episode 13 | airingAt 14 | media { 15 | title { 16 | romaji 17 | } 18 | siteUrl 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/queries/random.graphql: -------------------------------------------------------------------------------- 1 | query($page: Int){ 2 | Page(page: $page, perPage: 50) { 3 | media(sort: [SCORE_DESC, ID_DESC], type: ANIME) { 4 | id 5 | title { 6 | romaji 7 | english 8 | native 9 | } 10 | averageScore 11 | genres 12 | description 13 | episodes 14 | status 15 | meanScore 16 | season 17 | startDate { 18 | year 19 | } 20 | studios(isMain: true) { 21 | edges { 22 | node { 23 | name 24 | } 25 | } 26 | } 27 | siteUrl 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anichan", 3 | "version": "2025.1.25.1", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "start": "node ./src/index.js", 8 | "unregister": "node ./src/utli/unregister_slash_commands.js", 9 | "debug": "nodemon ./src/index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@discordjs/builders": "^1.10.0", 16 | "@discordjs/rest": "^2.4.2", 17 | "axios": "^1.7.9", 18 | "discord-api-types": "^0.37.117", 19 | "discord.js": "^14.17.3", 20 | "dotenv": "^16.4.7", 21 | "i18n": "^0.15.1", 22 | "minecraft-server-util": "^5.4.4", 23 | "nodemon": "^3.1.9", 24 | "weather-js": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/queries/search.graphql: -------------------------------------------------------------------------------- 1 | query ($id: Int) { 2 | Media (id: $id, type: ANIME) { 3 | id 4 | siteUrl 5 | title { 6 | romaji 7 | english 8 | native 9 | } 10 | description 11 | coverImage { 12 | large 13 | } 14 | format 15 | episodes 16 | status 17 | startDate { 18 | year 19 | month 20 | day 21 | } 22 | endDate { 23 | year 24 | month 25 | day 26 | } 27 | season 28 | averageScore 29 | meanScore 30 | studios(isMain: true) { 31 | edges { 32 | node { 33 | name 34 | } 35 | } 36 | } 37 | genres 38 | } 39 | } -------------------------------------------------------------------------------- /src/queries/anime.graphql: -------------------------------------------------------------------------------- 1 | query ($name: String) { 2 | Media (search: $name, type: ANIME) { 3 | id 4 | siteUrl 5 | title { 6 | romaji 7 | english 8 | native 9 | } 10 | description 11 | coverImage { 12 | large 13 | } 14 | format 15 | episodes 16 | status 17 | startDate { 18 | year 19 | month 20 | day 21 | } 22 | endDate { 23 | year 24 | month 25 | day 26 | } 27 | season 28 | averageScore 29 | meanScore 30 | studios(isMain: true) { 31 | edges { 32 | node { 33 | name 34 | } 35 | } 36 | } 37 | genres 38 | } 39 | } -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | const { Client, GatewayIntentBits, ActivityType } = require('discord.js'); 2 | const dotenv = require('dotenv'); 3 | dotenv.config(); 4 | const client = new Client({ intents: [GatewayIntentBits.Guilds] }); 5 | const language = require('./language/language_setup.js'); 6 | 7 | const activities = [ 8 | { type: ActivityType.Watching, text: 'anime' }, 9 | { type: ActivityType.Watching, text: 'anilist.co' }, 10 | { type: ActivityType.Custom, text: 'manga' }, 11 | ]; 12 | 13 | function setRandomActivity() { 14 | const randomActivity = activities[Math.floor(Math.random() * activities.length)]; 15 | client.user.setActivity(randomActivity.text, { type: randomActivity.type }); 16 | } 17 | 18 | client.once('ready', () => { 19 | console.log(`${language.__n(`global.status_ready`)}`); 20 | 21 | setRandomActivity(); 22 | setInterval(() => { 23 | setRandomActivity(); 24 | }, 10 * 60 * 1000); 25 | }); 26 | 27 | client.login(process.env.BOT_TOKEN); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Anichan-Projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/others/switch_language.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const language = require("./../../language/language_setup.js"); 4 | 5 | module.exports = { 6 | owner: true, 7 | data: new SlashCommandBuilder() 8 | .setName("switch_language") 9 | .setDescription(`${language.__n("language.command_description")}`) 10 | .addStringOption((option) => 11 | option 12 | .setName("language") 13 | .setDescription(`${language.__n("language.language_option")}`) 14 | .setRequired(true) 15 | .addChoices( 16 | { name: "English", value: "en" }, 17 | { name: "Vietnamese", value: "vi" } 18 | ) 19 | ), 20 | async execute(interaction) { 21 | const languageres = interaction.options.getString("language"); 22 | language.setLocale(languageres); 23 | const response = language.__n(languageres); 24 | 25 | const embed = new EmbedBuilder() 26 | .setTitle(`${language.__n("language.language_switch")}`) 27 | .setDescription(`${language.__n("language.response_language")} ${response}`) 28 | .setColor("#66ffff"); 29 | 30 | await interaction.reply({ embeds: [embed], ephemeral: true }); 31 | }, 32 | }; -------------------------------------------------------------------------------- /src/bancheck.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { EmbedBuilder } = require('discord.js'); 4 | const language = require('./language/language_setup'); 5 | 6 | async function checkBan(interaction) { 7 | const userId = interaction.user.id; 8 | const userName = interaction.user.username; 9 | const banlistDir = path.join(__dirname, 'banlist'); 10 | const banFilePath = path.join(banlistDir, `${userId}.txt`); 11 | 12 | if (fs.existsSync(banFilePath)) { 13 | const banData = fs.readFileSync(banFilePath, 'utf8').split(', '); 14 | const [time, date, reason] = banData; 15 | const bantime = time +" "+ date; 16 | 17 | const embed = new EmbedBuilder() 18 | .setTitle(`${language.__n('userban.bantitle')}`) 19 | .setThumbnail(interaction.user.avatarURL()) 20 | .addFields( 21 | { name: `${language.__n('userban.username')}`, value: userName, inline: true}, 22 | { name: `${language.__n('userban.uuid')}`, value: userId, inline: true }, 23 | { name: `${language.__n('userban.bantime')}`, value: bantime, inline: true }, 24 | { name: `${language.__n('userban.reason')}`, value: reason || `${language.__n('userban.noreason')}`, inline: true } 25 | ) 26 | .setFooter({ text: `${language.__n('userban.contact')}` }) 27 | .setTimestamp(); 28 | 29 | await interaction.reply({ embeds: [embed], ephemeral: false }); 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | module.exports = { checkBan }; -------------------------------------------------------------------------------- /src/utli/unregister_slash_commands.js: -------------------------------------------------------------------------------- 1 | const { REST } = require('@discordjs/rest'); 2 | const { Routes } = require('discord.js'); 3 | require('dotenv').config(); 4 | 5 | const token = process.env.BOT_TOKEN; 6 | const clientId = process.env.CLIENT_ID; 7 | 8 | const rest = new REST({ version: '10' }).setToken(token); 9 | 10 | (async () => { 11 | try { 12 | console.log('Started removing all slash commands.'); 13 | 14 | const globalCommands = await rest.get(Routes.applicationCommands(clientId)); 15 | const globalCommandIds = globalCommands.map(command => command.id); 16 | 17 | for (const commandId of globalCommandIds) { 18 | await rest.delete(Routes.applicationCommand(clientId, commandId)); 19 | } 20 | 21 | console.log('Successfully removed all global slash commands.'); 22 | 23 | const guilds = await rest.get(Routes.userGuilds()); 24 | 25 | for (const guild of guilds) { 26 | const guildCommands = await rest.get(Routes.applicationGuildCommands(clientId, guild.id)); 27 | const guildCommandIds = guildCommands.map(command => command.id); 28 | 29 | for (const commandId of guildCommandIds) { 30 | await rest.delete(Routes.applicationGuildCommand(clientId, guild.id, commandId)); 31 | } 32 | 33 | console.log(`Successfully removed all slash commands in guild ${guild.id}.`); 34 | } 35 | 36 | console.log('Successfully removed all slash commands in all guilds.'); 37 | } catch (error) { 38 | console.error('Error removing slash commands:', error); 39 | } 40 | })(); -------------------------------------------------------------------------------- /src/commands/others/ban_check.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { SlashCommandBuilder } = require('@discordjs/builders'); 4 | const { EmbedBuilder } = require('discord.js'); 5 | const language = require("../../language/language_setup"); 6 | 7 | module.exports = { 8 | data: new SlashCommandBuilder() 9 | .setName('checkban') 10 | .setDescription('Check if the user is banned'), 11 | async execute(interaction) { 12 | const userId = interaction.user.id; 13 | const banlistDir = path.join(__dirname, '../../banlist'); 14 | const banFilePath = path.join(banlistDir, `${userId}.txt`); 15 | 16 | if (fs.existsSync(banFilePath)) { 17 | const banData = fs.readFileSync(banFilePath, 'utf8').split(', '); 18 | const [time, date, reason] = banData; 19 | const bantime = time + " "+date; 20 | 21 | const embed = new EmbedBuilder() 22 | .setTitle(`${language.__n('userban.bantitle')}`) 23 | .setThumbnail(interaction.user.avatarURL()) 24 | .addFields( 25 | { name: `${language.__n('userban.username')}`, value: userName, inline: true}, 26 | { name: `${language.__n('userban.uuid')}`, value: userId, inline: true }, 27 | { name: `${language.__n('userban.bantime')}`, value: bantime, inline: true }, 28 | { name: `${language.__n('userban.reason')}`, value: reason || `${language.__n('userban.noreason')}`, inline: true } 29 | ) 30 | .setFooter({ text: `${language.__n('userban.contact')}` }) 31 | .setTimestamp(); 32 | 33 | await interaction.reply({ embeds: [embed], ephemeral: true }); 34 | } else { 35 | await interaction.reply(`${language.__n('userban.noban')}`); 36 | } 37 | }, 38 | }; -------------------------------------------------------------------------------- /src/commands/others/avatar.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const language = require('./../../language/language_setup.js'); 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('avatar') 8 | .setDescription(`${language.__n('avatar.command_description')}`) 9 | .addUserOption(option => 10 | option.setName('user') 11 | .setDescription(`${language.__n('avatar.user_name')}`) 12 | .setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const user = interaction.options.getUser('user'); 18 | const member = interaction.guild.members.cache.find(m => m.user.id === user.id) || interaction.member; 19 | 20 | const avatar = member.user.displayAvatarURL({ format: 'png', dynamic: true, size: 1024 }); 21 | 22 | const embed = new EmbedBuilder() 23 | .setTitle(`${member.user.username} Avatar`) 24 | .setURL(avatar) 25 | .setImage(avatar) 26 | .setFooter({ 27 | text: `${language.__n('avatar.requested_by')}: ${interaction.user.username}`, 28 | iconURL: interaction.user.displayAvatarURL({ format: 'png', dynamic: true, size: 1024 }) 29 | }) 30 | .setColor('#eb3434'); 31 | 32 | await interaction.editReply({ embeds: [embed] }); 33 | } catch (error) { 34 | console.error(`${language.__n('global.error')}`, error); 35 | if (interaction.replied || interaction.deferred) { 36 | await interaction.editReply(`${language.__n('global.error_reply')}`); 37 | } else { 38 | await interaction.reply(`${language.__n('global.error_reply')}`); 39 | } 40 | } 41 | }, 42 | }; -------------------------------------------------------------------------------- /src/commands/others/help.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const fs = require("fs"); 4 | const path = require('path'); 5 | const language = require('./../../language/language_setup.js'); 6 | 7 | module.exports = { 8 | data: new SlashCommandBuilder() 9 | .setName("help") 10 | .setDescription(`${language.__n('help.command_description')}`), 11 | async execute(interaction) { 12 | try { 13 | await interaction.deferReply(); 14 | 15 | const embed = new EmbedBuilder() 16 | .setTitle(`${language.__n('help.command_title')}`) 17 | .setDescription(`${language.__n('help.embed_description')}`) 18 | .setTimestamp(); 19 | 20 | const commandsDirectory = path.join(__dirname, '..'); 21 | const commandFolders = fs.readdirSync(commandsDirectory).filter(file => fs.statSync(path.join(commandsDirectory, file)).isDirectory()); 22 | 23 | for (const folder of commandFolders) { 24 | const folderPath = path.join(commandsDirectory, folder); 25 | const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith('.js')); 26 | 27 | for (const file of commandFiles) { 28 | const filePath = path.join(folderPath, file); 29 | const command = require(filePath); 30 | embed.addFields({ name: command.data.name, value: command.data.description }); 31 | } 32 | } 33 | 34 | await interaction.editReply({ embeds: [embed], ephemeral: true }); 35 | } catch (error) { 36 | console.error(`${language.__n('global.error')}`, error); 37 | if (interaction.replied || interaction.deferred) { 38 | await interaction.editReply(`${language.__n('global.error_reply')}`); 39 | } else { 40 | await interaction.reply(`${language.__n('global.error_reply')}`); 41 | } 42 | } 43 | }, 44 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/staff.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('staff') 11 | .setDescription(`${language.__n('staff.command_description')}`) 12 | .addStringOption(option => option.setName('name').setDescription(`${language.__n('staff.staff_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const staffName = interaction.options.getString('name'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/staff.graphql'), 'utf8'); 19 | const variables = { search: staffName }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const staffData = data.data.Staff; 33 | 34 | if (!staffData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${staffName}**`); 36 | } 37 | 38 | const embed = new EmbedBuilder() 39 | .setTitle(`${language.__n('staff.staff_info')}: ${staffData.name.first} ${staffData.name.last}`) 40 | .setURL(staffData.siteUrl) 41 | .setDescription(staffData.description || `${language.__n('global.unavailable')}`) 42 | .setImage(staffData.image.large) 43 | .setTimestamp(); 44 | 45 | await interaction.editReply({ embeds: [embed] }); 46 | } catch (error) { 47 | console.error(`${language.__n('global.error')}`, error); 48 | if (interaction.replied || interaction.deferred) { 49 | await interaction.editReply(`${language.__n('global.error_reply')}`); 50 | } else { 51 | await interaction.reply(`${language.__n('global.error_reply')}`); 52 | } 53 | } 54 | }, 55 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/charactersearch.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('character_search') 11 | .setDescription(`${language.__n('character_search.command_description')}`) 12 | .addStringOption(option => option.setName('character').setDescription(`${language.__n('character_search.command_description')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const character = interaction.options.getString('character'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/characters.graphql'), 'utf8'); 19 | const variables = { character }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const characterData = data.data.Character; 33 | 34 | if (!characterData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${character}**`); 36 | } 37 | 38 | const embed = new EmbedBuilder() 39 | .setTitle(`${language.__n('character_search.anime_list')} ${characterData.name.full}`) 40 | .setDescription(`${language.__n('character_search.anime_list')} **${characterData.name.full}**:`) 41 | .setTimestamp(); 42 | 43 | characterData.media.nodes.forEach(anime => { 44 | const animeTitle = anime.title.romaji || `${language.__n('global.unavailable')}`; 45 | embed.addFields({ name: animeTitle, value: '\u200B', inline: false }); 46 | }); 47 | 48 | await interaction.editReply({ embeds: [embed] }); 49 | } catch (error) { 50 | console.error(`${language.__n('global.error')}`, error); 51 | if (interaction.replied || interaction.deferred) { 52 | await interaction.editReply(`${language.__n('global.error_reply')}`); 53 | } else { 54 | await interaction.reply(`${language.__n('global.error_reply')}`); 55 | } 56 | } 57 | }, 58 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/characters.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('characters') 11 | .setDescription(`${language.__n('character.command_description')}`) 12 | .addStringOption(option => option.setName('name').setDescription(`${language.__n('character.character_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const characterName = interaction.options.getString('name'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/characters.graphql'), 'utf8'); 19 | const variables = { search: characterName }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const characterData = data.data.Character; 33 | 34 | if (!characterData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${characterName}**`); 36 | } 37 | 38 | let description = characterData.description || `${language.__n('global.no_description')}`; 39 | if (description && description.length > 600) { 40 | description = description.slice(0, 600) + '...'; 41 | } 42 | 43 | const uniqueAnimeAppearances = [...new Set(characterData.media.nodes.map(node => node.title.romaji))]; 44 | 45 | const embed = new EmbedBuilder() 46 | .setTitle(characterData.name.full) 47 | .setURL(characterData.siteUrl) 48 | .setDescription(description) 49 | .addFields({ name: `${language.__n('character.anime_appearances')}`, value: uniqueAnimeAppearances.join(', ') || `${language.__n('global.no_results')}` }) 50 | .setImage(characterData.image.large) 51 | .setColor('#C6FFFF') 52 | .setTimestamp(); 53 | 54 | await interaction.editReply({ embeds: [embed] }); 55 | } catch (error) { 56 | console.error(`${language.__n('global.error')}`, error); 57 | if (interaction.replied || interaction.deferred) { 58 | await interaction.editReply(`${language.__n('global.error_reply')}`); 59 | } else { 60 | await interaction.reply(`${language.__n('global.error_reply')}`); 61 | } 62 | } 63 | }, 64 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | AniChan is a bot for browsing AniList from within Discord using JavaScript. You can search for anime, light novels, get user stats, and more. 4 | 5 | ## Install 6 | 7 | ### Requirements: 8 | - [Discord.js v14](https://www.npmjs.com/package/discord.js/v/14.16.3) 9 | - Nodejs: Not lower than version [18.9.0](https://nodejs.org/download/release/v18.9.0/). Recommend: [Nodejs 18](https://nodejs.org/download/release/latest-hydrogen/) 10 | 11 | ### Install 12 | - Clone the repository: `git clone https://github.com/Anichan-Projects/AniChan.git` 13 | 14 | - Install the library: `npm install` 15 | 16 | - Edit the variables in the `.env-exmaple` file then rename the file to `.env` 17 | 18 | - Start the bot with the command: `npm run start` or `node ./src/index.js` 19 | 20 | - For debugging: use the command: `npm run debug` or `nodemon ./src/index.js` 21 | 22 | ### Features 23 | 24 | - Search for and display info about anime, light novels, and trending anime from AniList 25 | - Search for the names of anime with the appearance of a certain character 26 | - Show AniList user stats 27 | - Show trending anime 28 | - Get information about anime characters 29 | - Get information about a studio and staff 30 | - Get weather infomation 31 | - Get avtar user 32 | - Anime image source finder 33 | 34 | And many other features. 35 | 36 | # Commands 37 | ## Anime Commands 38 | - `/user`: Get AniList user stats. 39 | - `/search image url`: Search for anime names using links to images. 40 | - `/search image upload`: Search for anime names using upload images. 41 | - `/manga`: Search for manga. 42 | - `/anime`: Search for anime. 43 | - `/character_search`: Search for the names of anime with the appearance of a certain character. 44 | - `/character`: Get information about anime characters. 45 | - `/trending`: Show trending anime. 46 | - `/studio`: Get information about a studio. 47 | - `/staff`: Get basic information about staff. 48 | - `/popular`: Get the list of popular anime. 49 | 50 | ## Other Commands 51 | - `/help`: Get bot command list. 52 | - `/stats`: Get bot stats. 53 | - `/avatar`: Get user avatar. 54 | - `/ascii`: Convert text to ASCII code. 55 | - `/weather`: Get weather infomation. 56 | - `/switch_language`: Switch bot language. Default is English (EN) (owner only use this command). 57 | 58 | # Issues 59 | 60 | Open issue [here](https://github.com/Anichan-Projects/AniChan/issues) or [join the discord server](https://discord.gg/PE29XWTTc5) 61 | 62 | # Attribution 63 | 64 | ✨ [AniList](https://anilist.co) & [AniChart](https://anichart.net) 65 | 66 | ✨ [GraphQL](https://graphql.org) 67 | 68 | 69 | # License 70 | 71 | AniChan is an open-source project under the [MIT License](https://en.wikipedia.org/wiki/MIT_License) that allows you to modify the code used for: 72 | 73 | - [x] Revision 74 | - [x] Allotment 75 | - [x] Personal use 76 | 77 | In addition, you must also comply with the [Terms of Service of the AniList API](https://anilist.gitbook.io/anilist-apiv2-docs/overview/overview). -------------------------------------------------------------------------------- /src/commands/anime_commands/user.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('user') 11 | .setDescription(`${language.__n('user.command_description')}`) 12 | .addStringOption(option => option.setName('username').setDescription(`${language.__n('user.user_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const username = interaction.options.getString('username'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/user.graphql'), 'utf8'); 19 | 20 | const response = await axios.post('https://graphql.anilist.co', { 21 | query: query, 22 | variables: { username } 23 | }, { 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | 'Accept': 'application/json', 27 | } 28 | }); 29 | 30 | const data = response.data; 31 | const userData = data.data.User; 32 | 33 | const userImage = `https://img.anili.st/user/${userData.id}`; 34 | if (!userData) { 35 | return interaction.editReply(`${language.__n('global.no_results')}: **${username}**`); 36 | } 37 | 38 | const embed = new EmbedBuilder() 39 | .setTitle(`${userData.name}'s infomation`) 40 | .setURL(userData.siteUrl) 41 | .setColor('#C6FFFF') 42 | .addFields( 43 | { 44 | name: `${language.__n('user.anime_count')}`, 45 | value: `${userData.statistics.anime.count} ${language.__n('user.anime_count')}.`, 46 | inline: true, 47 | }, 48 | { 49 | name: `${language.__n('user.minutes_watched')}`, 50 | value: `${userData.statistics.anime.minutesWatched} ${language.__n('user.minutes_watched')}`, 51 | inline: true, 52 | }, 53 | { 54 | name: `${language.__n('user.manga_count')}`, 55 | value: `${userData.statistics.manga.count} ${language.__n('user.manga_count')}.`, 56 | inline: true, 57 | }, 58 | { 59 | name: `${language.__n('user.chapters_read')}`, 60 | value: `${userData.statistics.manga.chaptersRead} ${language.__n('user.chapters_read')}.`, 61 | inline: true, 62 | } 63 | ) 64 | .setImage(userImage) 65 | .setTimestamp(); 66 | 67 | await interaction.editReply({ embeds: [embed] }); 68 | } catch (error) { 69 | console.error(`${language.__n('global.error')}`, error); 70 | if (interaction.replied || interaction.deferred) { 71 | await interaction.editReply(`${language.__n('global.error_reply')}`); 72 | } else { 73 | await interaction.reply(`${language.__n('global.error_reply')}`); 74 | } 75 | } 76 | }, 77 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { Client, GatewayIntentBits, Collection, Partials } = require('discord.js'); 2 | const { REST } = require('@discordjs/rest'); 3 | const { Routes } = require('discord-api-types/v10'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const dotenv = require('dotenv'); 7 | const language = require('./language/language_setup.js'); 8 | const { checkBan } = require('./bancheck.js'); 9 | 10 | dotenv.config(); 11 | 12 | const client = new Client({ 13 | intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], 14 | partials: [Partials.Message, Partials.Channel, Partials.Reaction] 15 | }); 16 | const token = process.env.BOT_TOKEN; 17 | 18 | const commands = new Collection(); 19 | const commandsDirectory = path.join(__dirname, 'commands'); 20 | const commandFolders = fs.readdirSync(commandsDirectory); 21 | for (const folder of commandFolders) { 22 | const folderPath = path.join(commandsDirectory, folder); 23 | const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith('.js')); 24 | for (const file of commandFiles) { 25 | const filePath = path.join(folderPath, file); 26 | const command = require(filePath); 27 | commands.set(command.data.name, command); 28 | } 29 | } 30 | 31 | client.once('ready', async () => { 32 | console.log(`${client.user.tag} ${language.__n(`global.ready`)}`); 33 | console.log(`${language.__n(`global.waiting_command`)}`); 34 | const commandsArray = commands.map(command => command.data.toJSON()); 35 | const rest = new REST({ version: '10' }).setToken(token); 36 | 37 | try { 38 | await rest.put(Routes.applicationCommands(client.user.id), { body: commandsArray }); 39 | console.log(`${language.__n(`global.command_register`)}`); 40 | } catch (error) { 41 | console.error(`${language.__n(`global.command_register_error`)}`, error); 42 | } 43 | }); 44 | 45 | (async () => { 46 | await client.login(token); 47 | })(); 48 | 49 | require('./status.js'); 50 | 51 | client.on('guildCreate', async (guild) => { 52 | try { 53 | console.log(`${language.__n(`global.guild_join`)}: ${guild.name} (ID: ${guild.id}).`); 54 | 55 | const commandsArray = commands.map(command => command.data.toJSON()); 56 | const rest = new REST({ version: '10' }).setToken(token); 57 | 58 | await rest.put(Routes.applicationGuildCommands(client.user.id, guild.id), { body: commandsArray }); 59 | 60 | console.log(`${language.__n(`global.command_register`)}: ${guild.name} (ID: ${guild.id})`); 61 | } catch (error) { 62 | console.error(`${language.__n(`global.server_register_error`)} ${guild.name} (ID: ${guild.id})`, error); 63 | } 64 | }); 65 | 66 | client.on('interactionCreate', async (interaction) => { 67 | if (!interaction.isCommand()) return; 68 | 69 | const { commandName } = interaction; 70 | 71 | const command = commands.get(commandName); 72 | if (!command) return; 73 | 74 | const isBanned = await checkBan(interaction); 75 | if (isBanned) return; 76 | 77 | try { 78 | await command.execute(interaction); 79 | } catch (error) { 80 | console.error(error); 81 | await interaction.reply(`${language.__n(`global.command_error`)}`); 82 | } 83 | }); -------------------------------------------------------------------------------- /src/commands/others/botstats.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const language = require('./../../language/language_setup.js'); 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('stats') 8 | .setDescription(`${language.__n('bot_stats.description')}`), 9 | async execute(interaction) { 10 | try { 11 | await interaction.deferReply(); 12 | 13 | const uptime = process.uptime(); 14 | const days = Math.floor(uptime / 86400); 15 | const hours = Math.floor(uptime / 3600) % 24; 16 | const minutes = Math.floor(uptime / 60) % 60; 17 | const seconds = Math.floor(uptime % 60); 18 | const embed = new EmbedBuilder() 19 | .setTitle(`${language.__n('bot_stats.title')}`) 20 | .setColor('#66ffff') 21 | .setFields( 22 | { 23 | name: `Bot Ping`, 24 | value: `${interaction.client.ws.ping}ms`, 25 | inline: true, 26 | }, 27 | { 28 | name: `${language.__n('bot_stats.uptime')}`, 29 | value: `${days}d ${hours}h ${minutes}m ${seconds}s`, 30 | inline: true, 31 | }, 32 | { 33 | name: `${language.__n('bot_stats.version')}`, 34 | value: `v${require('../../../package.json').version}`, 35 | inline: true, 36 | }, 37 | { 38 | name: `CPU`, 39 | value: `${(process.cpuUsage().system / 1024 / 1024).toFixed(2)}%`, 40 | inline: true, 41 | }, 42 | { 43 | name: `RAM`, 44 | value: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`, 45 | inline: true, 46 | }, 47 | { 48 | name: `${language.__n('bot_stats.disk_usage')}`, 49 | value: `${(process.memoryUsage().external / 1024 / 1024).toFixed(2)} MB`, 50 | inline: true, 51 | }, 52 | { 53 | name: `${language.__n('bot_stats.os')}`, 54 | value: `${process.platform} ${process.arch}`, 55 | inline: true, 56 | }, 57 | { 58 | name: `${language.__n('bot_stats.node')}`, 59 | value: `${process.version}`, 60 | inline: true, 61 | }, 62 | { 63 | name: `${language.__n('bot_stats.library')}`, 64 | value: `Discord.js v${require('discord.js').version}`, 65 | inline: true, 66 | }, 67 | ); 68 | 69 | await interaction.editReply({ embeds: [embed] }); 70 | } catch (error) { 71 | console.error(`${language.__n('global.error')}`, error); 72 | if (interaction.replied || interaction.deferred) { 73 | await interaction.editReply(`${language.__n('global.error_reply')}`); 74 | } else { 75 | await interaction.reply(`${language.__n('global.error_reply')}`); 76 | } 77 | } 78 | } 79 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/anime.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('anime') 11 | .setDescription(`${language.__n('anime.command_description')}`) 12 | .addStringOption(option => option.setName('name').setDescription(`${language.__n('anime.anime_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const animeName = interaction.options.getString('name'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/anime.graphql'), 'utf8'); 19 | const variables = { name: animeName }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const animeData = data.data.Media; 33 | 34 | if (!animeData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${animeName}**`); 36 | } 37 | 38 | const genres = animeData.genres; 39 | if (genres.includes('Ecchi') || genres.includes('Hentai')) { 40 | return interaction.editReply(`**${language.__n('global.nsfw_block')} ${animeName}**\n${language.__n('global.nsfw_block_reason')}`); 41 | } 42 | 43 | let description = animeData.description; 44 | if (description) { 45 | description = description.replace(/\n*
/g, ''); 46 | if (description.length > 600) { 47 | description = description.slice(0, 600) + '...'; 48 | } 49 | } 50 | 51 | const embedImage = "https://img.anili.st/media/" + animeData.id; 52 | 53 | const embed = new EmbedBuilder() 54 | .setTitle(animeData.title.romaji) 55 | .setURL(animeData.siteUrl) 56 | .setDescription(description) 57 | .setColor('#66FFFF') 58 | .addFields( 59 | { name: `${language.__n('global.episodes')}`, value: `${animeData.episodes || `${language.__n('global.unavailable')}`}`, inline: true }, 60 | { name: `${language.__n('global.status')}`, value: `${animeData.status}`, inline: true }, 61 | { name: `${language.__n('global.average_score')}`, value: `${animeData.averageScore}/100`, inline: true }, 62 | { name: `${language.__n('global.mean_score')}`, value: `${animeData.meanScore}/100`, inline: true }, 63 | { name: `${language.__n('global.season')}`, value: `${animeData.season} - ${animeData.startDate.year}`, inline: true }, 64 | { name: `${language.__n('global.studio')}`, value: `${animeData.studios.edges.map(edge => edge.node.name).join(', ')}`, inline: true } 65 | ) 66 | .setImage(embedImage) 67 | .setTimestamp(); 68 | 69 | await interaction.editReply({ embeds: [embed] }); 70 | } catch (error) { 71 | console.error(`${language.__n('global.error')}`, error); 72 | if (interaction.replied || interaction.deferred) { 73 | await interaction.editReply(`${language.__n('global.error_reply')}`); 74 | } else { 75 | await interaction.reply(`${language.__n('global.error_reply')}`); 76 | } 77 | } 78 | }, 79 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/trending.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('trending') 11 | .setDescription(`${language.__n('trending.command_description')}`), 12 | async execute(interaction) { 13 | try { 14 | await interaction.deferReply(); 15 | 16 | const query = fs.readFileSync(path.join(__dirname, '../../queries/trending.graphql'), 'utf8'); 17 | 18 | const response = await axios.post('https://graphql.anilist.co', { 19 | query: query 20 | }, { 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | 'Accept': 'application/json', 24 | } 25 | }); 26 | 27 | const data = response.data; 28 | const trendingAnime = data.data.Page.media; 29 | let currentPage = 0; 30 | 31 | const updateEmbed = () => { 32 | const anime = trendingAnime[currentPage]; 33 | const description = anime.description ? anime.description.replace(/<[^>]+>/g, '').slice(0, 250) + '...' : `${language.__n('global.unavailable')}`; 34 | const embedImage = "https://img.anili.st/media/" + anime.id; 35 | const embed = new EmbedBuilder() 36 | .setTitle(anime.title.romaji) 37 | .setURL(anime.siteUrl) 38 | .setDescription(`__**${language.__n('global.description')}:**__ ${description}\n__**${language.__n('global.average_score')}:**__ ${anime.averageScore}/100\n__**${language.__n('global.mean_score')}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${language.__n('global.unavailable')}`}\n\n__**${language.__n('global.page')}:**__ ${currentPage + 1}/${trendingAnime.length}`) 39 | .setImage(embedImage) 40 | .setTimestamp(); 41 | 42 | const row = new ActionRowBuilder() 43 | .addComponents( 44 | new ButtonBuilder() 45 | .setCustomId('prev') 46 | .setLabel(`${language.__n('global.preview_button')}`) 47 | .setStyle(ButtonStyle.Primary) 48 | .setDisabled(currentPage === 0), 49 | new ButtonBuilder() 50 | .setCustomId('next') 51 | .setLabel(`${language.__n('global.next_button')}`) 52 | .setStyle(ButtonStyle.Primary) 53 | .setDisabled(currentPage === trendingAnime.length - 1) 54 | ); 55 | 56 | return { embeds: [embed], components: [row] }; 57 | }; 58 | 59 | await interaction.editReply(updateEmbed()); 60 | 61 | const filter = i => i.customId === 'prev' || i.customId === 'next'; 62 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); 63 | 64 | collector.on('collect', async i => { 65 | if (i.customId === 'prev' && currentPage > 0) { 66 | currentPage--; 67 | } else if (i.customId === 'next' && currentPage < trendingAnime.length - 1) { 68 | currentPage++; 69 | } 70 | await i.update(updateEmbed()); 71 | }); 72 | 73 | collector.on('end', async () => { 74 | try { 75 | await interaction.editReply({ components: [] }); 76 | } catch (error) { 77 | console.error(`${language.__n('global.error')}`, error); 78 | } 79 | }); 80 | } catch (error) { 81 | console.error(`${language.__n('global.error')}`, error); 82 | if (interaction.replied || interaction.deferred) { 83 | await interaction.editReply(`${language.__n('global.error_reply')}`); 84 | } else { 85 | await interaction.reply(`${language.__n('global.error_reply')}`); 86 | } 87 | } 88 | }, 89 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/manga.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('manga') 11 | .setDescription(`${language.__n('manga.command_description')}`) 12 | .addStringOption(option => option.setName('name').setDescription(`${language.__n('manga.manga_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const mangaName = interaction.options.getString('name'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/manga.graphql'), 'utf8'); 19 | const variables = { name: mangaName }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const mangaData = data.data.Media; 33 | 34 | if (!mangaData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${mangaName}**`); 36 | } 37 | 38 | const mangaGenre = mangaData.genres; 39 | if (mangaGenre.includes('Ecchi') || mangaGenre.includes('Hentai')) { 40 | return interaction.editReply(`**${language.__n('global.nsfw_block')} ${mangaName}**\n${language.__n('global.nsfw_block_reason')}`); 41 | } 42 | 43 | const description = mangaData.description ? mangaData.description.slice(0, 500) + '...' : 'Không có thông tin.'; 44 | 45 | const embedImage = "https://img.anili.st/media/" + mangaData.id; 46 | console.log(embedImage); 47 | const embed = new EmbedBuilder() 48 | .setTitle(mangaData.title.romaji) 49 | .setURL(mangaData.siteUrl) 50 | .setDescription(description) 51 | .addFields( 52 | { 53 | name: `${language.__n('global.chapters')}`, 54 | value: mangaData.chapters ? mangaData.chapters : `${language.__n('global.unavailable')}`, 55 | inline: true 56 | }, 57 | { 58 | name: `${language.__n('global.genres')}`, 59 | value: mangaData.genres.join(', '), 60 | inline: true 61 | }, 62 | { 63 | name: `${language.__n('global.average_score')}`, 64 | value: `${mangaData.averageScore}/100`, 65 | inline: true 66 | }, 67 | { 68 | name: `${language.__n('global.mean_score')}`, 69 | value: `${mangaData.meanScore ? mangaData.meanScore + '/100' : `${language.__n('global.unavailable')}`}`, 70 | inline: true 71 | }, 72 | ) 73 | .setImage(embedImage) 74 | .setTimestamp(); 75 | 76 | await interaction.editReply({ embeds: [embed] }); 77 | } catch (error) { 78 | console.error(`${language.__n('global.error')}`, error); 79 | if (interaction.replied || interaction.deferred) { 80 | await interaction.editReply(`${language.__n('global.error_reply')}`); 81 | } else { 82 | await interaction.reply(`${language.__n('global.error_reply')}`); 83 | } 84 | } 85 | }, 86 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/random.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const axios = require("axios"); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require("./../../language/language_setup.js"); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName("random") 11 | .setDescription(`${language.__n("random.command_description")}`), 12 | 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | const randomPage = Math.floor(Math.random() * 419);// 419 is the last page of the AniList API (update: 2025/02/1) 17 | console.log(`Random page: ${randomPage}`); 18 | const variables = { page: randomPage }; 19 | 20 | const query = fs.readFileSync(path.join(__dirname, '../../queries/random.graphql'), 'utf8'); 21 | const response = await axios.post("https://graphql.anilist.co", { 22 | query: query, 23 | variables: variables, 24 | }, { 25 | headers: { 26 | "Content-Type": "application/json", 27 | Accept: "application/json", 28 | } 29 | }); 30 | 31 | const animeList = response.data.data.Page.media; 32 | 33 | if (!animeList.length) { 34 | return interaction.editReply(language.__n("global.no_results")); 35 | } 36 | const randomAnime = animeList[Math.floor(Math.random() * animeList.length)]; 37 | 38 | const restrictedGenres = ["Ecchi", "Hentai"]; 39 | if (randomAnime.genres.some(genre => restrictedGenres.includes(genre))) { 40 | return interaction.editReply( 41 | `**${language.__n("global.nsfw_block")} ${randomAnime.title.romaji}**\n${language.__n("global.nsfw_block_reason")}` 42 | ); 43 | } 44 | 45 | let description = randomAnime.description ? randomAnime.description.replace(/\n*
/g, "") : ""; 46 | if (description.length > 600) { 47 | description = description.slice(0, 600) + "..."; 48 | } 49 | 50 | const embedImage = `https://img.anili.st/media/${randomAnime.id}`; 51 | 52 | const embed = new EmbedBuilder() 53 | .setTitle(randomAnime.title.romaji) 54 | .setURL(randomAnime.siteUrl) 55 | .setDescription(description) 56 | .setColor("#66FFFF") 57 | .addFields( 58 | { 59 | name: `${language.__n("global.episodes")}`, 60 | value: `${randomAnime.episodes || language.__n("global.unavailable")}`, 61 | inline: true, 62 | }, 63 | { 64 | name: `${language.__n("global.status")}`, 65 | value: `${randomAnime.status}`, 66 | inline: true, 67 | }, 68 | { 69 | name: `${language.__n("global.average_score")}`, 70 | value: `${randomAnime.averageScore}/100`, 71 | inline: true, 72 | }, 73 | { 74 | name: `${language.__n("global.mean_score")}`, 75 | value: `${randomAnime.meanScore}/100`, 76 | inline: true, 77 | }, 78 | { 79 | name: `${language.__n("global.season")}`, 80 | value: `${randomAnime.season} - ${randomAnime.startDate.year}`, 81 | inline: true, 82 | }, 83 | { 84 | name: `${language.__n("global.studio")}`, 85 | value: `${randomAnime.studios.edges.map(edge => edge.node.name).join(", ") || language.__n("global.unavailable")}`, 86 | inline: true, 87 | } 88 | ) 89 | .setImage(embedImage) 90 | .setTimestamp(); 91 | 92 | await interaction.editReply({ embeds: [embed] }); 93 | 94 | } catch (error) { 95 | console.error(`${language.__n("global.error")}`, error); 96 | if (interaction.replied || interaction.deferred) { 97 | await interaction.editReply(`${language.__n("global.error_reply")}`); 98 | } else { 99 | await interaction.reply(`${language.__n("global.error_reply")}`); 100 | } 101 | } 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /src/commands/anime_commands/popular.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('popular') 11 | .setDescription(`${language.__n('popular.command_description')}`), 12 | async execute(interaction) { 13 | try { 14 | await interaction.deferReply(); 15 | 16 | const query = fs.readFileSync(path.join(__dirname, '../../queries/popular.graphql'), 'utf8'); 17 | 18 | const response = await axios.post('https://graphql.anilist.co', { 19 | query: query 20 | }, { 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | 'Accept': 'application/json', 24 | } 25 | }); 26 | 27 | const data = response.data; 28 | const popularAnime = data.data.Page.media; 29 | let currentPage = 0; 30 | 31 | const updateEmbed = () => { 32 | const anime = popularAnime[currentPage]; 33 | const description = anime.description ? anime.description.replace(/<[^>]+>/g, '').slice(0, 250) + '...' : `${language.__n('global.unavailable')}`; 34 | const embedImage = "https://img.anili.st/media/" + anime.id; 35 | const embed = new EmbedBuilder() 36 | .setTitle(anime.title.romaji) 37 | .setURL(anime.siteUrl) 38 | .setDescription(`__**${language.__n('global.description')}:**__ ${description}\n__**${language.__n('global.average_score')}:**__ ${anime.averageScore}/100\n__**${language.__n('global.mean_score')}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${language.__n('global.unavailable')}`}\n\n__**${language.__n('global.page')}:**__ ${currentPage + 1}/${popularAnime.length}`) 39 | .setImage(embedImage) 40 | .setTimestamp(); 41 | 42 | const row = new ActionRowBuilder() 43 | .addComponents( 44 | new ButtonBuilder() 45 | .setCustomId('prev') 46 | .setLabel(`${language.__n('global.preview_button')}`) 47 | .setStyle(ButtonStyle.Primary) 48 | .setDisabled(currentPage === 0), 49 | new ButtonBuilder() 50 | .setCustomId('next') 51 | .setLabel(`${language.__n('global.next_button')}`) 52 | .setStyle(ButtonStyle.Primary) 53 | .setDisabled(currentPage === popularAnime.length - 1) 54 | ); 55 | 56 | return { embeds: [embed], components: [row] }; 57 | }; 58 | 59 | await interaction.editReply(updateEmbed()); 60 | 61 | const filter = i => i.customId === 'prev' || i.customId === 'next'; 62 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); 63 | 64 | collector.on('collect', async i => { 65 | if (i.customId === 'prev' && currentPage > 0) { 66 | currentPage--; 67 | } else if (i.customId === 'next' && currentPage < popularAnime.length - 1) { 68 | currentPage++; 69 | } 70 | await i.update(updateEmbed()); 71 | }); 72 | 73 | collector.on('end', async () => { 74 | try { 75 | await interaction.editReply({ components: [] }); 76 | } catch (error) { 77 | console.error(`${language.__n('global.error')}`, error); 78 | } 79 | }); 80 | } catch (error) { 81 | console.error(`${language.__n('global.error')}`, error); 82 | if (interaction.replied || interaction.deferred) { 83 | await interaction.editReply(`${language.__n('global.error_reply')}`); 84 | } else { 85 | await interaction.reply(`${language.__n('global.error_reply')}`); 86 | } 87 | } 88 | }, 89 | }; -------------------------------------------------------------------------------- /src/commands/others/weather.js: -------------------------------------------------------------------------------- 1 | const weather = require('weather-js'); 2 | const { SlashCommandBuilder } = require('@discordjs/builders'); 3 | const { EmbedBuilder } = require('discord.js'); 4 | const language = require('./../../language/language_setup.js'); 5 | 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName('weather') 9 | .setDescription(`${language.__n('weather.command_description')}`) 10 | .addStringOption(option => option.setName('location').setDescription(`${language.__n('weather.location')}`).setRequired(true)), 11 | 12 | async execute(interaction) { 13 | try { 14 | await interaction.deferReply(); 15 | 16 | const location = interaction.options.getString('location'); 17 | 18 | weather.find({ search: location, degreeType: 'C' }, async function (error, result) { 19 | if (error) { 20 | console.error(`${language.__n('global.error')}`, error); 21 | return interaction.editReply(`${language.__n('global.error_reply')}`); 22 | } 23 | if (result === undefined || result.length === 0) { 24 | return interaction.editReply(`${language.__n('global.no_results')}`); 25 | } 26 | 27 | const current = result[0].current; 28 | const location = result[0].location; 29 | 30 | const embed = new EmbedBuilder() 31 | .setTitle(current.observationpoint) 32 | .setDescription(`${current.skytext}`) 33 | .setThumbnail(current.imageUrl) 34 | .setTimestamp() 35 | .addFields( 36 | { 37 | name: `${language.__n('weather.longitude')}`, 38 | value: location.long, 39 | inline: true, 40 | }, 41 | { 42 | name: `${language.__n('weather.latitude')}`, 43 | value: location.lat, 44 | inline: true, 45 | }, 46 | { 47 | name: `${language.__n('weather.degreetype')}`, 48 | value: `°${location.degreetype}`, 49 | inline: true, 50 | }, 51 | { 52 | name: `${language.__n('weather.current_temperature')}`, 53 | value: `${current.temperature}°${location.degreetype}`, 54 | inline: true, 55 | }, 56 | { 57 | name: `${language.__n('weather.feels_like')}`, 58 | value: `${current.feelslike}°${location.degreetype}`, 59 | inline: true, 60 | }, 61 | { 62 | name: `${language.__n('weather.winddisplay')}`, 63 | value: `${current.winddisplay}`, 64 | inline: true, 65 | }, 66 | { 67 | name: `${language.__n('weather.humidity')}`, 68 | value: `${current.humidity}%`, 69 | inline: true, 70 | }, 71 | { 72 | name: `${language.__n('weather.observationtime')}`, 73 | value: `${current.observationtime}, GMT ${location.timezone}`, 74 | inline: true, 75 | } 76 | ) 77 | .setFooter({ text: `${interaction.client.user.username}`, iconURL: interaction.client.user.displayAvatarURL({ format: 'png', dynamic: true, size: 1024 }) }) 78 | .setColor('#66FFFF'); 79 | 80 | await interaction.editReply({ embeds: [embed], ephemeral: true }); 81 | }); 82 | } catch (error) { 83 | console.error(`${language.__n('global.error')}`, error); 84 | if (interaction.replied || interaction.deferred) { 85 | await interaction.editReply(`${language.__n('global.error_reply')}`); 86 | } else { 87 | await interaction.reply(`${language.__n('global.error_reply')}`); 88 | } 89 | } 90 | }, 91 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/schedule.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('schedule') 11 | .setDescription(`${language.__n('schedule.command_description')}`), 12 | async execute(interaction) { 13 | try { 14 | await interaction.deferReply(); 15 | 16 | const query = fs.readFileSync(path.join(__dirname, '../../queries/schedule.graphql'), 'utf8'); 17 | let currentPage = 1; 18 | const perPage = 10; 19 | const airingAtGreater = Math.floor(Date.now() / 1000); 20 | 21 | const fetchPage = async (page) => { 22 | const variables = { page, perPage, airingAtGreater }; 23 | const response = await axios.post('https://graphql.anilist.co', { 24 | query: query, 25 | variables: variables 26 | }, { 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | 'Accept': 'application/json', 30 | } 31 | }); 32 | return response.data.data.Page; 33 | }; 34 | 35 | let pageData = await fetchPage(currentPage); 36 | 37 | if (!pageData.airingSchedules.length) { 38 | return interaction.editReply(`${language.__n('global.no_results')}`); 39 | } 40 | 41 | const updateEmbed = () => { 42 | const scheduleList = pageData.airingSchedules.map(item => { 43 | const airDate = new Date(item.airingAt * 1000).toLocaleString(); 44 | return `**${item.media.title.romaji}** - Episode ${item.episode} - Airing at: ${airDate} - [Link](${item.media.siteUrl})`; 45 | }).join('\n'); 46 | 47 | const embed = new EmbedBuilder() 48 | .setTitle(`${language.__n('schedule.airing_schedule')}`) 49 | .setDescription(scheduleList) 50 | .setTimestamp(); 51 | 52 | const row = new ActionRowBuilder() 53 | .addComponents( 54 | new ButtonBuilder() 55 | .setCustomId('prev') 56 | .setLabel(`${language.__n('global.preview_button')}`) 57 | .setStyle(ButtonStyle.Primary) 58 | .setDisabled(currentPage === 1), 59 | new ButtonBuilder() 60 | .setCustomId('next') 61 | .setLabel(`${language.__n('global.next_button')}`) 62 | .setStyle(ButtonStyle.Primary) 63 | .setDisabled(!pageData.pageInfo.hasNextPage) 64 | ); 65 | 66 | return { embeds: [embed], components: [row] }; 67 | }; 68 | 69 | await interaction.editReply(updateEmbed()); 70 | 71 | const filter = i => i.customId === 'prev' || i.customId === 'next'; 72 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 200000 }); 73 | 74 | collector.on('collect', async i => { 75 | if (i.customId === 'prev' && currentPage > 1) { 76 | currentPage--; 77 | } else if (i.customId === 'next' && pageData.pageInfo.hasNextPage) { 78 | currentPage++; 79 | } 80 | pageData = await fetchPage(currentPage); 81 | await i.update(updateEmbed()); 82 | }); 83 | 84 | collector.on('end', async () => { 85 | try { 86 | await interaction.editReply({ components: [] }); 87 | } catch (error) { 88 | console.error(`${language.__n('global.error')}`, error); 89 | } 90 | }); 91 | } catch (error) { 92 | console.error(`${language.__n('global.error')}`, error); 93 | if (interaction.replied || interaction.deferred) { 94 | await interaction.editReply(`${language.__n('global.error_reply')}`); 95 | } else { 96 | await interaction.reply(`${language.__n('global.error_reply')}`); 97 | } 98 | } 99 | }, 100 | }; -------------------------------------------------------------------------------- /src/commands/minecraft_commands/mcuser.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('mcuser') 8 | .setDescription('Get Minecraft account information') 9 | .addStringOption(option => option.setName('identifier').setDescription('Minecraft username or UUID').setRequired(true)), 10 | async execute(interaction) { 11 | await interaction.deferReply(); 12 | 13 | const identifier = interaction.options.getString('identifier'); 14 | let uuid, username; 15 | 16 | try { 17 | const profileResponse = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${identifier}`); 18 | uuid = profileResponse.data.id; 19 | username = profileResponse.data.name; 20 | 21 | const sessionResponse = await axios.get(`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`); 22 | const properties = JSON.parse(Buffer.from(sessionResponse.data.properties[0].value, 'base64').toString('utf-8')); 23 | const skinUrl = properties.textures.SKIN.url; 24 | const capeUrl = properties.textures.CAPE ? properties.textures.CAPE.url : 'No cape available'; 25 | 26 | const headUrl = `https://cravatar.eu/helmhead/${username}`; 27 | 28 | const embed = new EmbedBuilder() 29 | .setTitle(`Minecraft User: ${username}`) 30 | .setThumbnail(headUrl) 31 | .addFields( 32 | { name: 'UUID', value: uuid, inline: true }, 33 | { name: 'Username', value: username, inline: true } 34 | ) 35 | .setTimestamp(); 36 | 37 | const row = new ActionRowBuilder() 38 | .addComponents( 39 | new StringSelectMenuBuilder() 40 | .setCustomId('mcuser_menu') 41 | .setPlaceholder('Select an option') 42 | .addOptions([ 43 | { 44 | label: 'Information', 45 | description: 'View user information', 46 | value: 'information', 47 | }, 48 | { 49 | label: 'Skin & Cape', 50 | description: 'View skin and cape information', 51 | value: 'skin_cape', 52 | }, 53 | ]) 54 | ); 55 | 56 | await interaction.editReply({ embeds: [embed], components: [row] }); 57 | 58 | const filter = i => i.customId === 'mcuser_menu' && i.user.id === interaction.user.id; 59 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); 60 | 61 | collector.on('collect', async i => { 62 | if (i.values[0] === 'information') { 63 | const infoEmbed = new EmbedBuilder() 64 | .setTitle(`Minecraft User: ${username}`) 65 | .setThumbnail(headUrl) 66 | .addFields( 67 | { name: 'UUID', value: uuid, inline: true }, 68 | { name: 'Username', value: username, inline: true } 69 | ) 70 | .setTimestamp(); 71 | await i.update({ embeds: [infoEmbed], components: [row] }); 72 | } else if (i.values[0] === 'skin_cape') { 73 | const skinCapeEmbed = new EmbedBuilder() 74 | .setTitle(`Skin & Cape for ${username}`) 75 | .setThumbnail(headUrl) 76 | .addFields( 77 | { name: 'Skin URL', value: `[Download Skin](${skinUrl})`, inline: true }, 78 | { name: 'Cape URL', value: capeUrl !== 'No cape available' ? `[Download Cape](${capeUrl})` : capeUrl, inline: true } 79 | ) 80 | .setTimestamp(); 81 | await i.update({ embeds: [skinCapeEmbed], components: [row] }); 82 | } 83 | }); 84 | 85 | collector.on('end', async () => { 86 | try { 87 | await interaction.editReply({ components: [] }); 88 | } catch (error) { 89 | console.error('Error clearing components:', error); 90 | } 91 | }); 92 | } catch (error) { 93 | console.error('Error fetching Minecraft user data:', error); 94 | await interaction.editReply('An error occurred while fetching the Minecraft user data. Please try again later.'); 95 | } 96 | }, 97 | }; -------------------------------------------------------------------------------- /src/commands/anime_commands/studio.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName('studio') 11 | .setDescription(`${language.__n('studio.command_description')}`) 12 | .addStringOption(option => option.setName('name').setDescription(`${language.__n('studio.studio_name')}`).setRequired(true)), 13 | async execute(interaction) { 14 | try { 15 | await interaction.deferReply(); 16 | 17 | const studioName = interaction.options.getString('name'); 18 | const query = fs.readFileSync(path.join(__dirname, '../../queries/studio.graphql'), 'utf8'); 19 | const variables = { search: studioName }; 20 | 21 | const response = await axios.post('https://graphql.anilist.co', { 22 | query: query, 23 | variables: variables 24 | }, { 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Accept': 'application/json', 28 | } 29 | }); 30 | 31 | const data = response.data; 32 | const studioData = data.data.Studio; 33 | 34 | if (!studioData) { 35 | return interaction.editReply(`${language.__n('global.no_results')} **${studioName}**`); 36 | } 37 | 38 | const animeList = studioData.media.nodes.map((anime, index) => { 39 | const animeTitle = anime.title.romaji; 40 | const animeUrl = anime.siteUrl; 41 | const animeYear = anime.startDate ? anime.startDate.year : `${language.__n('global.unavailable')}`; 42 | return `${index + 1}. [${animeTitle}](${animeUrl}) - ${language.__n('studio.product_year')}: ${animeYear}`; 43 | }).join('\n'); 44 | 45 | const pageSize = 10; 46 | const totalPages = Math.ceil(studioData.media.nodes.length / pageSize); 47 | let currentPage = 0; 48 | 49 | const updateEmbed = () => { 50 | const startIdx = currentPage * pageSize; 51 | const endIdx = startIdx + pageSize; 52 | const displayedAnime = animeList.split('\n').slice(startIdx, endIdx).join('\n'); 53 | 54 | const embed = new EmbedBuilder() 55 | .setTitle(`${language.__n('studio.studio_info')} ${studioData.name}`) 56 | .setURL(studioData.siteUrl) 57 | .setDescription(`${language.__n('studio.product_list')} ${studioData.name}:\n${displayedAnime}`) 58 | .setTimestamp(); 59 | 60 | const row = new ActionRowBuilder() 61 | .addComponents( 62 | new ButtonBuilder() 63 | .setCustomId('prev') 64 | .setLabel(`${language.__n('global.preview_button')}`) 65 | .setStyle(ButtonStyle.Primary) 66 | .setDisabled(currentPage === 0), 67 | new ButtonBuilder() 68 | .setCustomId('next') 69 | .setLabel(`${language.__n('global.next_button')}`) 70 | .setStyle(ButtonStyle.Primary) 71 | .setDisabled(currentPage === totalPages - 1) 72 | ); 73 | 74 | return { embeds: [embed], components: [row] }; 75 | }; 76 | 77 | await interaction.editReply(updateEmbed()); 78 | 79 | const filter = i => i.customId === 'prev' || i.customId === 'next'; 80 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); 81 | 82 | collector.on('collect', async i => { 83 | if (i.customId === 'prev' && currentPage > 0) { 84 | currentPage--; 85 | } else if (i.customId === 'next' && currentPage < totalPages - 1) { 86 | currentPage++; 87 | } 88 | await i.update(updateEmbed()); 89 | }); 90 | 91 | collector.on('end', async () => { 92 | try { 93 | await interaction.editReply({ components: [] }); 94 | } catch (error) { 95 | console.error(`${language.__n('global.error')}`, error); 96 | } 97 | }); 98 | } catch (error) { 99 | console.error(`${language.__n('global.error')}`, error); 100 | if (interaction.replied || interaction.deferred) { 101 | await interaction.editReply(`${language.__n('global.error_reply')}`); 102 | } else { 103 | await interaction.reply(`${language.__n('global.error_reply')}`); 104 | } 105 | } 106 | }, 107 | }; -------------------------------------------------------------------------------- /src/language/language/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "tracemoe_api_limit": "Because of the trace.moe server's limited requirements, you can only use this command once every 60 minutes.", 4 | "ready": "Ready", 5 | "status_ready": "Bot status ready", 6 | "command_error": "An error occurred while executing the command: ", 7 | "command_register": "Command registered", 8 | "waiting_command": "Waiting for register command...", 9 | "guild_join": "Joined a new server: ", 10 | "command_register_error": "Error when registering command: ", 11 | "server_register_error": "Error when registering command for server: ", 12 | "preview_button": "Preview", 13 | "next_button": "Next", 14 | "page": "Page", 15 | "unavailable": "Unavailable", 16 | "no_description": "No description.", 17 | "description": "Description", 18 | "error": "Error: ", 19 | "error_reply": "An error occurred. Please try again later.", 20 | "no_results": "No results found for search: ", 21 | "nsfw_block": "AniChan block content: ", 22 | "nsfw_block_reason": "__Reason:__ To protect your server from Discord's service, AniChan blocks search results containing 18+ content.", 23 | "episodes": "Episodes", 24 | "chapters": "Chapters", 25 | "genres": "Genres", 26 | "status": "Status", 27 | "average_score": "Average score", 28 | "mean_score": "Mean score", 29 | "season": "Season", 30 | "studio": "Studio", 31 | "minute": "minute", 32 | "retry": "Retry" 33 | }, 34 | "help": { 35 | "command_description": "Shows the usage and functions of all commands.", 36 | "command_title": "Command list", 37 | "embed_description": "List of available commands:" 38 | }, 39 | "anime_season": { 40 | "command_description": "Get the anime schedule for a current season.", 41 | "anime_name": "Schedule list" 42 | }, 43 | "anime": { 44 | "command_description": "Search for information about a specific anime.", 45 | "anime_name": "Anime name" 46 | }, 47 | "character": { 48 | "command_description": "Search for information about a specific character.", 49 | "character_name": "Character name", 50 | "anime_appearances": "Anime appearances" 51 | }, 52 | "character_search": { 53 | "command_description": "Search for anime series that contain character.", 54 | "character_name": "Character name", 55 | "anime_list": "List of anime containing characters: " 56 | }, 57 | "manga": { 58 | "command_description": "Search for information about a specific manga.", 59 | "manga_name": "Manga name" 60 | }, 61 | "search": { 62 | "command_description": "Search anime by image. (Supports maximum file size of 25MB)", 63 | "image_link": "Image link", 64 | "cut_black_borders": "Cut black borders", 65 | "similarity": "Similarity", 66 | "appears_episode": "Appears in episode: ", 67 | "file_too_large": "File too large. Maximum file size is 25MB.", 68 | "tracemoe_api_limit": "The request limit for the trace.moe server has been reached. Please try again later.", 69 | "image_option": "upload option", 70 | "upload_image": "Upload image" 71 | }, 72 | "staff": { 73 | "command_description": "Search for information about a specific staff.", 74 | "staff_name": "Staff name", 75 | "staff_info": "Staff info" 76 | }, 77 | "studio": { 78 | "command_description": "Get information about the studio and a list of anime produced by the studio.", 79 | "studio_name": "Studio name", 80 | "product_year": "Product year", 81 | "studio_info": "Studio info", 82 | "product_list": "List of anime produced by the studio" 83 | }, 84 | "trending": { 85 | "command_description": "Get a list of 10 trending anime on AniList.", 86 | "trending_title": "Trending anime", 87 | "trending_description": "Description" 88 | }, 89 | "user": { 90 | "command_description": "Get information about a specific user.", 91 | "user_name": "User name", 92 | "anime_count": "anime count", 93 | "manga_count": "manga count", 94 | "minutes_watched": "minutes watched", 95 | "chapters_read": "chapters read" 96 | }, 97 | "ascii": { 98 | "command_description": "Convert text to ASCII.", 99 | "text": "Text" 100 | }, 101 | "avatar": { 102 | "command_description": "Get the avatar of a user.", 103 | "user_name": "User name", 104 | "requested_by": "Requested by" 105 | }, 106 | "ping": { 107 | "description": "Bot ping." 108 | }, 109 | "weather": { 110 | "command_description": "Get the weather for a specific location.", 111 | "location": "Location", 112 | "longitude": "Longitude", 113 | "latitude": "Latitude", 114 | "degreetype": "Degree type", 115 | "current_temperature": "Current temperature", 116 | "feels_like": "Feels like", 117 | "winddisplay": "Wind", 118 | "humidity": "Humidity", 119 | "observationtime": "Update time" 120 | }, 121 | "language": { 122 | "command_description": "Change the bot's language", 123 | "language_switch": "Bot language has been set to:", 124 | "response_language": "Response language has been set to: ", 125 | "select_language": "Select language", 126 | "language_option": "Language list" 127 | }, 128 | "langlist": { 129 | "command_description": "Get a list of available language & current language.", 130 | "title": "Current language:", 131 | "description": "Language list: " 132 | }, 133 | "bot_stats": { 134 | "description": "Get bot information.", 135 | "title": "Bot information", 136 | "uptime": "Uptime", 137 | "version": "Version", 138 | "disk_usage": "Disk usage", 139 | "os": "Operating system", 140 | "node": "Node.js version", 141 | "library": "Library version" 142 | }, 143 | "popular": { 144 | "command_description": "Get the list of 10 popular anime on AniList." 145 | }, 146 | "en": "English", 147 | "vi": "Vietnamese", 148 | "schedule": { 149 | "command_description": "Get the airing schedule for a specific season.", 150 | "airing_schedule": "Airing schedule" 151 | }, 152 | "userban": { 153 | "bantitle": "You have been banned from using AniChan", 154 | "username": "Username", 155 | "uuid": "UUID", 156 | "bantime": "Ban time", 157 | "reason": "Reason", 158 | "contact": "❗ Contact the bot support server for more details about the ban.", 159 | "noban": "You are not banned." 160 | }, 161 | "random": { 162 | "command_description": "Get random anime." 163 | } 164 | } -------------------------------------------------------------------------------- /src/language/language/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "tracemoe_api_limit": "Vì bị giới hạn yêu cầu đến máy chủ trace.moe, bạn chỉ có thể sử dụng lệnh này mỗi 60 phút một lần.", 4 | "ready": "Sẵn sàng", 5 | "status_ready": "Trạng thái bot sẵn sàng", 6 | "command_error": "Đã xảy ra lỗi khi thực hiện lệnh này.", 7 | "command_register": "Đã đăng ký lệnh", 8 | "waiting_command": "Đang đăng kí lệnh...", 9 | "guild_join": "Đã tham gia máy chủ: ", 10 | "command_register_error": "Lỗi khi đăng ký lệnh: ", 11 | "server_register_error": "Lỗi khi đăng ký lệnh cho máy chủ: ", 12 | "preview_button": "Trang trước", 13 | "next_button": "Trang sau", 14 | "unavailable": "không xác định", 15 | "no_description": "Không có mô tả", 16 | "description": "Mô tả", 17 | "error": "Lỗi: ", 18 | "error_reply": "Đã xảy ra lỗi khi tìm kiếm thông tin. Vui lòng thử lại sau.", 19 | "no_results": "Không tìm thấy: ", 20 | "nsfw_block": "AniChan đã chặn kết quả tìm kiếm: ", 21 | "nsfw_block_reason": "__Lý do:__ Để bảo vệ máy chủ của bạn khỏi điều khoản dịch vụ của Discord, AniChan chặn các kết quả tìm kiếm chứa nội dung người lớn.", 22 | "episodes": "Số Tập", 23 | "chapters": "Số Chương", 24 | "genres": "Thể loại", 25 | "status": "Trạng thái", 26 | "average_score": "Điểm đánh giá", 27 | "mean_score": "Điểm xếp hạng", 28 | "season": "Mùa", 29 | "studio": "Studio", 30 | "minute": "phút", 31 | "retry": "Thử lại sau", 32 | "page": "Trang" 33 | }, 34 | "help": { 35 | "command_description": "Hiển thị cách sử dụng và chức năng của tất cả các lệnh.", 36 | "command_title": "Danh sách các lệnh", 37 | "embed_description": "Dưới đây là danh sách lệnh có sẵn: " 38 | }, 39 | "anime": { 40 | "command_description": "Tìm kiếm thông tin về một bộ anime cụ thể.", 41 | "anime_name": "Tên anime" 42 | }, 43 | "character": { 44 | "command_description": "Tìm kiếm thông tin về một nhân vật cụ thể.", 45 | "character_name": "Tên nhân vật", 46 | "anime_appearances": "Xuất hiện trong anime: " 47 | }, 48 | "character_search": { 49 | "command_description": "Tìm kiếm thông tin về một nhân vật cụ thể.", 50 | "character_name": "Tên nhân vật", 51 | "anime_list": "Danh sách anime có chứa nhân vật: " 52 | }, 53 | "manga": { 54 | "command_description": "Tìm kiếm thông tin về một bộ manga cụ thể.", 55 | "manga_name": "Tên manga" 56 | }, 57 | "search": { 58 | "command_description": "Tìm kiếm anime bằng hình ảnh. (Hỗ trợ dung lượng ảnh tối đa 25MB)", 59 | "image_link": "Liên kết đến hình ảnh", 60 | "cut_black_borders": "Cắt bỏ viền đen", 61 | "similarity": "Tỉ lệ trùng khớp", 62 | "appears_episode": "Xuất hiện trong tập: ", 63 | "file_too_large": "Dung lượng ảnh quá lớn. Vui lòng thử lại với ảnh có dung lượng nhỏ hơn 25MB.", 64 | "tracemoe_api_limit": "Đã đạt giới hạn yêu cầu đến máy chủ trace.moe. Vui lòng thử lại sau.", 65 | "image_option": "Chọn kểu tải lên", 66 | "upload_image": "Tải ảnh lên" 67 | }, 68 | "staff": { 69 | "command_description": "Tìm kiếm thông tin về một nhân viên cụ thể.", 70 | "staff_name": "Tên staff", 71 | "staff_info": "Thông tin cá nhân của staff" 72 | }, 73 | "studio": { 74 | "command_description": "Lấy thông tin về studio và danh sách anime được sản xuất bởi studio.", 75 | "studio_name": "Tên studio", 76 | "product_year": "Năm sản xuất", 77 | "studio_info": "Thông tin studio", 78 | "product_list": "Danh sách các anime được sản xuất bởi studio" 79 | }, 80 | "trending": { 81 | "command_description": "Hiển thị danh sách 10 bộ anime đang thịnh hành trên AniList.", 82 | "trending_title": "Danh sách anime đang thịnh hành", 83 | "trending_description": "Danh sách 10 bộ anime đang thịnh hành trên AniList." 84 | }, 85 | "user": { 86 | "command_description": "Xem thông tin về người dùng trên AniList.", 87 | "user_name": "Tên người dùng", 88 | "anime_count": "bộ anime", 89 | "manga_count": "bộ manga", 90 | "minutes_watched": "phút đã xem", 91 | "chapters_read": "chương đã đọc" 92 | }, 93 | "ascii": { 94 | "command_description": "Chuyển văn bản thành ASCII.", 95 | "text": "Văn bản cần chuyển", 96 | "text_limit": "Văn bản không được vượt quá 2000 ký tự." 97 | }, 98 | "avatar": { 99 | "command_description": "Lấy ảnh đại diện của người dùng.", 100 | "user_name": "Tên người dùng", 101 | "requested_by": "Yêu cầu bởi" 102 | }, 103 | "ping": { 104 | "description": "Kiểm tra độ trễ của bot." 105 | }, 106 | "weather": { 107 | "command_description": "Xem thông tin thời tiết của một địa điểm cụ thể.", 108 | "location": "Địa điểm", 109 | "longitude": "Kinh độ", 110 | "latitude": "Vĩ độ", 111 | "degreetype": "Đơn vị nhiệt độ", 112 | "current_temperature": "Nhiệt độ đo được", 113 | "feels_like": "Nhiệt độ cảm nhận", 114 | "winddisplay": "Tốc độ gió", 115 | "humidity": "Độ ẩm", 116 | "observationtime": "Cập nhật lúc" 117 | }, 118 | "language": { 119 | "command_description": "Chuyển đổi ngôn ngữ cho bot.", 120 | "language_option": "Danh sách ngôn ngữ", 121 | "language_switch": "Ngôn ngữ đã thay đổi", 122 | "select_language": "Chọn ngôn ngữ", 123 | "response_language": "Ngôn ngữ hiện tại của bot đã được đặt thành: " 124 | }, 125 | "langlist": { 126 | "command_description": "Xem ngôn ngữ hiện tại của bot và danh sách ngôn ngữ có sẵn.", 127 | "title": "Ngôn ngữ hiện tại của bot: ", 128 | "description": "Danh sách ngôn ngữ có sẵn: " 129 | }, 130 | "bot_stats": { 131 | "description": "Lấy thông tin về bot.", 132 | "title": "Thông tin bot", 133 | "uptime": "Hoạt động", 134 | "version": "Phiên bản", 135 | "disk_usage": "Dung lượng ổ đĩa sử dụng", 136 | "node": "Phiên bản Node.js", 137 | "library": "Phiên bản thư viện", 138 | "os": "Hệ điều hành" 139 | }, 140 | "popular": { 141 | "command_description": "Lấy danh sách 10 anime phổ biến" 142 | }, 143 | "en": "Tiếng Anh", 144 | "vi": "Tiếng Việt", 145 | "schedule": { 146 | "command_description": "Lấy lịch phát sóng anime.", 147 | "airing_schedule": "Lịch phát sóng" 148 | }, 149 | "userban": { 150 | "bantitle": "Bạn đã bị cấm sử dụng AniChan", 151 | "username": "Tên người dùng", 152 | "uuid": "UUID", 153 | "bantime": "Thời gian cấm", 154 | "reason": "Lý do", 155 | "contact": "❗ Liên hệ với quản trị viên trong máy chủ hỗ trợ để biết thêm thông tin.", 156 | "noban": "Bạn không bị cấm sử dụng." 157 | }, 158 | "random": { 159 | "command_description": "Lấy anime ngẫu nhiên." 160 | } 161 | } -------------------------------------------------------------------------------- /src/commands/anime_commands/search.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require('@discordjs/builders'); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const axios = require('axios'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const language = require('./../../language/language_setup.js'); 7 | const commandCooldown = new Map(); 8 | 9 | module.exports = { 10 | cooldown: 60, 11 | data: new SlashCommandBuilder() 12 | .setName('search') 13 | .setDescription(`${language.__n('search.command_description')}`) 14 | .addSubcommandGroup(group => 15 | group.setName('image') 16 | .setDescription(`${language.__n('search.image_option')}`) 17 | .addSubcommand(subcommand => 18 | subcommand.setName('url') 19 | .setDescription(`${language.__n('search.image_link')}`) 20 | .addStringOption(option => 21 | option.setName('url') 22 | .setDescription(`${language.__n('search.image_link')}`) 23 | .setRequired(true)) 24 | .addBooleanOption(option => 25 | option.setName('cut_black_borders') 26 | .setDescription(`${language.__n('search.cut_black_borders')}`) 27 | .setRequired(true))) 28 | .addSubcommand(subcommand => 29 | subcommand.setName('upload') 30 | .setDescription(`${language.__n('search.upload_image')}`) 31 | .addAttachmentOption(option => 32 | option.setName('upload') 33 | .setDescription(`${language.__n('search.upload_image')}`) 34 | .setRequired(true)) 35 | .addBooleanOption(option => 36 | option.setName('cut_black_borders') 37 | .setDescription(`${language.__n('search.cut_black_borders')}`) 38 | .setRequired(true)))), 39 | async execute(interaction) { 40 | try { 41 | await interaction.deferReply(); 42 | 43 | const subcommand = interaction.options.getSubcommand(); 44 | const cutBorders = interaction.options.getBoolean('cut_black_borders'); 45 | let imageUrl; 46 | 47 | if (subcommand === 'url') { 48 | imageUrl = interaction.options.getString('url'); 49 | } else if (subcommand === 'upload') { 50 | const uploadedImage = interaction.options.getAttachment('upload'); 51 | imageUrl = uploadedImage.url; 52 | } 53 | 54 | if (commandCooldown.has(interaction.user.id)) { 55 | const lastUsage = commandCooldown.get(interaction.user.id); 56 | const currentTime = Date.now(); 57 | const cooldownTime = 60 * 60 * 1000; 58 | 59 | if (currentTime - lastUsage < cooldownTime) { 60 | const remainingTime = cooldownTime - (currentTime - lastUsage); 61 | const remainingMinutes = Math.ceil(remainingTime / (60 * 1000)); 62 | return interaction.editReply(`${language.__n('global.tracemoe_api_limit')} ${language.__n('global.retry')} ${remainingMinutes} ${language.__n('global.minute')}.`); 63 | } 64 | } 65 | 66 | let apiUrl = 'https://api.trace.moe/search?url=' + encodeURIComponent(imageUrl); 67 | if (cutBorders) { 68 | apiUrl = 'https://api.trace.moe/search?cutBorders&url=' + encodeURIComponent(imageUrl); 69 | } 70 | 71 | const response = await axios.get(apiUrl); 72 | const data = response.data; 73 | 74 | if (data.result && data.result.length > 0) { 75 | const animeid = data.result[0].anilist; 76 | 77 | const query = fs.readFileSync(path.join(__dirname, '../../queries/search.graphql'), 'utf8'); 78 | const variables = { id: animeid }; 79 | const graphqlResponse = await axios.post('https://graphql.anilist.co', { 80 | query: query, 81 | variables: variables 82 | }, { 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | 'Accept': 'application/json', 86 | } 87 | }); 88 | 89 | const graphqlData = graphqlResponse.data; 90 | const media = graphqlData.data.Media; 91 | const animename = media.title.english || media.title.romaji || media.title.native; 92 | const embedImage = "https://img.anili.st/media/" + animeid; 93 | let description = media.description; 94 | if (description && description.length > 400) { 95 | description = description.slice(0, 400) + '...'; 96 | } 97 | const genres = media.genres; 98 | if (genres.includes('Ecchi') || genres.includes('Hentai')) { 99 | return interaction.editReply(`**${language.__n('global.nsfw_block')} ${animename}**\n${language.__n('global.nsfw_block_reason')}`); 100 | } 101 | const episode = data.result[0].episode; 102 | const similarity = (data.result[0].similarity * 100).toFixed(0); 103 | 104 | const embed = new EmbedBuilder() 105 | .setTitle(`Anime: ${animename}`) 106 | .setURL(media.siteUrl) 107 | .setDescription(`${language.__n('global.description')}: ${description}`) 108 | .addFields( 109 | { name: `${language.__n('search.appears_episode')}`, value: `${episode}`, inline: true }, 110 | { name: `${language.__n('search.similarity')}`, value: `${similarity} %`, inline: true } 111 | ) 112 | .setImage(embedImage); 113 | 114 | await interaction.editReply({ embeds: [embed] }); 115 | commandCooldown.set(interaction.user.id, Date.now()); 116 | } else { 117 | interaction.editReply(`${language.__n('global.error_reply')}`); 118 | } 119 | } catch (error) { 120 | console.error(`${language.__n('global.error')}`, error); 121 | if (interaction.replied || interaction.deferred) { 122 | return interaction.editReply(`${language.__n('global.error_reply')}`); 123 | } else { 124 | return interaction.reply(`${language.__n('global.error_reply')}`); 125 | } 126 | } 127 | }, 128 | }; --------------------------------------------------------------------------------