diff --git a/.gitea/workflows/pe-docker.yml b/.gitea/workflows/pe-docker.yml index 179d2e7..9e13c26 100644 --- a/.gitea/workflows/pe-docker.yml +++ b/.gitea/workflows/pe-docker.yml @@ -10,14 +10,12 @@ env: DHUB_PWORD: ${{ secrets.DHUB_PWORD }} jobs: - build: - runs-on: self-hosted - steps: - name: Pull latest from Git run: | + echo "Branch: ${{ gitea.head_ref }}" pwd whoami mkdir -p /var/lib/act_runner/ @@ -29,7 +27,7 @@ jobs: cd nodbot git pull fi - git checkout ${{ gitea.ref}} + git checkout ${{ gitea.head_ref }} - name: Build the Docker image run: | cd /var/lib/act_runner/nodbot diff --git a/.gitea/workflows/production-docker.yml b/.gitea/workflows/production-docker.yml index d4e5c6b..c524d9b 100644 --- a/.gitea/workflows/production-docker.yml +++ b/.gitea/workflows/production-docker.yml @@ -3,33 +3,43 @@ name: NodBot Production Dockerization on: pull_request: branches: - - main + - main env: DHUB_UNAME: ${{ secrets.DHUB_UNAME }} DHUB_PWORD: ${{ secrets.DHUB_PWORD }} jobs: - build: - runs-on: self-hosted - steps: - name: Pull latest from Git run: | + echo "Branch: ${{ gitea.head_ref }}" pwd whoami - cd /root/nodbot - git pull - git checkout $GITHUB_HEAD_REF + mkdir -p /var/lib/act_runner/ + cd /var/lib/act_runner/ + if [ ! -d "nodbot" ]; then + git clone https://git.vfsh.dev/voidf1sh/nodbot + cd nodbot + else + cd nodbot + git pull + fi + git checkout ${{ gitea.head_ref }} - name: Build the Docker image run: | - cd /root/nodbot + cd /var/lib/act_runner/nodbot docker build . --file Dockerfile --tag v0idf1sh/nodbot - name: Log into Docker Hub run: docker login -u $DHUB_UNAME -p $DHUB_PWORD - name: Push image to Docker Hub run: | - cd /root/nodbot - docker push v0idf1sh/nodbot \ No newline at end of file + cd /var/lib/act_runner/nodbot + docker push v0idf1sh/nodbot + - name: Restart the container + run: | + cd /srv/docker/nodbot + docker-compose down + docker-compose up -d \ No newline at end of file diff --git a/README.md b/README.md index 7d31f60..7b473ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # About Nodbot Nodbot is a content saving and serving Discord bot. Nodbot is able to search Tenor for GIFs, save custom copypastas, and look up marijuana strain information. Nodbot is in semi-active development by voidf1sh. It's buggy as hell and very shoddily built. Don't use it. +# Status +This should be ready to merge into `main`, let it run a couple days with testing before creating a PR. METAR and D-ATIS are implemented. TAFs will come later as they're more complicated. + # Nodbot Help Use the `/help` command to see the bot's help message. @@ -13,12 +16,13 @@ Use the `/help` command to see the bot's help message. # Immediate To-Do -1. ~~Sanitize inputs for SQL queries.~~ Done. +1. ~~Sanitize inputs for SQL queries.~~ 2. ~~Move environment variables so they don't get included in the image.~~ 3. Implement error handling on all actions. -4. Ephemeral responses to some/most slash commands. +4. ~~Ephemeral responses to some/most slash commands.~~ 5. Comment the code! Document! 6. Check for and create database tables if necessary. Handle errors. +7. Readjust keyword autoresponses to be more generic instead of hard coded # Deploy NodBot Yourself @@ -31,6 +35,13 @@ Use the `/help` command to see the bot's help message. 6. Configure your environment variables as outlined below. 7. Fire it up with `node main.js` +# Recent Changes + +* Added METAR via AviationWeather.gov API +* Added D-ATIS via datis.clowd.io API +* Updated how keyword autoresponses are handled +* Changed `.joint` to reduce duplication and repetition by implementing an Ashtray and Roaches + ## Table Structure ``` diff --git a/config.json b/config.json index 467e255..488a934 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,7 @@ { "guildId": "868542949737246730", "validCommands": [], - "roaches": [] + "roaches": [], + "icaoIds": [], + "datisICAOs": [] } \ No newline at end of file diff --git a/dot-commands/datis.js b/dot-commands/datis.js new file mode 100644 index 0000000..e6948f7 --- /dev/null +++ b/dot-commands/datis.js @@ -0,0 +1,28 @@ +const fn = require('../functions'); + +module.exports = { + name: 'datis', + description: 'Lookup dATIS for an airport', + usage: 'ICAO.datis', + alias: [ 'atis' ], + async execute(message, commandData) { + try { + const icaoId = commandData.args.toUpperCase(); + if (icaoId.length !== 4) throw new Error('Invalid ICAO ID. Provide only one ICAO code at a time like KBOS'); + if (fn.avWx.datis.validate(icaoId)) { + const datisData = await fn.avWx.datis.getData(icaoId); + const messagePayload = fn.avWx.datis.parseData(datisData); + message.reply(messagePayload); + } else { + message.reply("No D-ATIS available for the specified ICAO ID."); + } + } catch (e) { + try { + message.reply(`D-ATIS Error: ${e.message}`); + console.error(e); + } catch (e) { + console.error(e); + } + } + } +} \ No newline at end of file diff --git a/dot-commands/metar.js b/dot-commands/metar.js new file mode 100644 index 0000000..33207e9 --- /dev/null +++ b/dot-commands/metar.js @@ -0,0 +1,26 @@ +const fn = require('../functions'); + +module.exports = { + name: 'metar', + description: 'Lookup METAR for an airport', + usage: 'ICAO.metar', + async execute(message, commandData) { + try { + // Parse the ICAOs into a CSV list by trimming whitespace and converting delimiters + // Also checks for validity of ICAOs + const icaoList = fn.avWx.parseICAOs(commandData); + const metarData = await fn.avWx.metar.getData(icaoList); + const messages = fn.avWx.metar.parseData(metarData); + messages.forEach(messagePayload => { + message.reply(messagePayload); + }); + } catch (e) { + try { + message.reply(`METAR Error: ${e.message}`); + console.error(e); + } catch (e) { + console.error(e); + } + } + } +} \ No newline at end of file diff --git a/functions.js b/functions.js index c3aa962..50fc8c5 100644 --- a/functions.js +++ b/functions.js @@ -14,6 +14,7 @@ const ownerId = process.env.ownerId; // filesystem const fs = require('fs'); +const zlib = require('zlib'); // Discord.js const Discord = require('discord.js'); @@ -25,6 +26,9 @@ const FuzzySearch = require('fuzzy-search'); // const OpenAI = require("openai"); // const openai = new OpenAI(); +// Axios for APIs +const axios = require('axios'); + // Various imports from other files const config = require('./config.json'); const strings = require('./strings.json'); @@ -384,6 +388,63 @@ const functions = { .setDescription("Generating a response, please stand by.") .setFooter({ text: "Ligma balls" }); return { embeds: [embed] }; + }, + avWx: { + metar(metarData) { + const wgst = metarData.wgst ? `G${metarData.wgst}` : ''; + const clouds = []; + const interAltim = Math.round((metarData.altim * 0.2952998057228486) * 10) + const altim = interAltim / 100; + metarData.clouds.forEach(cloudLayer => { + if (cloudLayer.base !== null) { + clouds.push(`${cloudLayer.cover} @ ${cloudLayer.base}'`); + } else { + clouds.push(`${cloudLayer.cover}`); + } + }); + const embed = new Discord.MessageEmbed() + .setAuthor({ name: `${metarData.name} [${metarData.icaoId}] METAR`, iconURL: "https://aviationweather.gov/img/icons/awc-logo-180.png"}) + // .setImage("https://media.discordapp.net/stickers/1175134632845516821.webp") + .setDescription(`**Do not use for real world flight planning or navigation.**`) + .setFooter({ text: "METAR by AviationWeather.gov for CumbHub LLC" }) + .addFields( + { name: 'Observation Time', value: `${metarData.reportTime}Z`, inline: true }, + { name: 'Temperature', value: `${metarData.temp}ºC/${metarData.dewp}ºC`, inline: true }, + { name: 'Winds', value: `${metarData.wdir.toString().padStart(3, '0')}º@${metarData.wspd}${wgst} kts`, inline: true }, + { name: 'Visibility', value: `${metarData.visib} SM`, inline: true }, + { name: 'Clouds', value: clouds.join('\n'), inline: true }, + { name: 'Altimeter', value: `${altim} inHg`, inline: true } + ) + return { content: metarData.rawOb, embeds: [embed] }; + }, + datis(datisData) { + const messageEmbed = new Discord.MessageEmbed() + .setAuthor({ name: `${datisData[0].airport} Digital ATIS` }) + // .setImage('https://media.discordapp.net/stickers/1175134632845516821.webp') + .setDescription(`**Do not use for real world flight planning or navigation.**`) + .setFooter({ text: 'D-ATIS by Clowd.io for CumbHub LLC' }) + + if (datisData.length > 1) { + datisData.forEach(data => { + if (data.type === 'dep') messageEmbed.addFields({ name: 'Departure Digital ATIS', value: data.datis, inline: false }); + if (data.type === 'arr') messageEmbed.addFields({ name: 'Arrival Digital ATIS', value: data.datis, inline: false }); + messageEmbed.addFields({ name: 'Information', value: data.code, inline: true }); + }) + messageEmbed.addFields( + { name: 'Retreival Time', value: `${new Date().toISOString()}`, inline: true } + ); + } else { + messageEmbed.addFields( + { name: 'Digital ATIS', value: datisData[0].datis, inline: false }, + { name: 'Information', value: `${datisData[0].code}`, inline: true }, + { name: 'Retreival Time', value: `${new Date().toISOString()}`, inline: true } + ) + } + + + const messagePayload = { embeds: [ messageEmbed ] }; + return messagePayload; + } } }, collect: { @@ -489,16 +550,16 @@ const functions = { } }, download: { - requests(client) { + async requests(client) { const query = 'SELECT * FROM requests WHERE status = \'Active\' ORDER BY id DESC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.requests(rows, client); }); }, - pastas(client) { + async pastas(client) { const query = 'SELECT * FROM pastas ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.pastas(rows, client); }); @@ -510,16 +571,16 @@ const functions = { functions.collections.gifs(rows, client); }); }, - joints(client) { + async joints(client) { const query = 'SELECT * FROM joints ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.joints(rows, client); }); }, - strain(strainName, interaction) { + async strain(strainName, interaction) { const query = `SELECT id, strain, type, effects, description, flavor, rating FROM strains WHERE strain = ${db.escape(strainName)}`; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (rows != undefined) { const strainInfo = { id: `${rows[0].id}`, @@ -534,16 +595,16 @@ const functions = { } }); }, - strains(client) { + async strains(client) { const query = 'SELECT id, strain FROM strains'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.strains(rows, client); }); }, - medicalAdvice(client) { + async medicalAdvice(client) { const query = 'SELECT * FROM medical_advice ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.medicalAdvice(rows, client); }); @@ -709,6 +770,99 @@ const functions = { } } }, + avWx: { + parseICAOs(commandData) { + let input = commandData.args.toUpperCase(); + // Replace newlines and different delimiters with a comma + let standardizedInput = input.replace(/[\s,;]+/g, ','); + + // Split the string by commas + let icaoArray = standardizedInput.split(','); + + // Trim each element to remove extra whitespace + icaoArray = icaoArray.map(icao => icao.trim()).filter(icao => icao.length > 0); + + icaoArray.forEach(icao => { + if (!(config.icaoIds.includes(icao))) throw new Error(`Invalid ICAO ID Detected: ${icao}`); + }); + + // Join the array into a comma-separated string + return icaoArray.join(','); + }, + metar: { + async getAllICAOs() { + const reqUrl = `https://aviationweather.gov/data/cache/stations.cache.json.gz` + try { + // Step 1: Download the GZipped file + const response = await axios({ + url: reqUrl, + method: 'GET', + responseType: 'arraybuffer', // Ensure we get the raw binary data + headers: { + 'Accept-Encoding': 'gzip' // Ensure the server sends gzipped content + } + }); + + // Step 2: Decompress the GZipped content + const buffer = Buffer.from(response.data); + zlib.gunzip(buffer, (err, decompressedBuffer) => { + if (err) { + console.error('An error occurred during decompression:', err); + return; + } + + // Step 3: Parse the decompressed JSON + const jsonString = decompressedBuffer.toString('utf-8'); + try { + const jsonData = JSON.parse(jsonString); + // console.log('Parsed JSON data:', jsonData); + jsonData.forEach(airport => { + config.icaoIds.push(airport.icaoId); + }); + // console.log(`ICAO IDs: ${config.icaoIds.length}\n\n${config.icaoIds}`) + } catch (jsonError) { + console.error('An error occurred while parsing JSON:', jsonError); + } + }); + } catch (error) { + console.error('An error occurred during the HTTP request:', error); + } + }, + async getData(icaoList) { + const reqUrl = `https://aviationweather.gov/api/data/metar?ids=${icaoList}&format=json`; + const response = await axios.get(reqUrl); + return response.data; + }, + parseData(metarData) { + let messages = []; + metarData.forEach(metar => { + messages.push(functions.embeds.avWx.metar(metar)); + }) + return messages; + } + }, + datis: { + async getAllICAOs() { + const reqUrl = 'https://datis.clowd.io/api/stations'; + const response = await axios.get(reqUrl); + response.data.forEach(icaoId => { + config.datisICAOs.push(icaoId); + }); + }, + validate(icaoId) { + return config.datisICAOs.includes(icaoId); + }, + async getData(icaoId) { + const reqUrl = `https://datis.clowd.io/api/${icaoId}`; + const response = await axios.get(reqUrl); + if (response.error !== undefined) throw new Error('The D-ATIS API returned an error:\n' + response.error); + return response.data; + }, + parseData(datisData) { + return functions.embeds.avWx.datis(datisData); + } + } + }, generateErrorId() { const digitCount = 10; const digits = []; diff --git a/main.js b/main.js index 4527304..55a347c 100644 --- a/main.js +++ b/main.js @@ -30,18 +30,21 @@ const strings = require('./strings.json'); const { GifData } = require('./CustomModules/NodBot.js'); const isDev = process.env.isDev; -client.once('ready', () => { +client.once('ready', async () => { fn.collections.slashCommands(client); fn.collections.dotCommands(client); fn.collections.setvalidCommands(client); fn.collections.roaches(client); - fn.download.gifs(client); - fn.download.pastas(client); - fn.download.joints(client); - fn.download.requests(client); - fn.download.strains(client); - fn.download.medicalAdvice(client); + await fn.download.gifs(client); + await fn.download.pastas(client); + await fn.download.joints(client); + await fn.download.requests(client); + await fn.download.strains(client); + await fn.download.medicalAdvice(client); console.log('Ready!'); + await fn.avWx.metar.getAllICAOs(); + await fn.avWx.datis.getAllICAOs(); + // console.log(JSON.stringify(icaoArray)); client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- <@${process.env.ownerId}>\nStartup Sequence Complete`); }); diff --git a/package.json b/package.json index e419fc5..566a3e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodbot", - "version": "3.2.3", + "version": "3.3.1", "description": "Nods and Nod Accessories, now with ChatGPT!", "main": "main.js", "dependencies": { diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..6380ff3 --- /dev/null +++ b/update.sh @@ -0,0 +1,4 @@ +#!/bin/bash +git pull +docker build . -t v0idf1sh/nodbot +docker push v0idf1sh/nodbot \ No newline at end of file