diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a3d94a3..be6c62d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,6 +2,9 @@ name: CI on: [push] +permissions: + contents: read + jobs: CI: runs-on: node @@ -13,6 +16,6 @@ jobs: with: node-version: 'latest' - name: install dependencies - run: npm install + run: npm ci - name: run tests - run: npm test \ No newline at end of file + run: npm test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index fa657a3..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Lint Codebase - -on: [push] - -jobs: - eslint: - runs-on: node - - steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 'latest' - - - name: Install dependencies - run: npm install - - - name: Run ESLint - run: npm run lint \ No newline at end of file diff --git a/.github/workflows/njsscan.yml b/.github/workflows/njsscan.yml new file mode 100644 index 0000000..ff888b4 --- /dev/null +++ b/.github/workflows/njsscan.yml @@ -0,0 +1,42 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow integrates njsscan with GitHub's Code Scanning feature +# nodejsscan is a static security code scanner that finds insecure code patterns in your Node.js applications + +name: njsscan sarif + +on: + push: + branches: [ "master", "devbot" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master", "devbot" ] + schedule: + - cron: '43 12 * * 6' + +permissions: + contents: read + +jobs: + njsscan: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + runs-on: ubuntu-latest + name: njsscan code scanning + steps: + - name: Checkout the code + uses: actions/checkout@v3 + - name: nodejsscan scan + id: njsscan + uses: ajinabraham/njsscan-action@v7 + with: + args: '. --sarif --output results.sarif || true' + - name: Upload njsscan report + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index 71b3e53..1b33dd4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ You can also change the global prefix. You should enter you discord user id, so you can use the admin commands. Every time you want to change something in this file, you have to restart the bot. -If the config is ever changed, you need to either delete it and repeat the steps above or take a look at the [util/createInitialConfig.js](./util/createInitialConfig.js) file. +If the config is ever changed, you need to either delete it and repeat the steps above or take a look at the util/createInitialConfig.js file. ## Known issues diff --git a/commands/admin/setPresence.js b/commands/admin/setPresence.js index bd6fe5f..5dbfd51 100644 --- a/commands/admin/setPresence.js +++ b/commands/admin/setPresence.js @@ -1,3 +1,4 @@ +const savePresence = require("../../util/savePresence"); const setPresence = require("../../util/setPresence"); module.exports = { @@ -10,12 +11,20 @@ module.exports = { ,"Updates once a minute if custom variables are used." ,"" ,"Custom Variables:" - ,"${guilds},${prefix},${uptime}"], + ,"${guilds},${prefix},${uptime},{members}"], admin: true, - execute({message, client, args, globalPrefix}) { - const savePresence = require("../../util/savePresence"); + execute({message, client, args, prefix}) { + let forceUpdate = false; + if(args.length > 1 && args[0] === "force") { + forceUpdate = true; + args.shift(); + } + if(args.length < 1){ + message.channel.send(`You need at least two arguments for this command, see \`${prefix}help setpresence\``) + return; + } let presenceType = args[0].toLocaleUpperCase(); - let sendText = "Updated presence"; + let sendText = "Presence has been set."; switch (presenceType) { case "PLAY": @@ -46,8 +55,11 @@ module.exports = { const firstArg = args[0].length + 1; let temp = args.join(" "); let presenceText = temp.slice(firstArg, temp.length) - setPresence({presenceText: presenceText,presenceType: presenceType, client: client, globalPrefix: globalPrefix}); savePresence(presenceType, presenceText, client); + if(forceUpdate) + setPresence({presenceText, presenceType, client}) + else + sendText = `${sendText} It will update ` } message.channel.send(sendText); diff --git a/commands/admin/update.js b/commands/admin/update.js index 7de9db0..7c9a75f 100644 --- a/commands/admin/update.js +++ b/commands/admin/update.js @@ -1,4 +1,5 @@ const calculateReloaded = require("../../util/calculateReloaded"); +const executeCommand = require("../../util/executeCommand"); const reloadCommands = require("../../util/reloadCommands"); module.exports = { @@ -27,10 +28,12 @@ module.exports = { let commitCount = stdout.split(/\r\n|\r|\n/).length - 1 sendText = `${sendText}\n\nLatest commits (${commitCount}):\n${stdout}` if(sendText.length >= 2000){ - sendText.slice(1955) + sendText = sendText.slice(1955) sendText = `${sendText}\n... Message is too long to show everything` } message.channel.send(sendText) + const githash = executeCommand(`git`, ["rev-parse", "--short", "HEAD"]); + client.githash = githash.error ? "N/A" : githash.output; if (err) console.log(stderr) }) } diff --git a/commands/admin/whitelist.js b/commands/admin/whitelist.js new file mode 100644 index 0000000..327e3de --- /dev/null +++ b/commands/admin/whitelist.js @@ -0,0 +1,107 @@ +const sqlite3 = require('sqlite3').verbose(); + +module.exports = { + name: 'whitelist', + description: 'Whitelist a command in a specific server, or for a user.', + admin: true, + async execute({message, args, prefix, client}) { + if(args.length < 3){ + message.channel.send(`You need to supply the command and guild that you want to whitelist\n\`${prefix}whitelist \``); + return; + } + let option = args[0]; + let command = args[1]; + let guildOrUser = args[2]; + + if(option === "guild" || option === "server"){ + if(guildOrUser === "this") guildOrUser = message.guild.id; + const whitelistedCommands = client.whitelist.guild.get(guildOrUser); + if(whitelistedCommands && whitelistedCommands.includes(command)){ + message.channel.send("Command is already whitelisted in that server.") + return; + } + // TODO: First check if the bot has access to that guild before whitelisting. + const databasePath = 'data/database.db' + const db = new sqlite3.Database(databasePath) + let err = false; + await new Promise((resolve, reject)=>{ + db.run(`INSERT INTO guild_whitelist ( + guildId, + command, + dateAdded + ) VALUES (?, ?, ?)`, + [ + guildOrUser, + command, + new Date().getTime() + ], function (error) { + if (error) { + console.error(error); + err = true; + resolve(); + }else{ + resolve(); + } + }); + }) + db.close(); + if(!err){ + if(whitelistedCommands){ + whitelistedCommands.push(command); + client.whitelist.guild.set(guildOrUser, whitelistedCommands); + }else { + client.whitelist.guild.set(guildOrUser, command); + } + message.channel.send(`"${command}" has been whitelisted in that server.`) + + } else message.channel.send("Could not whitelist the server with that command. Check the logs.") + return; + }else if (option === "user"){ + const whitelistedCommands = client.whitelist.user.get(guildOrUser); + if(whitelistedCommands && whitelistedCommands.includes(command)){ + message.channel.send("Command is already whitelisted for that user.") + return; + } + const databasePath = 'data/database.db' + const db = new sqlite3.Database(databasePath) + let err = false; + await new Promise((resolve, reject)=>{ + db.run(`INSERT INTO user_whitelist ( + userId, + command, + dateAdded + ) VALUES (?, ?, ?)`, + [ + guildOrUser, + command, + new Date().getTime() + ], function (error) { + if (error) { + console.error(error); + err = true; + resolve(); + }else{ + resolve(); + } + }); + }) + db.close(); + if(!err){ + if(whitelistedCommands){ + whitelistedCommands.push(command); + client.whitelist.user.set(guildOrUser, whitelistedCommands); + }else { + client.whitelist.user.set(guildOrUser, command); + } + message.channel.send(`"${command}" has been whitelisted for ${guildOrUser}.`) + + } else message.channel.send("Could not whitelist the server with that command. Check the logs.") + return; + } + + + + // TODO: Add ability to remove server from whitelist. + } + +}; \ No newline at end of file diff --git a/commands/info/botinfo.js b/commands/info/botinfo.js index df700bf..b806459 100644 --- a/commands/info/botinfo.js +++ b/commands/info/botinfo.js @@ -1,6 +1,6 @@ const {EmbedBuilder} = require('discord.js'); const getCreationDate = require('../../util/getCreationDate'); -const getGuildCount = require('../../util/getGuildCount'); +const getGuildInfo = require('../../util/getGuildInfo'); @@ -8,15 +8,28 @@ module.exports = { name: 'botinfo', description: 'Shows information about the bot', execute({message, client, prefix}) { - let guildCount = getGuildCount(client) + let guildInfo = getGuildInfo(client) + let descriptionArr = [`Name: ${client.user.username}`, + `Prefix: ${prefix}`, + `Total Servers: ${guildInfo.guildCount}`, + `Total Members: ${guildInfo.totalMembers} (${guildInfo.uniqueMemberCount} unique)`, + `Total Commands: ${client.commands.size}`, + `Creation Date: ${getCreationDate(client)}`, + `Source: [Click Here](https://github.com/SileNce5k/discord_bot)`, + `Current Version: ${client.githash}` + ] + + let description = ""; + descriptionArr.forEach(desc => { + description += `${desc}\n`; + }) + const embed = new EmbedBuilder() .setColor(15780145) .setTitle("Information about bot") .setTimestamp() .setAuthor({name: client.user.username, iconURL: client.user.avatarURL({ format: 'png', dynamic: true, size: 2048 })}) - .addFields({ - name: "General info", value: `Name: ${client.user.username}\nPrefix: ${prefix}\nTotal Servers: ${guildCount}\nTotal Commands: ${client.commands.size}\nCreation Date: ${getCreationDate(client)}\nSource: [Click Here](https://github.com/SileNce5k/discord_bot)`, - },) + .setDescription(description) message.channel.send({embeds :[embed]}) diff --git a/commands/info/guilds.js b/commands/info/guilds.js index c4eb45e..480fdda 100644 --- a/commands/info/guilds.js +++ b/commands/info/guilds.js @@ -3,10 +3,10 @@ module.exports = { description: 'Returns guild names', admin: true, execute({message, client}) { - let guildNames = ""; - client.guilds.cache.each(guild => { - guildNames = guildNames + guild.name + "\n"; - }); + let guildNames = client.guilds.cache + .sort((a, b) => b.memberCount - a.memberCount) + .map(guild => `${guild.name} (${guild.memberCount} members)`) + .join("\n"); message.channel.send(guildNames) } }; \ No newline at end of file diff --git a/commands/info/help.js b/commands/info/help.js index e88df47..da05e34 100644 --- a/commands/info/help.js +++ b/commands/info/help.js @@ -1,3 +1,4 @@ +const fs = require('fs'); const {EmbedBuilder} = require('discord.js'); const getSubdirFiles = require('../../util/getSubdirFiles'); @@ -12,10 +13,10 @@ module.exports = { execute({ message, args, prefix, client }) { let commands = "" let commandFiles = getSubdirFiles('commands/') - let x = false; let fieldName = `Page [[page]]/${Math.round(client.commands.size / 10)}`; let iteration = 0; let num_in_args = false; + let added_commands = 0; let page = -1; @@ -30,7 +31,6 @@ module.exports = { fieldName = fieldName.replace("[[page]]", page); const max_commands = iteration + 10; - if (x) return; const embed = new EmbedBuilder() .setColor(15780145) @@ -51,12 +51,14 @@ module.exports = { if (!command.admin){ if(start < iteration){ }else{ + added_commands++ commands = commands + `${prefix}${command.name} | ${command.description}\n`; iteration++ } start++; } }else if(args[0] === command.name){ + fieldName = "Description"; commands = commands + `${prefix}${command.name}\n` embed.setTitle(command.name.charAt(0).toUpperCase() + command.name.slice(1)) if(command.moreHelp){ @@ -64,7 +66,6 @@ module.exports = { commands = commands + `${element}\n` }); } else { - fieldName = "Description"; commands = commands + `${command.description}`; } break; diff --git a/commands/info/mc.js b/commands/info/mc.js new file mode 100644 index 0000000..dac751b --- /dev/null +++ b/commands/info/mc.js @@ -0,0 +1,103 @@ +// Code is taken from https://github.com/stphnduvall/mcstatus/blob/master/src/index.ts +// and converted to pure js. + +const net = require('net') +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + name: 'mc', + description: 'get minecraft server information', + hidden: true, + needsWhitelist: true, + async execute({ message, args }) { + let host = ""; + let port = 25565; + if (args[0]) host = args[0]; + + if (host.includes(":")) { + port = host.replace(/.+(?:\:)/g, ""); + host = host.match(/.+(?:\:)/g, "")[0].replace(":", ""); + } + if(host === "") return message.channel.send("No host provided") + let info = await getMinecraftServerInfo(host, port); + if (info) { + const embed = new EmbedBuilder() + embed.setColor("#ee7939") + embed.setTimestamp() + embed.addFields( + { name: "ping", value: info.ping.toString(), inline: false }, + { name: "Player Count", value: info.playercount.toString(), inline: false }, + { name: "Max Players", value: info.maxPlayers.toString(), inline: false }, + { name: "MOTD", value: info.motd, inline: false }, + ) + + message.channel.send({ embeds: [embed] }); + } else { + message.channel.send("Something went wrong\nThe minecraft server is likely not reachable from the discord bot") + } + + } +}; + + + +async function getMinecraftServerInfo(host, port = 25565) { + let serverInfo = { + ping: undefined, + maxPlayers: undefined, + version: undefined, + playercount: undefined, + motd: undefined + } + + let startTime = new Date(); + let data; + let ping; + serverInfo = await new Promise((resolve) => { + const client = net.connect({ host, port }, () => { + ping = Math.round(new Date().getMilliseconds() - startTime.getMilliseconds()); + + let buff = Buffer.from([0xFE, 0x01]); + client.write(buff); + + }) + let error = false; + client.on('data', (d) => { + data = d.toString() + client.destroy(); + + }) + client.once('error', (error) => { + console.error(error) + error = true; + }) + client.once('connectionAttemptFailed', (ip) => { + console.error("in attempt failed") + error = true; + }) + + client.once('connectionAttemptTimeout', (ip) => { + console.error("in attempt timeout") + error = true; + }) + client.on('close', () => { + if (!error) { + let _serverInfo = data?.split('\x00\x00\x00'); + + if (!_serverInfo) { + console.log("Something went wrong.") + resolve(serverInfo) + } + serverInfo.version = _serverInfo[2].replace(/\u0000/g, '') + serverInfo.motd = _serverInfo[3].replace(/\u0000/g, '') + serverInfo.playercount = Number(_serverInfo[4].replace(/\u0000/g, '')) + serverInfo.maxPlayers = Number(_serverInfo[5].replace(/\u0000/g, '')) + serverInfo.ping = Number(ping) + resolve(serverInfo) + } else { + resolve(0) + } + }) + }) + return serverInfo +} diff --git a/commands/info/pfp.js b/commands/info/pfp.js index 68a4640..dd76507 100644 --- a/commands/info/pfp.js +++ b/commands/info/pfp.js @@ -16,9 +16,9 @@ module.exports = { let user = message.guild.members.cache.get(info); let guildPfp = user.avatarURL({format: 'png', dynamic: true, size: 4096}); let globalPfp = user.user.avatarURL({format: 'png', dynamic: true, size: 4096}); - let sendText = `Global pfp:\n${globalPfp}`; + let sendText = `${globalPfp}`; if(guildPfp != null){ - sendText = `${sendText}\nGuild pfp:\n${guildPfp}`; + sendText = `Global pfp${sendText}\nGuild pfp:\n${guildPfp}`; } message.channel.send(sendText) diff --git a/commands/info/serverinfo.js b/commands/info/serverinfo.js index 1526c32..1f329de 100644 --- a/commands/info/serverinfo.js +++ b/commands/info/serverinfo.js @@ -15,6 +15,7 @@ module.exports = { {name: "Server Name", value: message.guild.name, inline: false}, {name: "Created", value: convertDateToISOString(message.guild.createdAt), inline: false}, {name: "Members", value: message.guild.memberCount.toString(), inline: false}, + {name: "Channels", value: message.guild.channels.channelCountWithoutThreads.toString(), inline: false}, ]) diff --git a/commands/misc/custom.js b/commands/misc/custom.js index 71885df..f684b3b 100644 --- a/commands/misc/custom.js +++ b/commands/misc/custom.js @@ -17,10 +17,13 @@ module.exports = { "custom remove - Delete your custom commands.", "custom owner - check owner of custom command", "custom list - list all custom commands", - "custom variables - list all variables you can use", - "custom count - display the amount of custom commands" + "custom variables - list all variables you can use" ], - async execute({message, args, client, prefix, owners}) { + execute({message, args, client, prefix, owners}) { + const customPath = 'data/customCommands.json'; + if(!fs.existsSync(customPath)){ + fs.writeFileSync(customPath,"[]") + } let sendText; let isEmbed = false; if (args){ @@ -28,21 +31,19 @@ module.exports = { let customMessage = args.slice(2, args.length).join(" "); switch (args[0].toLowerCase()) { - case "add":{ + case "add": if(!customMessage) { message.channel.send("Message can't be empty"); return; } - sendText = await addCustomCommand(client, customName, customMessage, message.author.id); + sendText = addCustomCommand(customName, customMessage, message.author.id); break; - } case "remove": - case "delete":{ + case "delete": sendText = deleteCustomCommand(customName, message.author.id, owners); break; - } - case "owner":{ - let author = await getOwnerOfCustomCommand(customName); + case "owner": + let author = getOwnerOfCustomCommand(customName); let user; if(!author) sendText = `${customName} does not exist` @@ -54,11 +55,10 @@ module.exports = { } break; - } - case "list": { + case "list": const embed = new EmbedBuilder(); - sendText = getAllCustomCommands(client); - if(sendText !== ""){ + sendText = getAllCustomCommands(); + if(sendText != ""){ embed.setColor(15780145) embed.addFields( { name: "Custom commands", value: sendText }, @@ -67,16 +67,13 @@ module.exports = { isEmbed = true; }else sendText = "NO CUSTOM COMMANDS" break; - } - case "variables": { + case "variables": sendText = "The variables you can use are:\n\n\n\n\n\n\n" break; - } - case "edit": { + case "edit": sendText = editCustomCommand(customName, message.author.id, customMessage) break; - } - case "show": { + case "show": let json = fs.readFileSync(customPath, 'utf8'); let customCommands = JSON.parse(json) sendText = "Command not found." @@ -86,22 +83,12 @@ module.exports = { } }); break; - } - case "rename": { + case "rename": sendText = renameCustomCommand(customName, args[2], message.author.id); break; - } - case "count": { - const customPath = './data/customCommands.json'; - let json = fs.readFileSync(customPath, 'utf8'); - let customCommands = JSON.parse(json) - sendText = `There are ${customCommands.length} custom commands in total`; - break; - } - default: { + default: sendText = `Argument not recognized.\n"${prefix}help custom" to see all arguments you can use.` break; - } } } if(isEmbed) message.channel.send({embeds :[sendText]}) diff --git a/commands/misc/dl.js b/commands/misc/dl.js new file mode 100644 index 0000000..13d95df --- /dev/null +++ b/commands/misc/dl.js @@ -0,0 +1,71 @@ +const path = require('path'); +const fs = require('fs') + +const executeCommand = require('../../util/executeCommand'); +module.exports = { + name: 'dl', + description: 'Download a video', + moreHelp: [ + "Usage: dl " + ], + async execute({message, args}) { + const downloadsDir = path.resolve(process.cwd(), 'data', 'downloads', Date.now().toString()); + const cookieFilepath = path.resolve(process.cwd(), 'data', 'cookies.txt') + if(!fs.existsSync(cookieFilepath)) { + message.channel.send("Some dependencies are needed for the command to work properly. Please let the bot's owner know.") + return; + } + fs.mkdirSync(downloadsDir, {recursive: true}); + + let url; + + if(args.length > 0){ + if(args[0].charAt(0) === '<' && args[0].charAt(args[0].length - 1) === '>'){ + args[0] = args[0].slice(1, args[0].length - 1) + } + try { + url = new URL(args[0]); + url = url.href; + } catch (error) { + this.cleanUp(downloadsDir); + message.channel.send("Could not parse the provided argument as a URL."); + return; + } + } else { + this.cleanUp(downloadsDir); + return message.channel.send("You have to provide a URL in an argument.") + } + + const originalMessage = await message.channel.send("Downloading video...") + + if(executeCommand("yt-dlp", [url, "-P", downloadsDir, "--cookies", cookieFilepath]).error){ + originalMessage.edit("An error occurred when downloading the video."); + this.cleanUp(downloadsDir); + return; + } + + + let files = fs.readdirSync(downloadsDir); + if(files.length < 1) { + this.cleanUp(downloadsDir); + originalMessage.edit("Something went wrong when downloading the video.") + return; + } + const filename = files[0]; + + await originalMessage.edit({ + content: null, + files: [{ + attachment: path.resolve(downloadsDir, filename) + }]}) + + this.cleanUp(downloadsDir); + + + }, + + cleanUp(downloadsDir){ + fs.rmSync(downloadsDir, {force: true, recursive: true}); + }, + +} diff --git a/commands/misc/emoji.js b/commands/misc/emoji.js index 58befa8..6ce12dc 100644 --- a/commands/misc/emoji.js +++ b/commands/misc/emoji.js @@ -19,8 +19,7 @@ module.exports = { try { num = num.slice(0, -1); } catch (e) { - console.error(e) - message.channel.send("There was an error. Check the logs"); + message.channel.send("There was an error."); return; } message.channel.send("https://cdn.discordapp.com/emojis/" + num + extension); diff --git a/commands/misc/fm.js b/commands/misc/fm.js index 615630c..e259f4c 100644 --- a/commands/misc/fm.js +++ b/commands/misc/fm.js @@ -4,6 +4,12 @@ const getCurrentCover = require("../../util/lastfm/getCurrentCover"); const getTopTracks = require("../../util/lastfm/getTopTracks"); const help = require("../info/help"); const parseMention = require("../../util/parseMention"); +const roast = require("../../util/lastfm/roast"); +const getTopArtists = require("../../util/lastfm/getTopArtists"); +const getTopAlbums = require("../../util/lastfm/getTopAlbums"); +const {EmbedBuilder} = require('discord.js'); +const getNickname = require('../../util/getNickname'); +const isWhitelisted = require("../../util/isWhitelisted"); module.exports = { name: 'fm', description: 'Last fm commands. See `help fm` for more info.', @@ -11,8 +17,11 @@ module.exports = { "They behave the same (for example: `fmtt` and `fm tt`)", "Set username: `fmset `", "Get current scrobble: `fm`", + "Get top artists `fmas`", + "Get top albums: `fmabl`", "Get top tracks: `fmtt`", "Get album cover for current scrobble: `fmcover`", + "Get a roast from an LLM using your top artists and albums: `fmroast`" ], async execute({ message, args, prefix, client }) { let sendText = {text: "Something went wrong.", embed: null}; @@ -28,9 +37,46 @@ module.exports = { args.shift(); sendText = await getTopTracks(message.author.id, args, message.guild); break; + case "topalbums": + case "topalbum": + case "abl": + args.shift(); + sendText = await getTopAlbums(message.author.id, args, message.guild) + break; + case "topartists": + case "topartist": + case "ta": + case "as": + args.shift(); + sendText = await getTopArtists(message.author.id, args, message.guild); + break; case "cover": sendText = await getCurrentCover(message.author.id, message.guild); break; + case "roast": + const commandName = "fmroast" + if(!isWhitelisted.isWhitelisted(commandName, client.whitelist, message.guild.id, message.author.id, isWhitelisted.whitelistTypes.EITHER)) { + message.channel.send("fmroast has not been whitelisted for you or this server.") + return; + } + let topArtists = await getTopArtists(message.author.id, ["yearly"], message.guild, true); + let topAlbums = await getTopAlbums(message.author.id, ["yearly"], message.guild, true); + message.channel.sendTyping(); + let result = await roast(topArtists, topAlbums); + + let parse = parseMention(message.author.id, message.guild) + let user = message.guild.members.cache.get(parse); + let nickname = getNickname(user, message.guild) + if (nickname == null) { + nickname = user.user.username; + } + const possesive = nickname.at(nickname.length - 1) === "s" ? "'" : "'s" + const embed = new EmbedBuilder() + .setAuthor({name: `${nickname}${possesive} AI roast`, iconURL: user.user.avatarURL({ dynamic: true, size: 4096 })}) + .setColor(15780145) + .setDescription(result); + sendText.embed = embed; + break; default: sendText.text = `${args[0]} is not a valid subcommand.\nSee \`${prefix}help fm\` for more info.`; break; @@ -41,14 +87,16 @@ module.exports = { if(sendText.embed != null){ let parse = parseMention(message.author.id, message.guild) let user = message.guild.members.cache.get(parse); - let roleColor = 15788778; - if (user.roles.color) { - roleColor = user.roles.color.color; + if(!sendText.embed.data.color){ + let roleColor = 15788778; + if (user.roles.color) { + roleColor = user.roles.color.color; + } + sendText.embed.setColor(roleColor); } - sendText.embed.setColor(roleColor); message.channel.send({embeds :[sendText.embed]}) }else{ message.channel.send(sendText.text.replaceAll("", prefix)); } } -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/commands/misc/ig.js b/commands/misc/ig.js new file mode 100644 index 0000000..21268b7 --- /dev/null +++ b/commands/misc/ig.js @@ -0,0 +1,28 @@ + +module.exports = { + name: 'ig', + description: 'Replaces Instagram links with kkinstagram and deletes the original message', + execute({message, args}) { + const noUrlErr = "You need to provide an Instagram link to use this command." + if(args.length < 1) { + message.channel.send(noUrlErr); + return; + } + let replacedLink = ""; + const regex = /(?<=\/)(instagram)\.com/g + if(args[0].startsWith("https://") || args[0].startsWith("http://")){ + replacedLink = args[0].replace(regex, "kkinstagram.com"); + } + if(replacedLink.length === 0 || args[0] === replacedLink){ + message.channel.send(noUrlErr); + return; + } + message.channel.send(replacedLink); + try{ + message.delete() + }catch(err){ + console.error(`${this.name}: An error occurred while trying to delete the original message.`) + console.error(err); + } + } +}; \ No newline at end of file diff --git a/commands/misc/ping.js b/commands/misc/ping.js index 6e23a0c..fe9fb09 100644 --- a/commands/misc/ping.js +++ b/commands/misc/ping.js @@ -1,7 +1,11 @@ module.exports = { name: 'ping', description: 'Just ping.', - execute({message, client}) { - message.channel.send(`Pong.\n${client.ws.ping}ms`) + async execute({message, client}) { + let apiLatency = client.ws.ping; + const time = process.hrtime.bigint(); + let initialMessage = await message.channel.send(`Pong.`) + const diff = Number(process.hrtime.bigint() - time) * 1e-6; + initialMessage.edit(`Pong.\nAPI latency: ${apiLatency} ms\nMessage latency: ${diff.toFixed(0)} ms.`); } }; \ No newline at end of file diff --git a/commands/misc/prefix.js b/commands/misc/prefix.js index 19ec7d1..954c834 100644 --- a/commands/misc/prefix.js +++ b/commands/misc/prefix.js @@ -1,10 +1,11 @@ const setServerPrefix = require("../../util/setServerPrefix"); +const { PermissionsBitField } = require('discord.js'); module.exports = { name: 'prefix', description: 'Change the prefix of the bot in this server.', execute({ message, client, args, prefix }) { - if (!message.member.permissions.has('MANAGE_GUILD')) { + if (!message.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { message.channel.send("You do not have sufficient permissions(MANAGE_GUILD) to change the prefix of this server.") return; } diff --git a/commands/misc/say.js b/commands/misc/say.js index e8ca4e6..2995f6a 100644 --- a/commands/misc/say.js +++ b/commands/misc/say.js @@ -1,6 +1,7 @@ module.exports = { name: 'say', description: 'Repeats arguments', + admin: true, execute({message, args}) { if(args.length == 0) diff --git a/commands/misc/tdoss.js b/commands/misc/tdoss.js new file mode 100644 index 0000000..f5430fe --- /dev/null +++ b/commands/misc/tdoss.js @@ -0,0 +1,100 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { writeFile } = require('node:fs/promises') +const { Readable } = require('node:stream') +const executeCommand = require('../../util/executeCommand'); + +module.exports = { + name: 'tdoss', + description: 'Combine picture with tdoss album cover template', + async execute({ message, args }) { + + let tdossDir = path.resolve(process.cwd(), 'data', 'tdoss'); + const tdossTemplate = path.resolve(process.cwd(), 'resources', 'tdoss_template.png'); + + const directory = path.resolve(tdossDir, Math.floor(new Date).toString()) + fs.mkdirSync(directory, {recursive: true}) + + let url = ""; + if(message.attachments.size > 0){ + url = message.attachments.first().url + + + } else if(args[0] && args[0].startsWith("https://") ){ + url = args[0]; + }else if(message.reference){ + let referencedMessage = await message.fetchReference(); + if(referencedMessage.attachments.size > 0){ + url = await referencedMessage.attachments.first().url; + } + else { + message.channel.send("The message you replied to doesn't have any attachments."); + return; + } + } + else { + let lastMessages = await message.channel.messages.fetch({limit: 20, cache: false}); + let lastMessageWithAttachment = lastMessages.find(m => m.attachments.size > 0); + if(!lastMessageWithAttachment){ + message.channel.send("Couldn't find an image to use this command on.") + fs.rmSync(`${directory}`, {recursive: true}) + return + } + url = lastMessageWithAttachment.attachments.first().url; + + } + // TODO: Download with correct extension. + message.channel.sendTyping(); + let downloadResult = await this.downloadImage(url, path.resolve(directory, "input.png")); + if(downloadResult.value != this.ERROR_CODES.SUCCESS){ + if(downloadResult.value === this.ERROR_CODES.FETCH_ERROR){ + message.channel.send(`Failed to download the provided image, got error '${downloadResult.errorMessage}'`); + }else if (downloadResult.value === this.ERROR_CODES.HTTP_ERROR){ + message.channel.send(`Failed to download the provided image, got response status '${downloadResult.errorMessage}'`); + }else if(downloadResult.value === this.ERROR_CODES.NOT_IMAGE){ + message.channel.send(`The provided url was not an image.`) + } + fs.rmSync(`${directory}`, {recursive: true}) + return; + } + + + const commandArgs = [tdossTemplate, "(", `${directory}/input.png`, "-resize", "800x800^", "-gravity", "center", "-extent", "1000x1000", ")", "-compose", "dst-over", "-composite", `${directory}/tdoss_result.png`] + if (executeCommand("magick", commandArgs).error === true) { + message.channel.send("Something went wrong during image manipulation.\nTry again and if it keeps happening, contact the owner of the bot.") + fs.rmSync(`${directory}`, {recursive: true}) + return + } + + let final_image = { + attachment: path.resolve(`${directory}/tdoss_result.png`), + name: "tdoss_result.png" + } + + await message.channel.send({files: [final_image]}) + fs.rmSync(`${directory}`, {recursive: true}) + }, + // https://stackoverflow.com/a/77210219 + async downloadImage(url, path) { + let res; + try { + res = await fetch(url); + } catch (error) { + return {value: this.ERROR_CODES.FETCH_ERROR, errorMessage: error.cause?.message || error.message}; + } + if(!res.ok) return {value: this.ERROR_CODES.HTTP_ERROR, errorMessage: res.status.toString()}; + const contentType = res.headers.get('content-type'); + if(!contentType || !contentType.startsWith("image")) return {value: this.ERROR_CODES.NOT_IMAGE, errorMessage: contentType || "No content-type header"}; + const stream = Readable.fromWeb(res.body) + await writeFile(path, stream); + return {value: this.ERROR_CODES.SUCCESS, errorMessage: ""}; + }, + ERROR_CODES: { + SUCCESS: 0, + HTTP_ERROR: 1, + NOT_IMAGE: 2, + FETCH_ERROR: 3 + } + +} diff --git a/commands/misc/timer.js b/commands/misc/timer.js index cecbebb..522a9aa 100644 --- a/commands/misc/timer.js +++ b/commands/misc/timer.js @@ -4,11 +4,11 @@ const parseTime = require('../../util/timer/parseTime'); const showTimer = require('../../util/timer/showTimer'); module.exports = { name: "timer", - description: "Set a timer for a time in minutes.", + description: "Set a timer for a date or time duration.", moreHelp: ["Usage:" ,"`timer [add|create] `" ,"`timer