Compare commits

...

81 commits
main ... main

Author SHA1 Message Date
MoSiren 664e00b7f4 Update data/events.json
Some checks failed
ptero-push / build (push) Has been cancelled
updated some pri codes
2025-04-18 19:35:53 -06:00
MoSiren cce9b12645 Update data/events.json
Some checks are pending
ptero-push / build (push) Waiting to run
Flash Flood Warning Is Level 5
2025-04-18 19:30:46 -06:00
MoSiren c82ab68c1e Nexlabs Outlooks
Some checks failed
ptero-push / build (push) Has been cancelled
2025-03-13 23:51:03 -06:00
Christopher Cookman a543386394 Made the same mistake twice, dang
Some checks failed
ptero-push / build (push) Has been cancelled
2025-02-12 07:57:20 -07:00
Christopher Cookman 1ae72ac979 hotfix
Some checks are pending
ptero-push / build (push) Waiting to run
2025-02-12 07:56:27 -07:00
Christopher Cookman 1a7c1735f4 Fix log?
Some checks are pending
ptero-push / build (push) Waiting to run
2025-02-12 07:54:59 -07:00
Christopher Cookman 5b487125aa Update some logging stuff
Some checks are pending
ptero-push / build (push) Waiting to run
2025-02-12 07:51:27 -07:00
Christopher Cookman 770813b4a7 Whooooops
Some checks failed
ptero-push / build (push) Has been cancelled
2025-02-09 10:30:11 -07:00
Christopher Cookman ded7a2373c Attempt to not throw error when we cant DM a user!
Some checks are pending
ptero-push / build (push) Waiting to run
2025-02-09 10:29:05 -07:00
Christopher Cookman 79ae2f4c25 Fix sat typo, mb
Some checks are pending
ptero-push / build (push) Waiting to run
2025-02-09 10:23:15 -07:00
Christopher Cookman e02faaab18 Add bypass to deletion for main guild
Some checks failed
ptero-push / build (push) Has been cancelled
2025-02-03 05:04:21 -07:00
Christopher Cookman bd275403e9 Add discord message when unknown event type trips
Some checks failed
ptero-push / build (push) Has been cancelled
2025-01-13 14:36:08 -07:00
Christopher Cookman 9b80f79436 Add Pilot Reports to events 2025-01-13 14:35:07 -07:00
Christopher Cookman 4bb679cef8 Add perm check to subscribe command
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-13 14:24:01 -07:00
Christopher Cookman e952ec60e7 Notify guild owners and remove subs
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 23:36:26 -07:00
Christopher Cookman 0227979176 Add full sub info to permission errors
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:18:24 -07:00
Christopher Cookman 9f31d18a6f Smol fix
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:06:31 -07:00
Christopher Cookman d491fda5c8 Im dumb
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:04:56 -07:00
Christopher Cookman 96912d325a Make embed smaller
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:04:09 -07:00
Christopher Cookman 00f97fd6e6 Damn, busy night lol
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:02:13 -07:00
Christopher Cookman 09f3fc957c Fix issue
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:01:17 -07:00
Christopher Cookman cbb1842100 Add message to log channel about failed sends
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 03:00:31 -07:00
Christopher Cookman f763a95472 bwuuuuh
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:54:21 -07:00
Christopher Cookman d88188182c Bwuh
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:52:16 -07:00
Christopher Cookman 8b94e7e3f8 Possibly notify of bad perms?
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:50:54 -07:00
Christopher Cookman d653bc3e90 Add discord error logs
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:41:40 -07:00
Christopher Cookman b5bf06a38d Fix that
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:36:44 -07:00
Christopher Cookman ae63a0d52d Add .catch to sending discord messages. Hopefully this will fix the log flooding lol
Some checks are pending
ptero-push / build (push) Waiting to run
2025-01-12 02:31:17 -07:00
MoSiren 73e42e2288 Updated WSW.
Some checks failed
ptero-push / build (push) Has been cancelled
2024-11-21 17:26:34 -07:00
MoSiren 9afdd5626e Update data/events.json
Some checks failed
ptero-push / build (push) Has been cancelled
2024-09-27 13:48:25 -06:00
MoSiren f6acab4d20 Update data/events.json
Some checks are pending
ptero-push / build (push) Waiting to run
NPW 3
2024-09-27 12:41:01 -06:00
MoSiren b10308ef36 Update data/events.json
Some checks failed
ptero-push / build (push) Has been cancelled
Fixed Hurricane Warnings and Tropical Storm Warnings that Show at lowest level. Now set to High instead of Lowest
2024-09-26 07:57:25 -06:00
Christopher Cookman aa8fe9498f
Update
Some checks failed
ptero-push / build (push) Has been cancelled
- Use cached URL for nwrstreams
2024-08-16 03:11:16 -06:00
Christopher Cookman d48fe3085a
Update
Some checks are pending
ptero-push / build (push) Waiting to run
- Make NWRplay use new GWES icecast server!
2024-08-15 23:09:16 -06:00
Christopher Cookman 662e0e775c
Fix
Some checks are pending
ptero-push / build (push) Waiting to run
- Restart after 10 minutes of inactivity from weather.im
2024-08-15 22:35:44 -06:00
Christopher Cookman 845fead37a
Backend Update
Some checks are pending
ptero-push / build (push) Waiting to run
- Send a message to the user who added the bot to a guild with support link
2024-08-15 22:33:18 -06:00
Christopher Cookman 70c30420fc uh
All checks were successful
ptero-push / build (push) Successful in 11s
2024-07-31 21:08:43 -06:00
Christopher Cookman 98673be5c7
# Major Bug Fix
Some checks failed
ptero-push / build (push) Has been cancelled
- "You had it set to 'W' for Wumbo, when you should've had it set to 'M' for mini."

