├── data
├── guildOption.json
├── queueData.js
└── wallpaper.jpg
├── Procfile
├── .DS_Store
├── .idea
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── compiler.xml
├── discord.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
└── Discord bot.iml
├── renovate.json
├── events
├── guildDelete.ts
├── guildCreate.ts
├── voiceStateUpdate.ts
├── ready.ts
├── messageCreate.ts
├── messageReactionRemove.ts
├── messageDelete.ts
├── guildMemberAdd.ts
├── messageUpdate.ts
├── messageReactionAdd.ts
└── interactionCreate.ts
├── .gitignore
├── config
└── config.example.json
├── tsconfig.json
├── commands
├── music
│ ├── clear.ts
│ ├── skip.ts
│ ├── stop.ts
│ ├── pause.ts
│ ├── loop.ts
│ ├── resume.ts
│ ├── loop-queue.ts
│ ├── nowplaying.ts
│ ├── remove.ts
│ ├── queue.ts
│ ├── related.ts
│ ├── lyric.ts
│ └── play.ts
├── util
│ ├── ping.ts
│ ├── snipe.ts
│ ├── editsnipe.ts
│ ├── cemoji.ts
│ └── reload.ts
├── fun
│ ├── sendDailyIllust.ts
│ └── pixiv.ts
├── info
│ ├── leaderboard.ts
│ ├── avatar.ts
│ ├── server.ts
│ ├── level.ts
│ ├── user-info.ts
│ ├── anime.ts
│ └── sauce.ts
├── moderation
│ ├── kick.ts
│ ├── ban.ts
│ ├── prune.ts
│ └── mute.ts
└── settings
│ └── set.ts
├── register.js
├── functions
├── fuzzysort.ts
├── spotify.ts
├── paginator.ts
├── ascii2d.ts
├── musicFunctions.ts
└── Util.ts
├── .eslintrc.json
├── package.json
├── .github
└── workflows
│ └── codeql-analysis.yml
├── ChinoKafuu.js
├── README.md
├── language
├── zh-CN.js
├── zh-TW.js
└── en-US.js
└── archive
└── auto-respond.js
/data/guildOption.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | worker: npm start
2 |
--------------------------------------------------------------------------------
/data/queueData.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | queue: new Map(),
3 | };
4 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinHongTan/ChinoKafuu/HEAD/.DS_Store
--------------------------------------------------------------------------------
/data/wallpaper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinHongTan/ChinoKafuu/HEAD/data/wallpaper.jpg
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "packageRules": [
6 | {
7 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
8 | "automerge": true
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/events/guildDelete.ts:
--------------------------------------------------------------------------------
1 | const { deleteGuildData } = require('../functions/Util');
2 |
3 | module.exports = {
4 | name: 'guildDelete',
5 | once: true,
6 | async execute(guild) {
7 | // delete info about the guild
8 | await deleteGuildData(guild.client, guild.id);
9 | },
10 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.json
3 | functions/commandReply.js
4 | functions/fuzzysort.js
5 | functions/updateIllust.js
6 | *.map
7 | functions/ascii2d.js
8 | my-backups
9 | functions/Util.js
10 | data/illusts.json
11 | functions/dynamicEmbed.js
12 | functions/paginator.js
13 | .DS_Store
14 | Brewfile
15 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/events/guildCreate.ts:
--------------------------------------------------------------------------------
1 | const { getGuildData, saveGuildData } = require('../functions/Util');
2 |
3 | module.exports = {
4 | name: 'guildCreate',
5 | once: true,
6 | async execute(guild) {
7 | // initialize guildData and save it into database
8 | const guildData = await getGuildData(guild.client, guild.id);
9 | guild.client.guildCollection.set(guild.id, guildData);
10 | await saveGuildData(guild.client, guild.id);
11 | },
12 | };
--------------------------------------------------------------------------------
/.idea/Discord bot.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/config/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "prefix": "c!",
3 | "clientId": "bot's-discord-id",
4 | "channelId": "ID-of-channel-to-log-errors/messages",
5 | "token": "discord-bot-token-here",
6 | "owner_id": "bot-owner's-discord-id",
7 | "sagiri_token": "saucenao-api-token-here",
8 | "genius_token": "lyric-api-token-here",
9 | "mongodb": "database-uri-here",
10 | "SpotifyClientID": "spotify-client-id",
11 | "SpotifyClientSecret": "spotify-client-secret",
12 | "PixivRefreshToken" : "used-to-login-pixiv"
13 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "CommonJS",
4 | "moduleResolution": "node",
5 | "target": "es2020",
6 | "lib": [
7 | "es6",
8 | "dom"
9 | ],
10 | "sourceMap": true,
11 | "types": [
12 | // add node as an option
13 | "node",
14 | ],
15 | "typeRoots": [
16 | // add path to @types
17 | "node_modules/@types"
18 | ],
19 | "experimentalDecorators": true,
20 | "emitDecoratorMetadata": true,
21 | "resolveJsonModule": true,
22 | "skipLibCheck": true,
23 | "allowSyntheticDefaultImports": true,
24 | "esModuleInterop": true,
25 | },
26 | "exclude": [
27 | "**/node_modules"
28 | ]
29 | }
--------------------------------------------------------------------------------
/commands/music/clear.ts:
--------------------------------------------------------------------------------
1 | const { error } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function clear(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | serverQueue.songs.splice(1);
9 | return error(command, language.cleared);
10 | }
11 | module.exports = {
12 | name: 'clear',
13 | guildOnly: true,
14 | data: new SlashCommandBuilder()
15 | .setName('clear')
16 | .setDescription('清除播放佇列')
17 | .setDescriptionLocalizations({
18 | 'en-US': 'Clear the song queue',
19 | 'zh-CN': '清除播放行列',
20 | 'zh-TW': '清除播放佇列',
21 | }),
22 | execute(interaction, language) {
23 | return clear(interaction, language);
24 | },
25 | };
--------------------------------------------------------------------------------
/commands/music/skip.ts:
--------------------------------------------------------------------------------
1 | const { success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function skip(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | serverQueue.player.stop();
9 | return success(command, language.skipped);
10 | }
11 | module.exports = {
12 | name: 'skip',
13 | guildOnly: true,
14 | aliases: ['next'],
15 | data: new SlashCommandBuilder()
16 | .setName('skip')
17 | .setDescription('跳過歌曲')
18 | .setDescriptionLocalizations({
19 | 'en-US': 'Skips a song.',
20 | 'zh-CN': '跳过歌曲',
21 | 'zh-TW': '跳過歌曲',
22 | }),
23 | execute(interaction, language) {
24 | return skip(interaction, language);
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/commands/music/stop.ts:
--------------------------------------------------------------------------------
1 | const { success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function stop(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | serverQueue.songs = [];
9 | serverQueue.player.stop();
10 | return success(command, language.stopped);
11 | }
12 | module.exports = {
13 | name: 'stop',
14 | guildOnly: true,
15 | data: new SlashCommandBuilder()
16 | .setName('stop')
17 | .setDescription('停止播放歌曲')
18 | .setDescriptionLocalizations({
19 | 'en-US': 'Stops playing songs.',
20 | 'zh-CN': '停止播放歌曲',
21 | 'zh-TW': '停止播放歌曲',
22 | }),
23 | async execute(interaction, language) {
24 | await stop(interaction, language);
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/commands/music/pause.ts:
--------------------------------------------------------------------------------
1 | const { success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function pause(command, language) {
5 | const serverQueue = await checkStats(command, true);
6 | if (serverQueue === 'error') return;
7 |
8 | if (serverQueue) {
9 | serverQueue.player.pause(true);
10 | serverQueue.playing = false;
11 | return success(command, language.pause);
12 | }
13 | }
14 | module.exports = {
15 | name: 'pause',
16 | guildOnly: true,
17 | data: new SlashCommandBuilder()
18 | .setName('pause')
19 | .setDescription('暫停播放歌曲!')
20 | .setDescriptionLocalizations({
21 | 'en-US': 'Pause the song!',
22 | 'zh-CN': '暂停播放歌曲!',
23 | 'zh-TW': '暫停播放歌曲!',
24 | }),
25 | execute(interaction, language) {
26 | return pause(interaction, language);
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/commands/util/ping.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 |
3 | module.exports = {
4 | name: 'ping',
5 | data: new SlashCommandBuilder()
6 | .setName('ping')
7 | .setDescription('取得我的網絡延遲~')
8 | .setDescriptionLocalizations({
9 | 'en-US': 'Get my latency~',
10 | 'zh-CN': '取得我的网络延迟~',
11 | 'zh-TW': ' 取得我的網絡延遲~',
12 | }),
13 | coolDown: 5,
14 | async execute(interaction, language) {
15 | const sent = await interaction.reply({
16 | embeds: [
17 | { description: language.pinging, color: 'BLUE' },
18 | ],
19 | fetchReply: true,
20 | });
21 | await interaction.editReply({
22 | embeds: [
23 | { description: `${language.heartbeat} ${interaction.client.ws.ping}ms.`, color: 'BLUE' },
24 | { description: `${language.latency} ${sent.createdTimestamp - interaction.createdTimestamp}ms`, color: 'BLUE' },
25 | ],
26 | });
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/commands/music/loop.ts:
--------------------------------------------------------------------------------
1 | const { success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function loop(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | if (serverQueue) {
9 | serverQueue.loop = !serverQueue.loop;
10 | if (serverQueue.loopQueue) serverQueue.loopQueue = false;
11 | return success(command, serverQueue.loop ? language.on : language.off);
12 | }
13 | }
14 | module.exports = {
15 | name: 'loop',
16 | guildOnly: true,
17 | data: new SlashCommandBuilder()
18 | .setName('loop')
19 | .setDescription('循環播放當前歌曲!')
20 | .setDescriptionLocalizations({
21 | 'en-US': 'Loop the currently played song!',
22 | 'zh-CN': '循环播放当前歌曲!',
23 | 'zh-TW': '循環播放當前歌曲!',
24 | }),
25 | async execute(interaction, language) {
26 | return loop(interaction, language);
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/commands/music/resume.ts:
--------------------------------------------------------------------------------
1 | const { error, success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function resume(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | if (serverQueue) {
9 | if (serverQueue.playing) return error(command, language.playing);
10 | serverQueue.player.unpause();
11 | serverQueue.playing = true;
12 | return success(command, language.resume);
13 | }
14 | }
15 | module.exports = {
16 | name: 'resume',
17 | guildOnly: true,
18 | data: new SlashCommandBuilder()
19 | .setName('resume')
20 | .setDescription('繼續播放歌曲!')
21 | .setDescriptionLocalizations({
22 | 'en-US': 'Resume the song!',
23 | 'zh-CN': '继续播放歌曲!',
24 | 'zh-TW': '繼續播放歌曲!',
25 | }),
26 | execute(interaction, language) {
27 | return resume(interaction, language);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/commands/music/loop-queue.ts:
--------------------------------------------------------------------------------
1 | const { success } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function loopQueue(command, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | if (serverQueue) {
9 | serverQueue.loopQueue = !serverQueue.loopQueue;
10 | if (serverQueue.loop) serverQueue.loop = false;
11 | return success(command, serverQueue.loopQueue ? language.on : language.off);
12 | }
13 | }
14 | module.exports = {
15 | name: 'loop-queue',
16 | guildOnly: true,
17 | aliases: ['lq', 'loopqueue'],
18 | data: new SlashCommandBuilder()
19 | .setName('loop-queue')
20 | .setDescription('循環播放歌曲清單')
21 | .setDescriptionLocalizations({
22 | 'en-US': 'Loop the currently played queue!',
23 | 'zh-CN': '循环播放歌曲清单',
24 | 'zh-TW': '循環播放歌曲清單',
25 | }),
26 | execute(interaction, language) {
27 | return loopQueue(interaction, language);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/events/voiceStateUpdate.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'voiceStateUpdate',
3 | async execute(oldState, newState) {
4 | if (newState.member.user.bot) return;
5 | const mainChannel = oldState.guild.channels.cache.find((channel) => channel.id === '881378732705718292');
6 | if (mainChannel) {
7 | const channels = mainChannel.parent.children;
8 | channels.each((channel) => {
9 | if (channel.id === '881378732705718292') return;
10 | if (channel.members.size < 1) channel.delete();
11 | });
12 | }
13 | if (newState.channelId === '881378732705718292') {
14 | const voiceChannel = await newState.guild.channels
15 | .create(`${newState.member.displayName}的頻道`, {
16 | type: 'GUILD_VOICE',
17 | userLimit: 99,
18 | parent: newState.guild.channels.cache.find(
19 | (channel) => channel.id === '881378732705718292',
20 | ).parent,
21 | });
22 | await newState.member.voice.setChannel(voiceChannel);
23 | }
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/commands/fun/sendDailyIllust.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { info, sendSuggestedIllust } = require('../../functions/Util.js');
3 | const refreshToken = process.env.PIXIV_REFRESH_TOKEN || require('../../config/config.json').PixivRefreshToken;
4 |
5 | async function sendDaily(command) {
6 | if (refreshToken) {
7 | await info(command, 'Sending...');
8 | await sendSuggestedIllust(await command.client.channels.fetch('970590759944335361'));
9 | await info(command, 'Done!');
10 | }
11 | }
12 |
13 | module.exports = {
14 | name: 'send_daily',
15 | aliases: ['daily'],
16 | coolDown: 3,
17 | ownerOnly: true,
18 | data: new SlashCommandBuilder()
19 | .setName('send_daily')
20 | .setDescription('發送每日圖')
21 | .setDescriptionLocalizations({
22 | 'en-US': 'Send daily illust manually',
23 | 'zh-CN': '发送每日图',
24 | 'zh-TW': '發送每日圖',
25 | }),
26 | async execute(interaction) {
27 | if (!refreshToken) return interaction.reply('沒token啦幹');
28 | await interaction.deferReply();
29 | await sendDaily(interaction);
30 | },
31 | };
--------------------------------------------------------------------------------
/register.js:
--------------------------------------------------------------------------------
1 | const { REST } = require('@discordjs/rest');
2 | const fs = require('fs');
3 | const { Routes } = require('discord-api-types/v9');
4 | const clientId = process.env.CLIENT_ID || require('./config/config.json').clientId;
5 | const token = process.env.TOKEN || require('./config/config.json').token;
6 |
7 | const rest = new REST({ version: '9' }).setToken(token);
8 | const commands = [];
9 | const commandFolders = fs.readdirSync('./commands');
10 | for (const folder of commandFolders) {
11 | const commandFiles = fs.readdirSync(`./commands/${folder}`).filter((file) => file.endsWith('.js'));
12 | for (const file of commandFiles) {
13 | const command = require(`./commands/${folder}/${file}`);
14 | console.log(command);
15 | commands.push(command.data.toJSON());
16 | }
17 | }
18 |
19 | (async () => {
20 | try {
21 | console.log('Started refreshing application (/) commands.');
22 |
23 | await rest.put(
24 | Routes.applicationCommands(clientId),
25 | { body: commands },
26 | );
27 |
28 | console.log('Successfully reloaded application (/) commands.');
29 | } catch (error) {
30 | console.error(error);
31 | }
32 | })();
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/commands/info/leaderboard.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util');
2 | const { MessageEmbed, Util } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | function getLeaderboard(command, guild) {
6 | const userList = command.client.guildCollection.get(guild.id).data.users;
7 | let leaderboard = '';
8 | userList.forEach((user, index) => {
9 | leaderboard += `${index + 1}. ${user.name}\n level: ${user.level} exp: ${user.exp}\n\n`;
10 | });
11 | return new MessageEmbed()
12 | .setTitle('Leaderboard')
13 | .setThumbnail(guild.iconURL({ format: 'png', dynamic: true }))
14 | .setColor('BLURPLE')
15 | .setDescription(Util.escapeMarkdown(leaderboard));
16 | }
17 |
18 | module.exports = {
19 | name: 'leaderboard',
20 | coolDown: 3,
21 | data: new SlashCommandBuilder()
22 | .setName('leaderboard')
23 | .setDescription('顯示伺服器排行榜')
24 | .setDescriptionLocalizations({
25 | 'en-US': 'Get server\'s leaderboard',
26 | 'zh-CN': '显示伺服器排行榜',
27 | 'zh-TW': '顯示伺服器排行榜',
28 | }),
29 |
30 | async execute(interaction) {
31 | return reply(interaction, { embeds: [getLeaderboard(interaction, interaction.guild)] });
32 | },
33 | };
--------------------------------------------------------------------------------
/events/ready.ts:
--------------------------------------------------------------------------------
1 | const { Collection } = require('discord.js');
2 | const channelId = process.env.CHANNEL_ID || require('../config/config.json').channelId;
3 | const util = require('util');
4 | const { getGuildData, saveGuildData } = require('../functions/Util.js');
5 |
6 | module.exports = {
7 | name: 'ready',
8 | once: true,
9 | async execute(client) {
10 | // when running on heroku, log to discord channel
11 | if (process.argv[2] === '-r' && channelId) {
12 | console.log = async function(d) {
13 | const logChannel = await client.channels.fetch(channelId);
14 | await logChannel.send(util.format(d) + '\n');
15 | };
16 |
17 | console.error = async function(d) {
18 | const logChannel = await client.channels.fetch(channelId);
19 | await logChannel.send(util.format(d) + '\n');
20 | };
21 | }
22 |
23 | console.log('Ready!');
24 | client.user.setPresence({
25 | activities: [{ name: 'c!help', type: 'LISTENING' }],
26 | });
27 |
28 | client.guildCollection = new Collection();
29 |
30 | await Promise.all(client.guilds.cache.map(async (guild) => {
31 | const guildData = await getGuildData(client, guild.id);
32 |
33 | // save guild options into a collection
34 | client.guildCollection.set(guild.id, guildData);
35 |
36 | await saveGuildData(client, guild.id);
37 | }));
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/functions/fuzzysort.ts:
--------------------------------------------------------------------------------
1 | import * as fuzzysort from "fuzzysort";
2 | import { Message } from "discord.js";
3 |
4 | interface MemberInfo {
5 | nickname: string;
6 | username: string;
7 | tag: string;
8 | discriminator: string;
9 | }
10 |
11 | interface Options {
12 | keys?: string[];
13 | limit?: number;
14 | }
15 |
16 | class FuzzySort{
17 | message: Message;
18 | array: object[];
19 | constructor(message: Message, searchArray?: object[]) {
20 | this.message = message;
21 | this.array = searchArray;
22 | if (!this.array) {
23 | this.array = this.message.guild.members.cache.map((member) => {
24 | let memberInfo : MemberInfo = {
25 | nickname: member.displayName,
26 | username: member.user.username,
27 | tag: member.user.tag,
28 | discriminator: member.user.discriminator
29 | };
30 | return memberInfo;
31 | });
32 | }
33 | }
34 | // search a keyword
35 | search(keyword: string, options: Options = {}) {
36 | let { keys = ["nickname", "username", "tag", "discriminator"], limit = 1 } = options;
37 | let result = fuzzysort.go(keyword, this.array, {
38 | keys,
39 | limit,
40 | });
41 | if (!result[0]) return;
42 | return this.message.guild.members.cache.find((m) => m.user.tag === result[0].obj["tag"]);
43 | }
44 | }
45 | module.exports = FuzzySort;
--------------------------------------------------------------------------------
/commands/moderation/kick.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { error, success } = require('../../functions/Util.js');
3 |
4 | async function kick(command, taggedUser, language) {
5 | if (!taggedUser) return error(command, language.noMention);
6 | if (taggedUser.id === command.author.id) return error(command, language.cantKickSelf);
7 | if (!taggedUser.kickable) return error(command, language.cannotKick);
8 | await taggedUser.kick();
9 | return success(command, command.kickSuccess.replace('${taggedUser.user.username}', taggedUser.user.username));
10 | }
11 |
12 | module.exports = {
13 | name: 'kick',
14 | guildOnly: true,
15 | usage: '[mention]',
16 | permissions: 'ADMINISTRATOR',
17 | data: new SlashCommandBuilder()
18 | .setName('kick')
19 | .setDescription('踢出群組成員')
20 | .setDescriptionLocalizations({
21 | 'en-US': 'Kick a server member out',
22 | 'zh-CN': '踢出群组成员',
23 | 'zh-TW': '踢出群組成員',
24 | })
25 | .addUserOption((option) => option
26 | .setName('member')
27 | .setDescription('要踢出的群員')
28 | .setDescriptionLocalizations({
29 | 'en-US': 'Member to kick',
30 | 'zh-CN': '要踢出的群员',
31 | 'zh-TW': '要踢出的群員',
32 | })
33 | .setRequired(true),
34 | ),
35 | async execute(interaction, language) {
36 | await kick(interaction, interaction.options.getUser('member'), language);
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/commands/moderation/ban.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { error, success } = require('../../functions/Util.js');
3 |
4 | async function ban(command, taggedUser, language) {
5 | if (!taggedUser) return error(command, language.noMention);
6 | if (taggedUser.id === command.author.id) return error(command, language.cantBanSelf);
7 | if (!taggedUser.bannable) return error(command, language.cannotBan);
8 | await command.guild.members.ban(taggedUser);
9 | return success(command, language.banSuccess.replace('${taggedUser.user.username}', taggedUser.user.username));
10 | }
11 |
12 | module.exports = {
13 | name: 'ban',
14 | guildOnly: true,
15 | usage: '[mention]',
16 | permissions: 'ADMINISTRATOR',
17 | data: new SlashCommandBuilder()
18 | .setName('ban')
19 | .setDescription('對群組成員停權')
20 | .setDescriptionLocalizations({
21 | 'en-US': 'Ban a server member',
22 | 'zh-CN': '对群组成员停权',
23 | 'zh-TW': '對群組成員停權',
24 | })
25 | .addUserOption((option) => option
26 | .setName('member')
27 | .setDescription('要停權的群員')
28 | .setDescriptionLocalizations({
29 | 'en-US': 'Member to ban',
30 | 'zh-CN': '要停权的群员',
31 | 'zh-TW': '要停權的群員',
32 |
33 | })
34 | .setRequired(true),
35 | ),
36 | async execute(interaction, language) {
37 | await ban(interaction, interaction.options.getUser('member'), language);
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/commands/moderation/prune.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { error } = require('../../functions/Util.js');
3 | function prune(command, args, language) {
4 | const amount = parseInt(args[0]) + 1;
5 |
6 | if (isNaN(amount)) {
7 | return error(command, language.invalidNum);
8 | } if (amount <= 2 || amount > 100) {
9 | return error(command, language.notInRange);
10 | }
11 |
12 | command.channel.bulkDelete(amount, true).catch((err) => {
13 | console.error(err);
14 | return error(command, language.pruneError);
15 | });
16 | }
17 | module.exports = {
18 | name: 'prune',
19 | aliases: ['cut', 'delete', 'del'],
20 | guildOnly: true,
21 | permissions: 'MANAGE_MESSAGES',
22 | data: new SlashCommandBuilder()
23 | .setName('prune')
24 | .setDescription('刪除多條訊息')
25 | .setDescriptionLocalizations({
26 | 'en-US': 'Bulk delete messages.',
27 | 'zh-CN': '删除多条讯息',
28 | 'zh-TW': '刪除多條訊息',
29 | })
30 | .addIntegerOption((option) => option
31 | .setName('number')
32 | .setDescription('批量刪除的訊息數量')
33 | .setDescriptionLocalizations({
34 | 'en-US': 'Number of messages to prune.',
35 | 'zh-CN': '批量删除的讯息数量',
36 | 'zh-TW': '批量刪除的訊息數量',
37 | })
38 | .setRequired(true),
39 | ),
40 | async execute(interaction, language) {
41 | await prune(interaction, [interaction.options.getInteger('number')], language);
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/commands/info/avatar.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | module.exports = {
6 | name: 'avatar',
7 | coolDown: 10,
8 | aliases: ['icon', 'pfp', 'av'],
9 | guildOnly: true,
10 | data: new SlashCommandBuilder()
11 | .setName('avatar')
12 | .setDescription('發送用戶頭像')
13 | .setDescriptionLocalizations({
14 | 'en-US': 'Send user avatar.',
15 | 'zh-CN': '发送用户头像',
16 | 'zh-TW': '發送用戶頭像',
17 | })
18 | .addUserOption((option) => option
19 | .setName('member')
20 | .setDescription('群員的頭像,如果没有指明群员,我将会发送你的头像')
21 | .setDescriptionLocalizations({
22 | 'en-US': 'member\'s avatar, will send your avatar if no arguments given',
23 | 'zh-CN': '群员的头像,如果没有指明群员,我将会发送你的头像',
24 | 'zh-TW': '群員的頭像,如果没有指明群员,我将会发送你的头像',
25 | }),
26 | ),
27 |
28 | async execute(interaction, language) {
29 | const user = interaction.options.getMember('member') ?? interaction.member;
30 | const embed = new MessageEmbed()
31 | .setTitle(language.memberAvatar.replace('${user.displayName}', user.displayName))
32 | .setColor(user.displayHexColor)
33 | .setImage(user.user.displayAvatarURL({
34 | format: 'png',
35 | dynamic: true,
36 | size: 2048,
37 | }));
38 | return reply(interaction, { embeds: [embed] });
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/events/messageCreate.ts:
--------------------------------------------------------------------------------
1 | const { addUserExp, getUserData, saveGuildData } = require('../functions/Util.js');
2 |
3 | module.exports = {
4 | name: 'messageCreate',
5 | async execute(message, client) {
6 | if (message.author.bot) return;
7 |
8 | if (message.guild) {
9 | const guildData = client.guildCollection.get(message.guild.id).data;
10 | const userData = guildData.users.find((user) => user.id === message.member.id) ??
11 | await getUserData(message.client, message.member);
12 | await saveGuildData(client, message.guild.id); // save in collection cache
13 | if (!('expAddTimestamp' in userData) || userData.expAddTimestamp + 60 * 1000 <= Date.now()) {
14 | await addUserExp(client, message.member);
15 | }
16 | const autoResponse = guildData.autoResponse;
17 | if (autoResponse && message.cleanContent in autoResponse) {
18 | const channelWebhooks = await message.channel.fetchWebhooks();
19 | const responseWebhook = channelWebhooks
20 | .find(webhook => webhook.name === 'autoResponse' && webhook.owner === client.user) ??
21 | await message.channel.createWebhook('autoResponse', { avatar: client.user.avatarURL });
22 | return responseWebhook.send({
23 | content: autoResponse[message.cleanContent][Math.floor(Math.random() * autoResponse[message.cleanContent].length)],
24 | username: client.user.username,
25 | avatarURL: client.user.avatarURL({ format: 'png', dynamic: true }),
26 | });
27 | }
28 | }
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:promise/recommended",
5 | "plugin:import/recommended"
6 | ],
7 | "env": {
8 | "node": true,
9 | "es6": true
10 | },
11 | "parserOptions": {
12 | "ecmaVersion": 2021
13 | },
14 | "rules": {
15 | "arrow-spacing": ["warn", { "before": true, "after": true }],
16 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
17 | "comma-dangle": ["error", "always-multiline"],
18 | "comma-spacing": "error",
19 | "comma-style": "error",
20 | "curly": ["error", "multi-line", "consistent"],
21 | "dot-location": ["error", "property"],
22 | "handle-callback-err": "off",
23 | "indent": ["error", 4],
24 | "keyword-spacing": "error",
25 | "max-nested-callbacks": ["error", { "max": 4 }],
26 | "max-statements-per-line": ["error", { "max": 2 }],
27 | "no-console": "off",
28 | "no-empty-function": "error",
29 | "no-floating-decimal": "error",
30 | "no-lonely-if": "error",
31 | "no-multi-spaces": "error",
32 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
33 | "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
34 | "no-trailing-spaces": ["error"],
35 | "no-var": "error",
36 | "object-curly-spacing": ["error", "always"],
37 | "prefer-const": "error",
38 | "quotes": ["error", "single"],
39 | "semi": ["error", "always"],
40 | "space-before-blocks": "error",
41 | "space-before-function-paren": ["error", {
42 | "anonymous": "never",
43 | "named": "never",
44 | "asyncArrow": "always"
45 | }],
46 | "space-in-parens": "error",
47 | "space-infix-ops": "error",
48 | "space-unary-ops": "error",
49 | "spaced-comment": "error",
50 | "yoda": "error"
51 | }
52 | }
--------------------------------------------------------------------------------
/commands/music/nowplaying.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util.js');
2 | const { format, checkStats } = require('../../functions/musicFunctions');
3 | const { MessageEmbed } = require('discord.js');
4 | const progressbar = require('string-progressbar');
5 | const { SlashCommandBuilder } = require('@discordjs/builders');
6 | async function nowPlaying(command, language) {
7 | const serverQueue = await checkStats(command, true);
8 | if (serverQueue === 'error') return;
9 | const resource = serverQueue?.resource;
10 |
11 | if (serverQueue) {
12 | const song = serverQueue.songs[0];
13 |
14 | const embed = new MessageEmbed()
15 | .setColor('#ff0000')
16 | .setTitle(language.npTitle)
17 | .setDescription(`[${song.title}](${song.url})\n\`[${format(resource.playbackDuration / 1000)}/${format(song.duration)}]\`\n${progressbar.splitBar(song.duration, resource.playbackDuration / 1000, 15)[0]}`)
18 | .setThumbnail(song.thumb)
19 | .addField(language.requester, `<@!${song.requester}>`)
20 | .setFooter({ text: language.musicFooter, iconURL: command.client.user.displayAvatarURL() });
21 | return reply(command, { embeds: [embed] });
22 | }
23 | }
24 | module.exports = {
25 | name: 'now-playing',
26 | guildOnly: true,
27 | aliases: ['np'],
28 | data: new SlashCommandBuilder()
29 | .setName('now-playing')
30 | .setDescription('查看目前正在播放的歌曲')
31 | .setDescriptionLocalizations({
32 | 'en-US': 'View currently played song.',
33 | 'zh-CN': '查看目前正在播放的歌曲',
34 | 'zh-TW': '查看目前正在播放的歌曲',
35 | }),
36 | execute(interaction, language, languageStr) {
37 | return nowPlaying(interaction, language, languageStr);
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/commands/music/remove.ts:
--------------------------------------------------------------------------------
1 | const { success, error } = require('../../functions/Util.js');
2 | const { checkStats } = require('../../functions/musicFunctions');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | async function remove(command, args, language) {
5 | const serverQueue = await checkStats(command);
6 | if (serverQueue === 'error') return;
7 |
8 | if (serverQueue) {
9 | args.forEach((number) => {
10 | const queuenum = Number(number);
11 | if (Number.isInteger(queuenum) && queuenum <= serverQueue.songs.length && queuenum > 0) {
12 | serverQueue.songs.splice(queuenum, 1);
13 | return success(command, language.removed.replace('${serverQueue.songs[queuenum].title}', serverQueue.songs[queuenum].title));
14 | } else {
15 | return error(command, language.invalidInt);
16 | }
17 | });
18 | }
19 | }
20 | module.exports = {
21 | name: 'remove',
22 | guildOnly: true,
23 | aliases: ['r'],
24 | data: new SlashCommandBuilder()
25 | .setName('remove')
26 | .setDescription('從清單中移除歌曲')
27 | .setDescriptionLocalizations({
28 | 'en-US': 'Removes a song from the song queue',
29 | 'zh-CN': '从清单中移除歌曲',
30 | 'zh-TW': '從清單中移除歌曲',
31 | })
32 | .addIntegerOption((option) => option
33 | .setName('index')
34 | .setDescription('要移除的歌曲的序號')
35 | .setDescriptionLocalizations({
36 | 'en-US': 'Index of song to remove',
37 | 'zh-CN': '要移除的歌曲的序号',
38 | 'zh-TW': '要移除的歌曲的序號',
39 | }),
40 | ),
41 | execute(interaction, language) {
42 | return remove(interaction, [interaction.options.getInteger('index')], language);
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/events/messageReactionRemove.ts:
--------------------------------------------------------------------------------
1 | const { MessageEmbed } = require('discord.js');
2 | const { extension } = require('../functions/Util.js');
3 |
4 | module.exports = {
5 | name: 'messageReactionRemove',
6 | async execute(reaction, user) {
7 | if (reaction.partial) await reaction.fetch();
8 | const { message } = reaction;
9 | if (message.author.id === user.id) return;
10 | if (reaction.emoji.name !== '⭐') return;
11 | const starChannel = message.guild.channels.cache.find(channel => channel.id === reaction.client.guildCollection.get(reaction.message.guild.id).data.starboard);
12 | if (!starChannel) message.channel.send('你還沒有設置starboard喲小可愛');
13 | const fetchedMessages = await starChannel.messages.fetch({ limit: 100 });
14 | const stars = fetchedMessages.filter((m) => m.embeds.length !== 0).find(m => m?.embeds[0]?.footer?.text?.startsWith('⭐') && m?.embeds[0]?.footer?.text?.endsWith(message.id));
15 | if (stars) {
16 | const star = /^⭐\s(\d{1,3})\s\|\s(\d{17,20})/.exec(stars.embeds[0].footer.text);
17 | const foundStar = stars.embeds[0];
18 | const image = message.attachments.size > 0 ? await extension(reaction, message.attachments.first().url) : '';
19 | const embed = new MessageEmbed()
20 | .setColor(foundStar.color)
21 | .setDescription(foundStar.description)
22 | .setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
23 | .setTimestamp()
24 | .setFooter({ text: `⭐ ${parseInt(star[1]) - 1} | ${message.id}` })
25 | .setImage(image);
26 | const starMsg = await starChannel.messages.fetch(stars.id);
27 | await starMsg.edit({ embeds: [embed] });
28 | if (parseInt(star[1]) - 1 === 0) return setTimeout(() => starMsg.delete(), 1000);
29 | }
30 | },
31 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord-bot",
3 | "version": "1.0.0",
4 | "description": "Discord bot :D ",
5 | "main": "ChinoKafuu.js",
6 | "scripts": {
7 | "prestart": "npm run build",
8 | "start": "node . -r",
9 | "watch": "tsc -p tsconfig.json -w",
10 | "build": "tsc --build tsconfig.json",
11 | "postinstall": "npm run build"
12 | },
13 | "engines": {
14 | "node": "22.x"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/ChinHongTan/ChinoKafuu"
19 | },
20 | "author": "ChinHong",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@discordjs/builders": "^1.8.2",
24 | "@discordjs/opus": "^0.9.0",
25 | "@discordjs/rest": "^2.3.0",
26 | "@discordjs/voice": "^0.17.0",
27 | "@ffmpeg-installer/ffmpeg": "^1.1.0",
28 | "axios": "^1.7.2",
29 | "btoa": "^1.2.1",
30 | "bytes": "^3.1.2",
31 | "canvas": "^2.11.2",
32 | "cron": "^3.1.7",
33 | "discord-api-types": "^0.37.92",
34 | "discord.js": "^14.15.3",
35 | "encoding": "^0.1.13",
36 | "ffmpeg-static": "^5.2.0",
37 | "fluent-ffmpeg": "^2.1.3",
38 | "form-data": "^4.0.0",
39 | "fuzzysort": "^3.0.2",
40 | "genius-lyrics": "^4.4.7",
41 | "googlethis": "^1.8.0",
42 | "html-to-text": "^9.0.5",
43 | "jsdom": "^24.1.0",
44 | "mongodb": "^6.8.0",
45 | "node-fetch": "^3.3.2",
46 | "pixiv.ts": "^0.6.0",
47 | "sagiri": "^3.6.0",
48 | "sodium": "^3.0.2",
49 | "solenolyrics": "^5.0.0",
50 | "soundcloud-downloader": "^1.0.0",
51 | "string-progressbar": "^1.0.4",
52 | "typescript": "^5.5.3",
53 | "youtube-sr": "^4.3.11",
54 | "ytdl-core": "^4.11.5",
55 | "ytpl": "^2.3.0"
56 | },
57 | "devDependencies": {
58 | "@types/fluent-ffmpeg": "^2.1.24",
59 | "@types/node": "^20.14.10",
60 | "@types/node-fetch": "^3.0.3",
61 | "eslint": "^9.6.0",
62 | "eslint-plugin-promise": "^6.4.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/commands/info/server.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | function server(command, language) {
5 | const embed = new MessageEmbed()
6 | .setTitle('Server Info')
7 | .setThumbnail(command.guild.iconURL())
8 | .setDescription(`Information about ${command.guild.name}`)
9 | .setColor('BLUE')
10 | .setAuthor({ name: `${command.guild.name} Info`, iconURL: command.guild.iconURL() })
11 | .addFields(
12 | { name: language.serverName, value: command.guild.name, inline: true },
13 | { name: language.serverOwner, value: command.guild.owner, inline: true },
14 | { name: language.memberCount, value: command.guild.memberCount, inline: true },
15 | { name: language.serverRegion, value: command.guild.region, inline: true },
16 | { name: language.highestRole, value: command.guild.roles.highest, inline: true },
17 | { name: language.serverCreatedAt, value: command.guild.createdAt, inline: true },
18 | { name: language.channelCount, value: command.guild.channels.cache.size, inline: true },
19 | )
20 | .setFooter({ text:'ChinoKafuu | Server Info', iconURL: command.client.user.displayAvatarURL() });
21 | return reply(command, { embeds: [embed] });
22 | }
23 | module.exports = {
24 | name: 'server',
25 | aliases: ['server-info'],
26 | guildOnly: true,
27 | coolDown: 5,
28 | data: new SlashCommandBuilder()
29 | .setName('server')
30 | .setDescription('取得伺服器的基本資料')
31 | .setDescriptionLocalizations({
32 | 'en-US': 'Get information about server.',
33 | 'zh-CN': '取得伺服器的基本资料',
34 | 'zh-TW': '取得伺服器的基本資料',
35 | }),
36 | async execute(interaction, language) {
37 | await server(interaction, language);
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/commands/util/snipe.ts:
--------------------------------------------------------------------------------
1 | const { reply, error, getSnipes } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | async function snipe(command, args, language) {
6 | const snipeWithGuild = await getSnipes(command.client, command.guild.id);
7 | if (!snipeWithGuild) return error(command, language.noSnipe);
8 | const snipes = snipeWithGuild.snipes;
9 | const arg = args[0] ?? 1;
10 |
11 | if (Number(arg) > 10) return error(command, language.exceed10);
12 | const msg = snipes?.[Number(arg) - 1];
13 | if (!msg) return error(command, language.invalidSnipe);
14 |
15 | const embed = new MessageEmbed()
16 | .setColor('RANDOM')
17 | .setAuthor({ name: msg.author, iconURL: msg.authorAvatar })
18 | .setDescription(msg.content)
19 | .setTimestamp(msg.timestamp)
20 | .setImage(msg.attachment);
21 | return reply(command, { embeds: [embed] });
22 | }
23 | module.exports = {
24 | name: 'snipe',
25 | guildOnly: true,
26 | data: new SlashCommandBuilder()
27 | .setName('snipe')
28 | .setDescription('狙擊一條訊息')
29 | .setDescriptionLocalizations({
30 | 'en-US': 'Snipe a message.',
31 | 'zh-CN': '狙击一条讯息',
32 | 'zh-TW': '狙擊一條訊息',
33 | })
34 | .addIntegerOption(option => option
35 | .setName('number')
36 | .setDescription('要狙擊的訊息,默認為1')
37 | .setDescriptionLocalizations({
38 | 'en-US':'message to snipe, default to 1',
39 | 'zh-CN': '要狙击的讯息,默認為1',
40 | 'zh-TW': '要狙擊的訊息,默認為1',
41 | })
42 | .setMinValue(1)
43 | .setMaxValue(10)
44 | .setRequired(true),
45 | ),
46 | async execute(interaction, language) {
47 | await snipe(interaction, [interaction.options.getInteger('number') ?? 1], language);
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/commands/util/editsnipe.ts:
--------------------------------------------------------------------------------
1 | const { error, reply, getEditSnipes } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | async function editSnipe(command, args, language) {
6 | const editSnipesWithGuild = await getEditSnipes(command.client, command.guild.id);
7 | const arg = args[0] ?? 1;
8 | if (!editSnipesWithGuild) return error(command, language.noSnipe);
9 |
10 | const editSnipes = editSnipesWithGuild.editSnipe;
11 | if (Number(arg) > 10) return error(command, language.exceed10);
12 | const msg = editSnipes?.[Number(arg) - 1];
13 | if (!msg) return error(command, language.invalidSnipe);
14 | const embed = new MessageEmbed()
15 | .setColor('RANDOM')
16 | .setAuthor({ name: msg.author, iconURL: msg.authorAvatar })
17 | .setDescription(msg.content)
18 | .setTimestamp(msg.timestamp)
19 | .setImage(msg.attachment);
20 | return reply(command, { embeds: [embed] });
21 | }
22 | module.exports = {
23 | name: 'edit-snipe',
24 | aliases: ['esnipe'],
25 | guildOnly: true,
26 | data: new SlashCommandBuilder()
27 | .setName('edit-snipe')
28 | .setDescription('狙擊已編輯的訊息')
29 | .setDescriptionLocalizations({
30 | 'en-US': 'Snipe an edited message.',
31 | 'zh-CN': '狙击已编辑的讯息',
32 | 'zh-TW': '狙擊已編輯的訊息',
33 | })
34 | .addIntegerOption((option) => option
35 | .setName('number')
36 | .setDescription('要狙擊的訊息,默認為1')
37 | .setDescriptionLocalizations({
38 | 'en-US': 'message to snipe, default to 1',
39 | 'zh-CN': '要狙击的讯息,默認為1',
40 | 'zh-TW': '要狙擊的訊息,默認為1',
41 | })
42 | .setMaxValue(10)
43 | .setMinValue(1)
44 | .setRequired(true),
45 | ),
46 | async execute(interaction, language) {
47 | await editSnipe(interaction, [interaction.options.getInteger('number') ?? 1], language);
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/events/messageDelete.ts:
--------------------------------------------------------------------------------
1 | const { MessageEmbed } = require('discord.js');
2 | const { saveGuildData } = require('../functions/Util');
3 |
4 | module.exports = {
5 | name: 'messageDelete',
6 | async execute(message) {
7 | // can't fetch anything
8 | if (message.partial || message.author.bot || !message.guild) return;
9 | const guildData = await message.client.guildCollection.get(message.guild.id);
10 |
11 | const snipe = {};
12 | const snipes = guildData.data.snipes;
13 | snipe.author = message.author.tag;
14 | snipe.authorAvatar = message.author.displayAvatarURL({
15 | format: 'png',
16 | dynamic: true,
17 | });
18 | snipe.content = message?.content ?? 'None';
19 | snipe.timestamp = message.createdAt;
20 | snipe.attachment = message.attachments.first()?.proxyURL;
21 |
22 | snipes.unshift(snipe);
23 | if (snipes.length > 10) snipes.pop();
24 | await saveGuildData(message.client, message.guild.id);
25 |
26 | const logEmbed = new MessageEmbed()
27 | .setTitle('**Message deleted**')
28 | .setDescription('A message was deleted.')
29 | .setColor('YELLOW')
30 | .addFields([
31 | {
32 | name: '**Member**',
33 | value: `${message.author}\n${message.author.id}`,
34 | inline: true,
35 | },
36 | {
37 | name: '**Channel**',
38 | value: `${message.channel}\n${message.channel.id}`,
39 | inline: true,
40 | },
41 | {
42 | name: '**Content**',
43 | value: `\`\`\`${message.content}\`\`\``,
44 | },
45 | ]);
46 | const logChannelId = guildData.data.channel;
47 | if (!logChannelId) return; // log channel not set
48 | const logChannel = await message.guild.channels.fetch(logChannelId);
49 | if (!logChannel) return;
50 | return logChannel.send({ embeds: [logEmbed] });
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/events/guildMemberAdd.ts:
--------------------------------------------------------------------------------
1 | const Canvas = require('canvas');
2 | const { MessageAttachment } = require('discord.js');
3 |
4 | module.exports = {
5 | name: 'guildMEmberAdd',
6 | async execute(member) {
7 | const applyText = (canvas, text) => {
8 | const context = canvas.getContext('2d');
9 | let fontSize = 70;
10 |
11 | do {
12 | context.font = `${(fontSize -= 10)}px sans-serif`;
13 | } while (context.measureText(text).width > canvas.width - 300);
14 |
15 | return context.font;
16 | };
17 |
18 | const channel = member.guild.channels.cache.find((ch) => ch.name === '閒聊-chat');
19 | if (!channel) return;
20 |
21 | const canvas = Canvas.createCanvas(700, 250);
22 | const context = canvas.getContext('2d');
23 |
24 | const background = await Canvas.loadImage('../data/wallpaper.jpg');
25 | context.drawImage(background, 0, 0, canvas.width, canvas.height);
26 |
27 | context.strokeStyle = '#74037b';
28 | context.strokeRect(0, 0, canvas.width, canvas.height);
29 |
30 | context.font = '28px sans-serif';
31 | context.fillStyle = '#ffffff';
32 | context.fillText('Welcome to the server,', canvas.width / 2.5, canvas.height / 3.5);
33 |
34 | context.font = applyText(canvas, `${member.displayName}!`);
35 | context.fillStyle = '#ffffff';
36 | context.fillText(`${member.displayName}!`, canvas.width / 2.5, canvas.height / 1.8);
37 |
38 | context.font = applyText(canvas, `${member.displayName}!`);
39 | context.fillStyle = '#ffffff';
40 | context.fillText('bruuuuuuuuuuuuuuuuuh', canvas.width / 2.5, canvas.height / 0.8);
41 |
42 | context.beginPath();
43 | context.arc(125, 125, 100, 0, Math.PI * 2, true);
44 | context.closePath();
45 | context.clip();
46 |
47 | const avatar = await Canvas.loadImage(member.user.displayAvatarURL({ format: 'jpg' }));
48 | context.drawImage(avatar, 25, 25, 200, 200);
49 |
50 | const attachment = new MessageAttachment(canvas.toBuffer(), 'welcome-image.png');
51 |
52 | channel.send(`Welcome to the server, ${member}!`, attachment);
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/commands/info/level.ts:
--------------------------------------------------------------------------------
1 | const { reply, getUserData } = require('../../functions/Util');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | async function level(command, member) {
6 | const userList = command.client.guildCollection.get(member.guild.id).data.users;
7 | let userData = userList.find((user) => user.id === member.id);
8 | if (!userData) userData = await getUserData(command.client, member);
9 | const userExp = userData.exp;
10 | const userLevel = userData.level;
11 | const expNeeded = userLevel * ((1 + userLevel) / 2) + 4;
12 | return new MessageEmbed()
13 | .setAuthor({
14 | name: member.displayName,
15 | iconURL: member.user.displayAvatarURL({ format: 'png', dynamic: true }),
16 | })
17 | .setColor('BLURPLE')
18 | .addField('Rank', '' + (userList.findIndex((user) => user.id === member.id) + 1) + '/' + '' + member.guild.memberCount)
19 | .addField('Level', '' + userLevel, true)
20 | .addField('Exp', '' + userExp + '/' + '' + expNeeded, true);
21 | }
22 |
23 | module.exports = {
24 | name: 'level',
25 | coolDown: 3,
26 | data: new SlashCommandBuilder()
27 | .setName('level')
28 | .setDescription('顯示成員的等級')
29 | .setDescriptionLocalizations({
30 | 'en-US': 'Get a member\'s level',
31 | 'zh-CN': '显示成员的等级',
32 | 'zh-TW': '顯示成員的等級',
33 | })
34 | .addUserOption((option) => option
35 | .setName('member')
36 | .setDescription('選擇群員,如果没有指明群员,我将会发送你的等級')
37 | .setDescriptionLocalizations({
38 | 'en-US': 'member\'s level, will send your level if no arguments given',
39 | 'zh-CN': '选择群员,如果没有指明群员,我将会发送你的等级',
40 | 'zh-TW': '選擇群員,如果没有指明群员,我将会发送你的等級',
41 | }),
42 | ),
43 | async execute(interaction) {
44 | const user = interaction.options.getMember('member');
45 | if (!user) {
46 | return reply(interaction, { embeds: [await level(interaction, interaction.member)] });
47 | }
48 | return reply(interaction, { embeds: [await level(interaction, user)] });
49 | },
50 | };
--------------------------------------------------------------------------------
/commands/util/cemoji.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { success, error } = require('../../functions/Util.js');
3 |
4 | async function addEmoji(command, string, language) {
5 | // id of the emoji
6 | const emojiID = string.match(/(?<=:.*:).+?(?=>)/g);
7 | // name of the emoji
8 | const emojiName = string.match(/(?<=<).+?(?=:\d+>)/g);
9 | if (!emojiID || !emojiName) return error(command, language.noEmoji);
10 | // combine id and name into an object
11 | const emojiObj = emojiName.reduce((obj, key, index) => {
12 | obj[key] = emojiID[index];
13 | return obj;
14 | }, {});
15 |
16 | for (const [name, id] of Object.entries(emojiObj)) {
17 | if (name.startsWith('a:')) {
18 | const emoji = await command.guild.emojis.create(`https://cdn.discordapp.com/emojis/${id}.gif?v=1`, name.substring(2));
19 | await success(command, language.addSuccess.replace('${emoji.name}', emoji.name).replace('${emoji}', emoji));
20 | } else {
21 | const emoji = await command.guild.emojis.create(`https://cdn.discordapp.com/emojis/${id}.png?v=1`, name.substring(1));
22 | await success(command, language.addSuccess.replace('${emoji.name}', emoji.name).replace('${emoji}', emoji));
23 | }
24 | }
25 | }
26 | module.exports = {
27 | name: 'cemoji',
28 | coolDown: 3,
29 | data: new SlashCommandBuilder()
30 | .setName('cemoji')
31 | .setDescription('從其他伺服器復製表情!(需要nitro)')
32 | .setDescriptionLocalizations({
33 | 'en-US': 'Copy emoji from other guilds! (nitro needed)',
34 | 'zh-CN': '从其他伺服器复制表情!(需要nitro)',
35 | 'zh-TW': '從其他伺服器復製表情!(需要nitro)',
36 | })
37 | .addStringOption(option => option
38 | .setName('emoji')
39 | .setDescription('要複製的表情(需要nitro)')
40 | .setDescriptionLocalizations({
41 | 'en-US': 'Emoji to copy(nitro needed)',
42 | 'zh-CN': '要复制的表情(需要nitro)',
43 | 'zh-TW': '要複製的表情(需要nitro)',
44 | })
45 | .setRequired(true),
46 | ),
47 | async execute(interaction, language) {
48 | await addEmoji(interaction, interaction.options.getString('emoji'), language);
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/commands/util/reload.ts:
--------------------------------------------------------------------------------
1 | const { success, error } = require('../../functions/Util.js');
2 | const fs = require('fs');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | async function reload(interaction, args, user) {
6 | if (!args.length) return error(interaction, `You didn't pass any command to reload, ${user}!`);
7 | const commandName = args[0].toLowerCase();
8 | const command = interaction.client.commands.get(commandName) || interaction.client.commands.find((cmd) => cmd.aliases && cmd.aliases.includes(commandName));
9 |
10 | if (!command) return error(interaction, `There is no command with name or alias \`${commandName}\`, ${user}!`);
11 |
12 | const commandFolders = fs.readdirSync('./commands');
13 | const folderName = commandFolders.find((folder) => fs.readdirSync(`./commands/${folder}`).includes(`${commandName}.js`));
14 |
15 | delete require.cache[require.resolve(`../${folderName}/${command.name}.js`)];
16 |
17 | try {
18 | const newCommand = require(`../${folderName}/${command.name}.js`);
19 | await interaction.client.commands.set(newCommand.name, newCommand);
20 | return success(interaction, `Command \`${command.name}\` was reloaded!`);
21 | } catch (err) {
22 | console.error(err);
23 | return error(interaction, `There was an error while reloading a command \`${command.name}\`:\n\`${error.message}\``);
24 | }
25 | }
26 | module.exports = {
27 | name: 'reload',
28 | data: new SlashCommandBuilder()
29 | .setName('reload')
30 | .setDescription('重新加載指令')
31 | .setDescriptionLocalizations({
32 | 'en-US': 'Reloads a command',
33 | 'zh-CN': '重新加载指令',
34 | 'zh-TW': '重新加載指令',
35 | })
36 | .addStringOption((option) => option
37 | .setName('command')
38 | .setDescription('重新加載的指令')
39 | .setDescriptionLocalizations({
40 | 'en-US': 'Command to reload',
41 | 'zh-CN': '重新加载的指令',
42 | 'zh-TW': '重新加載的指令',
43 | })
44 | .setRequired(true),
45 | ),
46 | args: true,
47 | ownerOnly: true,
48 | async execute(interaction) {
49 | await reload(interaction, [interaction.options.getString('command')], interaction.user);
50 | },
51 | };
52 |
--------------------------------------------------------------------------------
/functions/spotify.ts:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const btoa = require('btoa');
3 |
4 | class Spotify {
5 | constructor(clientId, clientSecret) {
6 | this.clientId = clientId;
7 | this.clientSecret = clientSecret;
8 | this.token = '';
9 | this.header = btoa(`${clientId}:${clientSecret}`);
10 | this.apiBase = 'https://api.spotify.com/v1/';
11 | }
12 |
13 | async getToken() {
14 | const { data } = await axios({
15 | method: 'post',
16 | url: 'https://accounts.spotify.com/api/token',
17 | data: 'grant_type=client_credentials',
18 | headers: {
19 | Authorization: `Basic ${this.header}`,
20 | },
21 | });
22 | data.expires_at = Math.round(Date.now() / 1000) + parseInt(data.expires_in);
23 | this.token = data;
24 | return this.token.access_token;
25 | }
26 |
27 | async makeRequest(url) {
28 | const token = await this.checktoken();
29 | const response = await axios({
30 | method: 'get',
31 | url,
32 | headers: {
33 | Authorization: `Bearer ${token}`,
34 | },
35 | });
36 | if (response.status !== 200) throw `Issue making request to ${url} status ${response.status} error ${response.a}`;
37 | return response.data;
38 | }
39 |
40 | async getTrack(id) {
41 | return await this.makeRequest(`${this.apiBase}tracks/${id}`);
42 | }
43 |
44 | async getAlbum(id) {
45 | return await this.makeRequest(`${this.apiBase}albums/${id}`);
46 | }
47 |
48 | async getPlaylistTrack(id) {
49 | return await this.makeRequest(`${this.apiBase}playlists/${id}/tracks`);
50 | }
51 |
52 | async getPlaylist(id) {
53 | return await this.makeRequest(`${this.apiBase}playlists/${id}`);
54 | }
55 |
56 | async checkToken() {
57 | if (this.token) {
58 | if (!this.checktime(this.token)) {
59 | return this.token;
60 | }
61 | }
62 | const token = await this.getToken();
63 | if (this.token === undefined) {
64 | throw console.error('Requested a token from Spotify, did not end up getting one');
65 | }
66 | return token;
67 | }
68 |
69 | async checkTime(token) {
70 | return token.expires_at - Math.round(Date.now() / 1000) < 60;
71 | }
72 | }
73 | module.exports = Spotify;
74 |
--------------------------------------------------------------------------------
/functions/paginator.ts:
--------------------------------------------------------------------------------
1 | import { CommandInteraction, MessageActionRow, MessageEmbed, MessageButton, ButtonInteraction } from 'discord.js'
2 |
3 | class Paginator {
4 | embedArray: MessageEmbed[];
5 | interaction: CommandInteraction;
6 | ephemeral: boolean = false;
7 | fetchReply: boolean = true;
8 | constructor(embedArray: MessageEmbed[], interaction: CommandInteraction, ephemeral?: boolean, fetchReply?: boolean) {
9 | this.embedArray = embedArray;
10 | this.interaction = interaction;
11 | if (this.ephemeral !== undefined) this.ephemeral = ephemeral;
12 | if (this.fetchReply !== undefined) this.fetchReply = fetchReply;
13 | }
14 |
15 | render(page: number = 0) {
16 | const embed = this.embedArray[page];
17 | const interaction = this.interaction;
18 | const row = new MessageActionRow()
19 | .addComponents(
20 | new MessageButton()
21 | .setCustomId('button1')
22 | .setLabel('⏮')
23 | .setStyle('PRIMARY')
24 | )
25 | .addComponents(
26 | new MessageButton()
27 | .setCustomId('button2')
28 | .setLabel('◀️')
29 | .setStyle('PRIMARY')
30 | )
31 | .addComponents(
32 | new MessageButton()
33 | .setCustomId('button3')
34 | .setLabel('▶️')
35 | .setStyle('PRIMARY')
36 | )
37 | .addComponents(
38 | new MessageButton()
39 | .setCustomId('button4')
40 | .setLabel('⏭')
41 | .setStyle('PRIMARY')
42 | );
43 | return interaction.reply({ embeds: [embed], ephemeral: this.ephemeral, fetchReply: this.fetchReply, components: [row] });
44 | }
45 |
46 | paginate(interaction: ButtonInteraction, page: number = 0) {
47 | switch (interaction.customId) {
48 | case 'button1':
49 | page = 0
50 | break;
51 | case 'button2':
52 | page -= 1;
53 | break;
54 | case 'button3':
55 | page += 1;
56 | break;
57 | case 'button4':
58 | page = this.embedArray.length - 1;
59 | break;
60 | }
61 | return this.render(page);
62 | }
63 | }
64 |
65 | module.exports = Paginator;
--------------------------------------------------------------------------------
/events/messageUpdate.ts:
--------------------------------------------------------------------------------
1 | const { MessageEmbed } = require('discord.js');
2 | const { saveGuildData } = require('../functions/Util');
3 |
4 | module.exports = {
5 | name: 'messageUpdate',
6 | async execute(oldMessage, newMessage) {
7 | if (!newMessage.guild || newMessage.author?.bot) return;
8 | if (oldMessage.partial) await oldMessage.fetch();
9 | if (newMessage.partial) await newMessage.fetch();
10 | const guildData = newMessage.client.guildCollection.get(newMessage.guild.id);
11 |
12 | const editSnipe = {};
13 | const editSnipes = guildData.data.editSnipes;
14 |
15 | editSnipe.author = newMessage.author.tag;
16 | editSnipe.authorAvatar = newMessage.author.displayAvatarURL({
17 | format: 'png',
18 | dynamic: true,
19 | });
20 | editSnipe.content = oldMessage?.content ?? 'None';
21 | editSnipe.timestamp = newMessage?.editedAt ?? Date.now(); // set time stamp to whenever this event is called
22 | editSnipe.attachment = oldMessage.attachments.first()?.proxyURL;
23 | editSnipes.unshift(editSnipe);
24 | if (editSnipes.length > 10) editSnipes.pop();
25 | await saveGuildData(newMessage.client, newMessage.guild.id);
26 |
27 | const logEmbed = new MessageEmbed()
28 | .setTitle('**Message deleted**')
29 | .setDescription('A message was deleted.')
30 | .setColor('YELLOW')
31 | .addFields([
32 | {
33 | name: '**Member**',
34 | value: `${newMessage.author}\n${newMessage.author.id}`,
35 | inline: true,
36 | },
37 | {
38 | name: '**Channel**',
39 | value: `${newMessage.channel}\n${newMessage.channel.id}`,
40 | inline: true,
41 | },
42 | {
43 | name: '**Previous**',
44 | value: oldMessage.content ? `\`\`\`${oldMessage.content}\`\`\`` : '*Content could not be recovered*',
45 | },
46 | {
47 | name: '**Updated**',
48 | value: `\`\`\`${newMessage.content}\`\`\``,
49 | },
50 | ]);
51 | const logChannelId = guildData.data.channel;
52 | if (!logChannelId) return; // log channel not set
53 | const logChannel = await newMessage.guild.channels.fetch(logChannelId);
54 | if (!logChannel) return;
55 | return logChannel.send({ embeds: [logEmbed] });
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/events/messageReactionAdd.ts:
--------------------------------------------------------------------------------
1 | const { MessageEmbed } = require('discord.js');
2 | const { extension } = require('../functions/Util.js');
3 |
4 | module.exports = {
5 | name: 'messageReactionAdd',
6 | async execute(reaction, user) {
7 | if (reaction.partial) await reaction.fetch();
8 | const { message } = reaction;
9 | if (reaction.emoji.name !== '⭐') return;
10 | if (message.author.id === user.id) return message.channel.send(`${user}不要自己給自己打星啦笑死`);
11 | if (message.author.bot) return message.channel.send(`${user}不能給機器人打星啦`);
12 | const starChannel = message.guild.channels.cache.find(channel => channel.id === reaction.client.guildCollection.get(reaction.message.guild.id).data.starboard);
13 | if (!starChannel) message.channel.send('你還沒有設置starboard喲小可愛');
14 | const fetchedMessages = await starChannel.messages.fetch({ limit: 100 });
15 | const stars = fetchedMessages.filter((m) => m.embeds.length !== 0).find(m => m?.embeds[0]?.footer?.text?.startsWith('⭐') && m?.embeds[0]?.footer?.text?.endsWith(message.id));
16 | if (stars) {
17 | const star = /^⭐\s(\d{1,3})\s\|\s(\d{17,20})/.exec(stars.embeds[0].footer.text);
18 | const foundStar = stars.embeds[0];
19 | const image = message.attachments.size > 0 ? await extension(reaction, message.attachments.first().url) : '';
20 | const embed = new MessageEmbed()
21 | .setColor(foundStar.color)
22 | .setDescription(foundStar.description)
23 | .setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
24 | .setTimestamp()
25 | .setFooter({ text: `⭐ ${parseInt(star[1]) + 1} | ${message.id}` })
26 | .setImage(image);
27 | const starMsg = await starChannel.messages.fetch(stars.id);
28 | await starMsg.edit({ embeds: [embed] });
29 | }
30 | if (!stars) {
31 | const image = message.attachments.size > 0 ? await extension(reaction, message.attachments.first().url) : '';
32 | if (image === '' && message.cleanContent.length < 1) return message.channel.send(`${user}, you cannot star an empty message.`);
33 | const embed = new MessageEmbed()
34 | .setColor(15844367)
35 | .setDescription(`${message.cleanContent}\n\n[訊息鏈接](${message.url})`)
36 | .setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
37 | .setTimestamp(new Date())
38 | .setFooter({ text: `⭐ 1 | ${message.id}` })
39 | .setImage(image);
40 | await starChannel.send({ embeds: [embed] });
41 | }
42 | },
43 | };
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '22 22 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v3
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v2
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v2
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v2
72 |
--------------------------------------------------------------------------------
/events/interactionCreate.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../functions/Util.js');
2 | const Discord = require('discord.js');
3 | const owner_id = process.env.OWNERID || require('../config/config.json').owner_id;
4 |
5 | module.exports = {
6 | name: 'interactionCreate',
7 | async execute(interaction, client) {
8 | // select menus does not contain command name, so customId is used
9 | // customId should be same as the name of the command
10 | const command = client.commands.get(interaction.commandName) ?? client.commands.get(interaction.customId);
11 | const guildOption = client.guildCollection.get(interaction?.guild.id);
12 | const language = client.language[guildOption?.data.language ?? 'en-US'][command.name];
13 |
14 | if (interaction.isAutocomplete()) {
15 | if (interaction.responded) return;
16 | return await command.autoComplete(interaction);
17 | }
18 | if (interaction.isSelectMenu()) {
19 | if (interaction.replied) return;
20 | return await command.selectMenu(interaction, language);
21 | }
22 | if (interaction.isButton()) {
23 | if (interaction.replied) return;
24 | return await command.button(interaction, language);
25 | }
26 | if (!interaction.isCommand()) return;
27 | if (command.ownerOnly) {
28 | if (interaction.user.id !== owner_id) {
29 | return reply(interaction, { content: 'This command is only available for the bot owner!', ephemeral: true });
30 | }
31 | }
32 |
33 | if (command.permissions) {
34 | const authorPerms = interaction.channel.permissionsFor(interaction.user);
35 | if (!authorPerms || !authorPerms.has(command.permissions)) {
36 | return reply(interaction, {
37 | content: `You cannot do this! Permission needed: ${command.permissions}`,
38 | ephemeral: true,
39 | });
40 | }
41 | }
42 |
43 | if (!command) return;
44 |
45 | // command cool down
46 | const { coolDowns } = client;
47 |
48 | if (!coolDowns.has(command.name)) {
49 | coolDowns.set(command.name, new Discord.Collection());
50 | }
51 |
52 | const now = Date.now();
53 | const timestamps = coolDowns.get(command.name);
54 | const coolDownAmount = (command.coolDown || 3) * 1000;
55 |
56 | if (timestamps.has(interaction.user.id)) {
57 | const expirationTime = timestamps.get(interaction.user.id) + coolDownAmount;
58 |
59 | if (now < expirationTime) {
60 | const timeLeft = (expirationTime - now) / 1000;
61 | return interaction.reply(`please wait ${timeLeft.toFixed(1)} more second(s) before reusing the \`${command.name}\` command.`);
62 | }
63 |
64 | timestamps.set(interaction.user.id, now);
65 | setTimeout(() => timestamps.delete(interaction.user.id), coolDownAmount);
66 | }
67 |
68 | try {
69 | await command.execute(interaction, language);
70 | } catch (error) {
71 | console.error(error);
72 | await reply(interaction, { content: 'There was an error while executing this command!', ephemeral: true });
73 | }
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/commands/music/queue.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util.js');
2 | const { format, checkStats } = require('../../functions/musicFunctions');
3 | const { MessageEmbed } = require('discord.js');
4 | const Paginator = require('../../functions/paginator');
5 | const { SlashCommandBuilder } = require('@discordjs/builders');
6 |
7 | function arrayChunks(array, chunkSize) {
8 | const resultArray = [];
9 | for (let i = 0; i < array.length; i += chunkSize) {
10 | const chunk = array.slice(i, i + chunkSize);
11 | resultArray.push(chunk);
12 | }
13 | return resultArray;
14 | }
15 |
16 | async function queueFunc(command, language) {
17 | const serverQueue = await checkStats(command);
18 | if (serverQueue === 'error') return;
19 |
20 | /**
21 | * Create a Discord embed message
22 | * @param {object} smallChunk - Song queue split by 10 songs
23 | * @return {object} Discord embed
24 | */
25 | function createEmbed(smallChunk) {
26 | const arr = smallChunk.map((item) => `${item.index}.[${item.title}](${item.url}) | ${format(item.duration)}`);
27 | const printQueue = arr.join('\n\n');
28 | return new MessageEmbed()
29 | .setColor('#ff0000')
30 | .setTitle(language.queueTitle)
31 | .setDescription(
32 | language.queueBody
33 | .replace('${serverQueue.songs[0].title}', serverQueue.songs[0].title)
34 | .replace('${serverQueue.songs[0].url}', serverQueue.songs[0].url)
35 | .replace('${printQueue}', printQueue)
36 | .replace('${serverQueue.songs.length - 1}', `${serverQueue.songs.length - 1}`));
37 | }
38 |
39 | if (serverQueue) {
40 | const songQueue = serverQueue.songs.slice(1);
41 | songQueue.forEach((item, index) => {
42 | item.index = index + 1;
43 | });
44 | const arrayChunk = arrayChunks(songQueue, 10);
45 | if (songQueue.length > 10) {
46 | const paginator = new Paginator(arrayChunk, command);
47 | const message = paginator.render();
48 | const collector = message.createMessageComponentCollector({
49 | filter: ({ customId, user }) =>
50 | ['button1', 'button2', 'button3', 'button4'].includes(customId) && user.id === command.member.id,
51 | idle: 60000,
52 | });
53 | collector.on('collect', async (button) => {
54 | await paginator.paginate(button, 0);
55 | });
56 | collector.on('end', async (button) => {
57 | if (!button.first()) {
58 | message.channel.send(language.timeout);
59 | await message.delete();
60 | }
61 | });
62 | } else {
63 | const embed = createEmbed(songQueue);
64 | return reply(command, { embeds: [embed] });
65 | }
66 | }
67 | }
68 | module.exports = {
69 | name: 'queue',
70 | guildOnly: true,
71 | aliases: ['q'],
72 | data: new SlashCommandBuilder()
73 | .setName('queue')
74 | .setDescription('查詢目前的播放清單')
75 | .setDescriptionLocalizations({
76 | 'en-US': 'Get the current song queue.',
77 | 'zh-CN': '查询目前的播放清单',
78 | 'zh-TW': '查詢目前的播放清單',
79 | }),
80 | async execute(interaction, language) {
81 | await queueFunc(interaction, language);
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/ChinoKafuu.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const Discord = require('discord.js');
3 | const token = process.env.TOKEN || require('./config/config.json').token;
4 |
5 | const mongodbURI = process.env.MONGODB_URI || require('./config/config.json').mongodb;
6 |
7 | const CronJob = require('cron').CronJob;
8 | const pixivRefreshToken = process.env.PIXIV_REFRESH_TOKEN || require('./config/config.json').PixivRefreshToken;
9 | const { updateIllust, sendSuggestedIllust } = require('./functions/Util.js');
10 |
11 | const en_US = require('./language/en-US.js');
12 | const zh_CN = require('./language/zh-CN.js');
13 | const zh_TW = require('./language/zh-TW.js');
14 |
15 | const client = new Discord.Client({
16 | intents: [
17 | Discord.Intents.FLAGS.GUILDS,
18 | Discord.Intents.FLAGS.GUILD_MEMBERS,
19 | Discord.Intents.FLAGS.GUILD_BANS,
20 | Discord.Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
21 | Discord.Intents.FLAGS.GUILD_INTEGRATIONS,
22 | Discord.Intents.FLAGS.GUILD_WEBHOOKS,
23 | Discord.Intents.FLAGS.GUILD_INVITES,
24 | Discord.Intents.FLAGS.GUILD_VOICE_STATES,
25 | Discord.Intents.FLAGS.GUILD_PRESENCES,
26 | Discord.Intents.FLAGS.GUILD_MESSAGES,
27 | Discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
28 | Discord.Intents.FLAGS.GUILD_MESSAGE_TYPING,
29 | Discord.Intents.FLAGS.DIRECT_MESSAGES,
30 | Discord.Intents.FLAGS.DIRECT_MESSAGE_REACTIONS,
31 | Discord.Intents.FLAGS.DIRECT_MESSAGE_TYPING,
32 | ],
33 | partials: ['MESSAGE', 'REACTION'],
34 | });
35 |
36 | client.commands = new Discord.Collection();
37 | client.language = { en_US, zh_CN, zh_TW };
38 |
39 | const commandFolders = fs.readdirSync('./commands');
40 |
41 | for (const folder of commandFolders) {
42 | const commandFiles = fs.readdirSync(`./commands/${folder}`).filter((file) => file.endsWith('.js'));
43 | for (const file of commandFiles) {
44 | const command = require(`./commands/${folder}/${file}`);
45 | client.commands.set(command.name, command);
46 | }
47 | }
48 |
49 | client.coolDowns = new Discord.Collection();
50 |
51 | const eventFiles = fs.readdirSync('./events').filter((file) => file.endsWith('.js'));
52 |
53 | for (const file of eventFiles) {
54 | const event = require(`./events/${file}`);
55 | if (event.once) {
56 | client.once(event.name, async (...args) => await event.execute(...args, client));
57 | } else {
58 | client.on(event.name, async (...args) => await event.execute(...args, client));
59 | }
60 | }
61 |
62 | // if mongodbURI was given
63 | if (mongodbURI) {
64 | const { MongoClient } = require('mongodb');
65 | const mongoClient = new MongoClient(mongodbURI);
66 |
67 | // Database Name
68 | const dbName = 'projectSekai';
69 | // Use connect method to connect to the server
70 | (async () => {
71 | await mongoClient.connect();
72 | console.log('Connected successfully to server');
73 | const db = mongoClient.db(dbName);
74 | client.guildDatabase = db.collection('GuildData');
75 | await client.login(token);
76 | })();
77 | } else {
78 | (async () => {
79 | await client.login(token);
80 | })();
81 | }
82 |
83 | // update pixiv illust list every day at noon
84 | if (pixivRefreshToken) {
85 | const job = new CronJob('00 12 * * *', async function() {
86 | console.log('Updating pixiv illust list...');
87 | await updateIllust('Chino Kafuu');
88 | await sendSuggestedIllust(await client.channels.fetch('970590759944335361'));
89 | console.log('Done!');
90 | }, null, false, 'Asia/Kuala_Lumpur');
91 | job.start();
92 | }
93 |
94 | // catch errors so that code wouldn't stop
95 | process.on('unhandledRejection', error => {
96 | console.log(error);
97 | });
98 |
--------------------------------------------------------------------------------
/commands/moderation/mute.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { warn, error, success } = require('../../functions/Util.js');
3 |
4 | async function mute(command, [taggedUser, reason]) {
5 | if (!command.member.permissions.has('MANAGE_ROLES')) return error(command, '**You Dont Have Permissions To Mute Someone! - [MANAGE_GUILD]**');
6 | if (!command.guild.me.permissions.has('MANAGE_ROLES')) return error(command, '**I Don\'t Have Permissions To Mute Someone! - [MANAGE_GUILD]**');
7 | const collection = command.client.guildOptions;
8 |
9 | if (!taggedUser) return warn(command, 'You need to tag a user in order to mute them!');
10 | if (taggedUser.user.bot) return warn(command, 'You can\'t mute bots!');
11 | if (taggedUser.id === command.member.user.id) return error(command, 'You Cannot Mute Yourself!');
12 | if (taggedUser.permissions.has('ADMINISTRATOR')) return error(command, 'You cannot mute an admin!');
13 | if (taggedUser.roles.highest.comparePositionTo(command.guild.me.roles.highest) >= 0 && (taggedUser.roles)) {
14 | return error(command, 'Cannot Mute This User!');
15 | }
16 |
17 | const guildOption = await collection.findOne({ id: command.guild.id }) ?? { id: command.guild.id, options: {} };
18 | let muteRole = guildOption.options['muteRole'];
19 | if (!muteRole) {
20 | try {
21 | muteRole = await command.guild.roles.create({
22 | name: 'muted',
23 | color: '#514f48',
24 | permissions: [],
25 | });
26 | for (const channel of command.guild.channels.cache.values()) {
27 | await channel.permissionOverwrites.create(muteRole, {
28 | SEND_MESSAGES: false,
29 | ADD_REACTIONS: false,
30 | SPEAK: false,
31 | CONNECT: false,
32 | });
33 | }
34 | } catch (e) {
35 | console.log(e);
36 | }
37 | }
38 | guildOption.options['muteRole'] = muteRole;
39 | if (taggedUser.roles.cache.has(muteRole.id)) return error(command, 'User is already muted!');
40 | await taggedUser.roles.set([muteRole]);
41 | const query = { id: command.guild.id };
42 | const options = { upsert: true };
43 | await collection.replaceOne(query, guildOption, options);
44 | return success(command, `Successfully Muted: ${taggedUser.user.username}! Reason: ${reason}`);
45 | }
46 | module.exports = {
47 | name: 'mute',
48 | guildOnly: true,
49 | usage: '[mention] [reason(optional)]',
50 | permissions: 'ADMINISTRATOR',
51 | data: new SlashCommandBuilder()
52 | .setName('mute')
53 | .setDescription('禁言群组成员')
54 | .setDescriptionLocalizations({
55 | 'en-US': 'Mute a server member',
56 | 'zh-CN': '禁言群组成员',
57 | 'zh-TW': '禁言群組成員',
58 | })
59 | .addUserOption((option) => option
60 | .setName('member')
61 | .setDescription('要禁言的群員')
62 | .setDescriptionLocalizations({
63 | 'en-US': 'Member to mute',
64 | 'zh-CN': '要禁言的群员',
65 | 'zh-TW': '要禁言的群員',
66 | })
67 | .setRequired(true),
68 | )
69 | .addStringOption((option) => option
70 | .setName('reason')
71 | .setDescription('禁言的原因')
72 | .setDescriptionLocalizations({
73 | 'en-US': 'Mute reason',
74 | 'zh-CN': '禁言的原因',
75 | 'zh-TW': '禁言的原因',
76 | }),
77 | ),
78 | async execute(interaction) {
79 | await interaction.deferReply();
80 | await mute(interaction, [interaction.options.getMember('member'), interaction.options.getString('reason')]);
81 | },
82 | };
--------------------------------------------------------------------------------
/commands/music/related.ts:
--------------------------------------------------------------------------------
1 | const { reply, warn } = require('../../functions/Util.js');
2 | const ytsr = require('youtube-sr').default;
3 | const { handleVideo, checkStats } = require('../../functions/musicFunctions');
4 | const ytdl = require('ytdl-core');
5 | const { SlashCommandBuilder } = require('@discordjs/builders');
6 | const scdl = require('soundcloud-downloader').default;
7 |
8 | async function related(command, language) {
9 | const serverQueue = await checkStats(command);
10 | if (serverQueue === 'error') return;
11 |
12 | const { voiceChannel } = serverQueue;
13 | const { songHistory } = serverQueue;
14 | const { songs } = serverQueue;
15 | const songHistoryUrls = songHistory.map((song) => song.url);
16 | const songUrls = songs.map((song) => song.url);
17 | const lastSong = songs[songs.length - 1];
18 |
19 | function avoidRepeatedSongs(result) {
20 | let url;
21 | for (let i = 0; i < result.length; i++) {
22 | url = result[i];
23 | if (songHistoryUrls.includes(url) || songUrls.includes(url)) {
24 | result.splice(i, 1);
25 | } else {
26 | break;
27 | }
28 | }
29 | return url;
30 | }
31 |
32 | async function playRelatedTrack(relatedVideos) {
33 | const urlList = relatedVideos.map((song) => song.video_url);
34 | const url = avoidRepeatedSongs(urlList);
35 | const videos = await ytsr.getVideo(url);
36 | await handleVideo([videos], voiceChannel, false, serverQueue, 'yt', command);
37 | }
38 |
39 | async function getRelatedTrackInfo(relatedVideos) {
40 | const relatedVidsInfo = [];
41 | await Promise.all(
42 | relatedVideos.map(async (vid) => {
43 | const searchUrl = `https://www.youtube.com/watch?v=${vid.id}`;
44 | const response = await ytdl.getBasicInfo(searchUrl);
45 | relatedVidsInfo.push(response.videoDetails);
46 | }),
47 | );
48 | return relatedVidsInfo;
49 | }
50 |
51 | function sortRelatedTracks(relatedVidsInfo) {
52 | let mark = 0;
53 | relatedVidsInfo.forEach((item) => {
54 | if (item.media.category === 'Music') mark += 1;
55 | if (item.category === 'Music') mark += 2;
56 | item.mark = mark;
57 | });
58 |
59 | relatedVidsInfo.sort((a, b) => b.mark - a.mark);
60 | return relatedVidsInfo;
61 | }
62 |
63 | await reply(command, language.relatedSearch, 'BLUE');
64 | let data, url, result, relatedVideos, urlList, relatedVidsInfo = [];
65 | let videos, authorId, bestTrack;
66 |
67 | switch (lastSong.source) {
68 | case 'sc':
69 | data = await scdl.getInfo(lastSong.url);
70 | relatedVideos = await scdl.related(data.id, 5, 0);
71 | urlList = videos.collection.map((song) => song.permalink_url);
72 | url = avoidRepeatedSongs(urlList);
73 | result = await scdl.getInfo(url).catch((err) => {
74 | console.log(err);
75 | throw warn(command, language.noResult);
76 | });
77 | await handleVideo(result, voiceChannel, false, serverQueue, 'sc', command);
78 | break;
79 | case 'yt':
80 | data = await ytdl.getInfo(lastSong.url);
81 | relatedVideos = data.related_videos;
82 | relatedVidsInfo = await getRelatedTrackInfo(relatedVideos);
83 | sortRelatedTracks(relatedVidsInfo);
84 |
85 | authorId = data.videoDetails.author.id;
86 | bestTrack = relatedVidsInfo.filter((vid) => vid.author.id === authorId && vid.mark > 0);
87 | if (bestTrack.length > 0) {
88 | await playRelatedTrack(bestTrack);
89 | } else {
90 | await playRelatedTrack(relatedVidsInfo);
91 | }
92 | break;
93 | }
94 | }
95 | module.exports = {
96 | name: 'related',
97 | guildOnly: true,
98 | aliases: ['re'],
99 | data: new SlashCommandBuilder()
100 | .setName('related')
101 | .setDescription('播放相關歌曲')
102 | .setDescriptionLocalizations({
103 | 'en-US': 'Play a related song',
104 | 'zh-CN': '播放相关歌曲',
105 | 'zh-TW': '播放相關歌曲',
106 | }),
107 | async execute(interaction, language) {
108 | await related(interaction, language);
109 | },
110 | };
111 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ChinoKafuu
5 |
6 |
7 |
8 |
9 | A nice little discord bot I make to learn JavaScript and how bots works XD
10 |
11 | The bot is not quite done yet, but I will keep on updating!
12 |
13 | And yes, Chino is the best girl in the world!
14 |
15 | [](https://www.codefactor.io/repository/github/chinhongtan/chinokafuu/overview/main)
16 | [](https://codebeat.co/projects/github-com-chinhongtan-chinokafuu-main)
17 | [](https://www.codacy.com/gh/ChinHongTan/ChinoKafuu/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ChinHongTan/ChinoKafuu&utm_campaign=Badge_Grade)
18 |
19 | ## Introduction
20 |
21 | This bot runs on [`Node.js v22`](https://nodejs.org/) and is built with [`discord.js v14`](https://discord.js.org/).
22 |
23 | ## Invite link
24 |
25 | Doesn't work for now as Im updating the bot
26 |
27 | ## Set up
28 | This project uses node.js v22 to run, so make sure you set up node.js first.
29 |
30 | Also, make sure you give the bot `Administrator` permission and has the `applications.commands` application scope enabled.
31 |
32 | First you have to clone the project:
33 | ```bash
34 | git clone https://github.com/ChinHongTan/ChinoKafuu.git
35 | ```
36 | Then you have to install all the npm modules
37 | ```bash
38 | cd ChinoKafuu
39 | npm i
40 | ```
41 | After that you will need to create a file named "config.json" in the "config" folder.
42 | An example "config.example.json" has been given in the "config" folder.
43 | Here's what you should fill in the config.json:
44 |
45 | ## Prepare config.json
46 | ```json
47 | {
48 | "prefix": "c!",
49 | "clientId": "bot's-discord-id",
50 | "channelId": "ID-of-channel-to-log-errors/messages",
51 | "token": "discord-bot-token-here",
52 | "owner_id": "bot-owner's-discord-id",
53 | "sagiri_token": "saucenao-api-token-here",
54 | "genius_token": "lyric-api-token-here",
55 | "mongodb": "database-uri-here",
56 | "SpotifyClientID": "spotify-client-id",
57 | "SpotifyClientSecret": "spotify-client-secret",
58 | "PixivRefreshToken" : "used-to-login-pixiv"
59 | }
60 | ```
61 | - prefix: The prefix you want to use to interact with the bot. (Use \help for more information.)
62 | - clientId: The discord id of the bot's main server. [How do I get it?](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-)
63 | - channelId: The id of channel for bot to `console.log` output at.
64 | Useful if you are hosting the bot on online platform, and wish to monitor the bot's logs without logging into the online platform.
65 | If the output is too long and exceeds discord's message length limit, the bot will not log anything and not sending any errors at the same time.
66 | Leave blank to make the bot log to terminal instead.
67 | - token: The token needed to get the bot online. [How do I get it?](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token)
68 | - owner_id: Bot owner's id
69 | - sagiri_token: Needed for reverse image searching using [Saucenao](https://saucenao.com/).
70 | You can get a token by [registering an account](https://saucenao.com/user.php) and going to the API page.
71 | - genius_token: Needed to search for a song's lyrics.[How do I get it?](https://genius.com/developers)
72 | - mongodb: uri to connect to the database. This bot uses MongoDB: [Getting started](https://www.mongodb.com/docs/manual/tutorial/getting-started/)
73 | - SpotifyClientID & SpotifyClientSecret: Needed to get songs from Spotify. [How do I get it?](https://developer.spotify.com/documentation/general/guides/authorization/app-settings/)
74 | - PixivRefreshToken: Needed to log in pixiv. [How do I get it?](https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362)
75 |
76 | Note that only `prefix`, `clientId`, `token` and `owner_id` are needed to get the bot online.
77 | The others are just optional features.
78 |
79 | Register the commands first:
80 | ```bash
81 | node register.js
82 | ```
83 |
84 | To start the bot:
85 | ```bash
86 | npm start
87 | ```
88 |
89 | **prefix: `c!`**
90 |
91 | Use `c!help` for detailed usage and information!
92 |
93 | Have fun playing the bot! >w<
94 |
--------------------------------------------------------------------------------
/commands/music/lyric.ts:
--------------------------------------------------------------------------------
1 | const { reply, error } = require('../../functions/Util.js');
2 |
3 | const solenolyrics = require('solenolyrics');
4 | const Genius = require('genius-lyrics');
5 | const queueData = require('../../data/queueData');
6 | const geniusToken = process.env.GENIUS || require('../../config/config.json').genius_token;
7 | const Client = geniusToken ? new Genius.Client(geniusToken) : undefined;
8 | const { queue } = queueData;
9 |
10 | const fetch = require('node-fetch');
11 | const htmlToText = require('html-to-text');
12 | const encoding = require('encoding');
13 | const { SlashCommandBuilder } = require('@discordjs/builders');
14 |
15 | const delim1 = '';
17 | const url = 'https://www.google.com/search?q=';
18 |
19 | async function search(fullURL) {
20 | let i = await fetch(fullURL);
21 | i = await i.textConverted();
22 | [, i] = i.split(delim1);
23 | if (i) {
24 | // lyrics exists
25 | [i] = i.split(delim2);
26 | return i;
27 | }
28 | // no lyrics found
29 | return undefined;
30 | }
31 |
32 | async function lyricsFinder(artist = '', title = '') {
33 | const i = await search(`${url}${encodeURIComponent(title + ' ' + artist)}+lyrics`) ??
34 | await search(`${url}${encodeURIComponent(title + ' ' + artist)}+song+lyrics`) ??
35 | await search(`${url}${encodeURIComponent(title + ' ' + artist)}+song`) ??
36 | await search(`${url}${encodeURIComponent(title + ' ' + artist)}`) ?? '';
37 |
38 | const ret = i.split('\n');
39 | let final = '';
40 | for (let j = 0; j < ret.length; j += 1) {
41 | final = `${final}${htmlToText.fromString(ret[j])}\n`;
42 | }
43 | return String(encoding.convert(final)).trim();
44 | }
45 |
46 | async function lyric(command, args, language) {
47 | const serverQueue = queue.get(command.guild.id);
48 | async function searchLyrics(keyword) {
49 | const msg = await reply(command, language.searching.replace('${keyword}', keyword), 'BLUE');
50 | let lyrics = await lyricsFinder(' ', keyword).catch((err) => console.error(err));
51 | if (!lyrics) lyrics = await solenolyrics.requestLyricsFor(encodeURIComponent(keyword));
52 | if (!lyrics && Client !== undefined) {
53 | const searches = await Client.songs.search(keyword);
54 | if (searches) lyrics = await searches[0].lyrics().catch((err) => console.log(err));
55 | }
56 | if (!lyrics) {
57 | return msg.edit({
58 | embeds: [{
59 | title: 'ERROR!',
60 | description: language.noLyricsFound.replace('${keyword}', keyword),
61 | color: 'RED',
62 | }],
63 | });
64 | }
65 | await msg.edit({
66 | embeds: [{
67 | title: language.title.replace('${keyword}', keyword),
68 | description: lyrics,
69 | color: 'YELLOW',
70 | }],
71 | });
72 | }
73 |
74 | if (serverQueue) {
75 | const songTitle = serverQueue.songs[0].title;
76 | const keyword = args[0] ? args[0] : songTitle;
77 | return await searchLyrics(keyword);
78 | }
79 | if (args[0]) return await searchLyrics(args[0]);
80 | return error(command, language.noKeyword);
81 | }
82 | module.exports = {
83 | name: 'lyric',
84 | guildOnly: true,
85 | aliases: ['ly'],
86 | data: new SlashCommandBuilder()
87 | .setName('lyric')
88 | .setDescription('搜索歌詞!')
89 | .setDescriptionLocalizations({
90 | 'en-US': 'Search for lyrics of a song!!',
91 | 'zh-CN': '搜索歌词!',
92 | 'zh-TW': '搜索歌詞!',
93 | })
94 | .addStringOption((option) => option
95 | .setName('keyword')
96 | .setDescription('要搜索歌詞的歌名,如果沒有提供歌名將會搜索目前正在播放的歌曲的歌詞')
97 | .setDescriptionLocalizations({
98 | 'en-US': 'Song title, will use the title of the currently played song if no title given.',
99 | 'zh-CN': '要搜索歌词的歌名,如果没有提供歌名将会搜索目前正在播放的歌曲的歌词',
100 | 'zh-TW': '要搜索歌詞的歌名,如果沒有提供歌名將會搜索目前正在播放的歌曲的歌詞',
101 | }),
102 | ),
103 | async execute(interaction, args, language) {
104 | await lyric(interaction, [interaction.options.getString('keyword')], language);
105 | },
106 | };
107 |
--------------------------------------------------------------------------------
/commands/info/user-info.ts:
--------------------------------------------------------------------------------
1 | const { reply } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 |
5 | function getUserInfo(author, language) {
6 | let activityDescription = '';
7 | if (author?.presence?.activities) {
8 | for (const activity of author.presence.activities) {
9 | // custom status
10 | if (activity.type === 4) {
11 | activityDescription += language.customStatus
12 | .replace('<:${name}:${id}>', activity.emoji)
13 | .replace('${state}', activity.state);
14 | } else {
15 | activityDescription += language.gameStatus
16 | .replace('${type}', activity.type)
17 | .replace('${name}', activity.name)
18 | .replace('${details}', activity.details ? activity.details : '');
19 | }
20 | }
21 | } else {
22 | activityDescription = language.notPlaying;
23 | }
24 | return new MessageEmbed()
25 | .setColor('#0099ff')
26 | .setTitle('User Info')
27 | .setAuthor({
28 | name: author.guild.name,
29 | iconURL: author.guild.iconURL({ dynamic: true }),
30 | url: 'https://loliconshelter.netlify.app/',
31 | })
32 | .setThumbnail(
33 | author.user.displayAvatarURL({
34 | format: 'png',
35 | dynamic: true,
36 | }),
37 | )
38 | .addFields(
39 | {
40 | name: language.tag,
41 | value: author.user.tag,
42 | inline: true,
43 | },
44 | {
45 | name: language.nickname,
46 | value: author.displayName,
47 | inline: true,
48 | },
49 | {
50 | name: language.id,
51 | value: author.id,
52 | inline: true,
53 | },
54 | {
55 | name: language.avatarURL,
56 | value: language.avatarValue
57 | .replace('${url}', author.user.displayAvatarURL({ format: 'png', dynamic: true })),
58 | inline: true,
59 | },
60 | {
61 | name: language.createdAt,
62 | value: author.user.createdAt.toLocaleDateString('zh-TW'),
63 | inline: true,
64 | },
65 | {
66 | name: language.joinedAt,
67 | value: author.joinedAt.toLocaleDateString('zh-TW'),
68 | inline: true,
69 | },
70 | {
71 | name: language.activity,
72 | value: activityDescription || 'None',
73 | inline: true,
74 | },
75 | {
76 | name: language.status,
77 | value: author?.presence?.status || 'Offline',
78 | inline: true,
79 | },
80 | {
81 | name: language.device,
82 | value: author?.presence?.clientStatus ? Object.keys(author.presence.clientStatus).join(', ') : 'None',
83 | inline: true,
84 | },
85 | {
86 | name: language.roles.replace('${author.roles.cache.size}', author.roles.cache.size),
87 | value: author.roles.cache.map((roles) => `${roles}`).join(', '),
88 | inline: false,
89 | },
90 | )
91 | .setTimestamp();
92 | }
93 | module.exports = {
94 | name: 'user-info',
95 | aliases: ['user', 'ui'],
96 | guildOnly: true,
97 | data: new SlashCommandBuilder()
98 | .setName('user-info')
99 | .setDescription('取得群組成員的基本資料')
100 | .setDescriptionLocalizations({
101 | 'en-US': 'Get a user\'s information',
102 | 'zh-CN': '取得群组成员的基本资料',
103 | 'zh-TW': '取得群組成員的基本資料',
104 | })
105 | .addUserOption((option) => option
106 | .setName('member')
107 | .setDescription('群員的資料,如果没有指明群员,我将会发送你的資料')
108 | .setDescriptionLocalizations({
109 | 'en-US': 'member\'s info, will send info about you if no arguments given',
110 | 'zh-CN': '群员的资料,如果没有指明群员,我将会发送你的资料',
111 | 'zh-TW': '群員的資料,如果没有指明群员,我将会发送你的資料',
112 | }),
113 | ),
114 | execute(interaction, language) {
115 | const user = interaction.options.getMember('member');
116 | if (!user) {
117 | return reply(interaction, { embeds: [getUserInfo(interaction.member, language)] });
118 | }
119 | return reply(interaction, { embeds: [getUserInfo(user, language)] });
120 | },
121 | };
122 |
--------------------------------------------------------------------------------
/commands/info/anime.ts:
--------------------------------------------------------------------------------
1 | const { SlashCommandBuilder } = require('@discordjs/builders');
2 | const { error } = require('../../functions/Util.js');
3 | const fetch = require('node-fetch');
4 | const { MessageEmbed } = require('discord.js');
5 | const Paginator = require('../../functions/paginator');
6 |
7 | async function anime(command, args, language) {
8 | function isValidHttpUrl(string) {
9 | let url;
10 |
11 | try {
12 | url = new URL(string);
13 | } catch (_) {
14 | return false;
15 | }
16 |
17 | return url.protocol === 'http:' || url.protocol === 'https:';
18 | }
19 |
20 | function createEmbed(response) {
21 | return new MessageEmbed()
22 | .setTitle(response.anilist.title.native)
23 | .setDescription(language.similarity.replace('${similarity}', response.similarity * 100))
24 | .setColor('#008000')
25 | .setImage(response.image)
26 | .addField(language.sourceURL, response.video)
27 | .addField(language.nativeTitle, response.anilist.title.native)
28 | .addField(language.romajiTitle, response.anilist.title.romaji)
29 | .addField(language.englishTitle, response.anilist.title.english)
30 | .addField(language.episode, response.episode.toString())
31 | .addField(language.NSFW, response.anilist.isAdult.toString());
32 | }
33 |
34 | if (args[0]) {
35 | if (!isValidHttpUrl(args[0])) {
36 | return error(command, language.invalidURL);
37 | }
38 | const e = await fetch(`https://api.trace.moe/search?cutBorders&anilistInfo&url=${encodeURIComponent(args[0])}`);
39 | const response = await e.json();
40 | const embedList = [];
41 | for (const responseObject of response.result) {
42 | embedList.push(createEmbed(responseObject));
43 | }
44 | const paginator = new Paginator(embedList, command);
45 | const message = paginator.render();
46 | const collector = message.createMessageComponentCollector({
47 | filter: ({ customId, user }) =>
48 | ['button1', 'button2', 'button3', 'button4'].includes(customId) && user.id === command.member.id,
49 | idle: 60000,
50 | });
51 | collector.on('collect', async (button) => {
52 | await paginator.paginate(button, 0);
53 | });
54 | collector.on('end', async (button) => {
55 | if (!button.first()) {
56 | message.channel.send(language.timeout);
57 | await message.delete();
58 | }
59 | });
60 | }
61 | let searchImage;
62 | const messages = await command.channel.messages.fetch({ limit: 25 });
63 | for (const msg of messages.values()) {
64 | if (msg.attachments.size > 0) {
65 | searchImage = msg.attachments.first().proxyURL;
66 | break;
67 | }
68 | }
69 | if (!searchImage) {
70 | return error(command, language.noImage);
71 | }
72 | const e = await fetch(`https://api.trace.moe/search?cutBorders&anilistInfo&url=${encodeURIComponent(searchImage)}`);
73 | const response = await e.json();
74 | const embedList = [];
75 | for (const responseObject of response.result) {
76 | embedList.push(createEmbed(responseObject));
77 | }
78 | const paginator = new Paginator(embedList, command);
79 | const message = paginator.render();
80 | const collector = message.createMessageComponentCollector({
81 | filter: ({ customId, user }) =>
82 | ['button1', 'button2', 'button3', 'button4'].includes(customId) && user.id === command.member.id,
83 | idle: 60000,
84 | });
85 | collector.on('collect', async (button) => {
86 | await paginator.paginate(button, 0);
87 | });
88 | collector.on('end', async (button) => {
89 | if (!button.first()) {
90 | message.channel.send(language.timeout);
91 | await message.delete();
92 | }
93 | });
94 | }
95 | module.exports = {
96 | name: 'anime',
97 | guildOnly: true,
98 | data: new SlashCommandBuilder()
99 | .setName('anime')
100 | .setDescription('根據動漫截圖查找動漫/查詢動漫相關信息')
101 | .setDescriptionLocalizations({
102 | 'en-US': 'Search for anime details or search anime with frames.',
103 | 'zh-CN': '根据动漫截图查找动漫/查询动漫相关信息',
104 | 'zh-TW': '根據動漫截圖查找動漫/查詢動漫相關信息',
105 | })
106 | .addStringOption((option) => option
107 | .setName('url')
108 | .setNameLocalizations({
109 | 'en-US': 'url',
110 | 'zh-CN': '网址',
111 | 'zh-TW': '網址',
112 | })
113 | .setDescription('輸入圖片網址,如果沒有網址將會搜索最後在頻道里上傳圖片')
114 | .setDescriptionLocalizations({
115 | 'en-US': 'URL of image, will search the last attachment uploaded in the channel if no url was given',
116 | 'zh-CN': '输入图片网址,如果没有网址将会搜索最后在频道里上传图片',
117 | 'zh-TW': '輸入圖片網址,如果沒有網址將會搜索最後在頻道里上傳圖片',
118 | }),
119 | ),
120 | async execute(interaction, language) {
121 | await anime(interaction, [interaction.options.getString('url')], language);
122 | },
123 | };
124 |
--------------------------------------------------------------------------------
/commands/info/sauce.ts:
--------------------------------------------------------------------------------
1 | const { error, info, edit } = require('../../functions/Util.js');
2 | const { MessageEmbed } = require('discord.js');
3 | const { searchByUrl } = require('../../functions/ascii2d.js');
4 | const sagiriToken = process.env.SAGIRI || require('../../config/config.json').sagiri_token;
5 | const sagiri = require('sagiri');
6 | let mySauce;
7 | if (sagiriToken) mySauce = sagiri(sagiriToken);
8 | const { SlashCommandBuilder } = require('@discordjs/builders');
9 |
10 | async function sauce(command, args, language) {
11 | /**
12 | * Generates a discord embed
13 | * @param {object} response - Response object from sauceNao API call
14 | * @return {MessageEmbed} Discord embed.
15 | */
16 | function createsauceNaoEmbed(response) {
17 | const sourceURL = response.url;
18 | let information = '';
19 | for (const [key, value] of Object.entries(response.raw.data)) {
20 | if (key === 'ext_urls') continue;
21 | information += `\`${key} : ${value}\`\n`;
22 | }
23 | // .setFooter(`page ${response.page + 1}/${response.total}`);
24 | return new MessageEmbed()
25 | .setTitle(response.site)
26 | .setDescription(language.similarity.replace('${similarity}', response.similarity))
27 | .setColor('#008000')
28 | .setImage(response.thumbnail)
29 | .addFields(
30 | { name: language.sourceURL, value: sourceURL },
31 | { name: language.additionalInfo, value: information },
32 | );
33 | }
34 |
35 | /**
36 | * Generates a discord embed
37 | * @param {object} response - Response object from ascii2d API call
38 | * @return {MessageEmbed} Discord embed.
39 | */
40 | function createascii2dEmbed(response) {
41 | const sourceURL = response?.source?.url ?? 'None';
42 | const title = response?.source?.title ?? 'None';
43 | const author = response?.source?.author ? language.sauceAuthor.replace('${authorInfo.name}', response?.source?.author.name).replace('${authorInfo.url}', response?.source?.author.url) : language.noAuthor;
44 | // .setFooter(`page ${response.page + 1}/${response.total}`);
45 | return new MessageEmbed()
46 | .setTitle(response?.source?.type ?? 'None')
47 | .setColor('#008000')
48 | .setImage(response.thumbnailUrl)
49 | .addFields(
50 | { name: language.sourceURL, value: sourceURL },
51 | { name: language.title, value: title },
52 | { name: language.author, value: author },
53 | );
54 | }
55 |
56 | /**
57 | * Search for an image from sauceNao or ascii2d
58 | * @param {string} searchImage - url of the target image to search for.
59 | */
60 | async function searchForImage(searchImage) {
61 | // start with saucenao
62 | const result = await mySauce(searchImage, { results: 5 });
63 | const response = result.filter((r) => r.similarity > 80);
64 |
65 | if (response.length > 0) {
66 | return edit(command, { embeds: [ createsauceNaoEmbed(response[0]) ] });
67 | }
68 | // search with ascii2d
69 | const result2 = await searchByUrl(searchImage, 'bovw');
70 | if (!result2.items || result2.items.length < 1) {
71 | return error(command, language.noResult);
72 | }
73 | // const response2 = result2.items.filter((r2) => r2.source !== 0);
74 | // sendEmbed(response2, mode);
75 | edit(command, { embeds: [ createascii2dEmbed(result2.items[0]) ] });
76 | }
77 |
78 | let searchImage = '';
79 |
80 | if (args[0]) {
81 | searchImage = args[0];
82 | return searchForImage(searchImage);
83 | }
84 | // no arguments were provided, fetch image from the channel instead
85 | const messages = await command.channel.messages.fetch({ limit: 25 });
86 | for (const msg of messages.values()) {
87 | if (msg.attachments.size > 0) {
88 | searchImage = msg.attachments.first().proxyURL;
89 | break;
90 | }
91 | }
92 | if (!searchImage) {
93 | return error(command, language.noImage);
94 | }
95 | await info(command, language.searchingSauce);
96 | await searchForImage(searchImage);
97 | }
98 |
99 | module.exports = {
100 | name: 'sauce',
101 | data: new SlashCommandBuilder()
102 | .setName('sauce')
103 | .setDescription('在SauceNao/Ascii2d網站上搜索圖源')
104 | .setDescriptionLocalizations({
105 | 'en-US': 'Search SauceNao/Ascii2d for an image source.',
106 | 'zh-CN': '在SauceNao/Ascii2d网站上搜索图源',
107 | 'zh-TW': '在SauceNao/Ascii2d網站上搜索圖源',
108 | })
109 | .addStringOption((option) => option
110 | .setName('url')
111 | .setDescription('要查詢的圖片的網址,如果沒有提供網址將會搜索最後在頻道里上傳的圖片')
112 | .setDescriptionLocalizations({
113 | 'en-US': 'URL of image, will search the last attachment uploaded in the channel if no url was given',
114 | 'zh-CN': '要查询的图片的网址,如果没有提供网址将会搜索最后在频道里上传的图片',
115 | 'zh-TW': '要查詢的圖片的網址,如果沒有提供網址將會搜索最後在頻道里上傳的圖片',
116 | }),
117 | ),
118 | coolDown: 5,
119 | async execute(interaction, language) {
120 | if (!sagiriToken) return interaction.reply('This command can\'t be used without SauceNAO token!');
121 | await sauce(interaction, [interaction.options.getString('url')], language);
122 | },
123 | };
124 |
--------------------------------------------------------------------------------
/functions/ascii2d.ts:
--------------------------------------------------------------------------------
1 | import * as bytes from 'bytes';
2 | import * as FormData from 'form-data';
3 | import * as fs from 'fs';
4 | import {JSDOM} from 'jsdom';
5 | import fetch from 'node-fetch';
6 |
7 | const baseURL = 'https://ascii2d.obfs.dev/'
8 | export type SearchMode = 'color' | 'bovw';
9 | export type FileType = 'jpeg' | 'png';
10 | export type SourceType =
11 | | 'pixiv'
12 | | 'twitter'
13 | | 'amazon'
14 | | 'dlsite'
15 | | 'tinami'
16 | | 'ニコニコ静画';
17 |
18 | export interface Author {
19 | name: string;
20 | url: string;
21 | }
22 |
23 | export interface Source {
24 | type: SourceType;
25 | title: string;
26 | url: string;
27 | author?: Author;
28 | }
29 |
30 | export interface ExternalInfo {
31 | ref: string;
32 | content: Source | string;
33 | }
34 |
35 | export interface Item {
36 | hash: string;
37 | thumbnailUrl: string;
38 | width: number;
39 | height: number;
40 | fileType: FileType;
41 | fileSize: number;
42 | source?: Source;
43 | externalInfo?: ExternalInfo;
44 | }
45 |
46 | export interface SearchResult {
47 | url: string;
48 | items: Item[];
49 | }
50 |
51 | function getDocument(htmlString: string) {
52 | const {
53 | window: {document},
54 | } = new JSDOM(htmlString);
55 | return document;
56 | }
57 |
58 | async function fetchDOM(endpoint: string): Promise
{
59 | return getDocument(await fetch(endpoint).then((res) => res.text()));
60 | }
61 |
62 | async function getAuthToken(): Promise {
63 | const document = await fetchDOM(baseURL);
64 | return document.querySelector(
65 | 'meta[name="csrf-token"]',
66 | )!.content;
67 | }
68 |
69 | function parseExternalSource(externalBox: Element): Source {
70 | const [titleElement, authorElement] = Array.from(
71 | externalBox.querySelectorAll('a')!,
72 | );
73 | return {
74 | type: externalBox
75 | .querySelector('img')!
76 | .alt.toLowerCase() as SourceType,
77 | title: titleElement.textContent!,
78 | url: titleElement.href,
79 | author: {
80 | name: authorElement.textContent!,
81 | url: authorElement.href,
82 | },
83 | };
84 | }
85 |
86 | function parseExternalInfo(itemBox: Element): ExternalInfo | undefined {
87 | const infoHeader = itemBox.querySelector('.info-header');
88 | if (!infoHeader) return;
89 | const externalBox = itemBox.querySelector('.external');
90 | if (!externalBox || !externalBox.textContent) return;
91 |
92 | const maybeSource = externalBox.querySelectorAll('img,a,a').length === 3;
93 |
94 | return {
95 | type: 'external',
96 | ref: infoHeader.textContent,
97 | content: maybeSource
98 | ? parseExternalSource(externalBox)
99 | : externalBox.textContent.trim(),
100 | } as ExternalInfo;
101 | }
102 |
103 | function parseSource(itemBox: Element): Source | undefined {
104 | const detailBox = itemBox.querySelector('.detail-box');
105 | if (!detailBox || detailBox.textContent!.trim() === '') return;
106 | const h6 = detailBox.querySelector('h6');
107 | if (!h6) return;
108 |
109 | const anchors = Array.from(h6.querySelectorAll('a')!);
110 | if (anchors[0] && anchors[0].textContent === 'amazon') {
111 | // amazon
112 | return {
113 | type: 'amazon',
114 | title: h6.childNodes[0].textContent!.trim(),
115 | url: anchors[0].href,
116 | };
117 | }
118 |
119 | if (anchors.length < 2) return;
120 | const [titleElement, authorElement] = anchors;
121 |
122 | return {
123 | type: h6.querySelector('small')!.textContent!.trim() as SourceType,
124 | title: titleElement.textContent!,
125 | url: titleElement.href,
126 | author: {
127 | name: authorElement.textContent!,
128 | url: authorElement.href,
129 | },
130 | };
131 | }
132 |
133 | function parseItem(itemBox: Element): Item {
134 | const hash = itemBox.querySelector('.hash')!.textContent!;
135 | const [size, fileType, fileSizeString] = itemBox
136 | .querySelector('small.text-muted')!
137 | .textContent!.split(' ');
138 | const thumbnailUrl =
139 | baseURL +
140 | itemBox.querySelector('.image-box > img')!.src;
141 | const [width, height] = size.split('x').map((s) => parseInt(s));
142 | const fileSize = bytes(fileSizeString);
143 |
144 | const item = {
145 | hash,
146 | thumbnailUrl,
147 | width,
148 | height,
149 | fileType: fileType.toLowerCase() as FileType,
150 | fileSize,
151 | } as Item;
152 |
153 | item.externalInfo = parseExternalInfo(itemBox);
154 | item.source = parseSource(itemBox);
155 |
156 | return item;
157 | }
158 |
159 | function parseSearchResult(htmlString: string): Item[] {
160 | const document = getDocument(htmlString);
161 | return Array.from(document.querySelectorAll('.item-box'))
162 | .slice(1)
163 | .map(parseItem);
164 | }
165 |
166 | async function getSearchHash(query: string | fs.ReadStream) {
167 | const searchType = query instanceof fs.ReadStream ? 'file' : 'uri';
168 | const token = await getAuthToken();
169 | const formData = new FormData();
170 | formData.append('authenticity_token', token);
171 | formData.append(searchType, query);
172 | const response = await fetch(`${baseURL}search/${searchType}`, {
173 | method: 'POST',
174 | body: formData,
175 | redirect: 'manual',
176 | });
177 |
178 | const url = response.headers.get('location');
179 | if (!url) {
180 | throw new Error(`Image size is too large`);
181 | }
182 | const searchHash = url.match(/\/([^/]+)$/)?.[1];
183 | if (!searchHash) {
184 | throw new Error(`Invalid image format`);
185 | }
186 | return searchHash;
187 | }
188 |
189 | export async function searchByUrl(
190 | imageUrl: string,
191 | mode: SearchMode = 'color',
192 | ): Promise {
193 | const hash = await getSearchHash(imageUrl);
194 | const url = `${baseURL}search/${mode}/${hash}`;
195 | const result = await fetch(url).then((res) => res.text());
196 | const items = parseSearchResult(result);
197 |
198 | return {url, items};
199 | }
--------------------------------------------------------------------------------
/commands/fun/pixiv.ts:
--------------------------------------------------------------------------------
1 | const { error, reply, generateIllustEmbed } = require('../../functions/Util.js');
2 | const Pixiv = require('pixiv.ts');
3 | const { SlashCommandBuilder } = require('@discordjs/builders');
4 | const refreshToken = process.env.PIXIV_REFRESH_TOKEN || require('../../config/config.json').PixivRefreshToken;
5 |
6 | // search pixiv for illusts
7 | async function pixivFunc(command, args, language) {
8 | const pixiv = await Pixiv.default.refreshLogin(refreshToken);
9 | let illusts = [];
10 | let illust;
11 | const subcommand = args[1];
12 | switch (subcommand) {
13 | case 'illust':
14 | try {
15 | illust = await pixiv.search.illusts({
16 | illust_id: command?.options?.getInteger('illust_id') ?? args[2],
17 | });
18 | } catch (err) {
19 | return error(command, language.noIllust);
20 | }
21 | break;
22 | case 'author':
23 | try {
24 | illusts = await pixiv.user.illusts({
25 | user_id: command?.options?.getInteger('author_id') ?? args[2],
26 | });
27 | } catch (err) {
28 | return error(command, language.noUser);
29 | }
30 | illust = illusts[Math.floor(Math.random() * illusts.length)];
31 | break;
32 | case 'query':
33 | illusts = await pixiv.search.illusts({
34 | word: command?.options?.getString('query') ?? args[2],
35 | r18: false,
36 | bookmarks: (command.options.getString('bookmarks') ?? args[3]) || '1000',
37 | });
38 | if (illusts.length === 0) return error(command, language.noResult);
39 | if (pixiv.search.nextURL && (command?.options?.getInteger('pages') ?? parseInt(args[4], 10)) !== 1) {
40 | illusts = await pixiv.util.multiCall({
41 | next_url: pixiv.search.nextURL, illusts,
42 | // minus 1 because we had already searched the first page
43 | }, (command.options.getInteger('pages') ?? parseInt(args[4])) - 1 || 0);
44 | }
45 | illust = illusts[Math.floor(Math.random() * illusts.length)];
46 | break;
47 | default:
48 | return error(command, language.unknownSubcommand);
49 | }
50 | const illustEmbed = generateIllustEmbed(illust);
51 | return reply(command, { embeds: illustEmbed });
52 | }
53 |
54 | module.exports = {
55 | name: 'pixiv',
56 | coolDown: 3,
57 | data: new SlashCommandBuilder()
58 | .setName('pixiv')
59 | .setDescription('在pixiv網站上搜索圖片')
60 | .setDescriptionLocalizations({
61 | 'en-US': 'Search and get an illust on pixiv',
62 | 'zh-CN': '在pixiv网站上搜索图片',
63 | 'zh-TW': '在pixiv網站上搜索圖片',
64 | })
65 | .addSubcommandGroup((group) => group
66 | .setName('search')
67 | .setDescription('在pixiv上搜索')
68 | .setDescriptionLocalizations({
69 | 'en-US': 'Search on pixiv',
70 | 'zh-CN': '在pixiv上搜索',
71 | 'zh-TW': '在pixiv上搜索',
72 | })
73 | .addSubcommand((subcommand) => subcommand
74 | .setName('illust')
75 | .setDescription('用ID搜索畫作')
76 | .setDescriptionLocalizations({
77 | 'en-US': 'Search an illust with given ID',
78 | 'zh-CN': '用ID搜索画作',
79 | 'zh-TW': '用ID搜索畫作',
80 | })
81 | .addIntegerOption((option) => option
82 | .setName('illust_id')
83 | .setDescription('畫作ID')
84 | .setDescriptionLocalizations({
85 | 'en-US': 'ID of the illust',
86 | 'zh-CN': '画作ID',
87 | 'zh-TW': '畫作ID',
88 | })
89 | .setRequired(true),
90 | ),
91 | )
92 | .addSubcommand((subcommand) => subcommand
93 | .setName('author')
94 | .setDescription('搜索並取得繪師隨機的一個畫作')
95 | .setDescriptionLocalizations({
96 | 'en-US': 'Search and get a random illust from the author',
97 | 'zh-CN': '搜索并取得绘师随机的一个画作',
98 | 'zh-TW': '搜索並取得繪師隨機的一個畫作',
99 | })
100 | .addIntegerOption((option) => option
101 | .setName('author_id')
102 | .setDescription('繪師ID')
103 | .setDescriptionLocalizations({
104 | 'en-US': 'ID of the author',
105 | 'zh-CN': '绘师ID',
106 | 'zh-TW': '繪師ID',
107 | })
108 | .setRequired(true),
109 | ),
110 | )
111 | .addSubcommand((subcommand) => subcommand
112 | .setName('query')
113 | .setDescription('在pixiv上搜索關鍵詞')
114 | .setDescriptionLocalizations({
115 | 'en-US': 'query to search illust on pixiv',
116 | 'zh-CN': '在pixiv上搜索关键词',
117 | 'zh-TW': '在pixiv上搜索關鍵詞',
118 | })
119 | .addStringOption((option) => option
120 | .setName('query')
121 | .setDescription('在pixiv上搜索關鍵詞')
122 | .setDescriptionLocalizations({
123 | 'en-US': 'Query to search illust on pixiv',
124 | 'zh-CN': '在pixiv上搜索关键词',
125 | 'zh-TW': '在pixiv上搜索關鍵詞',
126 | })
127 | .setRequired(true)
128 | .setAutocomplete(true),
129 | )
130 | .addStringOption((option) => option
131 | .setName('bookmarks')
132 | .setDescription('用書籤數量過濾畫作, 默認為1000個書籤')
133 | .setDescriptionLocalizations({
134 | 'en-US': 'filter search results with bookmarks, default to 1000 bookmarks',
135 | 'zh-CN': '用书签数量过滤画作, 默认为1000个书签',
136 | 'zh-TW': '用書籤數量過濾畫作, 默認為1000個書籤',
137 | })
138 | .addChoices(
139 | { name: '50', value: '50' },
140 | { name: '100', value: '100' },
141 | { name: '300', value: '300' },
142 | { name: '500', value: '500' },
143 | { name: '1000', value: '1000' },
144 | { name: '3000', value: '3000' },
145 | { name: '5000', value: '5000' },
146 | { name: '10000', value: '10000' },
147 | ),
148 | )
149 | .addIntegerOption((option) => option
150 | .setName('pages')
151 | .setDescription('搜索頁數(頁數越多搜索時間越長), 默認為1')
152 | .setDescriptionLocalizations({
153 | 'en-US': 'how many pages to search (more pages = longer), default to 1',
154 | 'zh-CN': '搜索页数(頁數越多搜索时间越长), 默认为1',
155 | 'zh-TW': '搜索頁數(頁數越多搜索時間越長), 默認為1',
156 | })
157 | .setMinValue(1)
158 | .setMaxValue(10),
159 | ),
160 | ),
161 | ),
162 | async execute(interaction, language) {
163 | if (!refreshToken) return interaction.reply(language.noToken);
164 | await interaction.deferReply();
165 | await pixivFunc(interaction, [interaction.options.getSubcommandGroup(), interaction.options.getSubcommand()], language);
166 | },
167 | async autoComplete(interaction) {
168 | const pixiv = await Pixiv.default.refreshLogin(refreshToken);
169 | const keyword = interaction.options.getString('query');
170 | const candidates = await pixiv.web.candidates({ keyword: keyword, lang: 'en' });
171 | const respondArray = [];
172 | candidates.candidates.forEach(tag => {
173 | respondArray.push({ name: `${tag.tag_name} <${tag.tag_translation ?? ''}>`, value: tag.tag_name });
174 | });
175 | // respond to the request
176 | return interaction.respond(respondArray);
177 | },
178 | };
179 |
--------------------------------------------------------------------------------
/commands/settings/set.ts:
--------------------------------------------------------------------------------
1 | const { error, success } = require('../../functions/Util.js');
2 | const { Role, TextChannel } = require('discord.js');
3 | const { saveGuildData } = require('../../functions/Util');
4 | const { SlashCommandBuilder } = require('@discordjs/builders');
5 |
6 | async function changeSettings(guildId, client, category, target) {
7 | const guildData = client.guildCollection.get(guildId);
8 | guildData.data[category] = target;
9 | client.guildCollection.set(guildId, guildData);
10 | await saveGuildData(client, guildId);
11 | }
12 |
13 | async function setLanguage(command, args, language) {
14 | if (args.length < 1) return error(command, language.noArgs);
15 | if (args[0] !== 'en_US' && args[0] !== 'zh_CN' && args[0] !== 'zh_TW') return error(command, language.languageNotSupported);
16 |
17 | await changeSettings(command.guild.id, command.client, 'language', args[0]);
18 | language = command.client.language[args[0]]['set'];
19 | await success(command, language.changeSuccess.replace('${args[0]}', args[0]));
20 | }
21 |
22 | async function setChannel(command, args, language, target) {
23 | if (args.length < 1) return error(command, language.noArgs);
24 | if (!(args[0] instanceof TextChannel)) return error(command, language.argsNotChannel);
25 | await changeSettings(command.guild.id, command.client, target, args[0].id);
26 | if (target === 'starboard') return success(command, language.logChannelChanged.replace('${args[0]}', args[0]));
27 | if (target === 'channel') return success(command, language.starboardChanged.replace('${args[0]}', args[0]));
28 | }
29 |
30 | async function addLevelReward(command, args, language) {
31 | if (args.length < 1) return error(command, language.noArgs);
32 | if (isNaN(args[0])) return error(command, language.argsNotNumber);
33 | if (!(args[1] instanceof Role) || !args[1]) return error(command, language.noRole);
34 |
35 | const rewards = command.client.guildCollection.get(command.guild.id).data.levelReward ?? {};
36 | rewards[args[0]] = args[1].id;
37 | await changeSettings(command.guild.id, command.client, 'levelReward', rewards);
38 | return success(command, language.levelRewardAdded.replace('${args[0]}', args[0]).replace('${args[1]}', args[1]));
39 | }
40 |
41 | async function removeLevelReward(command, args, language) {
42 | if (args.length < 1) return error(command, language.noArgs);
43 | if (!(args[0] instanceof Role)) return error(command, language.noRole);
44 |
45 | const rewards = command.client.guildCollection.get(command.guild.id).data.levelReward;
46 | if (!rewards) return error(command, '你還沒有設置等級獎勵!');
47 | for (const r in rewards) {
48 | if (rewards[r] === args[1].id) {
49 | delete rewards[r];
50 | }
51 | }
52 | await changeSettings(command.guild.id, command.client, 'levelReward', rewards);
53 | return success(command, language.levelRewardRemoved.replace('${args[0]}', args[0]).replace('${args[1]}', args[1]));
54 | }
55 |
56 | module.exports = {
57 | name: 'set',
58 | guildOnly: true,
59 | data: new SlashCommandBuilder()
60 | .setName('set')
61 | .setDescription('调整我的伺服器設定')
62 | .setDescriptionLocalizations({
63 | 'en-US': 'Adjust my settings in this server',
64 | 'zh-CN': '调整我的伺服器设定',
65 | 'zh-TW': '调整我的伺服器設定',
66 | })
67 | .addSubcommand((subcommand) => subcommand
68 | .setName('language')
69 | .setDescription('設定我使用的語言')
70 | .setDescriptionLocalizations({
71 | 'en-US': 'Set the language I use',
72 | 'zh-CN': '设定我使用的语言',
73 | 'zh-TW': '設定我使用的語言',
74 | })
75 | .addStringOption((option) => option
76 | .setName('language')
77 | .setDescription('設定我使用的語言')
78 | .setDescriptionLocalizations({
79 | 'en-US': 'Set the language I use',
80 | 'zh-CN': '设定我使用的语言',
81 | 'zh-TW': '設定我使用的語言',
82 | })
83 | .addChoices(
84 | { name: 'en-US', value: 'en_US' },
85 | { name: 'zh-CN', value: 'zh_CN' },
86 | { name: 'zh-TW', value: 'zh_TW' },
87 | )
88 | .setRequired(true),
89 | ),
90 | )
91 | .addSubcommand((subcommand) => subcommand
92 | .setName('log_channel')
93 | .setDescription('讓我記錄群內事件的頻道')
94 | .setDescriptionLocalizations({
95 | 'en-US': 'Channel for me to log server events in!',
96 | 'zh-CN': '让我记录群內事件的频道',
97 | 'zh-TW': '讓我記錄群內事件的頻道',
98 | })
99 | .addChannelOption((option) => option
100 | .setName('channel')
101 | .setDescription('讓我記錄群內事件的頻道')
102 | .setDescriptionLocalizations({
103 | 'en-US': 'Channel for me to log server events in!',
104 | 'zh-CN': '让我记录群內事件的频道',
105 | 'zh-TW': '讓我記錄群內事件的頻道',
106 | })
107 | .setRequired(true),
108 | ),
109 | )
110 | .addSubcommand((subcommand) => subcommand
111 | .setName('starboard')
112 | .setDescription('設置名句精華頻道')
113 | .setDescriptionLocalizations({
114 | 'en-US': 'Set starboard channel!',
115 | 'zh-CN': '设置名句精华频道',
116 | 'zh-TW': '設置名句精華頻道',
117 | })
118 | .addChannelOption((option) => option
119 | .setName('channel')
120 | .setDescription('名句精華頻道')
121 | .setDescriptionLocalizations({
122 | 'en-US': 'Starboard channel',
123 | 'zh-CN': '名句精华频道',
124 | 'zh-TW': '名句精華頻道',
125 | })
126 | .setRequired(true),
127 | ),
128 | )
129 | .addSubcommand((subcommand) => subcommand
130 | .setName('add_level_reward')
131 | .setDescription('設置用戶升級時給予的獎勵')
132 | .setDescriptionLocalizations({
133 | 'en-US': 'Set reward given when a user levels up.',
134 | 'zh-CN': '设置用户升级时给予的奖励',
135 | 'zh-TW': '設置用戶升級時給予的獎勵',
136 | })
137 | .addIntegerOption((option) => option
138 | .setName('level')
139 | .setDescription('等級')
140 | .setDescriptionLocalizations({
141 | 'en-US': 'Level',
142 | 'zh-CN': '等级',
143 | 'zh-TW': '等級',
144 | })
145 | .setRequired(true),
146 | )
147 | .addRoleOption((option) => option
148 | .setName('role')
149 | .setDescription('給予的身份組')
150 | .setDescriptionLocalizations({
151 | 'en-US': 'Role to give',
152 | 'zh-CN': '给予的身份组',
153 | 'zh-TW': '給予的身份組',
154 | })
155 | .setRequired(true),
156 | ),
157 | )
158 | .addSubcommand((subcommand) => subcommand
159 | .setName('remove_level_reward')
160 | .setDescription('移除獎勵身份組')
161 | .setDescriptionLocalizations({
162 | 'en-US': 'Remove reward role',
163 | 'zh-CN': '移除奖励身份组',
164 | 'zh-TW': '移除獎勵身份組',
165 | })
166 | .addRoleOption((option) => option
167 | .setName('channel')
168 | .setDescription('要刪除的身份組')
169 | .setDescriptionLocalizations({
170 | 'en-US': 'Role to remove',
171 | 'zh-CN': '要删除的身份组',
172 | 'zh-TW': '要刪除的身份組',
173 | })
174 | .setRequired(true),
175 | ),
176 | ),
177 | async execute(interaction, language) {
178 | switch (interaction.options.getSubcommand()) {
179 | case 'language':
180 | return await setLanguage(interaction, [interaction.options.getString('language')], language);
181 | case 'log_channel':
182 | return await setChannel(interaction, [interaction.options.getChannel('channel')], language, 'channel');
183 | case 'starboard':
184 | return await setChannel(interaction, [interaction.options.getChannel('channel')], language, 'starboard');
185 | case 'add_level_reward':
186 | return await addLevelReward(interaction, [interaction.options.getInteger('level'), interaction.options.getRole('role')], language);
187 | case 'remove_level_reward':
188 | return await removeLevelReward(interaction, [interaction.options.getRole('role')], language);
189 | }
190 | },
191 | };
192 |
--------------------------------------------------------------------------------
/language/zh-CN.js:
--------------------------------------------------------------------------------
1 | /* eslint quotes: 0 */
2 | module.exports = {
3 | "backup": {
4 | "invalidBackupID": "您必须输入有效的备份文件ID!",
5 | "backupInformation": "备份信息",
6 | "backupID": "备份文件ID",
7 | "serverID": "伺服器ID",
8 | "backupSize": "文件大小",
9 | "backupCreatedAt": "创建于",
10 | "noBackupFound": "找不到\\`${backupID}\\`这个ID!",
11 | "notAdmin": "你必须是伺服器管理员才能请求备份!",
12 | "startBackup": "开始备份...\n频道最大备份讯息量: ${max}\n图片格式: base64",
13 | "doneBackupDM": "✅ | 备份已创建! 要加载备份, 请在目标伺服器中输入此指令: \\`${prefix}load ${backupData.id}\\`!",
14 | "doneBackupGuild": "备份已创建。备份ID已发送至私讯!",
15 | "warningBackup": "加载备份后, 所有的原本的频道,身分组等将无法复原! 输入 `-confirm` 确认!",
16 | "timesUpBackup": "时间到! 已取消备份加载!",
17 | "startLoadingBackup": "✅ | 开始加载备份!",
18 | "backupError": "🆘 | 很抱歉,出了点问题... 请检查我有没有管理员权限!",
19 | "doneLoading": "✅ | 群组备份完成!",
20 | "outOfRange": "频道最大备份讯息数不能小于0或大于1000!",
21 | },
22 | "8ball": {
23 | "noQuestion": "你要问问题啦干",
24 | "reply1": "这是必然",
25 | "reply2": "肯定是的",
26 | "reply3": "不用怀疑",
27 | "reply4": "毫无疑问",
28 | "reply5": "你能相信它",
29 | "reply6": "如我所见,是的",
30 | "reply7": "很有可能",
31 | "reply8": "看起来很好",
32 | "reply9": "是的",
33 | "reply10": "种种迹象指出「是的」",
34 | "reply11": "回覆拢统,再试试",
35 | "reply12": "待会再问",
36 | "reply13": "最好现在不告诉你",
37 | "reply14": "现在无法预测",
38 | "reply15": "专心再问一遍",
39 | "reply16": "想的美",
40 | "reply17": "我的回覆是「不」",
41 | "reply18": "我的来源说「不」",
42 | "reply19": "看起来不太好",
43 | "reply20": "很可疑",
44 | "reply": "神奇八号球 🎱 回答:",
45 | },
46 | "connect4": {
47 | "board": "现在轮到 ${round.name}!\n${boardStr}",
48 | "invalidMove": "你不能往那边放棋子!",
49 | "win": "${round.name} 赢了!",
50 | },
51 | "loli": {
52 | "noToken": "沒有pixiv refreshToken不能使用这个指令!",
53 | },
54 | "pixiv": {
55 | "noToken": "沒有pixiv refreshToken不能使用这个指令!",
56 | "noIllust": "这个画作不存在!",
57 | "noUser": "这个用户不存在!",
58 | "noResult": "没有找到结果!",
59 | "unknownSubcommand": "无效的子指令!",
60 | },
61 | "updateIllust": {
62 | "noToken": "沒有pixiv refreshToken不能使用这个指令!",
63 | },
64 | "yt-together": {
65 | "notInVC": "加入语音频道后才能使用此指令!",
66 | },
67 | "anime": {
68 | "similarity": "相似度: ${similarity}%",
69 | "sourceURL": "**来源链接**",
70 | "nativeTitle": "日语标题",
71 | "romajiTitle": "罗马音标题",
72 | "englishTitle": "英语标题",
73 | "episode": "集数",
74 | "NSFW": "NSFW",
75 | "invalidURL": "请输入正确的网址!",
76 | "noImage": "你要先上传一张图片才能使用这个指令!",
77 | },
78 | "avatar": {
79 | "yourAvatar": "__你的头像__",
80 | "userAvatar": "__${user.username}的头像__",
81 | "memberAvatar": "__${user.displayName}的头像__",
82 | "noMember": "找不到匹配 \\`${keyword}\\` 的用户!",
83 | },
84 | "google": {
85 | },
86 | "help": {
87 | "helpTitle": "指令列表",
88 | "helpPrompt": "这是我所有的指令:",
89 | "helpPrompt2": "\n你可以发送 \\`${prefix}help [command name]\\` 来查询指令详情!",
90 | "helpSend": "我已经把我的指令列表私讯给你了!",
91 | "invalidCmd": "该指令不存在!",
92 | "cmdName": "**名字:**",
93 | "cmdAliases": "**别名:**",
94 | "cmdDescription": "**描述:**",
95 | "cmdUsage": "**用法:**",
96 | "cmdCoolDown": "**冷却:**",
97 | },
98 | "invite": {
99 | "invite": "邀請我!",
100 | },
101 | "run": {
102 | "invalidUsage": "无效用法! 无效的语言/代码。",
103 | "wait": "请稍等...",
104 | "usage": "用法: c!run <语言> [代码](有无代码块皆可)",
105 | "notSupported": "不支持该语言!",
106 | "outputTooLong": "输出过长 (超过2000个字符/40行),所以我把它上传到了这里: ${link}",
107 | "postError": "输出太长了, 但是我无法将输出上传到网站上。",
108 | "noOutput": "没有输出!",
109 | },
110 | "sauce": {
111 | "similarity": "相似度: ${similarity}%",
112 | "sourceURL": "**来源链接**",
113 | "searchingSauce": "正在搜索图片...",
114 | "additionalInfo": "额外信息",
115 | "noAuthor": "找不到作者信息!",
116 | "sauceAuthor": "名字: ${authorInfo.name}\n链接: ${authorInfo.url}",
117 | "title":"标题",
118 | "author":"作者",
119 | },
120 | "server": {
121 | "serverInfo": "伺服器资料",
122 | "serverName": "伺服器名字",
123 | "serverOwner": "伺服器拥有者",
124 | "memberCount": "伺服器人数",
125 | "serverRegion": "伺服器区域",
126 | "highestRole": "最高身份组",
127 | "serverCreatedAt": "伺服器创造时间",
128 | "channelCount": "伺服器频道数",
129 | },
130 | "user-info": {
131 | "customStatus": "__自定义活动__\n<:${name}:${id}> ${state}\n",
132 | "gameStatus": "__${type}__\n${name}\n${details}",
133 | "notPlaying": "用户没在玩游戏",
134 | "uiTitle": "用户资料",
135 | "tag": "Tag",
136 | "nickname": "昵称",
137 | "id": "ID",
138 | "avatarURL": "头像",
139 | "avatarValue": "[点我](${url})",
140 | "createdAt": "账户创造时间",
141 | "joinedAt": "加入伺服器时间",
142 | "activity": "活动",
143 | "none": "无",
144 | "status": "状态",
145 | "device": "设备",
146 | "roles": "身分组(${author.roles.cache.size})",
147 | },
148 | "ban": {
149 | "noMention": "你要提及一个人才能对他停权!",
150 | "cantBanSelf": "你不能对你自己停权啦",
151 | "cannotBan": "不能对这个人停权",
152 | "banSuccess": "成功对 ${taggedUser.user.username}停权!",
153 | },
154 | "kick": {
155 | "noMention": ":warning: | 你要提及一个人才能踢出他!",
156 | "cantKickSelf": ":x: | 你不能踢出你自己啦",
157 | "cannotKick": "不能踢出这个人!",
158 | "kickSuccess": "成功踢出 ${taggedUser.user.username}!",
159 | },
160 | "prune": {
161 | "invalidNum": "这看起来不像是有效的数字!",
162 | "notInRange": "请输入1到99之间的数字!",
163 | "pruneError": "在尝试删除讯息时发生了错误!",
164 | },
165 | "clear": {
166 | "cleared": "播放清单已清除!",
167 | },
168 | "loop": {
169 | "on": "循环模式开启!",
170 | "off": "循环模式关闭!",
171 | },
172 | "loop-queue": {
173 | "on": "清单循环模式开启!",
174 | "off": "清单循环模式关闭!",
175 | },
176 | "lyric": {
177 | "searching": ":mag: | 正在搜寻 ${keyword}...",
178 | "noLyricsFound": "找不到 `${keyword}` 的歌词!",
179 | "title": "`${keyword}` 的歌词",
180 | "noKeyword": "没有提供关键词!",
181 | },
182 | "now-playing": {
183 | "npTitle": "**正在播放 ♪**",
184 | "requester": "请求者:",
185 | "musicFooter": "音乐系统",
186 | },
187 | "pause": {
188 | "pause": "已暂停!",
189 | },
190 | "play": {
191 | "notInVC": "加入语音频道后才能使用此指令!",
192 | "cantJoinVC": "我需要加入频道和说话的权限!",
193 | "importAlbum1": "✅ | 专辑: **${title}** 导入中",
194 | "importAlbum2": "✅ | 专辑: **${videos[0].title}** 导入中 **${i}**",
195 | "importAlbumDone": "✅ | 专辑: **${title}** 已加入到播放清单!",
196 | "importPlaylist1": "✅ | 播放列表: **${title}** 导入中",
197 | "importPlaylist2": "✅ | 播放列表: **${videos[0].title}** 导入中 **${i}**",
198 | "importPlaylistDone": "✅ | 播放列表: **${title}** 已加入到播放清单!",
199 | "noResult": "我找不到任何搜寻结果!",
200 | "noArgs": "不要留白拉干",
201 | "searching": "🔍 | 正在搜索 ${keyword}...",
202 | "choose": "请选择歌曲",
203 | "timeout": "时间到!",
204 | },
205 | "queue": {
206 | "queueTitle": "播放清单",
207 | "queueBody": "**正在播放**\n[${serverQueue.songs[0].title}](${serverQueue.songs[0].url})\n\n**在清单中**\n${printQueue}\n${serverQueue.songs.length - 1} 首歌",
208 | },
209 | "related": {
210 | "relatedSearch": "🔍 | 正在搜寻相关歌曲...",
211 | "noResult": "我找不到任何搜寻结果!",
212 | },
213 | "remove": {
214 | "removed": "已移除 ${serverQueue.songs[queuenum].title}!",
215 | "invalidInt": "请输入有效的数字!",
216 | },
217 | "resume": {
218 | "playing": "我已经在播放了!",
219 | "resume": "继续播放!",
220 | },
221 | "skip": {
222 | "skipped": "已跳过歌曲",
223 | },
224 | "stop": {
225 | "stopped": "已停止播放",
226 | },
227 | "set": {
228 | "languageNotSupported": "不支持该语言!",
229 | "changeSuccess": "成功更换语言至`${args[0]}`!",
230 | "argsNotChannel": "没有提供频道!",
231 | "argsNotRole": "没有提供身份组!",
232 | "argsNotNumber": "没有提供数字!",
233 | "noRole": "没有提供身份组!",
234 | "logChannelChanged": "记录频道调整至 ${args[0]}!",
235 | "starboardChanged": "名句精华调整至 ${args[0]}!",
236 | "levelRewardAdded": "成功添加身份组奖励: ${args[0]} => ${args[1]}!",
237 | "levelRewardRemoved": "成功移除身份组奖励: ${args[0]} => ${args[1]}!",
238 | },
239 | "cemoji": {
240 | "noEmoji": "请告诉我要复制哪个表情!",
241 | "addSuccess": "表情 \\`${emoji.name}\\` ${emoji} 已被加入到伺服器中!",
242 | },
243 | "edit-snipe": {
244 | "exceed10": "不能超过10!",
245 | "invalidSnipe": "无效狙击!",
246 | "noSnipe": "没有能狙击的讯息!",
247 | },
248 | "snipe": {
249 | "exceed10": "不能超过10!",
250 | "invalidSnipe": "无效狙击!",
251 | "noSnipe": "没有能狙击的讯息!",
252 | },
253 | "ping": {
254 | "pinging": "Pinging...",
255 | "heartbeat": "Websocket 心跳:",
256 | "latency": "往返延迟:",
257 | },
258 | };
--------------------------------------------------------------------------------
/language/zh-TW.js:
--------------------------------------------------------------------------------
1 | /* eslint quotes: 0 */
2 | module.exports = {
3 | "backup": {
4 | "invalidBackupID": "您必須輸入有效的備份文件ID!",
5 | "backupInformation": "備份信息",
6 | "backupID": "備份文件ID",
7 | "serverID": "伺服器ID",
8 | "backupSize": "文件大小",
9 | "backupCreatedAt": "創建於",
10 | "noBackupFound": "找不到\\`${backupID}\\`這個ID!",
11 | "notAdmin": "你必須是伺服器管理員才能請求備份!",
12 | "startBackup": "開始備份...\n頻道最大備份訊息量: ${max}\n圖片格式: base64",
13 | "doneBackupDM": "✅ | 備份已創建! 要加載備份, 請在目標伺服器中輸入此指令: \\`${prefix}load ${backupData.id}\\`!",
14 | "doneBackupGuild": "備份已創建。備份ID已發送至私訊!",
15 | "warningBackup": "加載備份後, 所有的原本的頻道,身分組等將無法復原! 輸入 `-confirm` 確認!",
16 | "timesUpBackup": "時間到! 已取消備份加載!",
17 | "startLoadingBackup": "✅ | 開始加載備份!",
18 | "backupError": "🆘 | 很抱歉,出了點問題... 請檢查我有沒有管理員權限!",
19 | "doneLoading": "✅ | 群組備份完成!",
20 | "outOfRange": "頻道最大備份訊息數不能小於0或大於1000!",
21 | },
22 | "8ball": {
23 | "noQuestion": "你要問問題啦幹",
24 | "reply1": "這是必然",
25 | "reply2": "肯定是的",
26 | "reply3": "不用懷疑",
27 | "reply4": "毫無疑問",
28 | "reply5": "你能相信它",
29 | "reply6": "如我所見,是的",
30 | "reply7": "很有可能",
31 | "reply8": "看起来很好",
32 | "reply9": "是的",
33 | "reply10": "種種跡象指出「是的」",
34 | "reply11": "回覆攏統,再試試",
35 | "reply12": "待會再問",
36 | "reply13": "最好現在不告訴你",
37 | "reply14": "現在無法預測",
38 | "reply15": "專心再問一遍",
39 | "reply16": "想的美",
40 | "reply17": "我的回覆是「不」",
41 | "reply18": "我的來源說「不」",
42 | "reply19": "看起来不太好",
43 | "reply20": "很可疑",
44 | "reply": "神奇八號球 🎱 回答:",
45 | },
46 | "connect4": {
47 | "board": "現在輪到 ${round.name}!\n${boardStr}",
48 | "invalidMove": "你不能往那邊放棋子!",
49 | "win": "${round.name} 贏了!",
50 | },
51 | "loli": {
52 | "noToken": "没有pixiv refreshToken不能使用這個指令!",
53 | },
54 | "pixiv": {
55 | "noToken": "沒有pixiv refreshToken不能使用這個指令!",
56 | "noIllust": "這個畫作不存在!",
57 | "noUser": "這個用戶不存在!",
58 | "noResult": "沒有找到結果!",
59 | "unknownSubcommand": "無效的子指令!",
60 | },
61 | "updateIllust": {
62 | "noToken": "沒有pixiv refreshToken不能使用這個指令!",
63 | },
64 | "yt-together": {
65 | "notInVC": "加入語音頻道後才能使用此指令!",
66 | },
67 | "anime": {
68 | "similarity": "相似度: ${similarity}%",
69 | "sourceURL": "**來源鏈接**",
70 | "nativeTitle": "日語標題",
71 | "romajiTitle": "羅馬音標題",
72 | "englishTitle": "英語標題",
73 | "episode": "集數",
74 | "NSFW": "NSFW",
75 | "invalidURL": "請輸入正確的網址!",
76 | "noImage": "你要先上傳一張圖片才能使用這個指令!",
77 | },
78 | "avatar": {
79 | "yourAvatar": "__你的頭像__",
80 | "userAvatar": "__${user.username}的頭像__",
81 | "memberAvatar": "__${user.displayName}的頭像__",
82 | "noMember": "找不到匹配 \\`${keyword}\\` 的用戶!",
83 | },
84 | "google": {
85 | },
86 | "help": {
87 | "helpTitle": "指令列表",
88 | "helpPrompt": "這是我所有的指令:",
89 | "helpPrompt2": "\n你可以發送 \\`${prefix}help [command name]\\` 來查詢指令詳情!",
90 | "helpSend": "我已經把我的指令列表私訊給你了!",
91 | "invalidCmd": "該指令不存在!",
92 | "cmdName": "**名字:**",
93 | "cmdAliases": "**別名:**",
94 | "cmdDescription": "**描述:**",
95 | "cmdUsage": "**用法:**",
96 | "cmdCoolDown": "**冷卻:**",
97 | },
98 | "invite": {
99 | "invite": "邀請我!",
100 | },
101 | "run": {
102 | "invalidUsage": "無效用法! 無效的語言/代碼。",
103 | "wait": "請稍等...",
104 | "usage": "用法: c!run <語言> [代碼](有無代碼塊皆可)",
105 | "notSupported": "不支持該語言!",
106 | "outputTooLong": "輸出過長 (超過2000個字符/40行),所以我把它上傳到了這裡: ${link}",
107 | "postError": "輸出太長了, 但是我無法將輸出上傳到網站上。",
108 | "noOutput": "沒有輸出!",
109 | },
110 | "sauce": {
111 | "similarity": "相似度: ${similarity}%",
112 | "sourceURL": "**來源鏈接**",
113 | "searchingSauce": "正在搜索圖片...",
114 | "additionalInfo": "額外信息",
115 | "noAuthor": "找不到作者信息!",
116 | "sauceAuthor": "名字: ${authorInfo.name}\n鏈接: ${authorInfo.url}",
117 | "title":"標題",
118 | "author":"作者",
119 | },
120 | "server": {
121 | "serverInfo": "伺服器資料",
122 | "serverName": "伺服器名字",
123 | "serverOwner": "伺服器擁有者",
124 | "memberCount": "伺服器人數",
125 | "serverRegion": "伺服器區域",
126 | "highestRole": "最高身份組",
127 | "serverCreatedAt": "伺服器創造時間",
128 | "channelCount": "伺服器頻道數",
129 | },
130 | "user-info": {
131 | "customStatus": "__自定義活動__\n<:${name}:${id}> ${state}\n",
132 | "gameStatus": "__${type}__\n${name}\n${details}",
133 | "notPlaying": "用戶沒在玩遊戲",
134 | "uiTitle": "用戶資料",
135 | "tag": "Tag",
136 | "nickname": "昵稱",
137 | "id": "ID",
138 | "avatarURL": "頭像",
139 | "avatarValue": "[點我](${url})",
140 | "createdAt": "賬戶創造時間",
141 | "joinedAt": "加入伺服器時間",
142 | "activity": "活動",
143 | "none": "無",
144 | "status": "狀態",
145 | "device": "設備",
146 | "roles": "身分組(${author.roles.cache.size})",
147 | },
148 | "ban": {
149 | "noMention": ":x: | 你要提及一個人才能對他停權!",
150 | "cantBanSelf": "你不能對你自己停權啦",
151 | "cannotBan": "不能對這個人停權",
152 | "banSuccess": "成功對 ${taggedUser.user.username}停權!",
153 | },
154 | "kick": {
155 | "noMention": ":warning: | 你要提及一個人才能踢出他!",
156 | "cantKickSelf": ":x: | 你不能踢出你自己啦",
157 | "cannotKick": "不能踢出這個人!",
158 | "kickSuccess": "成功踢出 ${taggedUser.user.username}!",
159 | },
160 | "prune": {
161 | "invalidNum": "這看起來不像是有效的數字!",
162 | "notInRange": "請輸入1到99之間的數字!",
163 | "pruneError": "在嘗試刪除訊息時發生了錯誤!",
164 | },
165 | "clear": {
166 | "cleared": "播放清單已清除!",
167 | },
168 | "loop": {
169 | "on": "循環模式開啟!",
170 | "off": "循環模式關閉!",
171 | },
172 | "loop-queue": {
173 | "on": "清單循環模式開啟!",
174 | "off": "清單循環模式關閉!",
175 | },
176 | "lyric": {
177 | "searching": ":mag: | 正在搜尋 ${keyword}...",
178 | "noLyricsFound": ":x: | 找不到 `${keyword}` 的歌詞!",
179 | "title": "`${keyword}` 的歌詞",
180 | "noKeyword": "沒有提供關鍵詞!",
181 | },
182 | "now-playing": {
183 | "npTitle": "**正在播放 ♪**",
184 | "requester": "請求者:",
185 | "musicFooter": "音樂系統",
186 | },
187 | "pause": {
188 | "pause": "已暫停!",
189 | },
190 | "play": {
191 | "notInVC": "加入語音頻道後才能使用此指令!",
192 | "cantJoinVC": "我需要加入頻道和說話的權限!",
193 | "importAlbum1": "✅ | 專輯: **${title}** 導入中",
194 | "importAlbum2": "✅ | 專輯: **${videos[0].title}** 導入中 **${i}**",
195 | "importAlbumDone": "✅ | 專輯: **${title}** 已加入到播放清單!",
196 | "importPlaylist1": "✅ | 播放列表: **${title}** 導入中",
197 | "importPlaylist2": "✅ | 播放列表: **${videos[0].title}** 導入中 **${i}**",
198 | "importPlaylistDone": "✅ | 播放列表: **${title}** 已加入到播放清單!",
199 | "noResult": "我找不到任何搜尋結果!",
200 | "noArgs": "不要留白拉幹",
201 | "searching": "🔍 | 正在搜索 ${keyword}...",
202 | "choose": "請選擇歌曲",
203 | "timeout": "時間到!",
204 | },
205 | "queue": {
206 | "queueTitle": "播放清單",
207 | "queueBody": "**正在播放**\n[${serverQueue.songs[0].title}](${serverQueue.songs[0].url})\n\n**在清單中**\n${printQueue}\n${serverQueue.songs.length - 1} 首歌",
208 | },
209 | "related": {
210 | "relatedSearch": "🔍 | 正在搜尋相關歌曲...",
211 | "noResult": "我找不到任何搜尋結果!",
212 | },
213 | "remove": {
214 | "removed": "已移除 ${serverQueue.songs[queuenum].title}!",
215 | "invalidInt": "請輸入有效的數字!",
216 | },
217 | "resume": {
218 | "playing": "我已經在播放了!",
219 | "resume": "繼續播放!",
220 | },
221 | "skip": {
222 | "skipped": "已跳過歌曲",
223 | },
224 | "stop": {
225 | "stopped": "已停止播放",
226 | },
227 | "set": {
228 | "languageNotSupported": "不支持該語言!",
229 | "changeSuccess": "成功更換語言至`${args[0]}`!",
230 | "argsNotChannel": "沒有提供頻道!",
231 | "argsNotRole": "沒有提供身份組!",
232 | "argsNotNumber": "沒有提供數字!",
233 | "noRole": "沒有提供身份組!",
234 | "logChannelChanged": "記錄頻道調整至 ${args[0]}!",
235 | "starboardChanged": "名句精華調整至 ${args[0]}!",
236 | "levelRewardAdded": "成功添加身份組獎勵: ${args[0]} => ${args[1]}!",
237 | "levelRewardRemoved": "成功移除身份組獎勵: ${args[0]} => ${args[1]}!",
238 | },
239 | "cemoji": {
240 | "noEmoji": "請告訴我要復製哪個表情!",
241 | "addSuccess": "表情 \\`${emoji.name}\\` ${emoji} 已被加入到伺服器中!",
242 | },
243 | "edit-snipe": {
244 | "exceed10": "不能超過10!",
245 | "invalidSnipe": "無效狙擊!",
246 | "noSnipe": "沒有能狙擊的訊息!",
247 | },
248 | "snipe": {
249 | "exceed10": "不能超過10!",
250 | "invalidSnipe": "無效狙擊!",
251 | "noSnipe": "沒有能狙擊的訊息!",
252 | },
253 | "ping": {
254 | "pinging": "Pinging...",
255 | "heartbeat": "Websocket 心跳:",
256 | "latency": "往返延遲:",
257 | },
258 | };
--------------------------------------------------------------------------------
/archive/auto-respond.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | const { saveGuildData, error, success } = require('../../functions/Util');
4 | const { MessageEmbed, Util } = require('discord.js');
5 |
6 | async function ar(command, args) {
7 | const [subcommandGroup, subcommand, ...options] = args;
8 | const guildData = command.client.guildCollection.get(command.guild.id).data;
9 | switch (subcommandGroup) {
10 | case 'character': {
11 | switch (subcommand) {
12 | case 'add': {
13 | return error(command, '暫時還不支持此功能!');
14 | }
15 | case 'remove': {
16 | return error(command, '暫時還不支持此功能!');
17 | }
18 | case 'list': {
19 | return error(command, '暫時還不支持此功能!');
20 | }
21 | }
22 | break;
23 | }
24 | case 'respond': {
25 | const autoResponse = guildData.autoResponse ?? {};
26 | switch (subcommand) {
27 | case 'add': {
28 | const message = command.options?.getString('message') ?? options[0];
29 | const reply = command.options?.getString('reply') ?? options[1];
30 | if (!(message in autoResponse)) autoResponse[message] = [reply];
31 | if (!autoResponse[message].includes(reply)) autoResponse[message].push(reply);
32 | guildData.autoResponse = autoResponse;
33 | await success(command, `已添加自動回復!\n觸發詞:${message}\n回復:${reply}`);
34 | return await saveGuildData(command.client, command.guild.id);
35 | }
36 | case 'remove': {
37 | const message = command.options?.getString('message') ?? options[0];
38 | const reply = command.options?.getString('reply') ?? options[1];
39 | const index = autoResponse[message].indexOf(reply);
40 | if (index > -1) autoResponse[message].splice(index, 1);
41 | if (!autoResponse[message].length) delete autoResponse[message];
42 | await success(command, `已移除自動回復!\n觸發詞:${message}\n回復:${reply}`);
43 | return await saveGuildData(command.client, command.guild.id);
44 | }
45 | case 'list': {
46 | const message = command.options?.getString('message') ?? options[0];
47 | let replies = '';
48 | autoResponse[message].forEach((reply, index) => {
49 | replies += `${index + 1}. ${reply}\n`;
50 | });
51 | return new MessageEmbed()
52 | .setTitle(`Replies invoked by ${message}`)
53 | .setColor('BLURPLE')
54 | .setDescription(Util.escapeMarkdown(replies));
55 | }
56 | }
57 | }
58 | }
59 | }
60 | module.exports = {
61 | name: 'auto-respond',
62 | aliases: ['ar'],
63 | guildOnly: true,
64 | description: {
65 | 'en-US': 'Auto responding a message',
66 | 'zh-CN': '自动回复讯息',
67 | 'zh-TW': '自動回復訊息',
68 | },
69 | subcommandGroups: [
70 | {
71 | name: 'character',
72 | description: {
73 | 'en-US': '.',
74 | 'zh-CN': '.',
75 | 'zh-TW': '.',
76 | },
77 | subcommands: [
78 | {
79 | name: 'add',
80 | description: {
81 | 'en-US': '.',
82 | 'zh-CN': '.',
83 | 'zh-TW': '.',
84 | },
85 | options: [
86 | {
87 | name: 'avatar',
88 | description: {
89 | 'en-US': '.',
90 | 'zh-CN': '.',
91 | 'zh-TW': '.',
92 | },
93 | type: 'STRING',
94 | required: true,
95 | },
96 | {
97 | name: 'name',
98 | description: {
99 | 'en-US': '.',
100 | 'zh-CN': '.',
101 | 'zh-TW': '.',
102 | },
103 | type: 'STRING',
104 | },
105 | ],
106 | },
107 | {
108 | name: 'remove',
109 | description: {
110 | 'en-US': '.',
111 | 'zh-CN': '.',
112 | 'zh-TW': '.',
113 | },
114 | options: [
115 | {
116 | name: 'name',
117 | description: {
118 | 'en-US': '.',
119 | 'zh-CN': '.',
120 | 'zh-TW': '.',
121 | },
122 | type: 'STRING',
123 | },
124 | ],
125 | },
126 | {
127 | name: 'list',
128 | description: {
129 | 'en-US': '.',
130 | 'zh-CN': '.',
131 | 'zh-TW': '.',
132 | },
133 | options: [
134 | {
135 | name: 'name',
136 | description: {
137 | 'en-US': '.',
138 | 'zh-CN': '.',
139 | 'zh-TW': '.',
140 | },
141 | type: 'STRING',
142 | },
143 | ],
144 | },
145 | ],
146 | },
147 | {
148 | name: 'respond',
149 | description: {
150 | 'en-US': '.',
151 | 'zh-CN': '.',
152 | 'zh-TW': '.',
153 | },
154 | subcommands: [
155 | {
156 | name: 'add',
157 | description: {
158 | 'en-US': '.',
159 | 'zh-CN': '.',
160 | 'zh-TW': '.',
161 | },
162 | options: [
163 | {
164 | name: 'message',
165 | description: {
166 | 'en-US': '.',
167 | 'zh-CN': '.',
168 | 'zh-TW': '.',
169 | },
170 | type: 'STRING',
171 | required: true,
172 | },
173 | {
174 | name: 'reply',
175 | description: {
176 | 'en-US': '.',
177 | 'zh-CN': '.',
178 | 'zh-TW': '.',
179 | },
180 | type: 'STRING',
181 | required: true,
182 | },
183 | ],
184 | },
185 | {
186 | name: 'remove',
187 | description: {
188 | 'en-US': '.',
189 | 'zh-CN': '.',
190 | 'zh-TW': '.',
191 | },
192 | options: [
193 | {
194 | name: 'message',
195 | description: {
196 | 'en-US': '.',
197 | 'zh-CN': '.',
198 | 'zh-TW': '.',
199 | },
200 | type: 'STRING',
201 | required: true,
202 | },
203 | {
204 | name: 'reply',
205 | description: {
206 | 'en-US': '.',
207 | 'zh-CN': '.',
208 | 'zh-TW': '.',
209 | },
210 | type: 'STRING',
211 | required: true,
212 | },
213 | ],
214 | },
215 | {
216 | name: 'list',
217 | description: {
218 | 'en-US': '.',
219 | 'zh-CN': '.',
220 | 'zh-TW': '.',
221 | },
222 | },
223 | ],
224 | },
225 | ],
226 | async execute(message, args, language) {
227 | await ar(message, args, language);
228 | },
229 | slashCommand: {
230 | async execute(interaction, language) {
231 | await ar(interaction, [interaction.getSubcommandGroup(), interaction.getSubcommand()], language);
232 | },
233 | },
234 | };
235 |
236 | */
237 |
238 | // will work on this later
--------------------------------------------------------------------------------
/commands/music/play.ts:
--------------------------------------------------------------------------------
1 | const { reply, edit, error } = require('../../functions/Util.js');
2 |
3 | const ytsr = require('youtube-sr').default;
4 | const ytpl = require('ytpl');
5 | const SpotifyClientID = process.env.SPOTIFY_CLIENT_ID || require('../../config/config.json').SpotifyClientID;
6 | const SpotifyClientSecret = process.env.SPOTIFY_CLIENT_SECRET || require('../../config/config.json').SpotifyClientSecret;
7 | const scdl = require('soundcloud-downloader').default;
8 | const Spotify = require('../../functions/spotify');
9 | let spotify;
10 | if (SpotifyClientID && SpotifyClientSecret) spotify = new Spotify(SpotifyClientID, SpotifyClientSecret);
11 | const { MessageActionRow, MessageSelectMenu, Message } = require('discord.js');
12 |
13 | const { waitImport, handleVideo } = require('../../functions/musicFunctions');
14 | const queueData = require('../../data/queueData');
15 | const { SlashCommandBuilder } = require('@discordjs/builders');
16 | const { queue } = queueData;
17 | const ytrx = /(?:youtube\.com.*[?|&](?:v|list)=|youtube\.com.*embed\/|youtube\.com.*v\/|youtu\.be\/)((?!videoseries)[a-zA-Z\d_-]*)/;
18 | const scrxt = new RegExp('^(?https://soundcloud.com/(?!sets|stats|groups|upload|you|mobile|stream|messages|discover|notifications|terms-of-use|people|pages|jobs|settings|logout|charts|imprint|popular[a-z\\d-_]{1,25})/(?!sets|playlist|stats|settings|logout|notifications|you|messages[a-z\\d-_]{1,100}(?:/s-[a-zA-Z\\d-_]{1,10})?))[a-z\\d-?=/]*$');
19 | const sprxtrack = /(http[s]?:\/\/)?(open\.spotify\.com)\//;
20 |
21 | async function play(command, args, language) {
22 | let serverQueue = queue.get(command.guild.id);
23 | if (!command.member.voice.channel) {
24 | return await error(command, language.notInVC);
25 | }
26 |
27 | const url = args[0];
28 |
29 | const voiceChannel = command.member.voice.channel;
30 |
31 | const permissions = voiceChannel.permissionsFor(command.client.user);
32 | if (!permissions.has('CONNECT') || !permissions.has('SPEAK')) {
33 | return error(command, language.cantJoinVC);
34 | }
35 |
36 | if (!serverQueue) {
37 | const queueConstruct = {
38 | textChannel: command.channel,
39 | voiceChannel,
40 | connection: null,
41 | songs: [],
42 | songHistory: [],
43 | volume: 5,
44 | playing: true,
45 | loop: false,
46 | loopQueue: false,
47 | player: undefined,
48 | resource: undefined,
49 | playMessage: undefined,
50 | filter: '',
51 | };
52 | queue.set(command.guild.id, queueConstruct);
53 | serverQueue = queue.get(command.guild.id);
54 | }
55 |
56 | async function processYoutubeLink(link) {
57 | if (!ytsr.validate(link, 'PLAYLIST_ID')) {
58 | const videos = await ytsr.getVideo(args[0]);
59 | return handleVideo([videos], voiceChannel, false, serverQueue, 'yt', command);
60 | }
61 | const playlist = await ytpl(link, { limit: Infinity });
62 | if (!playlist) return;
63 | const result = await waitImport(playlist.title, playlist.estimatedItemCount, command);
64 | if (result) {
65 | playlist.items.forEach((video) => handleVideo(video, voiceChannel, true, serverQueue, 'ytlist', command));
66 | }
67 | }
68 |
69 | async function processSpotifyLink(link) {
70 | link = `spotify:${link.replace(sprxtrack, '').replace('/', ':').replace('?.*', '')}`;
71 | const part = link.split(':');
72 | const Id = part[part.length - 1];
73 | let result;
74 |
75 | if (link.includes('track')) {
76 | result = await spotify.getTrack(Id);
77 | const videos = await ytsr.search(
78 | `${result.artists[0].name} ${result.name}`,
79 | { limit: 1, type: 'video' },
80 | );
81 | return await handleVideo(videos, voiceChannel, false, serverQueue, 'yt', command);
82 | }
83 |
84 | if (link.includes('album')) {
85 | result = await spotify.getAlbum(Id);
86 | const title = result.name;
87 | const m = await edit(command, language.importAlbum1.replace('${title}', title), 'BLUE');
88 | for (const i in result.tracks.items) {
89 | const videos = await ytsr.search(
90 | `${result.artists[0].name} ${result.tracks.items[i].name}`,
91 | { limit: 1, type: 'video' },
92 | );
93 | await handleVideo(videos, voiceChannel, true, serverQueue, 'yt', command);
94 | await m.edit(language.importAlbum2.replace('${videos[0].title}', videos[0].title).replace('${i}', i));
95 | }
96 | return m.edit(language.importAlbumDone.replace('${title}', title));
97 | }
98 |
99 | if (link.includes('playlist')) {
100 | result = await spotify.getPlaylist(Id);
101 |
102 | const title = result.name;
103 | const lenght = result.tracks.total;
104 |
105 | const wait = await waitImport(title, lenght, command);
106 | if (!wait) {
107 | const videos = await ytsr.search(
108 | `${result.tracks.items[0].track.artists[0].name} ${result.tracks.items[0].track.name}`,
109 | { limit: 1, type: 'video' },
110 | );
111 | return await handleVideo(videos, voiceChannel, false, serverQueue, 'yt', command);
112 | }
113 |
114 | const m = await edit(command, language.importPlaylist1.replace('${title}', title), 'BLUE');
115 | // eslint-disable-next-line no-constant-condition
116 | while (true) {
117 | for (const i in result.tracks.items) {
118 | const videos = await ytsr.search(
119 | `${result.tracks.items[i].track.artists[0].name} ${result.tracks.items[i].track.name}`,
120 | { limit: 1, type: 'video' },
121 | );
122 | await handleVideo(videos, voiceChannel, true, serverQueue, 'yt', command);
123 | await m.edit(language.importPlaylist2.replace('${videos[0].title}', videos[0].title).replace('${i}', i));
124 | }
125 | if (!result.tracks.next) {
126 | break;
127 | } else {
128 | result = await spotify.makeRequest(result.tracks.next);
129 | }
130 | }
131 | return m.edit(language.importPlaylistDone.replace('${title}', title));
132 | }
133 | }
134 |
135 | async function processSoundcloudLink(link) {
136 | if (scdl.isPlaylistURL(link)) {
137 | const data = await scdl.getSetInfo(link).catch((err) => {
138 | console.log(err);
139 | return edit(command, language.noResult, 'RED');
140 | });
141 | const wait = await waitImport(data.title, data.tracks.length, command);
142 | let m;
143 | if (wait) {
144 | m = await edit(command, language.importPlaylist1.replace('${data.title}', data.title), 'BLUE');
145 | for (const i in data.tracks) {
146 | await handleVideo(data.tracks[i], voiceChannel, true, serverQueue, 'sc', command);
147 | await m.edit(language.importPlaylist2.replace('${data.tracks[i].title}', data.tracks[i].title).replace('${i}', i));
148 | }
149 | }
150 | return m.edit(language.importPlaylistDone.replace('${data.title}', data.title));
151 | }
152 | if (link.match(scrxt)) {
153 | const data = await scdl.getInfo(link).catch((err) => {
154 | console.log(err);
155 | throw edit(command, language.noResult, 'RED');
156 | });
157 | await handleVideo(data, voiceChannel, true, serverQueue, 'sc', command);
158 | }
159 | }
160 |
161 | if (!args[0]) return error(command, language.noArgs);
162 | if (url.match(ytrx)) return processYoutubeLink(url);
163 | if (url.startsWith('https://open.spotify.com/')) {
164 | if (!SpotifyClientID || !SpotifyClientSecret) return error(command, 'Spotify songs cannot be processed!');
165 | return processSpotifyLink(url);
166 | }
167 | if (url.startsWith('https://soundcloud.com/')) return processSoundcloudLink(url);
168 |
169 | const keyword = command instanceof Message ? command.content.substring(command.content.indexOf(' ') + 1) : args[0];
170 | let msg = await reply(command, language.searching.replace('${keyword}', keyword), 'BLUE');
171 | const videos = await ytsr.search(keyword);
172 | const options = videos.map((video) => ({
173 | label: video.channel.name.length > 20 ? `${video.channel.name.slice(0, 20)}...` : video.channel.name,
174 | description: `${video.title.length > 35 ? `${video.title.slice(0, 30)}...` : video.title} - ${video.durationFormatted}`,
175 | value: (videos.indexOf(video)).toString(),
176 | }));
177 |
178 | const row = new MessageActionRow()
179 | .addComponents(
180 | new MessageSelectMenu()
181 | .setCustomId('select')
182 | .setPlaceholder('Nothing selected')
183 | .addOptions(options),
184 | );
185 | msg = await msg.edit({ content: language.choose, components: [row] });
186 | serverQueue.playMessage = msg;
187 | const filter = (interaction) => {
188 | return interaction.customId === 'select' && interaction.user.id === command.member.id;
189 | };
190 | const collector = msg.createMessageComponentCollector({ filter, time: 100000 });
191 | collector.on('collect', async (menu) => {
192 | await handleVideo([videos[menu.values[0]]], voiceChannel, false, serverQueue, 'yt', command);
193 | });
194 | collector.on('end', async (menu) => {
195 | if (!menu.first()) {
196 | msg.channel.send(language.timeout);
197 | await msg.delete();
198 | }
199 | });
200 | }
201 |
202 | module.exports = {
203 | name: 'play',
204 | guildOnly: true,
205 | aliases: ['p'],
206 | data: new SlashCommandBuilder()
207 | .setName('play')
208 | .setDescription('根據關鍵字或鏈接播放歌曲')
209 | .setDescriptionLocalizations({
210 | 'en-US': 'Play a song based on a given url or a keyword',
211 | 'zh-CN': '根据关键字或链接播放歌曲',
212 | 'zh-TW': '根據關鍵字或鏈接播放歌曲',
213 | })
214 | .addStringOption((option) => option
215 | .setName('song')
216 | .setDescription('YouTube/SoundCloud/Spotify鏈接/在YouTube上搜索的關鍵詞')
217 | .setDescriptionLocalizations({
218 | 'en-US': 'YouTube/SoundCloud/Spotify Link / keyword to search on YouTube',
219 | 'zh-CN': 'YouTube/SoundCloud/Spotify链接/在YouTube上搜索的关键词',
220 | 'zh-TW': 'YouTube/SoundCloud/Spotify鏈接/在YouTube上搜索的關鍵詞',
221 | }),
222 | ),
223 | async execute(interaction, language) {
224 | await interaction.deferReply();
225 | await play(interaction, [interaction.options.getString('song')], language);
226 | },
227 | };
228 |
--------------------------------------------------------------------------------
/functions/musicFunctions.ts:
--------------------------------------------------------------------------------
1 | import ytdl from 'ytdl-core';
2 | import { PassThrough } from 'stream';
3 | import ffmpegPath from '@ffmpeg-installer/ffmpeg';
4 | import Ffmpeg from 'fluent-ffmpeg';
5 |
6 | import { reply, edit, error } from './Util.js';
7 |
8 | Ffmpeg.setFfmpegPath(ffmpegPath.path);
9 | import { Utils, Embed, Guild, Message } from 'discord.js';
10 | import scdl from 'soundcloud-downloader';
11 | import * as queueData from '../data/queueData';
12 | import { AudioPlayerStatus, StreamType, createAudioPlayer, createAudioResource, joinVoiceChannel } from '@discordjs/voice';
13 |
14 | const { queue } = queueData;
15 |
16 | async function play(guild: Guild, song, message: Message) {
17 | const serverQueue = queue.get(message.guild.id);
18 | const stream = new PassThrough({
19 | highWaterMark: 50,
20 | });
21 | let proc: Ffmpeg.FfmpegCommand;
22 | if (!song) {
23 | serverQueue.player.stop(true);
24 | serverQueue.connection.destroy();
25 | queue.delete(guild.id);
26 | return;
27 | }
28 | switch (song.source) {
29 | case 'yt':
30 | proc = Ffmpeg(ytdl(song.url, { quality: 'highestaudio', filter: 'audioonly', highWaterMark: 1 << 25 }));
31 | break;
32 | case 'sc':
33 | proc = Ffmpeg(await scdl.download(song.url));
34 | break;
35 | default:
36 | proc = Ffmpeg(ytdl(song.url, { quality: 'highestaudio', filter: 'audioonly', highWaterMark: 1 << 25 }));
37 | break;
38 | }
39 |
40 | proc.addOptions(['-ac', '2', '-f', 'opus', '-ar', '48000']);
41 | // proc.withAudioFilter("bass=g=5");
42 | proc.on('error', (err) => {
43 | if (err.message === 'Output stream closed') {
44 | return;
45 | }
46 | console.log(`an error happened: ${err.message}`);
47 | console.log(err);
48 | });
49 | proc.writeToStream(stream, { end: true });
50 | const resource = createAudioResource(stream, {
51 | inputType: StreamType.Arbitrary,
52 | inlineVolume: true,
53 | });
54 | const player = createAudioPlayer();
55 | player.play(resource);
56 | serverQueue.player = player;
57 | serverQueue.connection.subscribe(player);
58 | player
59 | .on(AudioPlayerStatus.Idle, () => {
60 | const playedSong = serverQueue.songs.shift();
61 | if (!serverQueue.loop) serverQueue.songHistory.push(playedSong);
62 | if (serverQueue.loopQueue) serverQueue.songs.push(playedSong);
63 | if (serverQueue.loop) serverQueue.songs.unshift(playedSong);
64 | stream.destroy();
65 | play(guild, serverQueue.songs[0], message);
66 | })
67 | .on('error', (err => {
68 | message.channel.send('An error happened!');
69 | console.log(err);
70 | }));
71 | resource.volume.setVolume(serverQueue.volume / 5);
72 | const embed = new MessageEmbed()
73 | .setThumbnail(song.thumb)
74 | .setAuthor({ name: '開始撥放', iconURL: message.member.user.displayAvatarURL() })
75 | .setColor('BLUE')
76 | .setTitle(song.title)
77 | .setURL(song.url)
78 | .setTimestamp(Date.now())
79 | .addField('播放者', `<@!${serverQueue.songs[0].requester}>`)
80 | .setFooter({ text: '音樂系統', iconURL: message.client.user.displayAvatarURL() });
81 | if (serverQueue.playMessage) {
82 | await serverQueue.playMessage.delete();
83 | }
84 | serverQueue.playMessage = await serverQueue.textChannel.send({ embeds: [embed] });
85 | }
86 | function hmsToSecondsOnly(str) {
87 | const p = str.split(':');
88 | let s = 0; let m = 1;
89 |
90 | while (p.length > 0) {
91 | s += m * parseInt(p.pop(), 10);
92 | m *= 60;
93 | }
94 |
95 | return s;
96 | }
97 | module.exports = {
98 | async waitImport(name, length, message) {
99 | // eslint-disable-next-line no-async-promise-executor
100 | return new Promise(async (resolve, reject) => {
101 | let embed = new MessageEmbed()
102 | .setAuthor({ name: '清單', iconURL: message.member.user.displayAvatarURL() })
103 | .setColor('BLUE')
104 | .setTitle('您要加入這個清單嗎')
105 | .setDescription(`清單: ${name}\n長度:${length}`)
106 | .setTimestamp(Date.now())
107 | .setFooter({ text: '音樂系統', iconURL: message.client.user.displayAvatarURL() });
108 | const m = await reply(message, { embeds: [embed] });
109 | await m.react('📥');
110 | await m.react('❌');
111 | const filter = (reaction, user) => ['📥', '❌'].includes(reaction.emoji.name) && user.id === message.member.user.id;
112 | const collected = await m.awaitReactions({
113 | filter,
114 | maxEmojis: 1,
115 | time: 10000,
116 | });
117 | switch (collected.first()?.emoji?.name) {
118 | case undefined:
119 | return;
120 | case '📥':
121 | embed = new MessageEmbed()
122 | .setAuthor({ name: '清單', iconURL: message.member.user.displayAvatarURL() })
123 | .setColor('BLUE')
124 | .setTitle('您加入了清單')
125 | .setDescription(`清單: ${name}`)
126 | .setTimestamp(Date.now())
127 | .setFooter({ text: '音樂系統', iconURL: message.client.user.displayAvatarURL() });
128 | await edit(m, embed);
129 | return resolve(true);
130 | case '❌':
131 | embed = new MessageEmbed()
132 | .setAuthor({ name: '清單', iconURL: message.member.user.displayAvatarURL() })
133 | .setColor('BLUE')
134 | .setTitle('您取消了加入清單')
135 | .setDescription(`清單: ${name}`)
136 | .setTimestamp(Date.now())
137 | .setFooter({ text: '音樂系統', iconURL: message.client.user.displayAvatarURL() });
138 | await edit(m, embed);
139 | return reject(false);
140 | }
141 | });
142 | },
143 | async handleVideo(videos, voiceChannel, playlist = false, serverQueue, source, message) {
144 | let song;
145 | switch (source) {
146 | case 'ytlist':
147 | song = {
148 | id: videos.id,
149 | title: Util.escapeMarkdown(videos.title),
150 | url: `https://www.youtube.com/watch?v=${videos.id}`,
151 | requester: message.member.id,
152 | duration: hmsToSecondsOnly(videos.duration),
153 | thumb: videos.thumbnails[0].url,
154 | source: 'yt',
155 | };
156 | break;
157 | case 'yt':
158 | song = {
159 | id: videos[0].id,
160 | title: Util.escapeMarkdown(videos[0].title),
161 | url: videos[0].url,
162 | requester: message.member.id,
163 | duration: videos[0].duration / 1000,
164 | thumb: videos[0].thumbnail.url,
165 | source: 'yt',
166 | };
167 | break;
168 | case 'sc':
169 | song = {
170 | id: videos.id,
171 | title: Util.escapeMarkdown(videos.title),
172 | url: videos.permalink_url,
173 | requester: message.member.id,
174 | duration: videos.duration / 1000,
175 | thumb: videos.artwork_url,
176 | source: 'sc',
177 | };
178 | break;
179 | default:
180 | break;
181 | }
182 | const embed = new MessageEmbed()
183 | .setThumbnail(song.thumb)
184 | .setAuthor({ name: '已加入播放佇列', iconURL: message.member.user.displayAvatarURL() })
185 | .setColor('BLUE')
186 | .setTitle(song.title)
187 | .setURL(song.url)
188 | .setTimestamp(Date.now())
189 | .addField('播放者', `<@!${song.requester}>`)
190 | .setFooter({ text:'音樂系統', iconURL: message.client.user.displayAvatarURL() });
191 |
192 | if (!serverQueue.songs[0]) {
193 | try {
194 | serverQueue.songs.push(song);
195 | serverQueue.connection = await joinVoiceChannel({
196 | channelId: voiceChannel.id,
197 | guildId: voiceChannel.guildId,
198 | adapterCreator: voiceChannel.guild.voiceAdapterCreator,
199 | });
200 | await reply(message, { embeds: [embed] });
201 | await play(message.guild, serverQueue.songs[0], message);
202 | } catch (err) {
203 | console.error(err);
204 | serverQueue.songs.length = 0;
205 | return message.channel.send(`I could not join the voice channel: ${err}`);
206 | }
207 | return;
208 | }
209 | serverQueue.songs.push(song);
210 | if (playlist) return;
211 | return reply(message, { embeds: [embed] });
212 | },
213 | async play(guild, song, message) {
214 | await play(guild, song, message);
215 | },
216 | format(duration) {
217 | // Hours, minutes and seconds
218 | const hrs = ~~(duration / 3600);
219 | const mins = ~~((duration % 3600) / 60);
220 | const secs = ~~duration % 60;
221 |
222 | // Output like "1:01" or "4:03:59" or "123:03:59"
223 | let ret = '';
224 |
225 | if (hrs > 0) {
226 | ret += `${hrs}:${mins < 10 ? '0' : ''}`;
227 | }
228 |
229 | ret += `${mins}:${secs < 10 ? '0' : ''}`;
230 | ret += `${secs}`;
231 | return ret;
232 | },
233 | async checkStats(command, checkPlaying = false) {
234 | const language = command.client.guildCollection.get(command.guild.id).data.language;
235 | const translate = {
236 | 'notInVC': {
237 | 'en-US': 'You have to join a voice channel before using this command!',
238 | 'zh-CN': '加入语音频道后才能使用此指令!',
239 | 'zh-TW': '加入语音频道后才能使用此指令!',
240 | },
241 | 'noSong': {
242 | 'en-US': 'There is no song in the queue!',
243 | 'zh-CN': '播放清单里没有歌曲!',
244 | 'zh-TW': '播放清單裏沒有歌曲!',
245 | },
246 | 'notPlayingMusic': {
247 | 'en-US': 'I\'m not playing any songs right now!',
248 | 'zh-CN': '目前我没有播放任何歌曲!',
249 | 'zh-TW': '目前我沒有播放任何歌曲!',
250 | },
251 | };
252 | const serverQueue = queue.get(command.guild.id);
253 |
254 | if (!command.member.voice.channel) {
255 | await error(command, translate[language].notInVC);
256 | return 'error';
257 | }
258 | if (!serverQueue) {
259 | await error(command, translate[language].noSong);
260 | return 'error';
261 | }
262 | if (checkPlaying && !serverQueue.playing) {
263 | await error(command, translate[language].notPlayingMusic);
264 | return 'error';
265 | }
266 | return serverQueue;
267 | },
268 | };
269 |
--------------------------------------------------------------------------------
/language/en-US.js:
--------------------------------------------------------------------------------
1 | /* eslint quotes: 0 */
2 | module.exports = {
3 | "backup": {
4 | "invalidBackupID":"You must specify a valid backup ID!",
5 | "backupInformation":"Backup Information",
6 | "backupID":"Backup ID",
7 | "serverID":"Server ID",
8 | "backupSize":"Size",
9 | "backupCreatedAt":"Created at",
10 | "noBackupFound":"No backup found for \\`${backupID}\\` !",
11 | "notAdmin":"You must be an administrator of this server to request a backup!",
12 | "startBackup":"Start creating backup...\nMax Messages per Channel: ${max}\nSave Images: base64",
13 | "doneBackupDM":"✅ | The backup has been created! To load it, type this command on the server of your choice: \\`${prefix}load ${backupData.id}\\`!",
14 | "doneBackupGuild":"Backup successfully created. The backup ID was sent in dm!",
15 | "warningBackup": "When the backup is loaded, all the channels, roles, etc. will be replaced! Type `-confirm` to confirm!",
16 | "timesUpBackup": "Time's up! Cancelled backup loading!",
17 | "startLoadingBackup": "✅ | Start loading the backup!",
18 | "backupError": "🆘 | Sorry, an error occurred... Please check that I have administrator permissions!",
19 | "doneLoading": "✅ | Backup loaded successfully!",
20 | "outOfRange": "Max messages per channel cannot exceed 1000 or lower than 0!",
21 | },
22 | "8ball": {
23 | "noQuestion": "Psst. You need to ask the 8ball a question, you know?",
24 | "reply1": "It is certain",
25 | "reply2": "It is decidedly so",
26 | "reply3": "Without a doubt",
27 | "reply4": "Yes, definitely",
28 | "reply5": "You may rely on it",
29 | "reply6": "As I see it, yes",
30 | "reply7": "Most likely",
31 | "reply8": "Outlook good",
32 | "reply9": "Yes",
33 | "reply10": "Signs point to yes",
34 | "reply11": "Reply hazy try again",
35 | "reply12": "Ask again later",
36 | "reply13": "Better not tell you now",
37 | "reply14": "Cannot predict now",
38 | "reply15": "Concentrate and ask again",
39 | "reply16": "Don't count on it",
40 | "reply17": "My reply is no",
41 | "reply18": "My sources say no",
42 | "reply19": "Outlook not so good",
43 | "reply20": "Very doubtful",
44 | "reply": "Our 🎱 replied with:",
45 | },
46 | "connect4": {
47 | "board": "It's now ${round.name}'s turn!\n${boardStr}",
48 | "invalidMove": "You can’t place the piece here!",
49 | "win": "${round.name} had won the game!",
50 | },
51 | "loli": {
52 | "noToken": "This command can't be used without pixiv refreshToken!",
53 | },
54 | "pixiv": {
55 | "noToken": "This command can't be used without pixiv refreshToken!",
56 | "noIllust": "Illust doesn't exist!",
57 | "noUser": "User doesn't exist!",
58 | "noResult": "No result found!",
59 | "unknownSubcommand": "Invalid subcommand used!",
60 | },
61 | "updateIllust": {
62 | "noToken": "This command can't be used without pixiv refreshToken!",
63 | },
64 | "yt-together": {
65 | "notInVC": "You have to join a voice channel before using this command!",
66 | },
67 | "anime": {
68 | "similarity": "Similarity: ${similarity}%",
69 | "sourceURL": "**Source URL**",
70 | "nativeTitle": "Native Title",
71 | "romajiTitle": "Romaji Title",
72 | "englishTitle": "English Title",
73 | "episode": "Episode",
74 | "NSFW": "NSFW",
75 | "invalidURL": "Please enter a valid http url!",
76 | "noImage": "You have to upload an image before using this command!",
77 | },
78 | "avatar": {
79 | "yourAvatar": "__Your avatar__",
80 | "userAvatar": "__${user.username}'s avatar__",
81 | "memberAvatar": "__${user.displayName}'s avatar__",
82 | "noMember": "Can't find a member matching \\`${keyword}\\`!",
83 | },
84 | "google": {
85 | },
86 | "help": {
87 | "helpTitle": "List of commands",
88 | "helpPrompt": "Here's a list of all my commands:",
89 | "helpPrompt2": "\nYou can send \\`${prefix}help [command name]\\` to get info on a specific command!",
90 | "helpSend": "I've sent you a DM with all my commands!",
91 | "invalidCmd": "That's not a valid command!",
92 | "cmdName": "**Name:**",
93 | "cmdAliases": "**Aliases:**",
94 | "cmdDescription": "**Description:**",
95 | "cmdUsage": "**Usage:**",
96 | "cmdCoolDown": "**Cool down:**",
97 | },
98 | "invite": {
99 | "invite": "Invite me!",
100 | },
101 | "run": {
102 | "invalidUsage": "Invalid usage! Invalid language/code.",
103 | "wait": "Please wait...",
104 | "usage": "Usage: c!run [code](with or without code block)",
105 | "notSupported": "Language not supported!",
106 | "outputTooLong": "Output was too long (more than 2000 characters or 40 lines) so I put it here: ${link}",
107 | "postError": "Your output was too long, but I couldn't make an online bin out of it",
108 | "noOutput": "No output!",
109 | },
110 | "sauce": {
111 | "similarity": "Similarity: ${similarity}%",
112 | "sourceURL": "**Source URL**",
113 | "searchingSauce": "Searching for image...",
114 | "additionalInfo": "Additional info",
115 | "noAuthor": "No info found!",
116 | "sauceAuthor": "Name: ${authorInfo.name}\nLink: ${authorInfo.url}",
117 | "title":"Title",
118 | "author":"Author",
119 | },
120 | "server": {
121 | "serverInfo": "Server Info",
122 | "serverName": "Server name",
123 | "serverOwner": "Server owner",
124 | "memberCount": "Member count",
125 | "serverRegion": "Server region",
126 | "highestRole": "Highest role",
127 | "serverCreatedAt": "Server created at",
128 | "channelCount": "Channel count",
129 | },
130 | "user-info": {
131 | "customStatus": "__Custom Status__\n<:${name}:${id}> ${state}\n",
132 | "gameStatus": "__${type}__\n${name}\n${details}",
133 | "notPlaying": "User is not playing.",
134 | "uiTitle": "User Info",
135 | "tag": "Tag",
136 | "nickname": "Nickname",
137 | "id": "ID",
138 | "avatarURL": "Avatar URL",
139 | "avatarValue": "[Click here](${url})",
140 | "createdAt": "Created At",
141 | "joinedAt": "Joined At",
142 | "activity": "Activity",
143 | "none": "None",
144 | "status": "Status",
145 | "device": "Device",
146 | "roles": "Roles(${author.roles.cache.size})",
147 | },
148 | "ban": {
149 | "noMention": "You need to mention a user in order to ban them!",
150 | "cantBanSelf": "You Cannot Ban Yourself!",
151 | "cannotBan": "Cannot Ban This User!",
152 | "banSuccess": "Successfully Banned: ${taggedUser.user.username}!",
153 | },
154 | "kick": {
155 | "noMention": "You need to mention a user in order to kick them!",
156 | "cantKickSelf": "You Cannot Kick Yourself!",
157 | "cannotKick": "Cannot Kick This User!",
158 | "kickSuccess": "Successfully Kicked: ${taggedUser.user.username}!",
159 | },
160 | "prune": {
161 | "invalidNum": "That doesn't seem to be a valid number.",
162 | "notInRange": "You need to input a number between 1 and 99.",
163 | "pruneError": "There was an error trying to prune messages in this channel!",
164 | },
165 | "clear": {
166 | "cleared": "Song queue cleared!",
167 | },
168 | "loop": {
169 | "on": "Loop mode on!",
170 | "off": "Loop mode off!",
171 | },
172 | "loop-queue": {
173 | "on": "Loop queue mode on!",
174 | "off": "Loop queue mode off!",
175 | },
176 | "lyric": {
177 | "searching": ":mag: | Searching lyrics for ${keyword}...",
178 | "noLyricsFound": "No lyrics found for `${keyword}`!",
179 | "title": "Lyric for `${keyword}`",
180 | "noKeyword": "No keyword given!",
181 | "pause": "Paused!",
182 | },
183 | "now-playing": {
184 | "npTitle": "**Now playing ♪**",
185 | "requester": "Requested by:",
186 | "musicFooter": "Music system",
187 | "pause": "Paused!",
188 | },
189 | "pause": {
190 | "pause": "Paused!",
191 | },
192 | "play": {
193 | "notInVC": "You have to join a voice channel before using this command!",
194 | "cantJoinVC": "I need the permissions to join and speak in your voice channel!",
195 | "importAlbum1": "✅ | Album: **${title}** importing",
196 | "importAlbum2": "✅ | Album: **${videos[0].title}** importing **${i}**",
197 | "importAlbumDone": "✅ | Album: **${title}** has been added to the queue!",
198 | "importPlaylist1": "✅ | Playlist: **${title}** importing",
199 | "importPlaylist2": "✅ | PlayList: **${videos[0].title}** importing **${i}**",
200 | "importPlaylistDone": "✅ | Playlist: **${title}** has been added to the queue!",
201 | "noResult": "🆘 | I could not obtain any search results.",
202 | "noArgs": "Please provide an argument!",
203 | "searching":"🔍 | Searching ${keyword}...",
204 | "choose":"Choose a song",
205 | "timeout":"Timeout",
206 | },
207 | "queue": {
208 | "queueTitle": "Song Queue",
209 | "queueBody": "**Now playing**\n[${serverQueue.songs[0].title}](${serverQueue.songs[0].url})\n\n**Queued Songs**\n${printQueue}\n${serverQueue.songs.length - 1} songs in queue",
210 | },
211 | "related": {
212 | "relatedSearch": "🔍 | Searching for related tracks...",
213 | "noResult": "I could not obtain any search results.",
214 | },
215 | "remove": {
216 | "removed": "Removed ${serverQueue.songs[queuenum].title}!",
217 | "invalidInt": "You have to enter a valid integer!",
218 | },
219 | "resume": {
220 | "playing": "I'm already playing!",
221 | "resume": "Resumed!",
222 | },
223 | "skip": {
224 | "skipped": "Skipped the song.",
225 | },
226 | "stop": {
227 | "stopped": "Stopped playing songs.",
228 | },
229 | "set": {
230 | "languageNotSupported": "Language not supported!",
231 | "changeSuccess": "Language changed successfully to `${args[0]}`!",
232 | "argsNotChannel": "Argument not a channel!",
233 | "argsNotRole": "Argument not a role!",
234 | "argsNotNumber": "Argument not a number!",
235 | "noRole": "No role provided!",
236 | "logChannelChanged": "Changed my log channel to ${args[0]}!",
237 | "starboardChanged": "Changed starboard channel to ${args[0]}!",
238 | "levelRewardAdded": "Added level reward: ${args[0]} => ${args[1]}!",
239 | "levelRewardRemoved": "Removed level reward: ${args[0]} => ${args[1]}!",
240 | },
241 | "cemoji": {
242 | "noEmoji": "Please specify an emoji to add!",
243 | "addSuccess": "The emoji \\`${emoji.name}\\` ${emoji} was successfully added to the server!",
244 | },
245 | "edit-snipe": {
246 | "exceed10": "You can't snipe beyond 10!",
247 | "invalidSnipe": "Not a valid snipe!",
248 | "noSnipe":"There's nothing to snipe!",
249 | },
250 | "snipe": {
251 | "exceed10": "You can't snipe beyond 10!",
252 | "invalidSnipe": "Not a valid snipe!",
253 | "noSnipe":"There's nothing to snipe!",
254 | },
255 | "ping": {
256 | "pinging": "Pinging...",
257 | "heartbeat": "Websocket heartbeat:",
258 | "latency": "Round trip latency:",
259 | },
260 | };
--------------------------------------------------------------------------------
/functions/Util.ts:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | Client,
4 | Collection,
5 | ColorResolvable,
6 | CommandInteraction, GuildMember,
7 | InteractionReplyOptions,
8 | Message,
9 | MessageEmbed,
10 | MessageReaction, Snowflake, TextChannel
11 | } from "discord.js";
12 |
13 | import Pixiv, {PixivIllust} from "pixiv.ts";
14 | import * as fs from "fs";
15 | import { Collection as DB } from "mongodb";
16 |
17 | const refreshToken = process.env.PIXIV_REFRESH_TOKEN || require('../config/config.json').PixivRefreshToken;
18 |
19 |
20 | interface SlashCommand {
21 | execute(interaction, language): Promise;
22 | autoComplete?(interaction): Promise,
23 | }
24 |
25 | interface Description {
26 | 'en-US': string,
27 | 'zh-CN': string,
28 | 'zh-TW': string,
29 | }
30 |
31 | interface BaseCommand {
32 | name: string,
33 | description: Description,
34 | }
35 |
36 | interface SubcommandOptions extends BaseCommand {
37 | type: 'STRING' | 'INTEGER' | 'BOOLEAN' | 'NUMBER' | 'USER' | 'CHANNEL' | 'ROLE' | 'MENTIONABLE',
38 | required?: boolean,
39 | choices?: [name: string, value: string][],
40 | min?: number,
41 | max?: number,
42 | autocomplete?: boolean
43 | }
44 |
45 | interface Subcommand extends BaseCommand {
46 | options?: SubcommandOptions[],
47 | }
48 |
49 | interface SubcommandGroup extends BaseCommand {
50 | subcommands: Subcommand[],
51 | }
52 |
53 | interface Translation {
54 | [message: string]: [translation: string],
55 | }
56 |
57 | interface Command extends BaseCommand {
58 | coolDown: number,
59 | slashCommand: SlashCommand,
60 | subcommandGroups?: SubcommandGroup[],
61 | subcommands?: Subcommand[],
62 | options?: SubcommandOptions[],
63 | execute(message: Message, args: string[], language: Translation),
64 | }
65 |
66 | interface Snipe {
67 | author: string,
68 | authorAvatar: string,
69 | content: string,
70 | timeStamp: Date,
71 | attachment?: string,
72 | }
73 |
74 | interface CustomClient extends Client {
75 | commands: Collection,
76 | language: { [commandName: string]: Translation },
77 | coolDowns: Collection>
78 | guildDatabase: DB,
79 | guildCollection: Collection
94 | userDatabase:DB,
95 | }
96 | type Language = 'en-US' | 'zh-CN' | 'zh-TW';
97 |
98 | // reply with embeds
99 |
100 | // check if something is string
101 | function isString(x: any): x is string {
102 | return typeof x === "string";
103 | }
104 |
105 | // reply to a user command
106 | export async function reply(command: CommandInteraction, response: string | InteractionReplyOptions, color?: ColorResolvable) {
107 | if (isString(response)) return command.reply({ embeds: [{ description: response, color: color }] });
108 | if (command.deferred) return await command.editReply(response);
109 | command.reply(response);
110 | return await command.fetchReply();
111 | }
112 |
113 | // edit a message or interaction
114 | export async function edit(command: CommandInteraction, response?: string | InteractionReplyOptions | MessageEmbed, color?: ColorResolvable) {
115 | if (isString(response)) return await command.editReply({ embeds: [{ description: response, color: color }], components: [], content: '\u200b' });
116 | if (response instanceof MessageEmbed) return await command.editReply({ embeds: [response], components: [], content: '\u200b' });
117 | return await command.editReply(response);
118 | }
119 |
120 | export async function error(command: CommandInteraction, response: InteractionReplyOptions) {
121 | const message = await reply(command, `❌ | ${response}`, 'Red');
122 | if (message instanceof Message) {
123 | setTimeout(() => message.delete(), 10000);
124 | }
125 | }
126 |
127 | export async function warn(command: CommandInteraction, response: InteractionReplyOptions) {
128 | return await reply(command, `⚠ | ${response}`, 'Yellow');
129 | }
130 |
131 | export async function success(command: CommandInteraction, response: InteractionReplyOptions) {
132 | return await reply(command, `✅ | ${response}`, 'Green');
133 | }
134 |
135 | export async function info(command: CommandInteraction, response: InteractionReplyOptions) {
136 | return await reply(command, `ℹ | ${response}`, 'Blue')
137 | }
138 |
139 | export async function updateIllust(query: string) {
140 | const pixiv = await Pixiv.refreshLogin(refreshToken);
141 | let illusts = await pixiv.search.illusts({ word: query, type: 'illust', bookmarks: '1000', search_target: 'partial_match_for_tags' });
142 | if (pixiv.search.nextURL) illusts = await pixiv.util.multiCall({ next_url: pixiv.search.nextURL, illusts });
143 |
144 | // filter out all r18 illusts
145 | let clean_illusts = illusts.filter((illust) => {
146 | return illust.x_restrict === 0 && illust.total_bookmarks >= 1000;
147 | });
148 | fs.writeFileSync('./data/illusts.json', JSON.stringify(clean_illusts));
149 | return;
150 | }
151 |
152 | function processIllustURL(illust: PixivIllust): string[] {
153 | const targetURL = [];
154 | if (illust.meta_pages.length === 0) {
155 | targetURL.push(illust.image_urls.medium.replace('pximg.net', 'pixiv.cat'));
156 | }
157 | if (illust.meta_pages.length > 5) {
158 | targetURL.push(illust.meta_pages[0].image_urls.medium.replace('pximg.net', 'pixiv.cat'));
159 | } else {
160 | for (let i = 0; i < illust.meta_pages.length; i++) {
161 | targetURL.push(illust.meta_pages[i].image_urls.medium.replace('pximg.net', 'pixiv.cat'));
162 | }
163 | }
164 | return targetURL;
165 | }
166 |
167 | export function generateIllustEmbed(illust: PixivIllust): MessageEmbed[] {
168 | const multipleIllusts = [];
169 |
170 | const targetURL = processIllustURL(illust);
171 |
172 | targetURL.forEach((URL) => {
173 | const imageEmbed = new MessageEmbed()
174 | .setURL('https://www.pixiv.net')
175 | .setImage(URL)
176 | .setColor('RANDOM');
177 | multipleIllusts.push(imageEmbed);
178 | });
179 |
180 | const descriptionEmbed = new MessageEmbed()
181 | .setTitle(illust.title)
182 | .setURL(`https://www.pixiv.net/en/artworks/${illust.id}`)
183 | .setColor('RANDOM')
184 | // remove html tags
185 | .setDescription(illust?.caption
186 | .replace(/\n/ig, '')
187 | .replace(/