From 7b4eacb78c48eaf090f82740a31f5b855ca4f3ff Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Mon, 1 Jul 2024 01:59:17 -0600 Subject: [PATCH] AAA --- bot.js | 493 +++++++++++++++++-------------------------------------- index.js | 338 +++++++++++++++++++++++++++++++++++++- 2 files changed, 478 insertions(+), 353 deletions(-) diff --git a/bot.js b/bot.js index eba18b5..b47d030 100644 --- a/bot.js +++ b/bot.js @@ -3,15 +3,13 @@ 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 html = require("html-entities"); 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"); @@ -25,18 +23,18 @@ const discord = new Discord.Client({ "DirectMessages" ], shards: getInfo().SHARD_LIST, // An array of shards that will get spawned - shardCount: getInfo().TOTAL_SHARDS, // Total number of shards + shardCount: getInfo().TOTAL_SHARDS, // Total number of shards }); discord.cluster = new ClusterClient(discord); // initialize the Client, so we access the .broadcastEval() const { REST, Routes } = require('discord.js'); +const { default: cluster } = require("cluster"); const rest = new REST({ version: '10' }).setToken(config.discord.token); - // Setup SQlite DB const db = new sqlite3.Database("channels.db", (err) => { if (err) { @@ -218,368 +216,182 @@ function setVolume(channel, volume) { } } -// 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; -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" +discord.cluster.on("message", async (message) => { + if (!message.type) return; // Invalid message, just ignore, prob me being stupid + switch (message.type) { + case "addChannel": // Add a channel to the iem array + iem.push(message.data); + break; + case "stanza": // Got a stanza, parse it + handleStanza(message.data); + break; + } }); -//debug(xmpp, true); +const handleStanza = (stanza) => { + 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)]; -xmpp.on("error", (err) => { - console.log(`${colors.red("[ERROR]")} XMPP Error: ${err}. Trying to reconnect...`); - setTimeout(() => { - xmpp.stop().then(() => { - start(); - }); - }, 5000); -}); + 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}`) + } -xmpp.on("offline", () => { - console.log(`${colors.yellow("[WARN]")} XMPP offline, trying to reconnect...`); - xmpp.disconnect().then(() => { - xmpp.stop().then(() => { - start(); - }) - }) -}); + 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++; -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" }))); - }); + // 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}` } } - // 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}`) + if (stanza.getChild("x").attrs.twitter_media) { + embed.image = { + url: stanza.getChild("x").attrs.twitter_media } + } - 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: "📄" - } + 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; + ] + } + // 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`); + 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(); - }); + // 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); + }).catch((err) => { + setTimeout(() => { + console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`) + trySend(); + }) + }); } - 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(); - }); + 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); + // User DM alert handling + db.all(`SELECT * FROM userAlerts WHERE iemchannel = ?`, [fromChannel], (err, rows) => { + if (err) { + console.error(err.message); } - console.log(`${colors.red("[ERROR]")} XMPP failed to start: ${err}.`); - xmpp.disconnect().then(() => { - xmpp.stop().then(() => { - start(); - }) - }) + 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(); + }); }); } -// END XMPP - // START DISCORD discord.on('ready', async () => { console.log(`${colors.cyan("[INFO]")} Logged in as ${discord.user.tag} cluster id ${discord.cluster.id}`); - + // Get all guilds, and log them discord.guilds.cache.forEach((guild) => { console.log(`${colors.cyan("[INFO]")} In guild: ${guild.name} (${guild.id})`); @@ -692,22 +504,13 @@ discord.on('ready', async () => { await (async () => { try { //Global - //if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands: ${JSON.stringify(commands, null, 2)}`); + // 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(); - 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) { diff --git a/index.js b/index.js index 3a026d9..ea16fee 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,336 @@ // Typescript: import { ClusterManager } from 'discord-hybrid-sharding' +const config = require("./config.json"); +const blacklist = require("./data/blacklist.json"); +const wfos = require("./data/wfos.json"); +const events = require("./data/events.json"); +const html = require("html-entities"); +const { client, xml } = require("@xmpp/client"); +const colors = require("colors") const { ClusterManager } = require('discord-hybrid-sharding'); -const config = require("./config.json") const manager = new ClusterManager(`${__dirname}/bot.js`, { - totalShards: 'auto', // or numeric shard count - /// Check below for more options - shardsPerClusters: 2, // 2 shards per process - totalClusters: 2, - mode: 'process', // you can also choose "worker" - token: config.discord.token, + totalShards: 2, // or numeric shard count + /// Check below for more options + shardsPerClusters: 1, // 2 shards per process + //totalClusters: 2, + mode: 'process', // you can also choose "worker" + token: config.discord.token, }); -manager.on('clusterCreate', cluster => console.log(`Launched Cluster ${cluster.id}`)); +// func to Generate random string, ({upper, lower, number, special}, length) +// 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); + } + resolve(rows.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] }; +} +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; +} + +// 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 + }; +} + +// 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); +} + +// Vars +var startup = true; +var iem = [] +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); + manager.broadcast({type: "addChannel", data: 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}`); + + + // 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) + }) + } + // Handle Discord + manager.broadcast({type: "stanza", stanza}) + } +}); + + + +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(); + }) + }) + }); +} + +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) + manager.spawn({ timeout: -1 }); \ No newline at end of file