├── 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 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 12 | 13 | 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 | banner 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 | [![CodeFactor](https://www.codefactor.io/repository/github/chinhongtan/chinokafuu/badge/main)](https://www.codefactor.io/repository/github/chinhongtan/chinokafuu/overview/main) 16 | [![codebeat badge](https://codebeat.co/badges/756b4af6-5758-4bdd-b34b-c312e8f6cf7a)](https://codebeat.co/projects/github-com-chinhongtan-chinokafuu-main) 17 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/3db0f95584064f65acafc9b751c1d042)](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 = '
'; 16 | const delim2 = '
'; 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(/]*>[\s\S]*?<\/style[^>]*>/ig, '') 188 | .replace(/]*>[\s\S]*?<\/head[^>]*>/ig, '') 189 | .replace(/]*>[\s\S]*?<\/script[^>]*>/ig, '') 190 | .replace(/<\/\s*(?:p|div)>/ig, '\n') 191 | .replace(/]*\/?>/ig, '\n') 192 | .replace(/<[^>]*>/ig, '') 193 | .replace(' ', ' ') 194 | .replace(/[^\S\r\n][^\S\r\n]+/ig, ' ')); 195 | multipleIllusts.push(descriptionEmbed); 196 | return multipleIllusts; 197 | } 198 | 199 | export async function sendSuggestedIllust(channel: TextChannel) { 200 | const pixiv = await Pixiv.refreshLogin(refreshToken); 201 | let following = await pixiv.user.following({ user_id: 43790997 }); 202 | let authors = following.user_previews 203 | if (pixiv.user.nextURL) authors = await pixiv.util.multiCall( { next_url: pixiv.user.nextURL, user_previews: authors }); 204 | const randomAuthor = authors[Math.floor(Math.random() * authors.length)]; 205 | let illusts = randomAuthor.illusts; 206 | if (pixiv.illust.nextURL) illusts = await pixiv.util.multiCall({ next_url: pixiv.search.nextURL, illusts }); 207 | let clean_illusts = illusts.filter((illust) => { 208 | return illust.x_restrict === 0 && illust.total_bookmarks >= 1000; 209 | }); 210 | const randomIllust = clean_illusts[Math.floor(Math.random() * illusts.length)]; 211 | const illustEmbed = generateIllustEmbed(randomIllust); 212 | return channel.send({ embeds: illustEmbed }) 213 | } 214 | 215 | export async function extension(reaction: MessageReaction, attachment: string) { 216 | const imageLink = attachment.split('.'); 217 | const typeOfImage = imageLink[imageLink.length - 1]; 218 | const image = /(jpg|jpeg|png|gif)/gi.test(typeOfImage); 219 | if (!image) return ''; 220 | return attachment; 221 | } 222 | 223 | export function getEditDistance(a: string, b: string) { 224 | if (a.length === 0) return b.length; 225 | if (b.length === 0) return a.length; 226 | 227 | const matrix = []; 228 | 229 | // increment along the first column of each row 230 | let i; 231 | for (i = 0; i <= b.length; i++) { 232 | matrix[i] = [i]; 233 | } 234 | 235 | // increment each column in the first row 236 | let j; 237 | for (j = 0; j <= a.length; j++) { 238 | matrix[0][j] = j; 239 | } 240 | 241 | // Fill in the rest of the matrix 242 | for (i = 1; i <= b.length; i++) { 243 | for (j = 1; j <= a.length; j++) { 244 | if (b.charAt(i - 1) === a.charAt(j - 1)) { 245 | matrix[i][j] = matrix[i - 1][j - 1]; 246 | } else { 247 | matrix[i][j] = Math.min( 248 | // substitution 249 | matrix[i - 1][j - 1] + 1, 250 | Math.min( 251 | matrix[i][j - 1] + 1, 252 | // insertion 253 | matrix[i - 1][j] + 1, 254 | ), 255 | // deletion 256 | ); 257 | } 258 | } 259 | } 260 | 261 | return matrix[b.length][a.length]; 262 | } 263 | 264 | // get guild data from database or local json file, generate one if none found 265 | export async function getGuildData(client: CustomClient, guildId: Snowflake) { 266 | let rawData; 267 | const collection = client.guildDatabase; 268 | const defaultData = { 269 | id: guildId, 270 | data: { 271 | language: 'zh-TW', 272 | snipes: [], 273 | editSnipes: [], 274 | users: [], 275 | }, 276 | }; 277 | if (collection) { 278 | rawData = await collection.findOne({ id: guildId }); 279 | } else { 280 | const buffer = fs.readFileSync(`./data/guildData.json`, 'utf-8'); 281 | const parsedJSON = JSON.parse(buffer); 282 | rawData = parsedJSON[guildId]; 283 | } 284 | return rawData ?? defaultData; 285 | } 286 | 287 | // save guild data to database, or local json file 288 | export async function saveGuildData(client: CustomClient, guildId: Snowflake) { 289 | const guildData = client.guildCollection.get(guildId); // collection cache 290 | const collection = client.guildDatabase; // database or json 291 | if (collection) { 292 | const query = { id: guildId }; 293 | const options = { upsert: true }; 294 | return collection.replaceOne(query, guildData, options); // save in mongodb 295 | } else { 296 | const rawData = fs.readFileSync(`./data/guildData.json`, 'utf-8'); 297 | const guildCollection = JSON.parse(rawData); 298 | guildCollection[guildId] = guildData; 299 | return fs.writeFileSync(`./data/guildData.json`, JSON.stringify(guildCollection)); // save in json 300 | } 301 | } 302 | 303 | export async function deleteGuildData(client: CustomClient, guildId: Snowflake) { 304 | const collection = client.guildDatabase; 305 | client.guildCollection.delete(guildId); 306 | if (collection) { 307 | const query = { id: guildId }; 308 | return collection.deleteOne(query) 309 | } else { 310 | const rawData = fs.readFileSync(`./data/guildData.json`, 'utf-8'); 311 | const guildCollection = JSON.parse(rawData); 312 | delete guildCollection[guildId]; 313 | return fs.writeFileSync(`./data/guildData.json`, JSON.stringify(guildCollection)); // save in json 314 | } 315 | } 316 | 317 | // get user data from database or local json file, generate one if none found 318 | export async function getUserData(client: CustomClient, member: GuildMember) { 319 | let rawData; 320 | const collection = client.guildDatabase; 321 | const defaultData = { 322 | id: member.id, 323 | name: member.user.tag, 324 | exp: 0, 325 | level: 1, 326 | }; 327 | if (collection) { 328 | rawData = await collection.findOne({ id: member.guild.id }); 329 | } else { 330 | const buffer = fs.readFileSync(`./data/guildData.json`, 'utf-8'); 331 | const parsedJSON = JSON.parse(buffer); 332 | rawData = parsedJSON[member.guild.id]; 333 | } 334 | const userList = rawData.data.users; 335 | const userData = userList?.find((user) => user.id === member.id); 336 | if (!userData) { 337 | client.guildCollection.get(member.guild.id).data.users.push(defaultData); // create a new profile 338 | } 339 | return userList?.find(user => user.id === member.id) ?? defaultData; 340 | } 341 | 342 | // add exp for a user 343 | export async function addUserExp(client: CustomClient, member: GuildMember) { 344 | const guildData = client.guildCollection.get(member.guild.id); 345 | const userData = guildData.data.users.find(user => user.id === member.id); // collection cache 346 | const levelRewards = guildData.data.levelReward; 347 | let exp = userData.exp; 348 | let level = userData.level; 349 | exp ++; 350 | if (userData.exp >= level * ((1 + level) / 2) + 4 ) { // level up 351 | level ++; 352 | if (levelRewards) { 353 | for (const [rewardLevel, reward] of Object.entries(levelRewards)) { 354 | if (level >= parseInt(rewardLevel) && !member.roles.cache.has(reward)) await member.roles.add(reward); 355 | } 356 | } 357 | exp = 0 358 | } 359 | userData.exp = exp; 360 | userData.level = level; 361 | userData['expAddTimestamp'] = Date.now(); 362 | guildData.data.users.sort((a, b) => { 363 | if (a.level === b.level) return (b.exp - a.exp); 364 | return (b.level - a.level); 365 | }) 366 | await saveGuildData(client, member.guild.id); 367 | } --------------------------------------------------------------------------------