AAA
This commit is contained in:
parent
fd4d2eef1d
commit
7b4eacb78c
491
bot.js
491
bot.js
|
@ -3,15 +3,13 @@ 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 = require("./data/nwrstreams.json")
|
||||
const html = require("html-entities");
|
||||
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");
|
||||
|
@ -25,18 +23,18 @@ const discord = new Discord.Client({
|
|||
"DirectMessages"
|
||||
],
|
||||
shards: getInfo().SHARD_LIST, // An array of shards that will get spawned
|
||||
shardCount: getInfo().TOTAL_SHARDS, // Total number of shards
|
||||
shardCount: getInfo().TOTAL_SHARDS, // Total number of shards
|
||||
});
|
||||
discord.cluster = new ClusterClient(discord); // initialize the Client, so we access the .broadcastEval()
|
||||
const {
|
||||
REST,
|
||||
Routes
|
||||
} = require('discord.js');
|
||||
const { default: cluster } = require("cluster");
|
||||
const rest = new REST({
|
||||
version: '10'
|
||||
}).setToken(config.discord.token);
|
||||
|
||||
|
||||
// Setup SQlite DB
|
||||
const db = new sqlite3.Database("channels.db", (err) => {
|
||||
if (err) {
|
||||
|
@ -218,363 +216,177 @@ function setVolume(channel, volume) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
|
||||
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"
|
||||
discord.cluster.on("message", async (message) => {
|
||||
if (!message.type) return; // Invalid message, just ignore, prob me being stupid
|
||||
switch (message.type) {
|
||||
case "addChannel": // Add a channel to the iem array
|
||||
iem.push(message.data);
|
||||
break;
|
||||
case "stanza": // Got a stanza, parse it
|
||||
handleStanza(message.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
//debug(xmpp, true);
|
||||
const handleStanza = (stanza) => {
|
||||
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)];
|
||||
|
||||
xmpp.on("error", (err) => {
|
||||
console.log(`${colors.red("[ERROR]")} XMPP Error: ${err}. Trying to reconnect...`);
|
||||
setTimeout(() => {
|
||||
xmpp.stop().then(() => {
|
||||
start();
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
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}`)
|
||||
}
|
||||
|
||||
xmpp.on("offline", () => {
|
||||
console.log(`${colors.yellow("[WARN]")} XMPP offline, trying to reconnect...`);
|
||||
xmpp.disconnect().then(() => {
|
||||
xmpp.stop().then(() => {
|
||||
start();
|
||||
})
|
||||
})
|
||||
});
|
||||
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++;
|
||||
|
||||
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" })));
|
||||
});
|
||||
// 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}`
|
||||
}
|
||||
}
|
||||
// Get new messages and log them, ignore old messages
|
||||
if (stanza.is("message") && stanza.attrs.type === "groupchat") {
|
||||
// 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}`)
|
||||
if (stanza.getChild("x").attrs.twitter_media) {
|
||||
embed.image = {
|
||||
url: stanza.getChild("x").attrs.twitter_media
|
||||
}
|
||||
}
|
||||
|
||||
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_id_raw,
|
||||
label: "Product Text",
|
||||
emoji: {
|
||||
name: "📄"
|
||||
}
|
||||
let discordMsg = {
|
||||
embeds: [embed],
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
label: "Product",
|
||||
style: 5,
|
||||
url: bodyData.url
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: 1,
|
||||
custom_id: 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((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;
|
||||
]
|
||||
}
|
||||
// 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((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`);
|
||||
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();
|
||||
});
|
||||
// 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) => {
|
||||
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);
|
||||
}).catch((err) => {
|
||||
setTimeout(() => {
|
||||
console.log(`${colors.red("[ERROR]")} Failed to fetch product text, retrying... ${err}`)
|
||||
trySend();
|
||||
})
|
||||
});
|
||||
}
|
||||
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();
|
||||
});
|
||||
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);
|
||||
// User DM alert handling
|
||||
db.all(`SELECT * FROM userAlerts WHERE iemchannel = ?`, [fromChannel], (err, rows) => {
|
||||
if (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
console.log(`${colors.red("[ERROR]")} XMPP failed to start: ${err}.`);
|
||||
xmpp.disconnect().then(() => {
|
||||
xmpp.stop().then(() => {
|
||||
start();
|
||||
})
|
||||
})
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// END XMPP
|
||||
|
||||
// START DISCORD
|
||||
|
||||
discord.on('ready', async () => {
|
||||
|
@ -692,22 +504,13 @@ discord.on('ready', async () => {
|
|||
await (async () => {
|
||||
try {
|
||||
//Global
|
||||
//if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands: ${JSON.stringify(commands, null, 2)}`);
|
||||
// if (config.debug >= 1) console.log(`${colors.magenta("[DEBUG]")} Registering global commands: ${JSON.stringify(commands, null, 2)}`);
|
||||
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) {
|
||||
|
|
338
index.js
338
index.js
|
@ -1,14 +1,336 @@
|
|||
// Typescript: import { ClusterManager } from 'discord-hybrid-sharding'
|
||||
const config = require("./config.json");
|
||||
const blacklist = require("./data/blacklist.json");
|
||||
const wfos = require("./data/wfos.json");
|
||||
const events = require("./data/events.json");
|
||||
const html = require("html-entities");
|
||||
const { client, xml } = require("@xmpp/client");
|
||||
const colors = require("colors")
|
||||
const { ClusterManager } = require('discord-hybrid-sharding');
|
||||
const config = require("./config.json")
|
||||
const manager = new ClusterManager(`${__dirname}/bot.js`, {
|
||||
totalShards: 'auto', // or numeric shard count
|
||||
/// Check below for more options
|
||||
shardsPerClusters: 2, // 2 shards per process
|
||||
totalClusters: 2,
|
||||
mode: 'process', // you can also choose "worker"
|
||||
token: config.discord.token,
|
||||
totalShards: 2, // or numeric shard count
|
||||
/// Check below for more options
|
||||
shardsPerClusters: 1, // 2 shards per process
|
||||
//totalClusters: 2,
|
||||
mode: 'process', // you can also choose "worker"
|
||||
token: config.discord.token,
|
||||
});
|
||||
|
||||
manager.on('clusterCreate', cluster => console.log(`Launched Cluster ${cluster.id}`));
|
||||
// func to Generate random string, ({upper, lower, number, special}, length)
|
||||
// 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);
|
||||
}
|
||||
resolve(rows.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] };
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Vars
|
||||
var startup = true;
|
||||
var iem = []
|
||||
var errCount = 0;
|
||||
const curUUID = generateUUID();
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
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);
|
||||
manager.broadcast({type: "addChannel", data: 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") {
|
||||
// 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}`);
|
||||
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
// Handle Discord
|
||||
manager.broadcast({type: "stanza", stanza})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
manager.spawn({ timeout: -1 });
|
Loading…
Reference in a new issue