From cea680a0137617f82cec352f4232df116ddfef75 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Wed, 19 Jun 2024 21:37:18 -0600 Subject: [PATCH] Add /forecast --- data/commands.json | 21 +++++++ funcs.js | 135 +++++++++++++++++++++++++++++++++++++++++++++ index.js | 13 +++++ package.json | 1 + 4 files changed, 170 insertions(+) create mode 100644 funcs.js diff --git a/data/commands.json b/data/commands.json index ac99c14..b0742a6 100644 --- a/data/commands.json +++ b/data/commands.json @@ -231,5 +231,26 @@ "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 + } + ] } ] \ No newline at end of file diff --git a/funcs.js b/funcs.js new file mode 100644 index 0000000..ae96507 --- /dev/null +++ b/funcs.js @@ -0,0 +1,135 @@ +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, 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, 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]; + const embed = { + title: `${period.name} in ${forecastData.properties.relativeLocation.properties.city} ${forecastData.properties.relativeLocation.properties.state}`, + description: 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 + '%', + } + + ], + footer: { + text: 'Data provided by the National Weather Service' + } + }; + embeds.push(embed); + } + return embeds; +} + + +module.exports = { + getCoordinates, + getForecast, + getWeatherBySearch, + generateDiscordEmbeds +}; \ No newline at end of file diff --git a/index.js b/index.js index a7cab7f..b0fceab 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ // 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"); @@ -1280,6 +1281,18 @@ discord.on("interactionCreate", async (interaction) => { }); break; + case "forecast": + await interaction.deferReply(); + periods = interaction.options.getInteger("periods") || 1; + funcs.getWeatherBySearch(interaction.options.getString("location")).then((weather) => { + 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.message}`); + }); + break; + diff --git a/package.json b/package.json index 7c13012..1b812e6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@xmpp/debug": "^0.13.0", "colors": "^1.4.0", "discord.js": "^14.15.2", + "geolib": "^3.3.4", "html-entities": "^2.5.2", "jimp": "^0.22.12", "sodium": "^3.0.2",