forked from ChrisChrome/weather-bot
1589 lines
54 KiB
JavaScript
1589 lines
54 KiB
JavaScript
// 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 nwrstreams = {callsigns:{}};
|
||
const Jimp = require("jimp");
|
||
const { client, xml } = require("@xmpp/client");
|
||
const fetch = require("node-fetch");
|
||
const html = require("html-entities")
|
||
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",
|
||
"DirectMessages"
|
||
]
|
||
});
|
||
const {
|
||
REST,
|
||
Routes
|
||
} = require('discord.js');
|
||
const rest = new REST({
|
||
version: '10'
|
||
}).setToken(config.discord.token);
|
||
|
||
// Setup SQlite DB
|
||
const db = new sqlite3.Database("channels.db", (err) => {
|
||
if (err) {
|
||
console.log(`${colors.red("[ERROR]")} Error connecting to database: ${err.message}`);
|
||
}
|
||
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 userAlerts (userid TEXT, iemchannel TEXT, filter TEXT, filterEvt TEXT, minPriority INT, custommessage TEXT);`);
|
||
db.run(`ALTER TABLE channels RENAME COLUMN filterevt TO filterEvt;`)
|
||
});
|
||
|
||
|
||
// Random funcs
|
||
function toTitleCase(str) {
|
||
return str.replace(
|
||
/\w\S*/g,
|
||
function (txt) {
|
||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||
}
|
||
);
|
||
}
|
||
|
||
const parseProductID = function (product_id) {
|
||
const [timestamp, station, wmo, pil] = product_id.split("-");
|
||
return {
|
||
timestamp: convertDate(timestamp),
|
||
originalTimestamp: timestamp,
|
||
station,
|
||
wmo,
|
||
pil
|
||
};
|
||
}
|
||
|
||
// Convert date format 202405080131 (YYYYMMddHHmm) to iso format, hours and mins is UTC
|
||
const convertDate = function (date) {
|
||
const year = date.substring(0, 4);
|
||
const month = date.substring(4, 6);
|
||
const day = date.substring(6, 8);
|
||
const hours = date.substring(8, 10);
|
||
const mins = date.substring(10, 12);
|
||
// Because they don't have seconds, assume current seconds
|
||
const secs = new Date().getSeconds();
|
||
return new Date(Date.UTC(year, month - 1, day, hours, mins, secs));
|
||
}
|
||
|
||
// Get number of unique channels in the database
|
||
const getUniqueChannels = function () {
|
||
return new Promise((resolve, reject) => {
|
||
db.all(`SELECT DISTINCT channelid FROM channels`, (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
// 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) {
|
||
url = string.match(/(https?:\/\/[^\s]+)/g);
|
||
if (!url) return { string, url: null };
|
||
const newString = string.replace(url[0], "");
|
||
return { string: newString, url: url[0] };
|
||
}
|
||
|
||
// Function to get the room name from the WFO code
|
||
const getWFOroom = function (code) {
|
||
code = code.toLowerCase();
|
||
if (wfos[code]) {
|
||
return wfos[code].room;
|
||
} else {
|
||
return code;
|
||
}
|
||
}
|
||
|
||
// Function to get WFO data
|
||
const getWFO = function (code) {
|
||
code = code.toLowerCase();
|
||
if (wfos[code]) {
|
||
return wfos[code];
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Get WFO data from room name
|
||
|
||
function getWFOByRoom(room) {
|
||
room = room.toLowerCase();
|
||
for (const key in wfos) {
|
||
if (wfos.hasOwnProperty(key) && wfos[key].room === room) {
|
||
return wfos[key];
|
||
}
|
||
}
|
||
return {
|
||
location: room,
|
||
room: room
|
||
};
|
||
}
|
||
|
||
// Voice funcs
|
||
function JoinChannel(channel, track, volume, message) {
|
||
connection = dVC.joinVoiceChannel({
|
||
channelId: channel.id,
|
||
guildId: channel.guild.id,
|
||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||
selfDeaf: true
|
||
});
|
||
|
||
|
||
resource = dVC.createAudioResource(track, { inlineVolume: true, silencePaddingFrames: 5 });
|
||
player = dVC.createAudioPlayer();
|
||
connection.player = player; // So we can access it later to pause/play/stop etc
|
||
resource.volume.setVolume(volume);
|
||
connection.subscribe(player)
|
||
player.play(resource);
|
||
connection.on(dVC.VoiceConnectionStatus.Ready, () => { player.play(resource); })
|
||
connection.on(dVC.VoiceConnectionStatus.Disconnected, async (oldState, newState) => {
|
||
try {
|
||
await Promise.race([
|
||
dVC.entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
||
dVC.entersState(connection, VoiceConnectionStatus.Connecting, 5_000),
|
||
]);
|
||
} catch (error) {
|
||
message.channel.send(`Failed to reconnect to the voice channel. Stopping for now.`);
|
||
connection.destroy();
|
||
return false;
|
||
}
|
||
});
|
||
player.on('error', error => {
|
||
console.error(`Error: ${error.message} with resource ${error.resource.metadata.title}`);
|
||
message.channel.send(`Error while streaming. Stopping for now.`);
|
||
player.stop();
|
||
});
|
||
player.on(dVC.AudioPlayerStatus.Playing, () => {
|
||
message.channel.send(`Playing stream in <#${channel.id}>`);
|
||
connection.paused = false;
|
||
});
|
||
player.on('idle', () => {
|
||
message.channel.send(`Stream idle.`);
|
||
})
|
||
return true;
|
||
}
|
||
|
||
function LeaveVoiceChannel(channel) {
|
||
// Get resource, player, etc, and destroy them
|
||
const connection = dVC.getVoiceConnection(channel.guild.id);
|
||
if (connection) {
|
||
connection.destroy();
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
function toggleVoicePause(channel) {
|
||
const connection = dVC.getVoiceConnection(channel.guild.id);
|
||
if (connection) {
|
||
if (connection.paused) {
|
||
connection.player.unpause();
|
||
connection.paused = false;
|
||
return true;
|
||
}
|
||
else {
|
||
connection.player.pause();
|
||
connection.paused = true;
|
||
return true
|
||
}
|
||
}
|
||
else {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
function setVolume(channel, volume) {
|
||
const connection = dVC.getVoiceConnection(channel.guild.id);
|
||
if (connection) {
|
||
connection.player.state.resource.volume.setVolume(volume);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// func to Generate random string, ({upper, lower, number, special}, length)
|
||
|
||
const generateRandomString = function (options, length) {
|
||
let result = '';
|
||
const characters = {
|
||
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||
lower: 'abcdefghijklmnopqrstuvwxyz',
|
||
number: '0123456789',
|
||
special: '!@#$%^&*()_+'
|
||
};
|
||
let chars = '';
|
||
for (const key in options) {
|
||
if (options[key]) {
|
||
chars += characters[key];
|
||
}
|
||
}
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Func to generate UUID
|
||
const generateUUID = function () {
|
||
return generateRandomString({ lower: true, upper: true, number: true }, 8) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 4) + "-" + generateRandomString({ lower: true, upper: true, number: true }, 12);
|
||
}
|
||
|
||
// Variable setup
|
||
var iem = []
|
||
var startup = true;
|
||
var startTimestap = new Date();
|
||
var messages = 0;
|
||
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",
|
||
resource: `discord-weather-bot-${generateRandomString({ upper: true, lower: true, number: true }, 5)}`, // Weird fix to "Username already in use"
|
||
});
|
||
|
||
//debug(xmpp, true);
|
||
|
||
xmpp.on("error", (err) => {
|
||
console.log(`${colors.red("[ERROR]")} XMPP Error: ${err}. Trying to reconnect...`);
|
||
setTimeout(() => {
|
||
xmpp.stop().then(() => {
|
||
start();
|
||
});
|
||
}, 5000);
|
||
});
|
||
|
||
xmpp.on("offline", () => {
|
||
console.log(`${colors.yellow("[WARN]")} XMPP offline, trying to reconnect...`);
|
||
xmpp.disconnect().then(() => {
|
||
xmpp.stop().then(() => {
|
||
start();
|
||
})
|
||
})
|
||
});
|
||
|
||
var restartTimer = null;
|
||
|
||
xmpp.on("stanza", (stanza) => {
|
||
// Debug stuff
|
||
if (config.debug >= 2) console.log(`${colors.magenta("[DEBUG]")} Stanza: ${stanza.toString()}`);
|
||
|
||
|
||
// Handle Room List
|
||
if (stanza.is("iq") && stanza.attrs.type === "result" && stanza.getChild("query")) {
|
||
query = stanza.getChild("query");
|
||
if (query.attrs.xmlns === "http://jabber.org/protocol/disco#items") {
|
||
query.getChildren("item").forEach((item) => {
|
||
// Check if the JID is on the blacklist, if so, ignore it
|
||
if (blacklist.includes(item.attrs.jid)) return;
|
||
// get proper name from wfos
|
||
const wfo = getWFOByRoom(item.attrs.jid.split("@")[0]);
|
||
item.attrs.properName = wfo.location;
|
||
iem.push(item.attrs);
|
||
console.log(`${colors.cyan("[INFO]")} Found room: ${item.attrs.jid}`);
|
||
// Join the room
|
||
//xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" })));
|
||
xmpp.send(xml("presence", { to: `${item.attrs.jid}/${curUUID}` }, xml("item", { role: "visitor" })));
|
||
});
|
||
}
|
||
}
|
||
// 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
|
||
fromChannel = stanza.attrs.from.split("@")[0];
|
||
// Ignores
|
||
if (!stanza.getChild("x")) return; // No PID, ignore it
|
||
if (!stanza.getChild("x").attrs.product_id) return;
|
||
|
||
const product_id = parseProductID(stanza.getChild("x").attrs.product_id);
|
||
const product_id_raw = stanza.getChild("x").attrs.product_id;
|
||
// Get body of message
|
||
const body = html.decode(stanza.getChildText("body"));
|
||
const bodyData = getFirstURL(body);
|
||
// get product id from "x" tag
|
||
var evt = events[product_id.pil.substring(0, 3)];
|
||
|
||
if (!evt) {
|
||
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}`)
|
||
}
|
||
|
||
evt.code = product_id.pil.substring(0, 3);
|
||
// Check timestamp, if not within 3 minutes, ignore it
|
||
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}`);
|
||
messages++;
|
||
|
||
|
||
// Handle NTFY
|
||
if (config.ntfy.enabled) {
|
||
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,
|
||
"tags": [`Timestamp: ${product_id.timestamp}`, `Station: ${product_id.station}`, `WMO: ${product_id.wmo}`, `PIL: ${product_id.pil}`, `Channel: ${fromChannel}`],
|
||
"priority": evt.priority,
|
||
"actions": [{ "action": "view", "label": "Product", "url": bodyData.url }, { "action": "view", "label": "Product Text", "url": `https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}` }]
|
||
}
|
||
if (stanza.getChild("x").attrs.twitter_media) {
|
||
ntfyBody.attach = stanza.getChild("x").attrs.twitter_media;
|
||
}
|
||
fetch(config.ntfy.server, {
|
||
method: 'POST',
|
||
body: JSON.stringify(ntfyBody),
|
||
headers: {
|
||
'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 (res.status !== 200) console.log(`${colors.red("[ERROR]")} NTFY failed for ${config.ntfy.prefix}${fromChannel} with status ${res.status} ${res.statusText}`);
|
||
|
||
|
||
}).catch((err) => {
|
||
console.error(err)
|
||
})
|
||
}
|
||
|
||
|
||
// Send discord msg
|
||
let embed = {
|
||
description: `<t:${product_id.timestamp / 1000}:T> <t:${product_id.timestamp / 1000}:R> ${bodyData.string}`,
|
||
color: parseInt(config.priorityColors[evt.priority].replace("#", ""), 16) || 0x000000,
|
||
timestamp: product_id.timestamp,
|
||
footer: {
|
||
text: `Station: ${product_id.station} PID: ${product_id_raw} Channel: ${fromChannel}`
|
||
}
|
||
}
|
||
if (stanza.getChild("x").attrs.twitter_media) {
|
||
embed.image = {
|
||
url: stanza.getChild("x").attrs.twitter_media
|
||
}
|
||
}
|
||
|
||
let discordMsg = {
|
||
embeds: [embed],
|
||
components: [
|
||
{
|
||
type: 1,
|
||
components: [
|
||
{
|
||
type: 2,
|
||
label: "Product",
|
||
style: 5,
|
||
url: bodyData.url
|
||
},
|
||
{
|
||
type: 2,
|
||
style: 1,
|
||
custom_id: `product|${product_id_raw}`,
|
||
label: "Product Text",
|
||
emoji: {
|
||
name: "📄"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
// Discord Channel Handling
|
||
db.all(`SELECT * FROM channels WHERE iemchannel = ?`, [fromChannel], (err, rows) => {
|
||
if (err) {
|
||
console.log(`${colors.red("[ERROR]")} ${err.message}`);
|
||
}
|
||
if (!rows) return; // No channels to alert
|
||
rows.forEach(async (row) => {
|
||
// Get Filters as arrays
|
||
if (!row.filterEvt) row.filterEvt = "";
|
||
if (!row.filter) row.filter = "";
|
||
let filterEvt = row.filterEvt.toLowerCase().split(",");
|
||
let filters = row.filter.toLowerCase().split(",");
|
||
if (evt.priority < row.minPriority) return;
|
||
// If the event type is not in th filter, ignore it. Make sure filterEvt isnt null
|
||
if (!filterEvt[0]) filterEvt = [];
|
||
if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return;
|
||
|
||
let channel = discord.channels.cache.get(row.channelid);
|
||
if (!channel) return console.log(`${colors.red("[ERROR]")} Channel ${row.channelid} not found`);
|
||
|
||
// fetch the product text
|
||
trySend = () => {
|
||
fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => {
|
||
// If neither the body nor the product text contains the filter, ignore it
|
||
res.text().then((text) => {
|
||
if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return;
|
||
thisMsg = JSON.parse(JSON.stringify(discordMsg));
|
||
thisMsg.content = row.custommessage || null;
|
||
channel.send(thisMsg).catch((err) => {
|
||
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})`);
|
||
});
|
||
});
|
||
}).catch((err) => {
|
||
setTimeout(() => {
|
||
console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`)
|
||
trySend();
|
||
})
|
||
});
|
||
}
|
||
trySend();
|
||
});
|
||
});
|
||
|
||
|
||
// User DM alert handling
|
||
db.all(`SELECT * FROM userAlerts WHERE iemchannel = ?`, [fromChannel], (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
if (!rows) return; // No users to alert
|
||
rows.forEach((row) => {
|
||
// Get Filters as arrays
|
||
if (!row.filterEvt) row.filterEvt = "";
|
||
if (!row.filter) row.filter = "";
|
||
let filterEvt = row.filterEvt.toLowerCase().split(",");
|
||
let filters = row.filter.toLowerCase().split(",");
|
||
|
||
// If priority is less than the min priority, ignore it
|
||
if (evt.priority < row.minPriority) return;
|
||
// If the event type is not in th filter, ignore it. Make sure filterEvt isnt null
|
||
if (!filterEvt[0]) filterEvt = [];
|
||
if (!filterEvt.includes(evt.code.toLowerCase()) && !filterEvt.length == 0) return;
|
||
let user = discord.users.cache.get(row.userid);
|
||
if (!user) return console.log(`${colors.red("[ERROR]")} User ${row.userid} not found`);
|
||
|
||
// fetch the product text
|
||
trySend = () => {
|
||
fetch(`https://mesonet.agron.iastate.edu/api/1/nwstext/${product_id_raw}`).then((res) => {
|
||
// If neither the body nor the product text contains the filter, ignore it
|
||
res.text().then((text) => {
|
||
if (!filters.some((filter) => body.toLowerCase().includes(filter)) && !filters.some((filter) => text.toLowerCase().includes(filter))) return;
|
||
thisMsg = JSON.parse(JSON.stringify(discordMsg));
|
||
thisMsg.content = row.custommessage || null;
|
||
user.send(thisMsg).catch((err) => {
|
||
console.error(err);
|
||
});
|
||
});
|
||
}).catch((err) => {
|
||
setTimeout(() => {
|
||
console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`)
|
||
trySend();
|
||
})
|
||
});;
|
||
}
|
||
trySend();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
|
||
xmpp.on("online", async (address) => {
|
||
if (config["uptime-kuma"].enabled) {
|
||
fetch(config["uptime-kuma"].url).then(() => {
|
||
console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`)
|
||
})
|
||
setInterval(() => {
|
||
// Send POST request to config["uptime-kuma"].url
|
||
fetch(config["uptime-kuma"].url).then(() => {
|
||
console.log(`${colors.cyan("[INFO]")} Sent heartbeat to Uptime Kuma`)
|
||
})
|
||
}, config["uptime-kuma"].interval * 1000) // Every X seconds
|
||
}
|
||
|
||
errCount = 0;
|
||
// Start listening on all channels, (dont ban me funny man)
|
||
// for (const channel in iem) {
|
||
// console.log(`Joining ${channel.name}`)
|
||
// await xmpp.send(xml("presence", { to: `${channel.jud}/${channel.name}` }));
|
||
// }
|
||
/* sub format
|
||
<presence to="botstalk@conference.weather.im/add9b8f1-038d-47ed-b708-6ed60075a82f" xmlns="jabber:client">
|
||
<x xmlns="http://jabber.org/protocol/muc#user">
|
||
<item>
|
||
<role>visitor</role>
|
||
</item>
|
||
</x>
|
||
</presence>
|
||
*/
|
||
|
||
// Request room list
|
||
// Automatically find room list
|
||
xmpp.send(xml("iq", { type: "get", to: "conference.weather.im", id: "rooms" }, xml("query", { xmlns: "http://jabber.org/protocol/disco#items" })));
|
||
// Join all channels (Old method)
|
||
// iem.forEach((channel => {
|
||
// console.log(`${colors.cyan("[INFO]")} Joining ${channel.jid}/${channel.name}/${curUUID}`)
|
||
// //xmpp.send(xml("presence", { to: `${channel.jid}/${channel.jid.split("@")[0]}` }));
|
||
// xmpp.send(xml("presence", { to: `${channel.jid}/${channel.name}/${curUUID}` }, xml("item", { role: "visitor" })));
|
||
// }))
|
||
|
||
console.log(`${colors.cyan("[INFO]")} Connected to XMPP server as ${address.toString()}`);
|
||
|
||
setTimeout(() => {
|
||
startup = false;
|
||
console.log(`${colors.cyan("[INFO]")} Startup complete, listening for messages...`);
|
||
}, 1000)
|
||
});
|
||
|
||
xmpp.on("close", () => {
|
||
console.log(`${colors.yellow("[WARN]")} XMPP connection closed, trying to reconnect...`);
|
||
xmpp.disconnect().then(() => {
|
||
xmpp.stop().then(() => {
|
||
start();
|
||
})
|
||
})
|
||
})
|
||
|
||
const start = () => {
|
||
startup = true;
|
||
xmpp.start().catch((err) => {
|
||
errCount++;
|
||
if (errCount >= 5) {
|
||
console.log(`${colors.red("[ERROR]")} XMPP failed to start after 5 attempts, exiting...`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`${colors.red("[ERROR]")} XMPP failed to start: ${err}.`);
|
||
xmpp.disconnect().then(() => {
|
||
xmpp.stop().then(() => {
|
||
start();
|
||
})
|
||
})
|
||
});
|
||
}
|
||
|
||
// END XMPP
|
||
|
||
// START DISCORD
|
||
|
||
discord.on('ready', async () => {
|
||
console.log(`${colors.cyan("[INFO]")} Logged in as ${discord.user.tag}`);
|
||
|
||
// Get all guilds, and log them
|
||
discord.guilds.cache.forEach((guild) => {
|
||
console.log(`${colors.cyan("[INFO]")} In guild: ${guild.name} (${guild.id})`);
|
||
});
|
||
|
||
// Do slash command stuff
|
||
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",
|
||
"options": [
|
||
{
|
||
"name": "sattelite",
|
||
"description": "The sattelite to get images from",
|
||
"type": 3,
|
||
"required": true,
|
||
"choices": []
|
||
}
|
||
]
|
||
}
|
||
for (const key in sattelites) {
|
||
satCommand.options[0].choices.push({
|
||
"name": key,
|
||
"value": key
|
||
});
|
||
}
|
||
commands.push(satCommand);
|
||
|
||
if (config.broadcastify.enabled) {
|
||
// Add commands to join vc, leave vc, and play stream
|
||
commands.push(
|
||
{
|
||
"name": "playbcfy",
|
||
"description": "Play the broadcastify stream",
|
||
"options": [
|
||
{
|
||
"name": "id",
|
||
"description": "The ID of the stream to play",
|
||
"type": 3,
|
||
"required": true
|
||
}
|
||
]
|
||
}
|
||
)
|
||
}
|
||
if (config.voice_enabled) {
|
||
// Add commands to join vc, leave vc, and play stream
|
||
commands.push(
|
||
{
|
||
"name": "leave",
|
||
"description": "Leave the current voice chat",
|
||
"default_member_permissions": 0
|
||
},
|
||
{
|
||
"name": "play",
|
||
"type": 1,
|
||
"description": "Play a stream",
|
||
"options": [
|
||
{
|
||
"name": "url",
|
||
"description": "The URL of the stream to play",
|
||
"type": 3,
|
||
"required": true
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"name": "pause",
|
||
"description": "Pause/Unpause the current stream",
|
||
"type": 1
|
||
},
|
||
{
|
||
"name": "volume",
|
||
"description": "Set the volume of the current stream",
|
||
"options": [
|
||
{
|
||
"name": "volume",
|
||
"description": "The volume to set",
|
||
"type": 4,
|
||
"required": true
|
||
}
|
||
]
|
||
}
|
||
)
|
||
|
||
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);
|
||
}
|
||
})();
|
||
|
||
start();
|
||
setTimeout(() => {
|
||
// Wait 10 seconds, if startup is still true, something went wrong
|
||
if (startup) {
|
||
console.log(`${colors.red("[ERROR]")} Startup failed, exiting...`);
|
||
process.exit(1);
|
||
}
|
||
}, 10000)
|
||
|
||
// Check all channels in DB, fetch them, if they dont exist, delete all subscriptions
|
||
db.all(`SELECT channelid FROM channels`, (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
rows.forEach((row) => {
|
||
const channel = discord.channels.cache.get(row.channelid);
|
||
if (!channel) {
|
||
// Delete the channel from the database and return
|
||
return db.run(`DELETE FROM channels WHERE channelid = ?`, [row.channelid], (err) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
console.log(`${colors.cyan("[INFO]")} Deleted channel ${row.channelid} from database`);
|
||
});
|
||
};
|
||
});
|
||
});
|
||
|
||
// Get all users in userAlerts and fetch them
|
||
db.all(`SELECT userid FROM userAlerts`, (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
rows.forEach((row) => {
|
||
discord.users.fetch(row.userid);
|
||
});
|
||
});
|
||
});
|
||
|
||
discord.on("interactionCreate", async (interaction) => {
|
||
switch (interaction.type) {
|
||
case Discord.InteractionType.ApplicationCommand:
|
||
switch (interaction.commandName) {
|
||
case "subscribe":
|
||
room = getWFOroom(interaction.options.getString("room"));
|
||
if (!iem.find((channel) => channel.jid.split("@")[0] === room)) {
|
||
interaction.reply({ content: "Invalid room", ephemeral: true });
|
||
return;
|
||
}
|
||
if (interaction.options.getString("filter")) {
|
||
filter = interaction.options.getString("filter").toLowerCase();
|
||
} else {
|
||
filter = "";
|
||
}
|
||
minPriority = interaction.options.getInteger("minpriority");
|
||
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) => {
|
||
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 { // We're in a DM
|
||
db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.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 userAlerts (userid, iemchannel, custommessage, filter, filterEvt, minPriority) VALUES (?, ?, ?, ? ,?, ?)`, [interaction.user.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 });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
break;
|
||
case "unsubscribe":
|
||
// Check that the room is valid
|
||
room = getWFOroom(interaction.options.getString("room"));
|
||
if (!iem.find((channel) => channel.jid.split("@")[0] === room)) {
|
||
interaction.reply({ content: "Invalid room", ephemeral: true });
|
||
return;
|
||
}
|
||
if (interaction.inGuild()) {
|
||
// Check if subbed
|
||
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 unsubscribe from room", ephemeral: true });
|
||
}
|
||
if (!row) {
|
||
return interaction.reply({ content: `Not subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true });
|
||
}
|
||
db.run(`DELETE FROM channels WHERE channelid = ? AND iemchannel = ?`, [interaction.channel.id, room], (err) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true });
|
||
}
|
||
interaction.reply({ content: `Unsubscribed from \`${getWFOByRoom(room).location}\``, ephemeral: true });
|
||
});
|
||
});
|
||
} else {
|
||
db.get(`SELECT * FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err, row) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true });
|
||
}
|
||
if (!row) {
|
||
return interaction.reply({ content: `Not subscribed to \`${getWFOByRoom(room).location}\``, ephemeral: true });
|
||
}
|
||
db.run(`DELETE FROM userAlerts WHERE userid = ? AND iemchannel = ?`, [interaction.user.id, room], (err) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
interaction.reply({ content: "Failed to unsubscribe from room", ephemeral: true });
|
||
}
|
||
interaction.reply({ content: `Unsubscribed from \`${getWFOByRoom(room).location}\``, ephemeral: true });
|
||
});
|
||
});
|
||
}
|
||
break;
|
||
case "list":
|
||
// List all the subscribed rooms
|
||
if (interaction.inGuild()) {
|
||
db.all(`SELECT * FROM channels WHERE channelid = ?`, [interaction.channel.id], (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
interaction.reply({ content: "Failed to get subscribed rooms", ephemeral: true });
|
||
}
|
||
if (!rows) {
|
||
return interaction.reply({ content: "No subscribed rooms", ephemeral: true });
|
||
}
|
||
let roomList = [];
|
||
rows.forEach((row) => {
|
||
roomList.push({
|
||
name: `${row.iemchannel}: ${getWFOByRoom(row.iemchannel).location}`,
|
||
value: `Message: \`\`${row.custommessage || "None"}\`\`\nFilter: \`\`${row.filter || "None"}\`\`\nEvent Filter: \`\`${row.filterEvt || "None"}\`\`\nMin Priority: \`\`${row.minPriority || "None"}\`\``
|
||
});
|
||
});
|
||
const embed = {
|
||
title: "Subscribed Rooms",
|
||
fields: roomList,
|
||
color: 0x00ff00
|
||
}
|
||
interaction.reply({ embeds: [embed], ephemeral: true });
|
||
});
|
||
} else {
|
||
db.all(`SELECT * FROM userAlerts WHERE userid = ?`, [interaction.user.id], (err, rows) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
interaction.reply({ content: "Failed to get subscribed rooms", ephemeral: true });
|
||
}
|
||
if (!rows) {
|
||
return interaction.reply({ content: "No subscribed rooms", ephemeral: true });
|
||
}
|
||
let roomList = [];
|
||
rows.forEach((row) => {
|
||
roomList.push({
|
||
name: `${row.iemchannel}: ${getWFOByRoom(row.iemchannel).location}`,
|
||
value: `Message: \`\`${row.custommessage || "None"}\`\`\nFilter: \`\`${row.filter || "None"}\`\`\nEvent Filter: \`\`${row.filterEvt || "None"}\`\`\nMin Priority: \`\`${row.minPriority || "None"}\`\``
|
||
});
|
||
});
|
||
const embed = {
|
||
title: "Subscribed Rooms",
|
||
fields: roomList,
|
||
color: 0x00ff00
|
||
}
|
||
interaction.reply({ embeds: [embed], ephemeral: true });
|
||
});
|
||
}
|
||
break;
|
||
case "about":
|
||
// Send an embed showing info about the bot, including number of guilds, number of subscribed rooms, etc
|
||
let guilds = discord.guilds.cache.size;
|
||
let channels = 0;
|
||
let uniques = 0;
|
||
await db.get(`SELECT COUNT(*) as count FROM channels`, async (err, row) => {
|
||
channels = row.count
|
||
|
||
await getUniqueChannels().then((unique) => {
|
||
uniques = unique.channels;
|
||
guilds = unique.guilds;
|
||
});
|
||
discord.users.fetch("289884287765839882").then((chrisUser) => {
|
||
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>`,
|
||
},
|
||
{
|
||
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()
|
||
}
|
||
}
|
||
interaction.reply({ embeds: [embed] });
|
||
})
|
||
});
|
||
break;
|
||
case "rooms":
|
||
// // Send an embed showing all the available rooms
|
||
// let roomList = "";
|
||
// iem.forEach((channel) => {
|
||
// room = channel.jid.split("@")[0]
|
||
// console.log(getWFOByRoom(room))
|
||
// roomList += `\`${room}\`: ${getWFOByRoom(room).location}\n`;
|
||
// });
|
||
// const roomEmbed = {
|
||
// title: "Available Rooms",
|
||
// description: roomList,
|
||
// color: 0x00ff00
|
||
// }
|
||
// interaction.reply({ embeds: [roomEmbed], ephemeral: true });
|
||
// Do the above, but paginate like the product text
|
||
let roomList = "";
|
||
iem.forEach((channel) => {
|
||
room = channel.jid.split("@")[0]
|
||
roomList += `\`${room}\`: ${getWFOByRoom(room).location || "Unknown"}\n`;
|
||
});
|
||
const pages = roomList.match(/[\s\S]{1,2000}(?=\n|$)/g);
|
||
embeds = pages.map((page, ind) => ({
|
||
title: `Available Rooms Pg ${ind + 1}/${pages.length}`,
|
||
description: page,
|
||
color: 0x00ff00
|
||
}));
|
||
interaction.reply({ embeds, ephemeral: true });
|
||
break;
|
||
case "setupall":
|
||
if (!interaction.inGuild()) return interaction.reply({ content: "This command can only be used in a guild", ephemeral: true });
|
||
if (!config.discord.owner) return interaction.reply({ content: "Owner not set in config", ephemeral: true });
|
||
if (interaction.user.id !== config.discord.owner) return interaction.reply({ content: "You are not the owner", ephemeral: true });
|
||
await interaction.deferReply({ ephemeral: true })
|
||
var category;
|
||
|
||
|
||
|
||
// New setup, we're pulling from wfos.json now
|
||
const chunks = [];
|
||
const chunkSize = 50;
|
||
const total = iem.length;
|
||
// wfos is object "wfo": {"location": "Text Name", "room": "roomname"}
|
||
for (let i = 0; i < total; i += chunkSize) {
|
||
chunks.push(iem.slice(i, i + chunkSize));
|
||
console.log(iem.slice(i, i + chunkSize))
|
||
}
|
||
|
||
|
||
|
||
chunks.forEach((chunk, index) => {
|
||
const categoryName = `Rooms ${index + 1}`;
|
||
interaction.guild.channels.create({
|
||
name: categoryName,
|
||
type: Discord.ChannelType.GuildCategory
|
||
}).then((newCategory) => {
|
||
console.log(`${colors.cyan("[INFO]")} Created category ${newCategory.name}`);
|
||
chunk.forEach((channel) => {
|
||
channelName = `${channel.jid.split("@")[0]}_${getWFOByRoom(channel.jid.split("@")[0]).location}`
|
||
if (channelName == "Unknown") channelName = channel.jid.split("@")[0]
|
||
newCategory.guild.channels.create({
|
||
name: channelName,
|
||
type: Discord.ChannelType.GuildText,
|
||
parent: newCategory,
|
||
topic: `Weather.im room for ${getWFOByRoom(channel.jid.split("@")[0]).location} - ${channel.jid.split("@")[0]}`
|
||
}).then((newChannel) => {
|
||
console.log(`${colors.cyan("[INFO]")} Created channel ${newChannel.name}`);
|
||
db.run(`INSERT INTO channels (channelid, iemchannel, custommessage) VALUES (?, ?, ?)`, [newChannel.id, channel.jid.split("@")[0], null], (err) => {
|
||
if (err) {
|
||
console.error(err.message);
|
||
}
|
||
console.log(`${colors.cyan("[INFO]")} Added channel ${newChannel.id} to database`);
|
||
});
|
||
}).catch((err) => {
|
||
console.log(`${colors.red("[ERROR]")} Failed to create channel: ${err.message}`);
|
||
});
|
||
});
|
||
}).catch((err) => {
|
||
console.log(`${colors.red("[ERROR]")} Failed to create category: ${err.message}`);
|
||
});
|
||
});
|
||
interaction.editReply({ content: "Setup complete", ephemeral: true });
|
||
break;
|
||
case "support":
|
||
// Generate an invite link to the support server (use widget channel)
|
||
const invite = await discord.guilds.cache.get(config.discord.mainGuild).channels.cache.get(config.discord.inviteChannel).createInvite();
|
||
const embed = {
|
||
title: "Support Server",
|
||
description: `Need help with the bot? Join the support server [here](${invite.url})`,
|
||
color: 0x00ff00
|
||
}
|
||
interaction.reply({ embeds: [embed] });
|
||
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 (!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)
|
||
if (!streamID.match(/^[a-zA-Z0-9]{1,10}$/)) return interaction.reply({ content: "Invalid stream ID", ephemeral: true });
|
||
// 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;
|
||
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)
|
||
if (res) {
|
||
interaction.reply({ content: "Playing Stream", ephemeral: true });
|
||
} else {
|
||
interaction.reply({ content: `Failed to play stream`, ephemeral: true });
|
||
}
|
||
break;
|
||
|
||
case "play": // Play generic stream
|
||
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;
|
||
// 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, interactionUrl, .1, interaction);
|
||
|
||
if (st) {
|
||
interaction.reply({ content: "Joined, trying to start playing.", ephemeral: true });
|
||
} else {
|
||
interaction.reply({ content: `Failed to play stream`, ephemeral: true });
|
||
}
|
||
break;
|
||
|
||
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 });
|
||
res = LeaveVoiceChannel(channel)
|
||
if (res) {
|
||
interaction.reply({ content: "Left voice channel", ephemeral: true });
|
||
} else {
|
||
interaction.reply({ content: "Failed to leave voice channel (Was i ever in one?)", ephemeral: true });
|
||
}
|
||
|
||
break;
|
||
case "pause": // Pause/unpause stream
|
||
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)
|
||
if (res) {
|
||
interaction.reply({ content: "Toggled pause", ephemeral: true });
|
||
} else {
|
||
interaction.reply({ content: "Failed to toggle pause", ephemeral: true });
|
||
}
|
||
break;
|
||
case "volume": // Set volume
|
||
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;
|
||
// Make sure volume isnt negative
|
||
if (volume < 0) volume = 0;
|
||
if (volume > 1) volume = 1;
|
||
res = setVolume(channel, volume)
|
||
if (res) {
|
||
interaction.reply({ content: `Set volume to ${volume * 100}%` });
|
||
} else {
|
||
interaction.reply({ content: "Failed to set volume", ephemeral: true });
|
||
}
|
||
break;
|
||
case "outlook":
|
||
day = interaction.options.getInteger("day");
|
||
type = interaction.options.getString("type");
|
||
if (day < 0 || day > 7) return interaction.reply({ content: "Invalid day", ephemeral: true });
|
||
if (type !== "fire" && type !== "convective") return interaction.reply({ content: "Invalid type", ephemeral: true });
|
||
url = outlookURLs[type][day];
|
||
await interaction.deferReply();
|
||
fetch(url).then((res) => {
|
||
if (res.status !== 200) {
|
||
interaction.editReply({ content: "Failed to get outlook", ephemeral: true });
|
||
return;
|
||
}
|
||
res.buffer().then(async (buffer) => {
|
||
// Check all overlays and add them to image as selected using Jimp
|
||
overlays = ["population", "city", "cwa", "rfc", "interstate", "county", "tribal", "artcc", "fema"]
|
||
await Jimp.read(buffer).then((image) => {
|
||
outImg = image;
|
||
cnt = 0;
|
||
sendMsg = setTimeout(() => {
|
||
interaction.editReply({
|
||
embeds: [{
|
||
title: `${toTitleCase(type)} Outlook Day ${day + 1}`,
|
||
image: {
|
||
url: `attachment://${type}_${day}.png`
|
||
},
|
||
color: 0x00ff00
|
||
}],
|
||
files: [{
|
||
attachment: buffer,
|
||
name: `${type}_${day}.png`
|
||
}]
|
||
});
|
||
}, 150)
|
||
overlays.forEach((overlay) => {
|
||
if (interaction.options.getBoolean(`${overlay}_overlay`)) {
|
||
clearTimeout(sendMsg);
|
||
Jimp.read(`./images/overlays/${overlay}.png`).then((overlayImage) => {
|
||
outImg.composite(overlayImage, 0, 0);
|
||
sendMsg = setTimeout(() => {
|
||
outImg.getBufferAsync(Jimp.MIME_PNG).then((buffer) => {
|
||
interaction.editReply({
|
||
embeds: [{
|
||
title: `${toTitleCase(type)} Outlook Day ${day + 1}`,
|
||
image: {
|
||
url: `attachment://${type}_${day}.png`
|
||
},
|
||
color: 0x00ff00
|
||
}],
|
||
files: [{
|
||
attachment: buffer,
|
||
name: `${type}_${day}.png`
|
||
}]
|
||
});
|
||
});
|
||
}, 150)
|
||
});
|
||
}
|
||
})
|
||
|
||
|
||
|
||
// interaction.editReply({
|
||
// embeds: [{
|
||
// title: `${toTitleCase(type)} Outlook Day ${day + 1}`,
|
||
// image: {
|
||
// url: `attachment://${type}_${day}.png`
|
||
// },
|
||
// color: 0x00ff00
|
||
// }],
|
||
// files: [{
|
||
// attachment: buffer,
|
||
// name: `${type}_${day}.png`
|
||
// }]
|
||
// });
|
||
});
|
||
});
|
||
}).catch((err) => {
|
||
interaction.editReply({ content: "Failed to get outlook", ephemeral: true });
|
||
console.log(`${colors.red("[ERROR]")} Failed to get outlook: ${err.message}`);
|
||
console.error(err);
|
||
});
|
||
break;
|
||
case "alertmap":
|
||
const alertmapurl = "https://forecast.weather.gov/wwamap/png/US.png"
|
||
await interaction.deferReply();
|
||
fetch(alertmapurl).then((res) => {
|
||
if (res.status !== 200) {
|
||
interaction.editReply({ content: "Failed to get alert map", ephemeral: true });
|
||
return;
|
||
}
|
||
res.buffer().then(async (buffer) => {
|
||
interaction.editReply({
|
||
embeds: [{
|
||
title: `Alert Map`,
|
||
image: {
|
||
url: `attachment://alerts.png`
|
||
},
|
||
color: 0x00ff00
|
||
}],
|
||
files: [{
|
||
attachment: buffer,
|
||
name: `alerts.png`
|
||
}]
|
||
});
|
||
});
|
||
}).catch((err) => {
|
||
interaction.editReply({ content: "Failed to get alert map", ephemeral: true });
|
||
console.log(`${colors.red("[ERROR]")} Failed to get alert map: ${err.message}`);
|
||
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 });
|
||
// Fetch all the images
|
||
productOptions = []
|
||
await (() => {
|
||
for (const key in sattelites[sat].products) {
|
||
// make a discord customid safe id for the product name, add it to the sattelites object
|
||
sattelites[sat].products[key].customid = key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
||
productOptions.push({
|
||
label: key,
|
||
value: sattelites[sat].products[key].customid
|
||
})
|
||
}
|
||
console.log(JSON.stringify(productOptions, null, 2))
|
||
})();
|
||
satMessages[interaction.id] = {
|
||
sat
|
||
}
|
||
await interaction.reply({
|
||
content: "Choose a product",
|
||
components: [
|
||
{
|
||
type: 1,
|
||
components: [
|
||
{
|
||
type: 3,
|
||
custom_id: `satproduct|${interaction.id}`,
|
||
label: "Product",
|
||
// map options to product names
|
||
options: productOptions
|
||
}
|
||
]
|
||
}
|
||
]
|
||
})
|
||
break;
|
||
|
||
case "forecast":
|
||
await interaction.deferReply();
|
||
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;
|
||
|
||
|
||
|
||
|
||
}
|
||
case Discord.InteractionType.MessageComponent:
|
||
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}`);
|
||
});
|
||
}
|
||
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(sattelites[sat].products).find(key => sattelites[sat].products[key].customid === product);
|
||
imageOptions = []
|
||
satMessages[interaction.customId.split("|")[1]] = {
|
||
sat,
|
||
product,
|
||
product_name,
|
||
images: {}
|
||
}
|
||
await (() => {
|
||
// for key, value in sattelites[sat].products[product_name]
|
||
console.log(product_name)
|
||
for (const key in sattelites[sat].products[product_name]) {
|
||
// make a discord customid safe id for the product name, add it to the sattelites object
|
||
//console.log(sattelites[sat].products[product_name])
|
||
if (key === "customid") continue;
|
||
satMessages[interaction.customId.split("|")[1]].images[key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()] = sattelites[sat].products[product_name][key];
|
||
imageOptions.push({
|
||
label: key,
|
||
value: key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()
|
||
})
|
||
}
|
||
})();
|
||
interaction.deferReply();
|
||
await interaction.message.edit({
|
||
content: "Choose an image to view",
|
||
components: [
|
||
{
|
||
type: 1,
|
||
components: [
|
||
{
|
||
type: 3,
|
||
custom_id: `satproduct2|${interaction.customId.split("|")[1]}`,
|
||
label: "Image",
|
||
// map options to product names
|
||
options: imageOptions
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}).then(() => {
|
||
interaction.deleteReply();
|
||
});
|
||
break;
|
||
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", 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"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
})
|
||
channel.send({
|
||
embeds: [
|
||
{
|
||
description: `I joined \`${guild.name}\``,
|
||
fields: [
|
||
{
|
||
"name": "User",
|
||
"value": `<@${user.id}> (@${user.username}) ${user.displayName}`
|
||
}
|
||
],
|
||
color: 0x00ff00
|
||
}
|
||
]
|
||
})
|
||
|
||
})
|
||
|
||
discord.on("guildDelete", (guild) => {
|
||
// 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
|
||
channel.send({
|
||
embeds: [
|
||
{
|
||
description: `I left \`${guild.name}\``,
|
||
color: 0xff0000
|
||
}
|
||
]
|
||
})
|
||
})
|
||
|
||
process.on("unhandledRejection", (error, promise) => {
|
||
console.log(`${colors.red("[ERROR]")} Unhandled Rejection @ ${promise}: ${error.stack}`);
|
||
// create errors folder if it doesnt exist
|
||
if (!fs.existsSync("./error")) {
|
||
fs.mkdirSync("./error");
|
||
}
|
||
// write ./error/rejection_timestamp.txt
|
||
fs.writeFileSync(`./error/rejection_${Date.now()}.txt`, `ERROR:\n${error}\n\nPROMISE:\n${JSON.stringify(promise)}`);
|
||
// Send discord log
|
||
const myGuild = discord.guilds.cache.get(config.discord.mainGuild);
|
||
const channel = myGuild.channels.cache.get(config.discord.logChannel);
|
||
channel.send({
|
||
embeds: [
|
||
{
|
||
description: `Unhandled Rejection\n\`\`\`${error}\n${JSON.stringify(promise)}\`\`\``,
|
||
color: 0xff0000
|
||
}
|
||
]
|
||
})
|
||
return;
|
||
});
|
||
|
||
process.on("uncaughtException", (error) => {
|
||
|
||
console.log(`${colors.red("[ERROR]")} Uncaught Exception: ${error.message}\n${error.stack}`);
|
||
if (!fs.existsSync("./error")) {
|
||
fs.mkdirSync("./error");
|
||
}
|
||
// write ./error/exception_timestamp.txt
|
||
fs.writeFileSync(`./error/exception_${Date.now()}.txt`, error.stack);
|
||
// Send message to log channel
|
||
const myGuild = discord.guilds.cache.get(config.discord.mainGuild);
|
||
const channel = myGuild.channels.cache.get(config.discord.logChannel);
|
||
channel.send({
|
||
embeds: [
|
||
{
|
||
description: `Uncaught Exception\n\`\`\`${error.message}\n${error.stack}\`\`\``,
|
||
color: 0xff0000
|
||
}
|
||
]
|
||
})
|
||
return;
|
||
});
|
||
|
||
// Login to discord
|
||
discord.login(config.discord.token); |