// Requires const fs = require("fs"); const config = require("./config.json"); const funcs = require("./funcs.js"); const wfos = require("./data/wfos.json"); const blacklist = require("./data/blacklist.json"); const events = require("./data/events.json"); const outlookURLs = require("./data/outlook.json"); const sattelites = require("./data/sattelites.json"); const nwrstreams = require("./data/nwrstreams.json") const Jimp = require("jimp"); const { client, xml } = require("@xmpp/client"); const fetch = require("node-fetch"); const html = require("html-entities") const Discord = require("discord.js"); const dVC = require("@discordjs/voice"); const colors = require("colors"); const sqlite3 = require("sqlite3").verbose(); const ws = require("ws"); // setup ws client to 127.0.0.1:9743 const wsClient = new ws("ws://127.0.0.1:9743"); // Setup Discord const discord = new Discord.Client({ intents: [ "Guilds", "GuildVoiceStates", "DirectMessages" ] }); // Setup SQlite DB const db = new sqlite3.Database("channels.db", (err) => { if (err) { console.log(`${colors.red("[ERROR]")} Error connecting to database: ${err.message}`); } console.log(`${colors.cyan("[INFO]")} Connected to the database`); }); // Random funcs function toTitleCase(str) { return str.replace( /\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } const parseProductID = function (product_id) { const [timestamp, station, wmo, pil] = product_id.split("-"); return { timestamp: convertDate(timestamp), originalTimestamp: timestamp, station, wmo, pil }; } // Convert date format 202405080131 (YYYYMMddHHmm) to iso format, hours and mins is UTC const convertDate = function (date) { const year = date.substring(0, 4); const month = date.substring(4, 6); const day = date.substring(6, 8); const hours = date.substring(8, 10); const mins = date.substring(10, 12); // Because they don't have seconds, assume current seconds const secs = new Date().getSeconds(); return new Date(Date.UTC(year, month - 1, day, hours, mins, secs)); } // Get number of unique channels in the database const getUniqueChannels = function () { return new Promise((resolve, reject) => { db.all(`SELECT DISTINCT channelid FROM channels`, (err, rows) => { if (err) { console.error(err.message); } // Go through channels. and get number of unique guilds const guilds = []; rows.forEach((row) => { const channel = discord.channels.cache.get(row.channelid); if (!channel) return; if (!guilds.includes(channel.guild.id)) { guilds.push(channel.guild.id); } }); resolve({ channels: rows.length, guilds: guilds.length }); }); }); } // Get first url in a string, return object {string, url} remove the url from the string const getFirstURL = function (string) { url = string.match(/(https?:\/\/[^\s]+)/g); if (!url) return { string, url: null }; const newString = string.replace(url[0], ""); return { string: newString, url: url[0] }; } // Function to get the room name from the WFO code const getWFOroom = function (code) { code = code.toLowerCase(); if (wfos[code]) { return wfos[code].room; } else { return code; } } // Function to get WFO data const getWFO = function (code) { code = code.toLowerCase(); if (wfos[code]) { return wfos[code]; } else { return null; } } // Get WFO data from room name function getWFOByRoom(room) { room = room.toLowerCase(); for (const key in wfos) { if (wfos.hasOwnProperty(key) && wfos[key].room === room) { return wfos[key]; } } return { location: room, room: room }; } // Voice funcs function JoinChannel(channel, track, volume, message) { connection = dVC.joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, adapterCreator: channel.guild.voiceAdapterCreator, selfDeaf: true }); resource = dVC.createAudioResource(track, { inlineVolume: true, silencePaddingFrames: 5 }); player = dVC.createAudioPlayer(); connection.player = player; // So we can access it later to pause/play/stop etc resource.volume.setVolume(volume); connection.subscribe(player) player.play(resource); connection.on(dVC.VoiceConnectionStatus.Ready, () => { player.play(resource); }) connection.on(dVC.VoiceConnectionStatus.Disconnected, async (oldState, newState) => { try { await Promise.race([ dVC.entersState(connection, VoiceConnectionStatus.Signalling, 5_000), dVC.entersState(connection, VoiceConnectionStatus.Connecting, 5_000), ]); } catch (error) { message.channel.send(`Failed to reconnect to the voice channel. Stopping for now.`); connection.destroy(); return false; } }); player.on('error', error => { console.error(`Error: ${error.message} with resource ${error.resource.metadata.title}`); message.channel.send(`Error while streaming. Stopping for now.`); player.stop(); }); player.on(dVC.AudioPlayerStatus.Playing, () => { message.channel.send(`Playing stream in <#${channel.id}>`); connection.paused = false; }); player.on('idle', () => { message.channel.send(`Stream idle.`); }) return true; } function LeaveVoiceChannel(channel) { // Get resource, player, etc, and destroy them const connection = dVC.getVoiceConnection(channel.guild.id); if (connection) { connection.destroy(); return true } return false } function toggleVoicePause(channel) { const connection = dVC.getVoiceConnection(channel.guild.id); if (connection) { if (connection.paused) { connection.player.unpause(); connection.paused = false; return true; } else { connection.player.pause(); connection.paused = true; return true } } else { return false; } }; function setVolume(channel, volume) { const connection = dVC.getVoiceConnection(channel.guild.id); if (connection) { connection.player.state.resource.volume.setVolume(volume); return true; } } // func to Generate random string, ({upper, lower, number, special}, length) const generateRandomString = function (options, length) { let result = ''; const characters = { upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', lower: 'abcdefghijklmnopqrstuvwxyz', number: '0123456789', special: '!@#$%^&*()_+' }; let chars = ''; for (const key in options) { if (options[key]) { chars += characters[key]; } } for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } // Func to generate UUID const generateUUID = function () { return generateRandomString({ lower: true, upper: true, number: true }, 8) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 12); } // Variable setup var iem = [] var startup = true; var startTimestap = new Date(); var messages = 0; // START DISCORD // listen on ws wsClient.on("message", (data) => { }); discord.on('ready', async () => { console.log(`${colors.cyan("[INFO]")} Logged in as ${discord.user.tag}`); // Get all guilds, and log them discord.guilds.cache.forEach((guild) => { console.log(`${colors.cyan("[INFO]")} In guild: ${guild.name} (${guild.id})`); }); //start(); setTimeout(() => { // Wait 10 seconds, if startup is still true, something went wrong if (startup) { console.log(`${colors.red("[ERROR]")} Startup failed, exiting...`); process.exit(1); } }, 10000) // Check all channels in DB, fetch them, if they dont exist, delete all subscriptions db.all(`SELECT channelid FROM channels`, (err, rows) => { if (err) { console.error(err.message); } rows.forEach((row) => { const channel = discord.channels.cache.get(row.channelid); if (!channel) { // Delete the channel from the database and return return db.run(`DELETE FROM channels WHERE channelid = ?`, [row.channelid], (err) => { if (err) { console.error(err.message); } console.log(`${colors.cyan("[INFO]")} Deleted channel ${row.channelid} from database`); }); }; }); }); // Get all users in userAlerts and fetch them db.all(`SELECT userid FROM userAlerts`, (err, rows) => { if (err) { console.error(err.message); } rows.forEach((row) => { discord.users.fetch(row.userid); }); }); }); discord.on("interactionCreate", async (interaction) => { switch (interaction.type) { case Discord.InteractionType.ApplicationCommand: switch (interaction.commandName) { case "subscribe": room = getWFOroom(interaction.options.getString("room")); if (!iem.find((channel) => channel.jid.split("@")[0] === room)) { interaction.reply({ content: "Invalid room", ephemeral: true }); return; } if (interaction.options.getString("filter")) { filter = interaction.options.getString("filter").toLowerCase(); } else { filter = ""; } minPriority = interaction.options.getInteger("minpriority"); filterEvt = interaction.options.getString("filterevt") || null; message = interaction.options.getString("message") || null; if (interaction.inGuild()) { db.get(`SELECT * FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err, row) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to subscribe to room", ephemeral: true }); } else if (row) { return interaction.reply({ content: `Already subscribed to \`${getWFOByRoom(room).location}\`\nIf you want to update a subscribtion, please unsubscribe and resubscribe. This will be made a command eventually.`, ephemeral: true }); } db.run(`INSERT INTO channels (channelid, iemchannel, custommessage, filter, filterevt, minPriority) VALUES (?, ?, ?, ? ,? ,?)`, [interaction.channel.id, room, message, filter, filterEvt, minPriority], (err) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to subscribe to room", ephemeral: true }); } else { interaction.reply({ content: `Subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true }); } }); }); } else { // We're in a DM db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to subscribe to room", ephemeral: true }); } else if (row) { return interaction.reply({ content: `Already subscribed to \`${getWFOByRoom(room).location}\`\nIf you want to update a subscribtion, please unsubscribe and resubscribe. This will be made a command eventually.`, ephemeral: true }); } db.run(`INSERT INTO userAlerts (userid, iemchannel, custommessage, filter, filterEvt, minPriority) VALUES (?, ?, ?, ? ,?, ?)`, [interaction.user.id, room, message, filter, filterEvt, minPriority], (err) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to subscribe to room", ephemeral: true }); } else { interaction.reply({ content: `Subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true }); } }); }); } break; case "unsubscribe": // Check that the room is valid room = getWFOroom(interaction.options.getString("room")); if (!iem.find((channel) => channel.jid.split("@")[0] === room)) { interaction.reply({ content: "Invalid room", ephemeral: true }); return; } if (interaction.inGuild()) { // Check if subbed db.get(`SELECT * FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err, row) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true }); } if (!row) { return interaction.reply({ content: `Not subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true }); } db.run(`DELETE FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true }); } interaction.reply({ content: `Unsubscribed from \`${getWFOByRoom(room).location}\``, ephemeral: true }); }); }); } else { db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true }); } if (!row) { return interaction.reply({ content: `Not subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true }); } db.run(`DELETE FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true }); } interaction.reply({ content: `Unsubscribed from \`${getWFOByRoom(room).location}\``, ephemeral: true }); }); }); } break; case "list": // List all the subscribed rooms if (interaction.inGuild()) { db.all(`SELECT * FROM channels WHERE channelid = ?`, [interaction.channel.id], (err, rows) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to get subscribed rooms", ephemeral: true }); } if (!rows) { return interaction.reply({ content: "No subscribed rooms", ephemeral: true }); } let roomList = []; rows.forEach((row) => { roomList.push({ name: `${row.iemchannel}: ${getWFOByRoom(row.iemchannel).location}`, value: `Message: \`\`${row.custommessage || "None"}\`\`\nFilter: \`\`${row.filter || "None"}\`\`\nEvent Filter: \`\`${row.filterEvt || "None"}\`\`\nMin Priority: \`\`${row.minPriority || "None"}\`\`` }); }); const embed = { title: "Subscribed Rooms", fields: roomList, color: 0x00ff00 } interaction.reply({ embeds: [embed], ephemeral: true }); }); } else { db.all(`SELECT * FROM userAlerts WHERE userid = ?`, [interaction.user.id], (err, rows) => { if (err) { console.error(err.message); interaction.reply({ content: "Failed to get subscribed rooms", ephemeral: true }); } if (!rows) { return interaction.reply({ content: "No subscribed rooms", ephemeral: true }); } let roomList = []; rows.forEach((row) => { roomList.push({ name: `${row.iemchannel}: ${getWFOByRoom(row.iemchannel).location}`, value: `Message: \`\`${row.custommessage || "None"}\`\`\nFilter: \`\`${row.filter || "None"}\`\`\nEvent Filter: \`\`${row.filterEvt || "None"}\`\`\nMin Priority: \`\`${row.minPriority || "None"}\`\`` }); }); const embed = { title: "Subscribed Rooms", fields: roomList, color: 0x00ff00 } interaction.reply({ embeds: [embed], ephemeral: true }); }); } break; case "about": // Send an embed showing info about the bot, including number of guilds, number of subscribed rooms, etc // let guilds = discord.guilds.cache.size; let channels = 0; let uniques = 0; await db.get(`SELECT COUNT(*) as count FROM channels`, async (err, row) => { channels = row.count // await getUniqueChannels().then((unique) => { // uniques = unique.channels; // guilds = unique.guilds; // }); // discord.users.fetch("289884287765839882").then((chrisUser) => { // const embed = { // title: "About Me!", // thumbnail: { // url: discord.user?.avatarURL() // }, // description: `I listen to all the weather.im rooms and send them to discord channels.\nI am open source, you can find my code [here!](https://github.com/ChrisChrome/iembot-2.0)\n\nThis bot is not affiliated with NOAA, the National Weather Service, or the IEM project.\n\nI am currently running a super beta version of the bot, so things may break. If you have any issues, please report them in the support server.`, // fields: [ // { // name: "Uptime", // value: `Since , Started `, // }, // // { // // name: "Caught Messages", // // value: `Got ${messages.toLocaleString()} messages since startup`, // // }, // // { // // name: "Guilds", // // value: guilds.toLocaleString(), // // inline: true // // }, // { // name: "Subscribed Rooms", // value: `${channels.toLocaleString()}`, // inline: true // }, // // { // // name: "Unique Channels", // // value: `${uniques.toLocaleString()} in ${guilds} guilds.`, // // inline: true // // } // ], // color: 0x00ff00, // footer: { // text: "Made by @chrischrome with <3", // icon_url: chrisUser.avatarURL() // } // } // interaction.reply({ embeds: [embed] }); // }) interaction.reply({ embeds: [ { title: "Temp About Me", description: "I am currently running a very early beta of sharding, so for the time being, this is all you get for the About Me, as collecting all the data is a bit more difficult.\n\nIf something is broken please report it to the support server [here](https://discord.gg/XthJjfU8TU)", } ] }) }); break; case "rooms": // // Send an embed showing all the available rooms // let roomList = ""; // iem.forEach((channel) => { // room = channel.jid.split("@")[0] // console.log(getWFOByRoom(room)) // roomList += `\`${room}\`: ${getWFOByRoom(room).location}\n`; // }); // const roomEmbed = { // title: "Available Rooms", // description: roomList, // color: 0x00ff00 // } // interaction.reply({ embeds: [roomEmbed], ephemeral: true }); // Do the above, but paginate like the product text let roomList = ""; iem.forEach((channel) => { room = channel.jid.split("@")[0] roomList += `\`${room}\`: ${getWFOByRoom(room).location || "Unknown"}\n`; }); const pages = roomList.match(/[\s\S]{1,2000}(?=\n|$)/g); embeds = pages.map((page, ind) => ({ title: `Available Rooms Pg ${ind + 1}/${pages.length}`, description: page, color: 0x00ff00 })); interaction.reply({ embeds, ephemeral: true }); break; case "setupall": if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!config.discord.owner) return interaction.reply({ content: "Owner not set in config", ephemeral: true }); if (interaction.user.id !== config.discord.owner) return interaction.reply({ content: "You are not the owner", ephemeral: true }); await interaction.deferReply({ ephemeral: true }) var category; // New setup, we're pulling from wfos.json now const chunks = []; const chunkSize = 50; const total = iem.length; // wfos is object "wfo": {"location": "Text Name", "room": "roomname"} for (let i = 0; i < total; i += chunkSize) { chunks.push(iem.slice(i, i + chunkSize)); console.log(iem.slice(i, i + chunkSize)) } chunks.forEach((chunk, index) => { const categoryName = `Rooms ${index + 1}`; interaction.guild.channels.create({ name: categoryName, type: Discord.ChannelType.GuildCategory }).then((newCategory) => { console.log(`${colors.cyan("[INFO]")} Created category ${newCategory.name}`); chunk.forEach((channel) => { channelName = `${channel.jid.split("@")[0]}_${getWFOByRoom(channel.jid.split("@")[0]).location}` if (channelName == "Unknown") channelName = channel.jid.split("@")[0] newCategory.guild.channels.create({ name: channelName, type: Discord.ChannelType.GuildText, parent: newCategory, topic: `Weather.im room for ${getWFOByRoom(channel.jid.split("@")[0]).location} - ${channel.jid.split("@")[0]}` }).then((newChannel) => { console.log(`${colors.cyan("[INFO]")} Created channel ${newChannel.name}`); db.run(`INSERT INTO channels (channelid, iemchannel, custommessage) VALUES (?, ?, ?)`, [newChannel.id, channel.jid.split("@")[0], null], (err) => { if (err) { console.error(err.message); } console.log(`${colors.cyan("[INFO]")} Added channel ${newChannel.id} to database`); }); }).catch((err) => { console.log(`${colors.red("[ERROR]")} Failed to create channel: ${err.message}`); }); }); }).catch((err) => { console.log(`${colors.red("[ERROR]")} Failed to create category: ${err.message}`); }); }); interaction.editReply({ content: "Setup complete", ephemeral: true }); break; case "support": // Generate an invite link to the support server (use widget channel) //const invite = await discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.inviteChannel).createInvite(); const embed = { title: "Support Server", //description: `Need help with the bot? Join the support server [here](${invite.url})`, description: `Need help with the bot? Join the support server [here](https://discord.gg/XthJjfU8TU)`, color: 0x00ff00 } interaction.reply({ embeds: [embed] }); break; case "playbcfy": // Play broadcastify stream if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!config.broadcastify.enabled) return interaction.reply({ content: "Broadcastify is not enabled", ephemeral: true }); streamID = interaction.options.getString("id"); // Check if the stream ID is valid (up to 10 digit alphanumeric) if (!streamID.match(/^[a-zA-Z0-9]{1,10}$/)) return interaction.reply({ content: "Invalid stream ID", ephemeral: true }); // Get the stream URL url = `https://${config.broadcastify.username}:${config.broadcastify.password}@audio.broadcastify.com/${streamID}.mp3`; // Get the channel channel = interaction.member.voice?.channel; if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); // Join the channel and play the stream res = JoinChannel(channel, url, .1, interaction) if (res) { interaction.reply({ content: "Playing Stream", ephemeral: true }); } else { interaction.reply({ content: `Failed to play stream`, ephemeral: true }); } break; case "play": // Play generic stream if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); // Use local variables & Get the URL interactionUrl = interaction.options.getString("url"); // Get the channel channel = interaction.member.voice?.channel; // Check if in channel if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); // Join the channel and play the stream st = JoinChannel(channel, interactionUrl, .1, interaction); if (st) { interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true }); } else { interaction.reply({ content: `Failed to play stream`, ephemeral: true }); } break; case "nwrplay": // Play NWR stream if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); // Get the callsign const callsign = interaction.options.getString("callsign"); // Get the URL associated with the callsign url = nwrstreams.callsigns[callsign]; // Get the voice channel channel = interaction.member.voice?.channel; // Use a unique variable name if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); // Join the channel and play the stream const streamStatus = JoinChannel(channel, url, .1, interaction); // Use a unique variable name if (streamStatus) { interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true }); } else { interaction.reply({ content: `Failed to play stream`, ephemeral: true }); } break; case "leave": // Leave Channel if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); channel = interaction.member.voice.channel; if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); res = LeaveVoiceChannel(channel) if (res) { interaction.reply({ content: "Left voice channel", ephemeral: true }); } else { interaction.reply({ content: "Failed to leave voice channel (Was i ever in one?)", ephemeral: true }); } break; case "pause": // Pause/unpause stream if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); channel = interaction.member.voice.channel; if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); res = toggleVoicePause(channel) if (res) { interaction.reply({ content: "Toggled pause", ephemeral: true }); } else { interaction.reply({ content: "Failed to toggle pause", ephemeral: true }); } break; case "volume": // Set volume if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); channel = interaction.member.voice.channel; if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); volume = interaction.options.getInteger("volume") / 100; // Make sure volume isnt negative if (volume < 0) volume = 0; if (volume > 1) volume = 1; res = setVolume(channel, volume) if (res) { interaction.reply({ content: `Set volume to ${volume * 100}%` }); } else { interaction.reply({ content: "Failed to set volume", ephemeral: true }); } break; case "outlook": day = interaction.options.getInteger("day"); type = interaction.options.getString("type"); if (day < 0 || day > 7) return interaction.reply({ content: "Invalid day", ephemeral: true }); if (type !== "fire" && type !== "convective") return interaction.reply({ content: "Invalid type", ephemeral: true }); url = outlookURLs[type][day]; await interaction.deferReply(); fetch(url).then((res) => { if (res.status !== 200) { interaction.editReply({ content: "Failed to get outlook", ephemeral: true }); return; } res.buffer().then(async (buffer) => { // Check all overlays and add them to image as selected using Jimp overlays = ["population", "city", "cwa", "rfc", "interstate", "county", "tribal", "artcc", "fema"] await Jimp.read(buffer).then((image) => { outImg = image; cnt = 0; sendMsg = setTimeout(() => { interaction.editReply({ embeds: [{ title: `${toTitleCase(type)} Outlook Day ${day + 1}`, image: { url: `attachment://${type}_${day}.png` }, color: 0x00ff00 }], files: [{ attachment: buffer, name: `${type}_${day}.png` }] }); }, 150) overlays.forEach((overlay) => { if (interaction.options.getBoolean(`${overlay}_overlay`)) { clearTimeout(sendMsg); Jimp.read(`./images/overlays/${overlay}.png`).then((overlayImage) => { outImg.composite(overlayImage, 0, 0); sendMsg = setTimeout(() => { outImg.getBufferAsync(Jimp.MIME_PNG).then((buffer) => { interaction.editReply({ embeds: [{ title: `${toTitleCase(type)} Outlook Day ${day + 1}`, image: { url: `attachment://${type}_${day}.png` }, color: 0x00ff00 }], files: [{ attachment: buffer, name: `${type}_${day}.png` }] }); }); }, 150) }); } }) // interaction.editReply({ // embeds: [{ // title: `${toTitleCase(type)} Outlook Day ${day + 1}`, // image: { // url: `attachment://${type}_${day}.png` // }, // color: 0x00ff00 // }], // files: [{ // attachment: buffer, // name: `${type}_${day}.png` // }] // }); }); }); }).catch((err) => { interaction.editReply({ content: "Failed to get outlook", ephemeral: true }); console.log(`${colors.red("[ERROR]")} Failed to get outlook: ${err.message}`); console.error(err); }); break; case "alertmap": const alertmapurl = "https://forecast.weather.gov/wwamap/png/US.png" await interaction.deferReply(); fetch(alertmapurl).then((res) => { if (res.status !== 200) { interaction.editReply({ content: "Failed to get alert map", ephemeral: true }); return; } res.buffer().then(async (buffer) => { interaction.editReply({ embeds: [{ title: `Alert Map`, image: { url: `attachment://alerts.png` }, color: 0x00ff00 }], files: [{ attachment: buffer, name: `alerts.png` }] }); }); }).catch((err) => { interaction.editReply({ content: "Failed to get alert map", ephemeral: true }); console.log(`${colors.red("[ERROR]")} Failed to get alert map: ${err.message}`); console.error(err); }); break; case "sattelite": // Get satellite images sat = interaction.options.getString("sattelite"); if (!sattelites[sat]) return interaction.reply({ content: "Invalid satellite", ephemeral: true }); // Fetch all the images await interaction.deferReply(); imageBuffers = {}; embeds = []; files = []; sattelites[sat].forEach(async (imgData) => { // Get a buffer for the data, and put that in imageBuffers with the "name" as the key fetch(imgData.url).then((res) => { if (res.status !== 200) { interaction.editReply({ content: "Failed to get satellite images", ephemeral: true }); return; } res.buffer().then((buffer) => { imageBuffers[imgData.name] = buffer; files.push({ attachment: buffer, name: `${imgData.name}.jpg` }); embeds.push({ title: `${sat} ${imgData.name}`, image: { url: `attachment://${imgData.name}.jpg` } }); // Check if we have all the images if (Object.keys(imageBuffers).length === sattelites[sat].length) { // Send the images interaction.editReply({ embeds, files }); } }); }).catch((err) => { interaction.editReply({ content: "Failed to get satellite images", ephemeral: true }); console.log(`${colors.red("[ERROR]")} Failed to get satellite images: ${err.message}`); console.error(err); }); }); break; case "forecast": await interaction.deferReply(); periods = interaction.options.getInteger("periods") || 1; funcs.getWeatherBySearch(interaction.options.getString("location")).then((weather) => { if (config.debug >= 1) console.log(JSON.stringify(weather, null, 2)) embeds = funcs.generateDiscordEmbeds(weather, periods); interaction.editReply({ embeds }); }).catch((err) => { interaction.editReply({ content: "Failed to get forecast", ephemeral: true }); if (config.debug >= 1) console.log(`${colors.red("[ERROR]")} Failed to get forecast: ${err}`); }); break; } case Discord.InteractionType.MessageComponent: if (interaction.customId) { const product_id = interaction.customId; url = `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id}`; await interaction.deferReply({ ephemeral: true }); fetch(url).then((res) => { if (res.status !== 200) { interaction.reply({ content: "Failed to get product text", ephemeral: true }); return; } // Retruns raw text, paginate it into multiple embeds if needed res.text().then(async (text) => { const pages = text.match(/[\s\S]{1,1900}(?=\n|$)/g); // const embeds = pages.map((page, ind) => ({ // title: `Product Text for ${product_id} Pg ${ind + 1}/${pages.length}`, // description: `\`\`\`${page}\`\`\``, // color: 0x00ff00 // })); const messages = pages.map((page, ind) => { return `\`\`\`${page}\`\`\`` }) messages.forEach(async (message) => { interaction.followUp({ content: message, ephemeral: true }); }) }); }).catch((err) => { interaction.reply({ content: "Failed to get product text", ephemeral: true }); console.log(`${colors.red("[ERROR]")} Failed to get product text: ${err.message}`); }); } break; } }); // discord.on("guildCreate", (guild) => { // // Get the main guild // const myGuild = discord.guilds.cache.get(config.discord.mainGuild); // // Get the log channel // const channel = myGuild.channels.cache.get(config.discord.logChannel); // // Send a message to the log channel // channel.send({ // embeds: [ // { // description: `I joined \`${guild.name}\``, // color: 0x00ff00 // } // ] // }) // }) // discord.on("guildDelete", (guild) => { // // Get the main guild // const myGuild = discord.guilds.cache.get(config.discord.mainGuild); // // Get the log channel // const channel = myGuild.channels.cache.get(config.discord.logChannel); // // Send a message to the log channel // channel.send({ // embeds: [ // { // description: `I left \`${guild.name}\``, // color: 0xff0000 // } // ] // }) // }) process.on("unhandledRejection", (error, promise) => { console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error}`); // create errors folder if it doesnt exist if (!fs.existsSync("./error")) { fs.mkdirSync("./error"); } // write ./error/rejection_timestamp.txt fs.writeFileSync(`./error/rejection_${Date.now()}.txt`, `ERROR:\n${error}\n\nPROMISE:\n${JSON.stringify(promise)}`); // Send discord log const myGuild = discord.guilds.cache.get(config.discord.mainGuild); const channel = myGuild.channels.cache.get(config.discord.logChannel); channel.send({ embeds: [ { description: `Unhandled Rejection\n\`\`\`${error}\n${JSON.stringify(promise)}\`\`\``, color: 0xff0000 } ] }) return; }); process.on("uncaughtException", (error) => { console.log(`${colors.red("[ERROR]")} Uncaught Exception: ${error.message}\n${error.stack}`); if (!fs.existsSync("./error")) { fs.mkdirSync("./error"); } // write ./error/exception_timestamp.txt fs.writeFileSync(`./error/exception_${Date.now()}.txt`, error.stack); // Send message to log channel const myGuild = discord.guilds.cache.get(config.discord.mainGuild); const channel = myGuild.channels.cache.get(config.discord.logChannel); channel.send({ embeds: [ { description: `Uncaught Exception\n\`\`\`${error.message}\n${error.stack}\`\`\``, color: 0xff0000 } ] }) return; }); // Login to discord discord.login(config.discord.token);