Fixed a small issue involving capitalization.
TL;DR Fixed evtFilter for guild channels :)
2024-07-16 21:51:11 -06:00
Christopher Cookman aac1420964
guess it cant be a dm thing
Some checks are pending
ptero-push / build (push) Waiting to run
2024-07-15 20:05:50 -06:00
Christopher Cookman 7a12ed05d3
G U H
Some checks are pending
ptero-push / build (push) Waiting to run
2024-07-15 20:03:01 -06:00
Christopher Cookman c223eabc62 Add PRIVACY.md
Some checks failed
ptero-push / build (push) Has been cancelled
Discorf
2024-07-05 16:01:56 -06:00
Christopher Cookman 6b9438ed10 Add TERMS.md
Some checks are pending
ptero-push / build (push) Waiting to run
For discorf
2024-07-05 15:59:44 -06:00
Christopher Cookman 928cb4b958
Use djs 14.14.1 (Weird issue with entitlements?)
Some checks failed
ptero-push / build (push) Has been cancelled
2024-07-02 17:50:47 -06:00
Christopher Cookman 42487f22b6
Guh
Some checks failed
ptero-push / build (push) Has been cancelled
2024-07-01 02:05:12 -06:00
Christopher Cookman 7b96229d01
Nvm
Some checks are pending
ptero-push / build (push) Waiting to run
2024-06-29 22:50:40 -06:00
Christopher Cookman df411d192d
Temporary fix to a much bigger future problem
Some checks are pending
ptero-push / build (push) Waiting to run
2024-06-29 22:47:03 -06:00
Christopher Cookman b76a77807b
Possible fix to #20
All checks were successful
ptero-push / build (push) Successful in 5s
2024-06-22 20:59:27 -06:00
Christopher Cookman 5168c5c582
Update UA
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-20 18:11:15 -06:00
Christopher Cookman 2ff7a2522a
Change | for ·
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-20 17:56:38 -06:00
Christopher Cookman d8b6d04edf
Doing stuff to /forecast 2024-06-20 17:55:56 -06:00
Christopher Cookman 70cce9e2ba Update funcs.js
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-20 12:45:54 -06:00
Christopher Cookman 1001d80313 Update funcs.js
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-20 11:11:44 -06:00
Christopher Cookman fbcc902bba Update funcs.js
All checks were successful
ptero-push / build (push) Successful in 6s
2024-06-20 11:10:26 -06:00
Christopher Cookman 6bdc4240be show full error on /forecast
All checks were successful
ptero-push / build (push) Successful in 5s
2024-06-20 11:07:14 -06:00
MoSiren be0d17cf6d Added "REP" RECCO Observations (tropical cyclone) NHC Product
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-20 03:56:39 +00:00
Christopher Cookman cea680a013
Add /forecast
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-19 21:37:18 -06:00
MoSiren ff0f4da7ad Updated Description on Setupall
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-19 03:04:58 +00:00
Christopher Cookman 539eccf7e2 Add about and support to self-added commands
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-18 21:03:31 -06:00
MoSiren 417777edb9 removed KGRX from NWRPLAY
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-19 02:57:37 +00:00
Christopher Cookman b382b95f6e
Test 2
All checks were successful
ptero-push / build (push) Successful in 4s
2024-06-18 20:54:29 -06:00
Christopher Cookman ea8c2be4a1
First try at runners on forgejo
Some checks are pending
ptero-push / build (push) Waiting to run
2024-06-18 20:53:22 -06:00
MoSiren 177f2c8dc3 added "AHO" NHC Product 2024-06-19 02:35:49 +00:00
Christopher Cookman c77577a382
Fix outlook command 2024-06-18 20:31:48 -06:00
Christopher Cookman 46000e92ee
Add ability to install app to user.
Will add more commands as needed
2024-06-18 20:26:45 -06:00
Christopher Cookman 04d3cb80e0
- Fix about command pfp
- Update sub/unsub command perms
2024-06-18 15:33:18 -06:00
Christopher Cookman dcf5518288 Merge pull request 'The "url" on line 1221 is a discord thing, you cannot change it to anything else' (#17) from Testing into main
Reviewed-on: ChrisChrome/weather-bot#17
2024-06-18 13:22:14 -06:00
Christopher Cookman 3e322de6cd
Just gonna make this change directly on main 2024-06-18 13:20:48 -06:00
Christopher Cookman 476f2fd946 Merge branch 'main' into Testing 2024-06-18 13:19:39 -06:00
Christopher Cookman ca785ea546
The "url" on line 1221 is a discord thing, you cannot change it to anything else 2024-06-18 13:19:04 -06:00
MoSiren 199cd70da5 nwrstream URL update 2024-06-18 08:40:02 +00:00
MoSiren 4b8d0515f9 Merge pull request 'Add NWRplay command' (#16) from Testing into main
Reviewed-on: ChrisChrome/weather-bot#16
2024-06-18 00:53:53 -06:00
MoSiren 64f4bc676e Fix URL 2024-06-18 06:47:07 +00:00
MoSiren a8aebe683c Error With Chris PFP not showing up when not in the discord server. 2024-06-18 06:45:51 +00:00
Christopher Cookman 249788ae8c Merge branch 'main' into Testing 2024-06-18 00:13:46 -06:00
Christopher Cookman 0f495feb55
Fix anthony's code :P 2024-06-18 00:09:40 -06:00
MoSiren ec176a87c2 Somewhat of a fix still issues where some commands dont show in certian servers. 2024-06-12 21:28:36 +00:00
Christopher Cookman 081b4ef261
Autocomplete nwrplay 2024-06-11 22:01:15 -06:00
MoSiren e5d9a687d4 NWR Play Works 2024-06-12 03:11:58 +00:00
MoSiren 0a9376d4d7 Trial For NWR Streams with own command. 2024-06-12 02:29:12 +00:00
MoSiren 2e92abfb21 Scary 2.0 2024-06-12 00:07:02 +00:00
MoSiren e9b206afff scary 2024-06-12 00:03:58 +00:00
15 changed files with 903 additions and 204 deletions

View file

@ -0,0 +1,15 @@
# 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"}'

1
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1 @@
{}

7
PRIVACY.md Normal file
View file

@ -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.

5
TERMS.md Normal file
View file

@ -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.

View file

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

View file

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

View file

@ -2,7 +2,7 @@
{
"name": "subscribe",
"description": "Subscribe to a weather.im room",
"default_member_permissions": 0,
"default_member_permissions": 16,
"options": [
{
"name": "room",
@ -66,7 +66,7 @@
{
"name": "unsubscribe",
"description": "Unsubscribe from a weather.im room",
"default_member_permissions": 0,
"default_member_permissions": 16,
"options": [
{
"name": "room",
@ -81,11 +81,13 @@
{
"name": "list",
"description": "List all subscribed rooms for this channel",
"default_member_permissions": 0
"default_member_permissions": 16
},
{
"name": "about",
"description": "About this bot"
"description": "About this bot",
"integration_types": [0,1],
"contexts": [0, 1, 2]
},
{
"name": "rooms",
@ -93,19 +95,23 @@
},
{
"name": "setupall",
"description": "[OWNER ONLY] Setup channels in a category for all rooms",
"description": "[BOT OWNER ONLY] Setup channels in a category for all rooms",
"default_member_permissions": 0,
"type": 1
},
{
"name": "support",
"description": "Get support for the bot",
"type": 1
"type": 1,
"integration_types": [0,1],
"contexts": [0, 1, 2]
},
{
"name": "outlook",
"description": "Get day 1-8 storm or fire outlook from the SPC",
"type": 1,
"integration_types": [0,1],
"contexts": [0, 1, 2],
"options": [
{
"name": "day",
@ -222,6 +228,29 @@
{
"name": "alertmap",
"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": {
"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",
@ -1514,5 +1514,17 @@
"GMT": {
"text": "AIRMET",
"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
}
}

11
data/nwrstreams.json Normal file
View file

@ -0,0 +1,11 @@
{
"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": [
"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",

187
data/satellites.json Normal file
View file

@ -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"
}
}
}
}

View file

@ -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"
}
]
}

138
funcs.js Normal file
View file

@ -0,0 +1,138 @@
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,11 +1,13 @@
// Requires
const fs = require("fs");
const config = require("./config.json");
const funcs = require("./funcs.js");
const wfos = require("./data/wfos.json");
const blacklist = require("./data/blacklist.json");
const events = require("./data/events.json");
const outlookURLs = require("./data/outlook.json");
const sattelites = require("./data/sattelites.json");
const satellites = require("./data/satellites.json");
const nwrstreams = {callsigns:{}};
const Jimp = require("jimp");
const { client, xml } = require("@xmpp/client");
const fetch = require("node-fetch");
@ -14,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",
@ -37,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;`)
});
@ -82,14 +89,24 @@ 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 });
});
});
}
// Get first url in a string, return object {string, url} remove the url from the string
const getFirstURL = function (string) {
const url = string.match(/(https?:\/\/[^\s]+)/g);
url = string.match(/(https?:\/\/[^\s]+)/g);
if (!url) return { string, url: null };
const newString = string.replace(url[0], "");
return { string: newString, url: url[0] };
@ -247,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",
@ -273,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()}`);
@ -298,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
@ -318,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);
@ -325,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,
@ -349,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}`);
@ -389,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: "📄"
@ -405,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 = "";
@ -431,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) => {
@ -476,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) => {
@ -582,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
@ -630,6 +751,7 @@ discord.on('ready', async () => {
},
{
"name": "play",
"type": 1,
"description": "Play a stream",
"options": [
{
@ -658,10 +780,27 @@ 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 () => {
try {
//Global
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);
@ -726,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) => {
@ -864,46 +1008,49 @@ discord.on("interactionCreate", async (interaction) => {
channels = row.count
await getUniqueChannels().then((unique) => {
uniques = unique;
uniques = unique.channels;
guilds = unique.guilds;
});
const embed = {
title: "About Me!",
thumbnail: {
url: discord.user.avatarURL()
},
description: `I listen to all the weather.im rooms and send them to discord channels.\nI am open source, you can find my code [here!](https://github.com/ChrisChrome/iembot-2.0)\n\nThis bot is not affiliated with NOAA, the National Weather Service, or the IEM project.`,
fields: [
{
name: "Uptime",
value: `Since <t:${Math.floor(startTimestap / 1000)}>, Started <t:${Math.floor(startTimestap / 1000)}:R>`,
discord.users.fetch("289884287765839882").then((chrisUser) => {
const embed = {
title: "About Me!",
thumbnail: {
url: discord.user?.avatarURL()
},
{
name: "Caught Messages",
value: `Got ${messages.toLocaleString()} messages since startup`,
},
{
name: "Guilds",
value: guilds.toLocaleString(),
inline: true
},
{
name: "Subscribed Rooms",
value: channels.toLocaleString(),
inline: true
},
{
name: "Unique Channels",
value: uniques.toLocaleString(),
inline: true
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>`,
},
{
name: "Caught Messages",
value: `Got ${messages.toLocaleString()} messages since startup`,
},
{
name: "Guilds",
value: guilds.toLocaleString(),
inline: true
},
{
name: "Subscribed Rooms",
value: `${channels.toLocaleString()}`,
inline: true
},
{
name: "Unique Channels",
value: `${uniques.toLocaleString()} in ${guilds} guilds.`,
inline: true
}
],
color: 0x00ff00,
footer: {
text: "Made by @chrischrome with <3",
icon_url: chrisUser.avatarURL()
}
],
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;
case "rooms":
@ -1000,7 +1147,7 @@ discord.on("interactionCreate", async (interaction) => {
break;
case "playbcfy": // Play broadcastify stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
if (!config.broadcastify.enabled) return interaction.reply({ content: "Broadcastify is not enabled", ephemeral: true });
streamID = interaction.options.getString("id");
// Check if the stream ID is valid (up to 10 digit alphanumeric)
@ -1008,7 +1155,7 @@ discord.on("interactionCreate", async (interaction) => {
// Get the stream URL
url = `https://${config.broadcastify.username}:${config.broadcastify.password}@audio.broadcastify.com/${streamID}.mp3`;
// Get the channel
channel = interaction.member.voice.channel;
channel = interaction.member.voice?.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream
res = JoinChannel(channel, url, .1, interaction)
@ -1020,16 +1167,16 @@ discord.on("interactionCreate", async (interaction) => {
break;
case "play": // Play generic stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
// Get the 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 });
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
// Use local variables & Get the URL
interactionUrl = interaction.options.getString("url");
// Get the channel
channel = interaction.member.voice.channel;
channel = interaction.member.voice?.channel;
// Check if in channel
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream
st = JoinChannel(channel, url, .1, interaction)
st = JoinChannel(channel, interactionUrl, .1, interaction);
if (st) {
interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true });
} else {
@ -1037,7 +1184,29 @@ discord.on("interactionCreate", async (interaction) => {
}
break;
case "leave": // Leaves Channel
case "nwrplay": // Play NWR stream
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
// Get the callsign
const callsign = interaction.options.getString("callsign");
// Get the URL associated with the callsign
url = nwrstreams.callsigns[callsign];
// Get the voice channel
channel = interaction.member.voice?.channel; // Use a unique variable name
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
// Join the channel and play the stream
const streamStatus = JoinChannel(channel, url, .1, interaction); // Use a unique variable name
if (streamStatus) {
interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true });
} else {
interaction.reply({ content: `Failed to play stream`, ephemeral: true });
}
break;
case "leave": // Leave Channel
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
channel = interaction.member.voice.channel;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
@ -1050,7 +1219,7 @@ discord.on("interactionCreate", async (interaction) => {
break;
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;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
res = toggleVoicePause(channel)
@ -1061,7 +1230,7 @@ discord.on("interactionCreate", async (interaction) => {
}
break;
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;
if (!channel) return interaction.reply({ content: "You need to be in a voice channel", ephemeral: true });
volume = interaction.options.getInteger("volume") / 100;
@ -1158,9 +1327,9 @@ discord.on("interactionCreate", async (interaction) => {
});
break;
case "alertmap":
url = "https://forecast.weather.gov/wwamap/png/US.png"
const alertmapurl = "https://forecast.weather.gov/wwamap/png/US.png"
await interaction.deferReply();
fetch(url).then((res) => {
fetch(alertmapurl).then((res) => {
if (res.status !== 200) {
interaction.editReply({ content: "Failed to get alert map", ephemeral: true });
return;
@ -1186,47 +1355,54 @@ 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":
await interaction.deferReply();
periods = interaction.options.getInteger("periods") || 1;
funcs.getWeatherBySearch(interaction.options.getString("location")).then((weather) => {
if (config.debug >= 1) console.log(JSON.stringify(weather, null, 2))
embeds = funcs.generateDiscordEmbeds(weather, periods);
interaction.editReply({ embeds });
}).catch((err) => {
interaction.editReply({ content: "Failed to get forecast", ephemeral: true });
if (config.debug >= 1) console.log(`${colors.red("[ERROR]")} Failed to get forecast: ${err}`);
});
break;
@ -1235,51 +1411,198 @@ discord.on("interactionCreate", async (interaction) => {
}
case Discord.InteractionType.MessageComponent:
if (interaction.customId) {
const product_id = interaction.customId;
const 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,2000}(?=\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
}
]
@ -1304,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");
@ -1347,5 +1670,7 @@ process.on("uncaughtException", (error) => {
return;
});
// Login to discord
discord.login(config.discord.token);

View file

@ -14,7 +14,8 @@
"@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",
"sodium": "^3.0.2",