diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..8d436aa --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,7 @@ +# Privacy Policy + +Just to make this simple, heres a list of whats stored and how it's used +- Discord channel/user IDs of subscribed channels/DMs - Should be obvious, in case it's not, we need these to know where to send alerts. +- IEM rooms and filters - Should also be obvious, but again, we need to know which rooms you subscribed to + +Other than that, occasional debug logging may be enabled to fix bugs, and any logs gathered from debugging will be erased immidiately. \ No newline at end of file diff --git a/TERMS.md b/TERMS.md new file mode 100644 index 0000000..d578ee4 --- /dev/null +++ b/TERMS.md @@ -0,0 +1,5 @@ +# Terms of Service +## This is mostly here to comply with Discord verified bot stuff + +Other than complying with the GPL-3.0 License when contributing/using the code of this project, this bot is NOT to be used as an emergency alerting system, and should NOT be trusted with life/property under any circumstances. +Chris Chrome and any other contributors are not to be held liable should this not work in an emergency, as you shouldn't be using it for emergency alerts. \ No newline at end of file diff --git a/data/blacklist.json b/data/blacklist.json index c390eb2..f0b3e61 100644 --- a/data/blacklist.json +++ b/data/blacklist.json @@ -91,6 +91,4 @@ "zsechat@conference.weather.im", "zdcchat@conference.weather.im", "znychat@conference.weather.im" - - ] \ No newline at end of file diff --git a/data/events.json b/data/events.json index 8422c4c..d4cbc95 100644 --- a/data/events.json +++ b/data/events.json @@ -217,7 +217,7 @@ }, "DSW": { "text": "Dust Storm Warning", - "priority": 5 + "priority": 4 }, "EFP": { "priority": 1, @@ -365,7 +365,7 @@ }, "FFW": { "text": "Flash Flood Warning", - "priority": 4 + "priority": 5 }, "FLN": { "priority": 1, @@ -385,7 +385,7 @@ }, "FRW": { "text": "Fire Warning", - "priority": 4 + "priority": 3 }, "FSH": { "priority": 1, @@ -680,7 +680,7 @@ "text": "Data Mgt Message" }, "NPW": { - "priority": 1, + "priority": 3, "text": "Non-Precipitation Warnings / Watches / Advisories" }, "NSH": { @@ -1160,7 +1160,7 @@ "text": "Tropical Cyclone Update" }, "TCV": { - "priority": 1, + "priority": 4, "text": "Tropical Cyclone Watch/Warning Break Points" }, "TIB": { @@ -1308,7 +1308,7 @@ "text": "Routine Space Environment Product Issued Weekly" }, "WOU": { - "priority": 4, + "priority": 5, "text": "Tornado/Severe Thunderstorm Watch" }, "WS1": { @@ -1345,7 +1345,7 @@ }, "WSW": { "text": "Winter Storm Warning", - "priority": 5 + "priority": 4 }, "WWA": { "priority": 1, @@ -1365,11 +1365,11 @@ }, "CFA": { "text": "Coastal Flood Watch", - "priority": 4 + "priority": 3 }, "FLA": { "text": "Flood Watch", - "priority": 2 + "priority": 3 }, "HWA": { "text": "High Wind Watch", @@ -1389,7 +1389,7 @@ }, "SVA": { "text": "Severe Thunderstorm Watch", - "priority": 4 + "priority": 5 }, "TOA": { "text": "Tornado Watch", @@ -1405,7 +1405,7 @@ }, "TSA": { "text": "Tsunami Watch", - "priority": 4 + "priority": 5 }, "TSW": { "text": "Tsunami Warning", @@ -1522,6 +1522,9 @@ "REP": { "text": "RECCO Observations (tropical cyclone)", "priority": 3 + }, + "PIR": { + "text": "Pilot Reports", + "priority": 1 } - } diff --git a/data/outlook.json b/data/outlook.json index a6450b3..a00c52d 100644 --- a/data/outlook.json +++ b/data/outlook.json @@ -1,13 +1,13 @@ { "convective": [ - "https://www.spc.noaa.gov/products/outlook/day1otlk.gif", - "https://www.spc.noaa.gov/products/outlook/day2otlk.gif", - "https://www.spc.noaa.gov/products/outlook/day3otlk.gif", - "https://www.spc.noaa.gov/products/exper/day4-8/day4prob.gif", - "https://www.spc.noaa.gov/products/exper/day4-8/day5prob.gif", - "https://www.spc.noaa.gov/products/exper/day4-8/day6prob.gif", - "https://www.spc.noaa.gov/products/exper/day4-8/day7prob.gif", - "https://www.spc.noaa.gov/products/exper/day4-8/day8prob.gif" + "https://weather.cod.edu/cdata/text/images/spc/co/day1/categorical/spccoday1.categorical.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day2/categorical/spccoday2.categorical.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day3/categorical/spccoday3.categorical.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day4/severe/spccoday4.severe.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day5/severe/spccoday5.severe.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day6/severe/spccoday6.severe.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day7/severe/spccoday7.severe.latest.png", + "https://climate.cod.edu/data/text/images/spc/co/day8/severe/spccoday8.severe.latest.png" ], "fire": [ "https://www.spc.noaa.gov/products/exper/fire_wx/imgs/day1otlk_fire.gif", diff --git a/data/satellites.json b/data/satellites.json new file mode 100644 index 0000000..c82967e --- /dev/null +++ b/data/satellites.json @@ -0,0 +1,187 @@ +{ + "GOES-16": { + "products": { + "Full Disk": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/GEOCOLOR/1808x1808.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/AirMass/1808x1808.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/13/1808x1808.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/10/1808x1808.jpg" + }, + "Floater 1": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M1/GEOCOLOR/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M1/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M1/10/1000x1000.jpg" + }, + "Floater 2": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M2/GEOCOLOR/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M2/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/MESO/M2/10/1000x1000.jpg" + }, + "United States": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/GEOCOLOR/2500x1500.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/AirMass/2500x1500.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/13/2500x1500.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/10/2500x1500.jpg" + }, + "Canada": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/can/GEOCOLOR/2250x1125.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/can/AirMass/2250x1125.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/can/13/2250x1125.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/can/10/2250x1125.jpg" + }, + "Mexico": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/mex/GEOCOLOR/1000x1000.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/mex/AirMass/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/mex/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/mex/10/1000x1000.jpg" + }, + "US East Coast": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/eus/GEOCOLOR/1000x1000.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/eus/AirMass/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/eus/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/eus/10/1000x1000.jpg" + }, + "Gulf of Mexico": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/gm/GEOCOLOR/1000x1000.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/gm/AirMass/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/gm/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/gm/10/1000x1000.jpg" + }, + "Puerto Rico": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/pr/GEOCOLOR/1200x1200.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/pr/AirMass/1200x1200.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/pr/13/1200x1200.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/pr/10/1200x1200.jpg" + } + } + }, + "GOES-18": { + "products": { + "Full Disk": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/FD/GEOCOLOR/1808x1808.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/FD/AirMass/1808x1808.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/FD/13/1808x1808.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/FD/10/1808x1808.jpg" + }, + "Floater 1": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M1/GEOCOLOR/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M1/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M1/10/1000x1000.jpg" + }, + "Floater 2": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M2/GEOCOLOR/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M2/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/MESO/M2/10/1000x1000.jpg" + }, + "US West Coast": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/wus/GEOCOLOR/1000x1000.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/wus/AirMass/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/wus/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/SECTOR/wus/10/1000x1000.jpg" + }, + "Hawaii": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/hi/GEOCOLOR/1200x1200.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/hi/AirMass/1200x1200.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/hi/13/1200x1200.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/hi/10/1200x1200.jpg" + }, + "Alaska": { + "Visible": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/ak/GEOCOLOR/1000x1000.jpg", + "Airmass": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/ak/AirMass/1000x1000.jpg", + "Infrared": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/ak/13/1000x1000.jpg", + "Water Vapor": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/ak/10/1000x1000.jpg" + } + } + }, + "Himawari": { + "products": { + "Full Disk": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/full_disk_ahi_true_color.jpg", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/full_disk_ahi_rgb_airmass.jpg", + "Infrared": "https://www.ssec.wisc.edu/data/geo/images/himawari09/latest-himawari09_11_fd.gif", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/himawari09/latest-himawari09_10_fd.gif" + }, + "Floater 1": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest/himawari-8/floater_02_geocolor.pngv", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest/himawari-8/floater_02_rgb_airmass.png" + }, + "American Samoa": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/american_samoa_ahi_natural_color.png", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/american_samoa_ahi_rgb_airmass.png" + }, + "Australia": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest/himawari-8/australia_true_color.jpg", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/australia_ahi_rgb_airmass.png" + }, + "New Zealand": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/new_zealand_ahi_natural_color.png", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/new_zealand_ahi_rgb_airmass.png" + }, + "Guam": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/guam_ahi_natural_color.png", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/guam_ahi_rgb_airmass.png" + }, + "Hawaii": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/hawaii_ahi_natural_color.png" + }, + "Japan": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/japan_ahi_natural_color.png", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/japan_ahi_rgb_airmass.png" + }, + "Russia": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest/himawari-8/eastern_russia_true_color.jpg" + }, + "China": { + "Visible": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/eastern_china_ahi_natural_color.png", + "Airmass": "https://rammb.cira.colostate.edu/ramsdis/online/images/latest_hi_res/himawari-8/eastern_china_ahi_rgb_airmass.png" + } + } + }, + "EWS-G2": { + "products": { + "Full Disk": { + "Visible": "https://www.ssec.wisc.edu/data/geo/images/ews-g1/latest_ews-g1_01_fd.gif", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/ews-g1/latest_ews-g1_03_fd.gif" + } + } + }, + "FY-2G": { + "products": { + "Full Disk": { + "Visible": "https://www.ssec.wisc.edu/data/geo/images/fy2g/latest_fy2g_01_fd.gif", + "Infrared": "https://www.ssec.wisc.edu/data/geo/images/fy2g/latest_fy2g_02_fd.gif", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/fy2g/latest_fy2g_04_fd.gif" + } + } + }, + "GK-2A": { + "products": { + "Full Disk": { + "Infrared": "https://kiwiweather.com/gk-2a/FD_sanchez.jpg" + } + } + }, + "Meteosat 9": { + "products": { + "Full Disk": { + "Visible": "https://www.ssec.wisc.edu/data/geo/images/met-iodc/latest_met-iodc_01_fd.jpg", + "Infrared": "https://www.ssec.wisc.edu/data/geo/images/met-iodc/latest_met-iodc_04_fd.jpg", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/met-iodc/latest_met-iodc_06_fd.jpg" + } + } + }, + "Meteosat 10": { + "products": { + "Full Disk": { + "Visible": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_01_fd.gif", + "Infrared": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_04_fd.gif", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_06_fd.gif" + }, + "Europe": { + "Visible": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_01_euro.gif", + "Infrared": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_04_euro.gif", + "Water Vapor": "https://www.ssec.wisc.edu/data/geo/images/met-prime/latest_met-prime_06_euro.gif" + } + } + } +} \ No newline at end of file diff --git a/data/sattelites.json b/data/sattelites.json deleted file mode 100644 index 94cda82..0000000 --- a/data/sattelites.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "GOES-16": [ - { - "name": "GeoColor", - "url": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/GEOCOLOR/latest.jpg" - }, - { - "name": "Infrared", - "url": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/CONUS/13/latest.jpg" - }, - { - "name": "FullDisk", - "url": "https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/GEOCOLOR/678x678.jpg" - } - ], - "GOES-18": [ - { - "name": "GeoColor", - "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/CONUS/GEOCOLOR/latest.jpg" - }, - { - "name": "Infrared", - "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/CONUS/13/latest.jpg" - }, - { - "name": "FullDisk", - "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/FD/GEOCOLOR/678x678.jpg" - } - ] -} \ No newline at end of file diff --git a/index.js b/index.js index abdce84..47ee6b2 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,8 @@ 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 satellites = require("./data/satellites.json"); +const nwrstreams = {callsigns:{}}; const Jimp = require("jimp"); const { client, xml } = require("@xmpp/client"); const fetch = require("node-fetch"); @@ -16,8 +16,12 @@ const Discord = require("discord.js"); const dVC = require("@discordjs/voice"); const colors = require("colors"); const sqlite3 = require("sqlite3").verbose(); + +satMessages = {}; + // Setup Discord const discord = new Discord.Client({ + intents: [ "Guilds", "GuildVoiceStates", @@ -32,7 +36,6 @@ const rest = new REST({ version: '10' }).setToken(config.discord.token); - // Setup SQlite DB const db = new sqlite3.Database("channels.db", (err) => { if (err) { @@ -40,8 +43,9 @@ const db = new sqlite3.Database("channels.db", (err) => { } 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 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);`); + db.run(`ALTER TABLE channels RENAME COLUMN filterevt TO filterEvt;`) }); @@ -85,7 +89,17 @@ const getUniqueChannels = function () { if (err) { console.error(err.message); } - resolve(rows.length); + // 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 }); }); }); } @@ -250,6 +264,24 @@ var errCount = 0; const curUUID = generateUUID(); +// nwrstreams setup +// get icecast json data +const fetchNWRstreams = () => { + fetch("https://icestats.weatherradio.org/").then((res) => { + res.json().then((json) => { + json.icestats.source.forEach((source) => { + nwrstreams.callsigns[source.server_name] = source.listenurl; + }); + }); + console.log(`${colors.cyan("[INFO]")} Fetched NWR streams`); + }).catch((err) => { + console.error(err); + }); +} + +fetchNWRstreams(); +setInterval(fetchNWRstreams, 5 * 60 * 1000); // Every 5 minutes + const xmpp = client({ service: "xmpp://conference.weather.im", domain: "weather.im", @@ -276,6 +308,8 @@ xmpp.on("offline", () => { }) }); +var restartTimer = null; + xmpp.on("stanza", (stanza) => { // Debug stuff if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`); @@ -301,6 +335,11 @@ xmpp.on("stanza", (stanza) => { } // Get new messages and log them, ignore old messages if (stanza.is("message") && stanza.attrs.type === "groupchat") { + clearTimeout(restartTimer) + restartTimer = setTimeout(() => { + console.log(`${colors.red("[FATAL]")} No messages from weather.im in 10 minutes, restarting!!!!!!!!!!!`) + process.exit(1) + }, 600000) // Stops spam from getting old messages if (startup) return; // Get channel name @@ -321,6 +360,16 @@ xmpp.on("stanza", (stanza) => { 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}`) + const logChannel = discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.logChannel); + logChannel.send({ + embeds: [ + { + title: "Unknown Event Type", + description: `Unknown event type: ${product_id.pil.substring(0, 3)}. Please check the logs for more details.`, + color: 0xff0000 + } + ] + }); } evt.code = product_id.pil.substring(0, 3); @@ -328,13 +377,14 @@ xmpp.on("stanza", (stanza) => { 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}`); + // if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} New message from ${fromChannel}`); + console.log(`${colors.cyan("[INFO]")} ${getWFOByRoom(fromChannel).location} - ${evt.text} - ${product_id.timestamp}`); messages++; // Handle NTFY if (config.ntfy.enabled) { - if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Sending NTFY for ${config.ntfy.prefix}${fromChannel}`) + //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, @@ -352,7 +402,7 @@ xmpp.on("stanza", (stanza) => { '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 (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}`); @@ -392,7 +442,7 @@ xmpp.on("stanza", (stanza) => { { type: 2, style: 1, - custom_id: product_id_raw, + custom_id: `product|${product_id_raw}`, label: "Product Text", emoji: { name: "📄" @@ -408,7 +458,7 @@ xmpp.on("stanza", (stanza) => { console.log(`${colors.red("[ERROR]")} ${err.message}`); } if (!rows) return; // No channels to alert - rows.forEach((row) => { + rows.forEach(async (row) => { // Get Filters as arrays if (!row.filterEvt) row.filterEvt = ""; if (!row.filter) row.filter = ""; @@ -434,6 +484,54 @@ xmpp.on("stanza", (stanza) => { console.error(err); }).then((msg) => { if (msg.channel.type === Discord.ChannelType.GuildAnnouncement) msg.crosspost(); + }).catch((err) => { + console.log(`${colors.yellow("[WARN]")} Failed to send message to ${channel.guild.name}/${channel.name} (${channel.guild.id}/${channel.id})`); + const logChannel = discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.logChannel); + logChannel.send({ + embeds: [ + { + title: "Failed to send message", + description: `There is likely an issue with permissions. Please notify the server owner if possible. + Guild: ${channel.guild.name} (${channel.guild.id}) + Channel: ${channel.name} (${channel.id}) + Guild Owner: <@${channel.guild.ownerId}> (${channel.guild.ownerId}) + Sub Info: \`\`\`json\n${JSON.stringify(row)}\`\`\``, + color: 0xff0000 + } + ] + }); + + discord.users.fetch(channel.guild.ownerId).then((user) => { + user.send({ + embeds: [ + { + title: "Issue with your subscribed channel.", + description: `There is likely an issue with permissions. Please check that I can send messages in <#${channel.id}>\nYour subscription has been removed, and you will need to resubscribe to get alerts.`, + color: 0xff0000, + fields: [ + { + name: "Guild", + value: `${channel.guild.name} (${channel.guild.id})` + }, + { + name: "Channel", + value: `${channel.name} (${channel.id})` + } + ] + } + ] + }).catch((err) => { + console.log(`${colors.red("[ERROR]")} Failed to send message to ${channel.guild.ownerId}`); + }).then(() => { + if (channel.guildId == config.discord.mainGuild) return; + db.run(`DELETE FROM channels WHERE channelid = ? AND iemchannel = ?`, [channel.id, fromChannel], (err) => { + if (err) { + console.error(err.message); + } + console.log(`${colors.cyan("[INFO]")} Deleted channel ${channel.id} from database`); + }); + }) + }); }); }); }).catch((err) => { @@ -479,6 +577,27 @@ xmpp.on("stanza", (stanza) => { thisMsg.content = row.custommessage || null; user.send(thisMsg).catch((err) => { console.error(err); + }).catch((err) => { + console.log(`${colors.yellow("[WARN]")} Failed to send message to ${user.tag} (${user.id})`); + const logChannel = discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.logChannel); + logChannel.send({ + embeds: [ + { + title: "Failed to send DM", + description: `User may have DMs disabled, or bot doesnt' share a server anymore!. + User: ${user.tag} (${user.id}) + Sub Info: \`\`\`json\n${JSON.stringify(row)}\`\`\``, + color: 0xff0000 + } + ] + }).then(() => { + db.run(`DELETE FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [user.id, fromChannel], (err) => { + if (err) { + console.error(err.message); + } + console.log(`${colors.cyan("[INFO]")} Deleted user ${user.id} from database`); + }); + }) }); }); }).catch((err) => { @@ -585,20 +704,19 @@ discord.on('ready', async () => { 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", + "name": "satellite", + "description": "Get the latest satellite images from a given satellite", "options": [ { - "name": "sattelite", - "description": "The sattelite to get images from", + "name": "satellite", + "description": "The satellite to get images from", "type": 3, "required": true, "choices": [] } ] } - for (const key in sattelites) { - // Push the key to the choices array + for (const key in satellites) { satCommand.options[0].choices.push({ "name": key, "value": key @@ -673,22 +791,16 @@ discord.on('ready', async () => { "description": "The URL of the stream to play", "type": 3, "required": true, - "choices": [] + "autocomplete": true } ] } - 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)}`); + if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands`); await rest.put(Routes.applicationCommands(discord.user.id), { body: commands }) } catch (error) { console.error(error); @@ -753,21 +865,26 @@ discord.on("interactionCreate", async (interaction) => { 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) => { + interaction.channel.send("Permission check").then((msg) => { + msg.delete(); + 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 { - interaction.reply({ content: `Subscribed to \`${getWFOByRoom(room).location}\``, 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 }); + } + }); }); + }).catch((err) => { + interaction.reply({ content: "Failed to subscribe to room. Bot does not have send message permissions here!", ephemeral: true }); }); } else { // We're in a DM db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => { @@ -891,7 +1008,8 @@ discord.on("interactionCreate", async (interaction) => { channels = row.count await getUniqueChannels().then((unique) => { - uniques = unique; + uniques = unique.channels; + guilds = unique.guilds; }); discord.users.fetch("289884287765839882").then((chrisUser) => { const embed = { @@ -916,12 +1034,12 @@ discord.on("interactionCreate", async (interaction) => { }, { name: "Subscribed Rooms", - value: channels.toLocaleString(), + value: `${channels.toLocaleString()}`, inline: true }, { name: "Unique Channels", - value: uniques.toLocaleString(), + value: `${uniques.toLocaleString()} in ${guilds} guilds.`, inline: true } ], @@ -1237,48 +1355,42 @@ discord.on("interactionCreate", async (interaction) => { 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 }); + case "satellite": // Get satellite images + sat = interaction.options.getString("satellite"); + if (!satellites[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` + productOptions = [] + await (() => { + for (const key in satellites[sat].products) { + // make a discord customid safe id for the product name, add it to the satellites object + satellites[sat].products[key].customid = key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); + productOptions.push({ + label: key, + value: satellites[sat].products[key].customid + }) + } + console.log(JSON.stringify(productOptions, null, 2)) + })(); + satMessages[interaction.id] = { + sat + } + await interaction.reply({ + content: "Choose a product", + components: [ + { + type: 1, + components: [ + { + type: 3, + custom_id: `satproduct|${interaction.id}`, + label: "Product", + // map options to product names + options: productOptions } - }); - // 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": @@ -1299,51 +1411,198 @@ discord.on("interactionCreate", async (interaction) => { } 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; + if (!interaction.customId) return; + switch (interaction.customId.split("|")[0]) { + case "product": + if (interaction.customId) { + const product_id = interaction.customId.split("|")[1]; + 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}`); + }); } - // 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 }); - }) + break; + case "satproduct": + satData = satMessages[interaction.customId.split("|")[1]]; + sat = satData.sat + product = interaction.values[0]; + // find the original product name + product_name = Object.keys(satellites[sat].products).find(key => satellites[sat].products[key].customid === product); + imageOptions = [] + satMessages[interaction.customId.split("|")[1]] = { + sat, + product, + product_name, + images: {} + } + await (() => { + // for key, value in satellites[sat].products[product_name] + console.log(product_name) + for (const key in satellites[sat].products[product_name]) { + // make a discord customid safe id for the product name, add it to the satellites object + //console.log(satellites[sat].products[product_name]) + if (key === "customid") continue; + satMessages[interaction.customId.split("|")[1]].images[key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()] = satellites[sat].products[product_name][key]; + imageOptions.push({ + label: key, + value: key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() + }) + } + })(); + interaction.deferReply(); + await interaction.message.edit({ + content: "Choose an image to view", + components: [ + { + type: 1, + components: [ + { + type: 3, + custom_id: `satproduct2|${interaction.customId.split("|")[1]}`, + label: "Image", + // map options to product names + options: imageOptions + } + ] + } + ] + }).then(() => { + interaction.deleteReply(); }); - }).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; + case "satproduct2": + satData = satMessages[interaction.customId.split("|")[1]]; + sat = satData.sat; + product = satData.product; + product_name = satData.product_name; + image = interaction.values[0]; + url = satData.images[image]; + // get filename from url + filename = url.split("/").pop(); + interaction.deferReply(); + // Get the image + fetch(url).then((res) => { + if (res.status !== 200) { + interaction.message.edit({ content: "Failed to get image", ephemeral: true }).then(() => { + interaction.deleteReply(); + }); + return; + } + embeds = []; + files = []; + + res.buffer().then(async (buffer) => { + files.push({ + attachment: buffer, + name: filename + }) + embeds.push({ + title: `${sat}/${product_name}/${image}`, + image: { + url: `attachment://${filename}` + }, + color: 0x00ff00 + }); + interaction.message.edit({ + embeds, + files, + components: [], + content: null + }).then(() => { + interaction.deleteReply(); + }); + } + ); + }).catch((err) => { + interaction.message.edit({ content: "Failed to get image", ephemeral: true }).then(() => { + interaction.deleteReply(); + }); + console.log(`${colors.red("[ERROR]")} Failed to get image: ${err.stack}`); + }); + break; } break; + + case Discord.InteractionType.ApplicationCommandAutocomplete: + //map nwrstreams + if (interaction.commandName === "nwrplay") { + let callsignSearch = interaction.options.getString("callsign"); + let callsigns = Object.keys(nwrstreams.callsigns); + let results = callsigns.filter((callsign) => callsign.toLowerCase().includes(callsignSearch.toLowerCase())); + if (results.length > 25) { + results = results.slice(0, 25); + } + interaction.respond(results.map((callsign) => ({ name: callsign, value: callsign }))); + } + break; + } - - }); -discord.on("guildCreate", (guild) => { +discord.on("guildCreate", async (guild) => { + let logs = await guild.fetchAuditLogs() + logs = logs.entries.filter(e => e.action === Discord.AuditLogEvent.BotAdd) + let user = logs.find(l => l.target?.id === discord.user.id)?.executor // 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 + let invite = await discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.inviteChannel).createInvite(); + user.send({ + embeds: [{ + description: `Thanks for adding ${discord.user.username}!\nIf you have **ANY** questions, comments, suggestions, bug reports, etc, please feel free to throw it by us in our support server!\n\nTo get started use \`/subscribe\` to get alerts!`, + color: 0x00ff00 + }], + components: [ + { + type: Discord.ComponentType.ActionRow, + components: [ + { + type: Discord.ComponentType.Button, + url: `https://discord.gg/${invite.code}`, + style: Discord.ButtonStyle.Link, + emoji: "ℹ", + label: "IEM Alerter Support Server" + } + ] + } + ] + }).catch((err) => { + console.log(`${colors.red("[ERROR]")} Failed to send message to user ${user.id}: ${err.message}`); + }) channel.send({ embeds: [ { description: `I joined \`${guild.name}\``, + fields: [ + { + "name": "User", + "value": `<@${user.id}> (@${user.username}) ${user.displayName}` + } + ], color: 0x00ff00 } ] @@ -1368,7 +1627,7 @@ discord.on("guildDelete", (guild) => { }) process.on("unhandledRejection", (error, promise) => { - console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error}`); + console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error.stack}`); // create errors folder if it doesnt exist if (!fs.existsSync("./error")) { fs.mkdirSync("./error"); @@ -1411,5 +1670,7 @@ process.on("uncaughtException", (error) => { return; }); + + // Login to discord discord.login(config.discord.token); \ No newline at end of file diff --git a/package.json b/package.json index 1b812e6..12f4929 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@xmpp/client": "^0.13.1", "@xmpp/debug": "^0.13.0", "colors": "^1.4.0", - "discord.js": "^14.15.2", + "discord.js": "14.14.1", "geolib": "^3.3.4", "html-entities": "^2.5.2", "jimp": "^0.22.12",