Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

15 changed files with 204 additions and 903 deletions

View file

@ -1,15 +0,0 @@
# Workflow that runs on `chris-runner`, tells a pterodactyl server at https://panel.chrischro.me/ to restart server 8f44f1fd-8d29-4553-96f1-05dacfca454f with token stored in PTERO_KEY
name: ptero-push
on:
push:
branches:
- main
jobs:
build:
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run script
run: |
curl -X POST -H "Authorization: Bearer ${{ secrets.PTERO_KEY }}" -H "Content-Type: application/json" https://panel.chrischro.me/api/client/servers/8f44f1fd-8d29-4553-96f1-05dacfca454f/power -d '{"signal":"restart"}'

View file

@ -1 +0,0 @@
{}

View file

@ -1,7 +0,0 @@
# 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.

View file

@ -1,5 +0,0 @@
# 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.

View file

@ -1,6 +1,5 @@
{ {
"debug": 0, "debug": 0,
"voice_enabled": true,
"priorityColors": { "priorityColors": {
"1": "#00AFFF", "1": "#00AFFF",
"2": "#00FF00", "2": "#00FF00",
@ -27,6 +26,7 @@
"username": "YOUR_USERNAME", "username": "YOUR_USERNAME",
"password": "YOUR_PASSWORD" "password": "YOUR_PASSWORD"
}, },
"voice_enabled": true,
"uptime-kuma": { "uptime-kuma": {
"enabled": false, "enabled": false,

View file

@ -91,4 +91,6 @@
"zsechat@conference.weather.im", "zsechat@conference.weather.im",
"zdcchat@conference.weather.im", "zdcchat@conference.weather.im",
"znychat@conference.weather.im" "znychat@conference.weather.im"
] ]

View file

@ -2,7 +2,7 @@
{ {
"name": "subscribe", "name": "subscribe",
"description": "Subscribe to a weather.im room", "description": "Subscribe to a weather.im room",
"default_member_permissions": 16, "default_member_permissions": 0,
"options": [ "options": [
{ {
"name": "room", "name": "room",
@ -66,7 +66,7 @@
{ {
"name": "unsubscribe", "name": "unsubscribe",
"description": "Unsubscribe from a weather.im room", "description": "Unsubscribe from a weather.im room",
"default_member_permissions": 16, "default_member_permissions": 0,
"options": [ "options": [
{ {
"name": "room", "name": "room",
@ -81,13 +81,11 @@
{ {
"name": "list", "name": "list",
"description": "List all subscribed rooms for this channel", "description": "List all subscribed rooms for this channel",
"default_member_permissions": 16 "default_member_permissions": 0
}, },
{ {
"name": "about", "name": "about",
"description": "About this bot", "description": "About this bot"
"integration_types": [0,1],
"contexts": [0, 1, 2]
}, },
{ {
"name": "rooms", "name": "rooms",
@ -95,23 +93,19 @@
}, },
{ {
"name": "setupall", "name": "setupall",
"description": "[BOT OWNER ONLY] Setup channels in a category for all rooms", "description": "[OWNER ONLY] Setup channels in a category for all rooms",
"default_member_permissions": 0, "default_member_permissions": 0,
"type": 1 "type": 1
}, },
{ {
"name": "support", "name": "support",
"description": "Get support for the bot", "description": "Get support for the bot",
"type": 1, "type": 1
"integration_types": [0,1],
"contexts": [0, 1, 2]
}, },
{ {
"name": "outlook", "name": "outlook",
"description": "Get day 1-8 storm or fire outlook from the SPC", "description": "Get day 1-8 storm or fire outlook from the SPC",
"type": 1, "type": 1,
"integration_types": [0,1],
"contexts": [0, 1, 2],
"options": [ "options": [
{ {
"name": "day", "name": "day",
@ -228,29 +222,6 @@
{ {
"name": "alertmap", "name": "alertmap",
"description": "Get a map of active alerts", "description": "Get a map of active alerts",
"type": 1, "type": 1
"integration_types": [0,1],
"contexts": [0, 1, 2]
},
{
"name": "forecast",
"description": "Get a forecast for a location",
"type": 1,
"integration_types": [0,1],
"contexts": [0, 1, 2],
"options": [
{
"name": "location",
"description": "Location to get forecast for (In the United States)",
"type": 3,
"required": true
},
{
"name": "periods",
"description": "Number of periods to get forecast for",
"type": 4,
"required": false
}
]
} }
] ]

View file

@ -217,7 +217,7 @@
}, },
"DSW": { "DSW": {
"text": "Dust Storm Warning", "text": "Dust Storm Warning",
"priority": 4 "priority": 5
}, },
"EFP": { "EFP": {
"priority": 1, "priority": 1,
@ -365,7 +365,7 @@
}, },
"FFW": { "FFW": {
"text": "Flash Flood Warning", "text": "Flash Flood Warning",
"priority": 5 "priority": 4
}, },
"FLN": { "FLN": {
"priority": 1, "priority": 1,
@ -385,7 +385,7 @@
}, },
"FRW": { "FRW": {
"text": "Fire Warning", "text": "Fire Warning",
"priority": 3 "priority": 4
}, },
"FSH": { "FSH": {
"priority": 1, "priority": 1,
@ -680,7 +680,7 @@
"text": "Data Mgt Message" "text": "Data Mgt Message"
}, },
"NPW": { "NPW": {
"priority": 3, "priority": 1,
"text": "Non-Precipitation Warnings / Watches / Advisories" "text": "Non-Precipitation Warnings / Watches / Advisories"
}, },
"NSH": { "NSH": {
@ -1160,7 +1160,7 @@
"text": "Tropical Cyclone Update" "text": "Tropical Cyclone Update"
}, },
"TCV": { "TCV": {
"priority": 4, "priority": 1,
"text": "Tropical Cyclone Watch/Warning Break Points" "text": "Tropical Cyclone Watch/Warning Break Points"
}, },
"TIB": { "TIB": {
@ -1308,7 +1308,7 @@
"text": "Routine Space Environment Product Issued Weekly" "text": "Routine Space Environment Product Issued Weekly"
}, },
"WOU": { "WOU": {
"priority": 5, "priority": 4,
"text": "Tornado/Severe Thunderstorm Watch" "text": "Tornado/Severe Thunderstorm Watch"
}, },
"WS1": { "WS1": {
@ -1345,7 +1345,7 @@
}, },
"WSW": { "WSW": {
"text": "Winter Storm Warning", "text": "Winter Storm Warning",
"priority": 4 "priority": 5
}, },
"WWA": { "WWA": {
"priority": 1, "priority": 1,
@ -1365,11 +1365,11 @@
}, },
"CFA": { "CFA": {
"text": "Coastal Flood Watch", "text": "Coastal Flood Watch",
"priority": 3 "priority": 4
}, },
"FLA": { "FLA": {
"text": "Flood Watch", "text": "Flood Watch",
"priority": 3 "priority": 2
}, },
"HWA": { "HWA": {
"text": "High Wind Watch", "text": "High Wind Watch",
@ -1389,7 +1389,7 @@
}, },
"SVA": { "SVA": {
"text": "Severe Thunderstorm Watch", "text": "Severe Thunderstorm Watch",
"priority": 5 "priority": 4
}, },
"TOA": { "TOA": {
"text": "Tornado Watch", "text": "Tornado Watch",
@ -1405,7 +1405,7 @@
}, },
"TSA": { "TSA": {
"text": "Tsunami Watch", "text": "Tsunami Watch",
"priority": 5 "priority": 4
}, },
"TSW": { "TSW": {
"text": "Tsunami Warning", "text": "Tsunami Warning",
@ -1514,17 +1514,5 @@
"GMT": { "GMT": {
"text": "AIRMET", "text": "AIRMET",
"priority": 1 "priority": 1
},
"AHO": {
"text": "High Density Observations (USAF/NOAA)",
"priority": 3
},
"REP": {
"text": "RECCO Observations (tropical cyclone)",
"priority": 3
},
"PIR": {
"text": "Pilot Reports",
"priority": 1
} }
} }

View file

@ -1,11 +0,0 @@
{
"callsigns": {
"WXL46": "https://icecast.sirenarchive.xyz/NWR/WXL46",
"WXJ61": "https://icecast.sirenarchive.xyz/NWR/WXJ61",
"KZZ57": "https://wxradio.org/IL-Rockford-KZZ57",
"KXI41": "https://wxradio.org/IL-CrystalLake-KXI41",
"KHB34": "https://wxradio.org/FL-Miami-KHB34",
"WNG663": "https://wxradio.org/FL-Princeton-WNG663",
"KEC80": "https://wxradio.org/GA-Atlanta-KEC80"
}
}

View file

@ -1,13 +1,13 @@
{ {
"convective": [ "convective": [
"https://weather.cod.edu/cdata/text/images/spc/co/day1/categorical/spccoday1.categorical.latest.png", "https://www.spc.noaa.gov/products/outlook/day1otlk.gif",
"https://climate.cod.edu/data/text/images/spc/co/day2/categorical/spccoday2.categorical.latest.png", "https://www.spc.noaa.gov/products/outlook/day2otlk.gif",
"https://climate.cod.edu/data/text/images/spc/co/day3/categorical/spccoday3.categorical.latest.png", "https://www.spc.noaa.gov/products/outlook/day3otlk.gif",
"https://climate.cod.edu/data/text/images/spc/co/day4/severe/spccoday4.severe.latest.png", "https://www.spc.noaa.gov/products/exper/day4-8/day4prob.gif",
"https://climate.cod.edu/data/text/images/spc/co/day5/severe/spccoday5.severe.latest.png", "https://www.spc.noaa.gov/products/exper/day4-8/day5prob.gif",
"https://climate.cod.edu/data/text/images/spc/co/day6/severe/spccoday6.severe.latest.png", "https://www.spc.noaa.gov/products/exper/day4-8/day6prob.gif",
"https://climate.cod.edu/data/text/images/spc/co/day7/severe/spccoday7.severe.latest.png", "https://www.spc.noaa.gov/products/exper/day4-8/day7prob.gif",
"https://climate.cod.edu/data/text/images/spc/co/day8/severe/spccoday8.severe.latest.png" "https://www.spc.noaa.gov/products/exper/day4-8/day8prob.gif"
], ],
"fire": [ "fire": [
"https://www.spc.noaa.gov/products/exper/fire_wx/imgs/day1otlk_fire.gif", "https://www.spc.noaa.gov/products/exper/fire_wx/imgs/day1otlk_fire.gif",

View file

@ -1,187 +0,0 @@
{
"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"
}
}
}
}

30
data/sattelites.json Normal file
View file

@ -0,0 +1,30 @@
{
"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"
}
]
}

138
funcs.js
View file

@ -1,138 +0,0 @@
const geolib = require("geolib");
// Use OSM API to get coordinates https://nominatim.openstreetmap.org/search?q=search+query&format=json&limit=1
const getCoordinates = async (location) => {
return new Promise((resolve, reject) => {
// Make location url friendly
location = encodeURIComponent(location);
const url = `https://nominatim.openstreetmap.org/search?q=${location}&format=json&limit=1`;
// use custom useragent (discord-iem-bot, chris@chrischro.me)
const options = {
headers: {
'User-Agent': 'Discord-IEM-Bot/1.0 (chris@chrischro.me)',
},
};
// Make request
fetch(url, options)
.then(response => response.json())
.then(data => {
if (data.length > 0) {
resolve({
lat: data[0].lat,
lon: data[0].lon,
});
} else {
reject('Location not found');
}
})
.catch(err => {
reject(err);
});
})
};
const getForecast = async (lat, lon) => {
return new Promise((resolve, reject) => {
const url = `https://api.weather.gov/points/${lat},${lon}`;
// use same custom ua
const options = {
headers: {
'User-Agent': 'Discord-IEM-Bot/1.0 (chris@chrischro.me)',
},
};
// Make request
fetch(url, options)
.then(response => response.json())
.then(data => {
if (data.properties?.forecast) {
fetch(data.properties.forecast)
.then(response => response.json())
.then(data2 => {
data2.properties.relativeLocation = data.properties.relativeLocation;
resolve(data2);
})
.catch(err => {
reject(err);
});
} else {
reject('Forecast not found');
}
})
.catch(err => {
reject(err);
});
})
};
const getWeatherBySearch = async (search) => {
return new Promise((resolve, reject) => {
getCoordinates(search)
.then(coords => {
getForecast(coords.lat, coords.lon)
.then(data => {
resolve(data);
})
.catch(err => {
reject(err);
});
})
.catch(err => {
reject(err);
});
})
};
const generateDiscordEmbeds = (forecastData, numOfDays) => {
// Take the first 7 periods and make them into embeds
const embeds = [];
if (!numOfDays) numOfDays = 1;
for (let i = 0; i < numOfDays; i++) {
const period = forecastData.properties.periods[i];
if (period.icon?.startsWith("/")) {
period.icon = `https://api.weather.gov${period.icon}`
}
const embed = {
title: `${period.name} in ${forecastData.properties.relativeLocation.properties.city} ${forecastData.properties.relativeLocation.properties.state}`,
description: `Valid <t:${new Date(period.startTime)/1000}:f> - <t:${new Date(period.endTime)/1000}:f>\n${period.detailedForecast}`,
timestamp: new Date(period.startTime),
thumbnail: {
url: period.icon,
},
fields: [
{
name: 'Temperature',
value: `${period.temperature}°F`,
inline: true
},
{
name: 'Wind',
value: `${period.windDirection}@${period.windSpeed}`,
inline: true
},
{
name: 'Precipitation',
value: period.probabilityOfPrecipitation?.value ? period.probabilityOfPrecipitation?.value + '%' : '0%',
inline: true
},
{
name: 'Humidity',
value: period.relativeHumidity?.value ? period.relativeHumidity.value + '%' : "0%",
}
],
footer: {
text: `Data provided by the National Weather Service · Elevation ${forecastData.properties.elevation.value}m`
}
};
embeds.push(embed);
}
return embeds;
}
module.exports = {
getCoordinates,
getForecast,
getWeatherBySearch,
generateDiscordEmbeds
};

613
index.js
View file

@ -1,13 +1,11 @@
// Requires // Requires
const fs = require("fs"); const fs = require("fs");
const config = require("./config.json"); const config = require("./config.json");
const funcs = require("./funcs.js");
const wfos = require("./data/wfos.json"); const wfos = require("./data/wfos.json");
const blacklist = require("./data/blacklist.json"); const blacklist = require("./data/blacklist.json");
const events = require("./data/events.json"); const events = require("./data/events.json");
const outlookURLs = require("./data/outlook.json"); const outlookURLs = require("./data/outlook.json");
const satellites = require("./data/satellites.json"); const sattelites = require("./data/sattelites.json");
const nwrstreams = {callsigns:{}};
const Jimp = require("jimp"); const Jimp = require("jimp");
const { client, xml } = require("@xmpp/client"); const { client, xml } = require("@xmpp/client");
const fetch = require("node-fetch"); const fetch = require("node-fetch");
@ -16,12 +14,8 @@ const Discord = require("discord.js");
const dVC = require("@discordjs/voice"); const dVC = require("@discordjs/voice");
const colors = require("colors"); const colors = require("colors");
const sqlite3 = require("sqlite3").verbose(); const sqlite3 = require("sqlite3").verbose();
satMessages = {};
// Setup Discord // Setup Discord
const discord = new Discord.Client({ const discord = new Discord.Client({
intents: [ intents: [
"Guilds", "Guilds",
"GuildVoiceStates", "GuildVoiceStates",
@ -43,9 +37,8 @@ const db = new sqlite3.Database("channels.db", (err) => {
} }
console.log(`${colors.cyan("[INFO]")} Connected to the database`); console.log(`${colors.cyan("[INFO]")} Connected to the database`);
// Create tables if they dont exist // 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(`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;`)
}); });
@ -89,24 +82,14 @@ const getUniqueChannels = function () {
if (err) { if (err) {
console.error(err.message); console.error(err.message);
} }
// Go through channels. and get number of unique guilds resolve(rows.length);
const guilds = [];
rows.forEach((row) => {
const channel = discord.channels.cache.get(row.channelid);
if (!channel) return;
if (!guilds.includes(channel.guild.id)) {
guilds.push(channel.guild.id);
}
});
resolve({ channels: rows.length, guilds: guilds.length });
}); });
}); });
} }
// Get first url in a string, return object {string, url} remove the url from the string // Get first url in a string, return object {string, url} remove the url from the string
const getFirstURL = function (string) { const getFirstURL = function (string) {
url = string.match(/(https?:\/\/[^\s]+)/g); const url = string.match(/(https?:\/\/[^\s]+)/g);
if (!url) return { string, url: null }; if (!url) return { string, url: null };
const newString = string.replace(url[0], ""); const newString = string.replace(url[0], "");
return { string: newString, url: url[0] }; return { string: newString, url: url[0] };
@ -264,24 +247,6 @@ var errCount = 0;
const curUUID = generateUUID(); 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({ const xmpp = client({
service: "xmpp://conference.weather.im", service: "xmpp://conference.weather.im",
domain: "weather.im", domain: "weather.im",
@ -308,8 +273,6 @@ xmpp.on("offline", () => {
}) })
}); });
var restartTimer = null;
xmpp.on("stanza", (stanza) => { xmpp.on("stanza", (stanza) => {
// Debug stuff // Debug stuff
if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`); if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`);
@ -335,11 +298,6 @@ xmpp.on("stanza", (stanza) => {
} }
// Get new messages and log them, ignore old messages // Get new messages and log them, ignore old messages
if (stanza.is("message") && stanza.attrs.type === "groupchat") { 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 // Stops spam from getting old messages
if (startup) return; if (startup) return;
// Get channel name // Get channel name
@ -360,16 +318,6 @@ xmpp.on("stanza", (stanza) => {
evt = { name: "Unknown", priority: 3 } evt = { name: "Unknown", priority: 3 }
console.log(`${colors.red("[ERROR]")} Unknown event type: ${product_id.pil.substring(0, 3)}. Fix me`); console.log(`${colors.red("[ERROR]")} Unknown event type: ${product_id.pil.substring(0, 3)}. Fix me`);
console.log(`${colors.magenta("[DEBUG]")} ${bodyData.string}`) 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); evt.code = product_id.pil.substring(0, 3);
@ -377,14 +325,13 @@ xmpp.on("stanza", (stanza) => {
const now = new Date(); const now = new Date();
const diff = (now - product_id.timestamp) / 1000 / 60; const diff = (now - product_id.timestamp) / 1000 / 60;
if (diff > 3) return; 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++; messages++;
// Handle NTFY // Handle NTFY
if (config.ntfy.enabled) { 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 = { ntfyBody = {
"topic": `${config.ntfy.prefix}${fromChannel}`, "topic": `${config.ntfy.prefix}${fromChannel}`,
"message": bodyData.string, "message": bodyData.string,
@ -402,7 +349,7 @@ xmpp.on("stanza", (stanza) => {
'Authorization': `Bearer ${config.ntfy.token}` 'Authorization': `Bearer ${config.ntfy.token}`
} }
}).then((res) => { }).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}`); if (res.status !== 200) console.log(`${colors.red("[ERROR]")} NTFY failed for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`);
@ -442,7 +389,7 @@ xmpp.on("stanza", (stanza) => {
{ {
type: 2, type: 2,
style: 1, style: 1,
custom_id: `product|${product_id_raw}`, custom_id: product_id_raw,
label: "Product Text", label: "Product Text",
emoji: { emoji: {
name: "📄" name: "📄"
@ -458,7 +405,7 @@ xmpp.on("stanza", (stanza) => {
console.log(`${colors.red("[ERROR]")} ${err.message}`); console.log(`${colors.red("[ERROR]")} ${err.message}`);
} }
if (!rows) return; // No channels to alert if (!rows) return; // No channels to alert
rows.forEach(async (row) => { rows.forEach((row) => {
// Get Filters as arrays // Get Filters as arrays
if (!row.filterEvt) row.filterEvt = ""; if (!row.filterEvt) row.filterEvt = "";
if (!row.filter) row.filter = ""; if (!row.filter) row.filter = "";
@ -484,54 +431,6 @@ xmpp.on("stanza", (stanza) => {
console.error(err); console.error(err);
}).then((msg) => { }).then((msg) => {
if (msg.channel.type === Discord.ChannelType.GuildAnnouncement) msg.crosspost(); 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) => { }).catch((err) => {
@ -577,27 +476,6 @@ xmpp.on("stanza", (stanza) => {
thisMsg.content = row.custommessage || null; thisMsg.content = row.custommessage || null;
user.send(thisMsg).catch((err) => { user.send(thisMsg).catch((err) => {
console.error(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) => { }).catch((err) => {
@ -704,19 +582,20 @@ discord.on('ready', async () => {
commands = require("./data/commands.json"); commands = require("./data/commands.json");
// Add dynamic commands (based on datas files) // Add dynamic commands (based on datas files)
satCommand = { satCommand = {
"name": "satellite", "name": "sattelite",
"description": "Get the latest satellite images from a given satellite", "description": "Get the latest sattelite images from a given sattelite",
"options": [ "options": [
{ {
"name": "satellite", "name": "sattelite",
"description": "The satellite to get images from", "description": "The sattelite to get images from",
"type": 3, "type": 3,
"required": true, "required": true,
"choices": [] "choices": []
} }
] ]
} }
for (const key in satellites) { for (const key in sattelites) {
// Push the key to the choices array
satCommand.options[0].choices.push({ satCommand.options[0].choices.push({
"name": key, "name": key,
"value": key "value": key
@ -751,7 +630,6 @@ discord.on('ready', async () => {
}, },
{ {
"name": "play", "name": "play",
"type": 1,
"description": "Play a stream", "description": "Play a stream",
"options": [ "options": [
{ {
@ -780,27 +658,10 @@ discord.on('ready', async () => {
] ]
} }
) )
nwrplayCommand = {
"name": "nwrplay",
"description": "Nwr stream",
"type": 1,
"options": [
{
"name": "callsign",
"description": "The URL of the stream to play",
"type": 3,
"required": true,
"autocomplete": true
}
]
}
commands.push(nwrplayCommand);
} }
await (async () => { await (async () => {
try { try {
//Global //Global
if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands`);
await rest.put(Routes.applicationCommands(discord.user.id), { body: commands }) await rest.put(Routes.applicationCommands(discord.user.id), { body: commands })
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -865,26 +726,21 @@ discord.on("interactionCreate", async (interaction) => {
filterEvt = interaction.options.getString("filterevt") || null; filterEvt = interaction.options.getString("filterevt") || null;
message = interaction.options.getString("message") || null; message = interaction.options.getString("message") || null;
if (interaction.inGuild()) { if (interaction.inGuild()) {
interaction.channel.send("Permission check").then((msg) => { db.get(`SELECT * FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err, row) => {
msg.delete(); if (err) {
db.get(`SELECT * FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err, row) => { console.error(err.message);
interaction.reply({ content: "Failed to subscribe to room", ephemeral: true });
} else if (row) {
return interaction.reply({ content: `Already subscribed to \`${getWFOByRoom(room).location}\`\nIf you want to update a subscribtion, please unsubscribe and resubscribe. This will be made a command eventually.`, ephemeral: true });
}
db.run(`INSERT INTO channels (channelid, iemchannel, custommessage, filter, filterevt, minPriority) VALUES (?, ?, ?, ? ,? ,?)`, [interaction.channel.id, room, message, filter, filterEvt, minPriority], (err) => {
if (err) { if (err) {
console.error(err.message); console.error(err.message);
interaction.reply({ content: "Failed to subscribe to room", ephemeral: true }); interaction.reply({ content: "Failed to subscribe to room", ephemeral: true });
} else if (row) { } else {
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 }); interaction.reply({ content: `Subscribed to \`${getWFOByRoom(room).location}\``, 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 } else { // We're in a DM
db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => { db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => {
@ -1008,49 +864,46 @@ discord.on("interactionCreate", async (interaction) => {
channels = row.count channels = row.count
await getUniqueChannels().then((unique) => { await getUniqueChannels().then((unique) => {
uniques = unique.channels; uniques = unique;
guilds = unique.guilds;
}); });
discord.users.fetch("289884287765839882").then((chrisUser) => { const embed = {
const embed = { title: "About Me!",
title: "About Me!", thumbnail: {
thumbnail: { url: discord.user.avatarURL()
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 <t:${Math.floor(startTimestap / 1000)}>, Started <t:${Math.floor(startTimestap / 1000)}:R>`,
}, },
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: "Caught Messages",
{ value: `Got ${messages.toLocaleString()} messages since startup`,
name: "Uptime", },
value: `Since <t:${Math.floor(startTimestap / 1000)}>, Started <t:${Math.floor(startTimestap / 1000)}:R>`, {
}, name: "Guilds",
{ value: guilds.toLocaleString(),
name: "Caught Messages", inline: true
value: `Got ${messages.toLocaleString()} messages since startup`, },
}, {
{ name: "Subscribed Rooms",
name: "Guilds", value: channels.toLocaleString(),
value: guilds.toLocaleString(), inline: true
inline: true },
}, {
{ name: "Unique Channels",
name: "Subscribed Rooms", value: uniques.toLocaleString(),
value: `${channels.toLocaleString()}`, inline: true
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()
} }
],
color: 0x00ff00,
footer: {
text: "Made by @chrischrome with <3",
icon_url: discord.users.cache.get("289884287765839882").avatarURL()
} }
interaction.reply({ embeds: [embed] }); }
}) interaction.reply({ embeds: [embed] });
}); });
break; break;
case "rooms": case "rooms":
@ -1147,7 +1000,7 @@ discord.on("interactionCreate", async (interaction) => {
break; break;
case "playbcfy": // Play broadcastify stream case "playbcfy": // Play broadcastify stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
if (!config.broadcastify.enabled) return interaction.reply({ content: "Broadcastify is not enabled", ephemeral: true }); if (!config.broadcastify.enabled) return interaction.reply({ content: "Broadcastify is not enabled", ephemeral: true });
streamID = interaction.options.getString("id"); streamID = interaction.options.getString("id");
// Check if the stream ID is valid (up to 10 digit alphanumeric) // Check if the stream ID is valid (up to 10 digit alphanumeric)
@ -1155,7 +1008,7 @@ discord.on("interactionCreate", async (interaction) => {
// Get the stream URL // Get the stream URL
url = `https://${config.broadcastify.username}:${config.broadcastify.password}@audio.broadcastify.com/${streamID}.mp3`; url = `https://${config.broadcastify.username}:${config.broadcastify.password}@audio.broadcastify.com/${streamID}.mp3`;
// Get the channel // Get the channel
channel = interaction.member.voice?.channel; channel = interaction.member.voice.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream // Join the channel and play the stream
res = JoinChannel(channel, url, .1, interaction) res = JoinChannel(channel, url, .1, interaction)
@ -1167,16 +1020,16 @@ discord.on("interactionCreate", async (interaction) => {
break; break;
case "play": // Play generic stream case "play": // Play generic stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
// Use local variables & Get the URL // Get the URL
interactionUrl = interaction.options.getString("url"); url = interaction.options.getString("url");
// Sanity check URL for funny stuff
if (!url.match(/https?:\/\/[^\s]+/)) return interaction.reply({ content: "Invalid URL", ephemeral: true });
// Get the channel // Get the channel
channel = interaction.member.voice?.channel; channel = interaction.member.voice.channel;
// Check if in channel
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream // Join the channel and play the stream
st = JoinChannel(channel, interactionUrl, .1, interaction); st = JoinChannel(channel, url, .1, interaction)
if (st) { if (st) {
interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true }); interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true });
} else { } else {
@ -1184,29 +1037,7 @@ discord.on("interactionCreate", async (interaction) => {
} }
break; break;
case "nwrplay": // Play NWR stream case "leave": // Leaves Channel
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
// Get the callsign
const callsign = interaction.options.getString("callsign");
// Get the URL associated with the callsign
url = nwrstreams.callsigns[callsign];
// Get the voice channel
channel = interaction.member.voice?.channel; // Use a unique variable name
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream
const streamStatus = JoinChannel(channel, url, .1, interaction); // Use a unique variable name
if (streamStatus) {
interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true });
} else {
interaction.reply({ content: `Failed to play stream`, ephemeral: true });
}
break;
case "leave": // Leave Channel
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
channel = interaction.member.voice.channel; channel = interaction.member.voice.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
@ -1219,7 +1050,7 @@ discord.on("interactionCreate", async (interaction) => {
break; break;
case "pause": // Pause/unpause stream case "pause": // Pause/unpause stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
channel = interaction.member.voice.channel; channel = interaction.member.voice.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
res = toggleVoicePause(channel) res = toggleVoicePause(channel)
@ -1230,7 +1061,7 @@ discord.on("interactionCreate", async (interaction) => {
} }
break; break;
case "volume": // Set volume case "volume": // Set volume
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true }); if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
channel = interaction.member.voice.channel; channel = interaction.member.voice.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true }); if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
volume = interaction.options.getInteger("volume") / 100; volume = interaction.options.getInteger("volume") / 100;
@ -1327,9 +1158,9 @@ discord.on("interactionCreate", async (interaction) => {
}); });
break; break;
case "alertmap": case "alertmap":
const alertmapurl = "https://forecast.weather.gov/wwamap/png/US.png" url = "https://forecast.weather.gov/wwamap/png/US.png"
await interaction.deferReply(); await interaction.deferReply();
fetch(alertmapurl).then((res) => { fetch(url).then((res) => {
if (res.status !== 200) { if (res.status !== 200) {
interaction.editReply({ content: "Failed to get alert map", ephemeral: true }); interaction.editReply({ content: "Failed to get alert map", ephemeral: true });
return; return;
@ -1355,54 +1186,47 @@ discord.on("interactionCreate", async (interaction) => {
console.error(err); console.error(err);
}); });
break; break;
case "satellite": // Get satellite images case "sattelite": // Get satellite images
sat = interaction.options.getString("satellite"); sat = interaction.options.getString("sattelite");
if (!satellites[sat]) return interaction.reply({ content: "Invalid satellite", ephemeral: true }); if (!sattelites[sat]) return interaction.reply({ content: "Invalid satellite", ephemeral: true });
// Fetch all the images // Fetch all the images
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
}
]
}
]
})
break;
case "forecast":
await interaction.deferReply(); await interaction.deferReply();
periods = interaction.options.getInteger("periods") || 1; imageBuffers = {};
funcs.getWeatherBySearch(interaction.options.getString("location")).then((weather) => { embeds = [];
if (config.debug >= 1) console.log(JSON.stringify(weather, null, 2)) files = [];
embeds = funcs.generateDiscordEmbeds(weather, periods); sattelites[sat].forEach(async (imgData) => {
interaction.editReply({ embeds }); // Get a buffer for the data, and put that in imageBuffers with the "name" as the key
}).catch((err) => { fetch(imgData.url).then((res) => {
interaction.editReply({ content: "Failed to get forecast", ephemeral: true }); if (res.status !== 200) {
if (config.debug >= 1) console.log(`${colors.red("[ERROR]")} Failed to get forecast: ${err}`); interaction.editReply({ content: "Failed to get satellite images", ephemeral: true });
return;
}
res.buffer().then((buffer) => {
imageBuffers[imgData.name] = buffer;
files.push({
attachment: buffer,
name: `${imgData.name}.jpg`
});
embeds.push({
title: `${sat} ${imgData.name}`,
image: {
url: `attachment://${imgData.name}.jpg`
}
});
// Check if we have all the images
if (Object.keys(imageBuffers).length === sattelites[sat].length) {
// Send the images
interaction.editReply({
embeds,
files
});
}
});
}).catch((err) => {
interaction.editReply({ content: "Failed to get satellite images", ephemeral: true });
console.log(`${colors.red("[ERROR]")} Failed to get satellite images: ${err.message}`);
console.error(err);
});
}); });
break; break;
@ -1411,198 +1235,51 @@ discord.on("interactionCreate", async (interaction) => {
} }
case Discord.InteractionType.MessageComponent: case Discord.InteractionType.MessageComponent:
if (!interaction.customId) return; if (interaction.customId) {
switch (interaction.customId.split("|")[0]) { const product_id = interaction.customId;
case "product": const url = `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id}`;
if (interaction.customId) { await interaction.deferReply({ ephemeral: true });
const product_id = interaction.customId.split("|")[1]; fetch(url).then((res) => {
url = `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id}`; if (res.status !== 200) {
await interaction.deferReply({ ephemeral: true }); interaction.reply({ content: "Failed to get product text", ephemeral: true });
fetch(url).then((res) => { return;
if (res.status !== 200) {
interaction.reply({ content: "Failed to get product text", ephemeral: true });
return;
}
// Retruns raw text, paginate it into multiple embeds if needed
res.text().then(async (text) => {
const pages = text.match(/[\s\S]{1,1900}(?=\n|$)/g);
// const embeds = pages.map((page, ind) => ({
// title: `Product Text for ${product_id} Pg ${ind + 1}/${pages.length}`,
// description: `\`\`\`${page}\`\`\``,
// color: 0x00ff00
// }));
const messages = pages.map((page, ind) => {
return `\`\`\`${page}\`\`\``
})
messages.forEach(async (message) => {
interaction.followUp({ content: message, ephemeral: true });
})
});
}).catch((err) => {
interaction.reply({ content: "Failed to get product text", ephemeral: true });
console.log(`${colors.red("[ERROR]")} Failed to get product text: ${err.message}`);
});
} }
break; // Retruns raw text, paginate it into multiple embeds if needed
case "satproduct": res.text().then(async (text) => {
satData = satMessages[interaction.customId.split("|")[1]]; const pages = text.match(/[\s\S]{1,2000}(?=\n|$)/g);
sat = satData.sat // const embeds = pages.map((page, ind) => ({
product = interaction.values[0]; // title: `Product Text for ${product_id} Pg ${ind + 1}/${pages.length}`,
// find the original product name // description: `\`\`\`${page}\`\`\``,
product_name = Object.keys(satellites[sat].products).find(key => satellites[sat].products[key].customid === product); // color: 0x00ff00
imageOptions = [] // }));
satMessages[interaction.customId.split("|")[1]] = { const messages = pages.map((page, ind) => {
sat, return `\`\`\`${page}\`\`\``
product, })
product_name, messages.forEach(async (message) => {
images: {} interaction.followUp({ content: message, ephemeral: true });
} })
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();
}); });
break; }).catch((err) => {
case "satproduct2": interaction.reply({ content: "Failed to get product text", ephemeral: true });
satData = satMessages[interaction.customId.split("|")[1]]; console.log(`${colors.red("[ERROR]")} Failed to get product text: ${err.message}`);
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; 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", async (guild) => { discord.on("guildCreate", (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 // Get the main guild
const myGuild = discord.guilds.cache.get(config.discord.mainGuild); const myGuild = discord.guilds.cache.get(config.discord.mainGuild);
// Get the log channel // Get the log channel
const channel = myGuild.channels.cache.get(config.discord.logChannel); const channel = myGuild.channels.cache.get(config.discord.logChannel);
// Send a message to the log channel // 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({ channel.send({
embeds: [ embeds: [
{ {
description: `I joined \`${guild.name}\``, description: `I joined \`${guild.name}\``,
fields: [
{
"name": "User",
"value": `<@${user.id}> (@${user.username}) ${user.displayName}`
}
],
color: 0x00ff00 color: 0x00ff00
} }
] ]
@ -1627,7 +1304,7 @@ discord.on("guildDelete", (guild) => {
}) })
process.on("unhandledRejection", (error, promise) => { process.on("unhandledRejection", (error, promise) => {
console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error.stack}`); console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error}`);
// create errors folder if it doesnt exist // create errors folder if it doesnt exist
if (!fs.existsSync("./error")) { if (!fs.existsSync("./error")) {
fs.mkdirSync("./error"); fs.mkdirSync("./error");
@ -1670,7 +1347,5 @@ process.on("uncaughtException", (error) => {
return; return;
}); });
// Login to discord // Login to discord
discord.login(config.discord.token); discord.login(config.discord.token);

View file

@ -14,8 +14,7 @@
"@xmpp/client": "^0.13.1", "@xmpp/client": "^0.13.1",
"@xmpp/debug": "^0.13.0", "@xmpp/debug": "^0.13.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"discord.js": "14.14.1", "discord.js": "^14.15.2",
"geolib": "^3.3.4",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"jimp": "^0.22.12", "jimp": "^0.22.12",
"sodium": "^3.0.2", "sodium": "^3.0.2",