v3.3.0-metars #14

Merged
voidf1sh merged 16 commits from v3.3.0-metars into main 2024-09-24 01:04:49 +00:00
10 changed files with 273 additions and 37 deletions

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
{
"guildId": "868542949737246730",
"validCommands": [],
"roaches": []
"roaches": [],
"icaoIds": [],
"datisICAOs": []
}

28
dot-commands/datis.js Normal file
View File

@ -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);
}
}
}
}

26
dot-commands/metar.js Normal file
View File

@ -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);
}
}
}
}

View File

@ -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 = [];

17
main.js
View File

@ -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`);
});

View File

@ -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": {

4
update.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
git pull
docker build . -t v0idf1sh/nodbot
docker push v0idf1sh/nodbot