From b1bcb1e24c7c31c2726ba097c3872103fe5e4f92 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 6 Jul 2024 00:53:43 -0600 Subject: [PATCH] guuuuh --- index.js | 618 +++++++++---------------------------------------------- shard.js | 562 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 656 insertions(+), 524 deletions(-) create mode 100644 shard.js diff --git a/index.js b/index.js index db9f7c1..d4d0ab3 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,9 @@ 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: [ @@ -24,13 +27,6 @@ const discord = new Discord.Client({ "DirectMessages" ] }); -const { - REST, - Routes -} = require('discord.js'); -const rest = new REST({ - version: '10' -}).setToken(config.discord.token); // Setup SQlite DB @@ -39,9 +35,6 @@ const db = new sqlite3.Database("channels.db", (err) => { console.log(`${colors.red("[ERROR]")} Error connecting to database: ${err.message}`); } console.log(`${colors.cyan("[INFO]")} Connected to the database`); - // Create tables if they dont exist - db.run(`CREATE TABLE IF NOT EXISTS channels (channelid TEXT, iemchannel TEXT, custommessage TEXT, minPriority INTEGER, "filter" TEXT, filterevt TEXT);`); - db.run(`CREATE TABLE IF NOT EXISTS userAlerts (userid TEXT, iemchannel TEXT, filter TEXT, filterEvt TEXT, minPriority INT, custommessage TEXT);`); }); @@ -256,333 +249,14 @@ var iem = [] var startup = true; var startTimestap = new Date(); var messages = 0; -var errCount = 0; -const curUUID = generateUUID(); - - -const xmpp = client({ - service: "xmpp://conference.weather.im", - domain: "weather.im", - resource: `discord-weather-bot-${generateRandomString({ upper: true, lower: true, number: true }, 5)}`, // Weird fix to "Username already in use" -}); - -//debug(xmpp, true); - -xmpp.on("error", (err) => { - console.log(`${colors.red("[ERROR]")} XMPP Error: ${err}. Trying to reconnect...`); - setTimeout(() => { - xmpp.stop().then(() => { - start(); - }); - }, 5000); -}); - -xmpp.on("offline", () => { - console.log(`${colors.yellow("[WARN]")} XMPP offline, trying to reconnect...`); - xmpp.disconnect().then(() => { - xmpp.stop().then(() => { - start(); - }) - }) -}); - -xmpp.on("stanza", (stanza) => { - // Debug stuff - if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`); - - - // Handle Room List - if (stanza.is("iq") && stanza.attrs.type === "result" && stanza.getChild("query")) { - query = stanza.getChild("query"); - if (query.attrs.xmlns === "http://jabber.org/protocol/disco#items") { - query.getChildren("item").forEach((item) => { - // Check if the JID is on the blacklist, if so, ignore it - if (blacklist.includes(item.attrs.jid)) return; - // get proper name from wfos - const wfo = getWFOByRoom(item.attrs.jid.split("@")[0]); - item.attrs.properName = wfo.location; - iem.push(item.attrs); - console.log(`${colors.cyan("[INFO]")} Found room: ${item.attrs.jid}`); - // Join the room - //xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" }))); - xmpp.send(xml("presence", { to: `${item.attrs.jid}/${curUUID}` }, xml("item", { role: "visitor" }))); - }); - } - } - // Get new messages and log them, ignore old messages - if (stanza.is("message") && stanza.attrs.type === "groupchat") { - // Stops spam from getting old messages - if (startup) return; - // Get channel name - fromChannel = stanza.attrs.from.split("@")[0]; - // Ignores - if (!stanza.getChild("x")) return; // No PID, ignore it - if (!stanza.getChild("x").attrs.product_id) return; - - const product_id = parseProductID(stanza.getChild("x").attrs.product_id); - const product_id_raw = stanza.getChild("x").attrs.product_id; - // Get body of message - const body = html.decode(stanza.getChildText("body")); - const bodyData = getFirstURL(body); - // get product id from "x" tag - var evt = events[product_id.pil.substring(0, 3)]; - - if (!evt) { - evt = { name: "Unknown", priority: 3 } - console.log(`${colors.red("[ERROR]")} Unknown event type: ${product_id.pil.substring(0, 3)}. Fix me`); - console.log(`${colors.magenta("[DEBUG]")} ${bodyData.string}`) - } - - evt.code = product_id.pil.substring(0, 3); - // Check timestamp, if not within 3 minutes, ignore it - const now = new Date(); - const diff = (now - product_id.timestamp) / 1000 / 60; - if (diff > 3) return; - if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} New message from ${fromChannel}`); - messages++; - - - // Handle NTFY - if (config.ntfy.enabled) { - if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Sending NTFY for ${config.ntfy.prefix}${fromChannel}`) - ntfyBody = { - "topic": `${config.ntfy.prefix}${fromChannel}`, - "message": bodyData.string, - "tags": [`Timestamp: ${product_id.timestamp}`, `Station: ${product_id.station}`, `WMO: ${product_id.wmo}`, `PIL: ${product_id.pil}`, `Channel: ${fromChannel}`], - "priority": evt.priority, - "actions": [{ "action": "view", "label": "Product", "url": bodyData.url }, { "action": "view", "label": "Product Text", "url": `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}` }] - } - if (stanza.getChild("x").attrs.twitter_media) { - ntfyBody.attach = stanza.getChild("x").attrs.twitter_media; - } - fetch(config.ntfy.server, { - method: 'POST', - body: JSON.stringify(ntfyBody), - headers: { - 'Authorization': `Bearer ${config.ntfy.token}` - } - }).then((res) => { - if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} NTFY sent for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`); - if (res.status !== 200) console.log(`${colors.red("[ERROR]")} NTFY failed for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`); - - - }).catch((err) => { - console.error(err) - }) - } - - - // Send discord msg - let embed = { - description: ` ${bodyData.string}`, - color: parseInt(config.priorityColors[evt.priority].replace("#", ""), 16) || 0x000000, - timestamp: product_id.timestamp, - footer: { - text: `Station: ${product_id.station} PID: ${product_id_raw} Channel: ${fromChannel}` - } - } - if (stanza.getChild("x").attrs.twitter_media) { - embed.image = { - url: stanza.getChild("x").attrs.twitter_media - } - } - - let discordMsg = { - embeds: [embed], - components: [ - { - type: 1, - components: [ - { - type: 2, - label: "Product", - style: 5, - url: bodyData.url - }, - { - type: 2, - style: 1, - custom_id: product_id_raw, - label: "Product Text", - emoji: { - name: "📄" - } - } - ] - } - ] - } - // Discord Channel Handling - db.all(`SELECT * FROM channels WHERE iemchannel = ?`, [fromChannel], (err, rows) => { - if (err) { - console.log(`${colors.red("[ERROR]")} ${err.message}`); - } - if (!rows) return; // No channels to alert - rows.forEach((row) => { - // Get Filters as arrays - if (!row.filterEvt) row.filterEvt = ""; - if (!row.filter) row.filter = ""; - let filterEvt = row.filterEvt.toLowerCase().split(","); - let filters = row.filter.toLowerCase().split(","); - if (evt.priority < row.minPriority) return; - // If the event type is not in th filter, ignore it. Make sure filterEvt isnt null - if (!filterEvt[0]) filterEvt = []; - if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return; - - let channel = discord.channels.cache.get(row.channelid); - if (!channel) return console.log(`${colors.red("[ERROR]")} Channel ${row.channelid} not found`); - - // fetch the product text - trySend = () => { - fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => { - // If neither the body nor the product text contains the filter, ignore it - res.text().then((text) => { - if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return; - thisMsg = JSON.parse(JSON.stringify(discordMsg)); - thisMsg.content = row.custommessage || null; - channel.send(thisMsg).catch((err) => { - console.error(err); - }).then((msg) => { - if (msg.channel.type === Discord.ChannelType.GuildAnnouncement) msg.crosspost(); - }); - }); - }).catch((err) => { - setTimeout(() => { - console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`) - trySend(); - }) - }); - } - trySend(); - }); - }); - - - // User DM alert handling - db.all(`SELECT * FROM userAlerts WHERE iemchannel = ?`, [fromChannel], (err, rows) => { - if (err) { - console.error(err.message); - } - if (!rows) return; // No users to alert - rows.forEach((row) => { - // Get Filters as arrays - if (!row.filterEvt) row.filterEvt = ""; - if (!row.filter) row.filter = ""; - let filterEvt = row.filterEvt.toLowerCase().split(","); - let filters = row.filter.toLowerCase().split(","); - - // If priority is less than the min priority, ignore it - if (evt.priority < row.minPriority) return; - // If the event type is not in th filter, ignore it. Make sure filterEvt isnt null - if (!filterEvt[0]) filterEvt = []; - if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return; - let user = discord.users.cache.get(row.userid); - if (!user) return console.log(`${colors.red("[ERROR]")} User ${row.userid} not found`); - - // fetch the product text - trySend = () => { - fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => { - // If neither the body nor the product text contains the filter, ignore it - res.text().then((text) => { - if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return; - thisMsg = JSON.parse(JSON.stringify(discordMsg)); - thisMsg.content = row.custommessage || null; - user.send(thisMsg).catch((err) => { - console.error(err); - }); - }); - }).catch((err) => { - setTimeout(() => { - console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`) - trySend(); - }) - });; - } - trySend(); - }); - }); - } -}); - - - -xmpp.on("online", async (address) => { - if (config["uptime-kuma"].enabled) { - fetch(config["uptime-kuma"].url).then(() => { - console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`) - }) - setInterval(() => { - // Send POST request to config["uptime-kuma"].url - fetch(config["uptime-kuma"].url).then(() => { - console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`) - }) - }, config["uptime-kuma"].interval * 1000) // Every X seconds - } - - errCount = 0; - // Start listening on all channels, (dont ban me funny man) - // for (const channel in iem) { - // console.log(`Joining ${channel.name}`) - // await xmpp.send(xml("presence", { to: `${channel.jud}/${channel.name}` })); - // } - /* sub format - - - - visitor - - - - */ - - // Request room list - // Automatically find room list - xmpp.send(xml("iq", { type: "get", to: "conference.weather.im", id: "rooms" }, xml("query", { xmlns: "http://jabber.org/protocol/disco#items" }))); - // Join all channels (Old method) - // iem.forEach((channel => { - // console.log(`${colors.cyan("[INFO]")} Joining ${channel.jid}/${channel.name}/${curUUID}`) - // //xmpp.send(xml("presence", { to: `${channel.jid}/${channel.jid.split("@")[0]}` })); - // xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" }))); - // })) - - console.log(`${colors.cyan("[INFO]")} Connected to XMPP server as ${address.toString()}`); - - setTimeout(() => { - startup = false; - console.log(`${colors.cyan("[INFO]")} Startup complete, listening for messages...`); - }, 1000) -}); - -xmpp.on("close", () => { - console.log(`${colors.yellow("[WARN]")} XMPP connection closed, trying to reconnect...`); - xmpp.disconnect().then(() => { - xmpp.stop().then(() => { - start(); - }) - }) -}) - -const start = () => { - startup = true; - xmpp.start().catch((err) => { - errCount++; - if (errCount >= 5) { - console.log(`${colors.red("[ERROR]")} XMPP failed to start after 5 attempts, exiting...`); - process.exit(1); - } - console.log(`${colors.red("[ERROR]")} XMPP failed to start: ${err}.`); - xmpp.disconnect().then(() => { - xmpp.stop().then(() => { - start(); - }) - }) - }); -} - -// END XMPP // START DISCORD +// listen on ws +wsClient.on("message", (data) => { + +}); + discord.on('ready', async () => { console.log(`${colors.cyan("[INFO]")} Logged in as ${discord.user.tag}`); @@ -591,121 +265,8 @@ discord.on('ready', async () => { console.log(`${colors.cyan("[INFO]")} In guild: ${guild.name} (${guild.id})`); }); - // Do slash command stuff - commands = require("./data/commands.json"); - // Add dynamic commands (based on datas files) - satCommand = { - "name": "sattelite", - "description": "Get the latest sattelite images from a given sattelite", - "options": [ - { - "name": "sattelite", - "description": "The sattelite to get images from", - "type": 3, - "required": true, - "choices": [] - } - ] - } - for (const key in sattelites) { - // Push the key to the choices array - satCommand.options[0].choices.push({ - "name": key, - "value": key - }); - } - commands.push(satCommand); - if (config.broadcastify.enabled) { - // Add commands to join vc, leave vc, and play stream - commands.push( - { - "name": "playbcfy", - "description": "Play the broadcastify stream", - "options": [ - { - "name": "id", - "description": "The ID of the stream to play", - "type": 3, - "required": true - } - ] - } - ) - } - if (config.voice_enabled) { - // Add commands to join vc, leave vc, and play stream - commands.push( - { - "name": "leave", - "description": "Leave the current voice chat", - "default_member_permissions": 0 - }, - { - "name": "play", - "type": 1, - "description": "Play a stream", - "options": [ - { - "name": "url", - "description": "The URL of the stream to play", - "type": 3, - "required": true - } - ] - }, - { - "name": "pause", - "description": "Pause/Unpause the current stream", - "type": 1 - }, - { - "name": "volume", - "description": "Set the volume of the current stream", - "options": [ - { - "name": "volume", - "description": "The volume to set", - "type": 4, - "required": true - } - ] - } - ) - - nwrplayCommand = { - "name": "nwrplay", - "description": "Nwr stream", - "type": 1, - "options": [ - { - "name": "callsign", - "description": "The URL of the stream to play", - "type": 3, - "required": true, - "choices": [] - } - ] - } - for (const key in nwrstreams.callsigns) { - nwrplayCommand.options[0].choices.push({ - "name": key, - "value": key - }); - } - commands.push(nwrplayCommand); - } - await (async () => { - try { - //Global - if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands: ${JSON.stringify(commands, null, 2)}`); - await rest.put(Routes.applicationCommands(discord.user.id), { body: commands }) - } catch (error) { - console.error(error); - } - })(); - - start(); + //start(); setTimeout(() => { // Wait 10 seconds, if startup is still true, something went wrong if (startup) { @@ -894,56 +455,64 @@ discord.on("interactionCreate", async (interaction) => { 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 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.`, - 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() + // 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)", } - } - interaction.reply({ embeds: [embed] }); - }) + ] + }) }); break; case "rooms": @@ -1030,10 +599,11 @@ discord.on("interactionCreate", async (interaction) => { 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 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](${invite.url})`, + description: `Need help with the bot? Join the support server [here](https://discord.gg/XthJjfU8TU)`, color: 0x00ff00 } interaction.reply({ embeds: [embed] }); @@ -1345,38 +915,38 @@ discord.on("interactionCreate", async (interaction) => { }); -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("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 - } - ] - }) -}) +// 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}`); diff --git a/shard.js b/shard.js new file mode 100644 index 0000000..f02c5eb --- /dev/null +++ b/shard.js @@ -0,0 +1,562 @@ +const config = require("./config.json"); +const { + REST, + Routes +} = require('discord.js'); +const rest = new REST({ + version: '10' +}).setToken(config.discord.token); +const sqlite3 = require("sqlite3").verbose(); +const ws = require("ws"); + +// 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`); + // Create tables if they dont exist + db.run(`CREATE TABLE IF NOT EXISTS channels (channelid TEXT, iemchannel TEXT, custommessage TEXT, minPriority INTEGER, "filter" TEXT, filterevt TEXT);`); + db.run(`CREATE TABLE IF NOT EXISTS userAlerts (userid TEXT, iemchannel TEXT, filter TEXT, filterEvt TEXT, minPriority INT, custommessage TEXT);`); +}); + +// setup ws server here for the shards to connect to +const wss = new ws.Server({ port: 9743 }); + +// Requires +const fs = require("fs"); +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 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 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] }; +} + +// 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 messages = 0; +var errCount = 0; +const curUUID = generateUUID(); + + +const xmpp = client({ + service: "xmpp://conference.weather.im", + domain: "weather.im", + resource: `discord-weather-bot-${generateRandomString({ upper: true, lower: true, number: true }, 5)}`, // Weird fix to "Username already in use" +}); + +//debug(xmpp, true); + +xmpp.on("error", (err) => { + console.log(`${colors.red("[ERROR]")} XMPP Error: ${err}. Trying to reconnect...`); + setTimeout(() => { + xmpp.stop().then(() => { + start(); + }); + }, 5000); +}); + +xmpp.on("offline", () => { + console.log(`${colors.yellow("[WARN]")} XMPP offline, trying to reconnect...`); + xmpp.disconnect().then(() => { + xmpp.stop().then(() => { + start(); + }) + }) +}); + +xmpp.on("stanza", (stanza) => { + // Debug stuff + if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`); + + + // Handle Room List + if (stanza.is("iq") && stanza.attrs.type === "result" && stanza.getChild("query")) { + query = stanza.getChild("query"); + if (query.attrs.xmlns === "http://jabber.org/protocol/disco#items") { + query.getChildren("item").forEach((item) => { + // Check if the JID is on the blacklist, if so, ignore it + if (blacklist.includes(item.attrs.jid)) return; + // get proper name from wfos + const wfo = getWFOByRoom(item.attrs.jid.split("@")[0]); + item.attrs.properName = wfo.location; + iem.push(item.attrs); + console.log(`${colors.cyan("[INFO]")} Found room: ${item.attrs.jid}`); + // Join the room + //xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" }))); + xmpp.send(xml("presence", { to: `${item.attrs.jid}/${curUUID}` }, xml("item", { role: "visitor" }))); + }); + } + } + // Get new messages and log them, ignore old messages + if (stanza.is("message") && stanza.attrs.type === "groupchat") { + // Stops spam from getting old messages + if (startup) return; + // Get channel name + fromChannel = stanza.attrs.from.split("@")[0]; + // Ignores + if (!stanza.getChild("x")) return; // No PID, ignore it + if (!stanza.getChild("x").attrs.product_id) return; + + const product_id = parseProductID(stanza.getChild("x").attrs.product_id); + const product_id_raw = stanza.getChild("x").attrs.product_id; + // Get body of message + const body = html.decode(stanza.getChildText("body")); + const bodyData = getFirstURL(body); + // get product id from "x" tag + var evt = events[product_id.pil.substring(0, 3)]; + + if (!evt) { + evt = { name: "Unknown", priority: 3 } + console.log(`${colors.red("[ERROR]")} Unknown event type: ${product_id.pil.substring(0, 3)}. Fix me`); + console.log(`${colors.magenta("[DEBUG]")} ${bodyData.string}`) + } + + evt.code = product_id.pil.substring(0, 3); + // Check timestamp, if not within 3 minutes, ignore it + const now = new Date(); + const diff = (now - product_id.timestamp) / 1000 / 60; + if (diff > 3) return; + if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} New message from ${fromChannel}`); + messages++; + + + // Handle NTFY + if (config.ntfy.enabled) { + if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Sending NTFY for ${config.ntfy.prefix}${fromChannel}`) + ntfyBody = { + "topic": `${config.ntfy.prefix}${fromChannel}`, + "message": bodyData.string, + "tags": [`Timestamp: ${product_id.timestamp}`, `Station: ${product_id.station}`, `WMO: ${product_id.wmo}`, `PIL: ${product_id.pil}`, `Channel: ${fromChannel}`], + "priority": evt.priority, + "actions": [{ "action": "view", "label": "Product", "url": bodyData.url }, { "action": "view", "label": "Product Text", "url": `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}` }] + } + if (stanza.getChild("x").attrs.twitter_media) { + ntfyBody.attach = stanza.getChild("x").attrs.twitter_media; + } + fetch(config.ntfy.server, { + method: 'POST', + body: JSON.stringify(ntfyBody), + headers: { + 'Authorization': `Bearer ${config.ntfy.token}` + } + }).then((res) => { + if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} NTFY sent for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`); + if (res.status !== 200) console.log(`${colors.red("[ERROR]")} NTFY failed for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`); + + + }).catch((err) => { + console.error(err) + }) + } + + + // Send discord msg + let embed = { + description: ` ${bodyData.string}`, + color: parseInt(config.priorityColors[evt.priority].replace("#", ""), 16) || 0x000000, + timestamp: product_id.timestamp, + footer: { + text: `Station: ${product_id.station} PID: ${product_id_raw} Channel: ${fromChannel}` + } + } + if (stanza.getChild("x").attrs.twitter_media) { + embed.image = { + url: stanza.getChild("x").attrs.twitter_media + } + } + + let discordMsg = { + embeds: [embed], + components: [ + { + type: 1, + components: [ + { + type: 2, + label: "Product", + style: 5, + url: bodyData.url + }, + { + type: 2, + style: 1, + custom_id: product_id_raw, + label: "Product Text", + emoji: { + name: "📄" + } + } + ] + } + ] + } + // Discord Channel Handling + db.all(`SELECT * FROM channels WHERE iemchannel = ?`, [fromChannel], (err, rows) => { + if (err) { + console.log(`${colors.red("[ERROR]")} ${err.message}`); + } + if (!rows) return; // No channels to alert + rows.forEach((row) => { + // Get Filters as arrays + if (!row.filterEvt) row.filterEvt = ""; + if (!row.filter) row.filter = ""; + let filterEvt = row.filterEvt.toLowerCase().split(","); + let filters = row.filter.toLowerCase().split(","); + if (evt.priority < row.minPriority) return; + // If the event type is not in th filter, ignore it. Make sure filterEvt isnt null + if (!filterEvt[0]) filterEvt = []; + if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return; + + // fetch the product text + trySend = () => { + fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => { + // If neither the body nor the product text contains the filter, ignore it + res.text().then((text) => { + if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return; + thisMsg = JSON.parse(JSON.stringify(discordMsg)); + thisMsg.content = row.custommessage || null; + // send data to all websocket clients + wss.clients.forEach((client) => { + client.send(JSON.stringify({ + type: "DISCORD_MESSAGE_CREATE", + data: { + channelid: row.channelid, + message: thisMsg + } + })); + }); + }); + }).catch((err) => { + setTimeout(() => { + console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`) + trySend(); + }) + }); + } + trySend(); + }); + }); + + + // User DM alert handling + db.all(`SELECT * FROM userAlerts WHERE iemchannel = ?`, [fromChannel], (err, rows) => { + if (err) { + console.error(err.message); + } + if (!rows) return; // No users to alert + rows.forEach((row) => { + // Get Filters as arrays + if (!row.filterEvt) row.filterEvt = ""; + if (!row.filter) row.filter = ""; + let filterEvt = row.filterEvt.toLowerCase().split(","); + let filters = row.filter.toLowerCase().split(","); + + // If priority is less than the min priority, ignore it + if (evt.priority < row.minPriority) return; + // If the event type is not in th filter, ignore it. Make sure filterEvt isnt null + if (!filterEvt[0]) filterEvt = []; + if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return; + + // fetch the product text + trySend = () => { + fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => { + // If neither the body nor the product text contains the filter, ignore it + res.text().then((text) => { + if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return; + thisMsg = JSON.parse(JSON.stringify(discordMsg)); + thisMsg.content = row.custommessage || null; + manager.broadcast({ + type: "SHARD_DM_MESSAGE_CREATE", + data: { + userid: row.userid, + message: thisMsg + } + }) + }); + }).catch((err) => { + setTimeout(() => { + console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`) + trySend(); + }) + });; + } + trySend(); + }); + }); + } +}); + + + +xmpp.on("online", async (address) => { + if (config["uptime-kuma"].enabled) { + fetch(config["uptime-kuma"].url).then(() => { + console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`) + }) + setInterval(() => { + // Send POST request to config["uptime-kuma"].url + fetch(config["uptime-kuma"].url).then(() => { + console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`) + }) + }, config["uptime-kuma"].interval * 1000) // Every X seconds + } + + errCount = 0; + // Start listening on all channels, (dont ban me funny man) + // for (const channel in iem) { + // console.log(`Joining ${channel.name}`) + // await xmpp.send(xml("presence", { to: `${channel.jud}/${channel.name}` })); + // } + /* sub format + + + + visitor + + + + */ + + // Request room list + // Automatically find room list + xmpp.send(xml("iq", { type: "get", to: "conference.weather.im", id: "rooms" }, xml("query", { xmlns: "http://jabber.org/protocol/disco#items" }))); + // Join all channels (Old method) + // iem.forEach((channel => { + // console.log(`${colors.cyan("[INFO]")} Joining ${channel.jid}/${channel.name}/${curUUID}`) + // //xmpp.send(xml("presence", { to: `${channel.jid}/${channel.jid.split("@")[0]}` })); + // xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" }))); + // })) + + console.log(`${colors.cyan("[INFO]")} Connected to XMPP server as ${address.toString()}`); + + setTimeout(() => { + startup = false; + console.log(`${colors.cyan("[INFO]")} Startup complete, listening for messages...`); + }, 1000) +}); + +xmpp.on("close", () => { + console.log(`${colors.yellow("[WARN]")} XMPP connection closed, trying to reconnect...`); + xmpp.disconnect().then(() => { + xmpp.stop().then(() => { + start(); + }) + }) +}) + +const start = () => { + startup = true; + xmpp.start().catch((err) => { + errCount++; + if (errCount >= 5) { + console.log(`${colors.red("[ERROR]")} XMPP failed to start after 5 attempts, exiting...`); + process.exit(1); + } + console.log(`${colors.red("[ERROR]")} XMPP failed to start: ${err}.`); + xmpp.disconnect().then(() => { + xmpp.stop().then(() => { + start(); + }) + }) + }); +} + +// END XMPP + +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)}`); + + 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); + return; +}); + +const { ShardingManager } = require('discord.js'); + +const manager = new ShardingManager('./index.js', { token: config.discord.token }); + +manager.on('shardCreate', shard => console.log(`Launched shard ${shard.id}`)); + +manager.spawn().then(() => { + start(); +}); \ No newline at end of file