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