weather-bot/index.js

365 lines
10 KiB
JavaScript

// Requires
const config = require("./config.json");
const { client, xml } = require("@xmpp/client");
const fetch = require("node-fetch");
const html = require("html-entities")
const Discord = require("discord.js");
const sqlite3 = require("sqlite3").verbose();
// Setup Discord
const discord = new Discord.Client({
intents: [
"Guilds"
]
});
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.error(err.message);
}
console.log("Connected to the channels database.");
// Create tables if they dont exist
db.run(`CREATE TABLE IF NOT EXISTS channels (channelid TEXT, iemchannel TEXT, custommessage TEXT)`);
});
// Setup stuff
var startup = true;
// Random funcs
const parseProductID = function (product_id) {
const [timestamp, station, wmo, pil] = product_id.split("-");
return {
timestamp: convertDate(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);
return new Date(Date.UTC(year, month - 1, day, hours, mins));
}
const xmpp = client({
service: "xmpp://conference.weather.im",
domain: "weather.im"
});
//debug(xmpp, true);
xmpp.on("error", (err) => {
console.log("ERROR")
console.error(err);
start();
});
xmpp.on("offline", () => {
console.log("offline");
start();
});
xmpp.on("stanza", (stanza) => {
// Stops spam from getting old messages
if (startup) return;
// Get new messages and log them, ignore old messages
if (stanza.is("message") && stanza.attrs.type === "groupchat") {
// 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;
// Get body of message
const body = html.decode(stanza.getChildText("body"));
// get product id from "x" tag
const product_id = parseProductID(stanza.getChild("x").attrs.product_id);
// 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;
let embed = {
title: "New Alert",
description: body,
color: 0x00ff00,
timestamp: product_id.timestamp,
footer: {
text: `Station: ${product_id.station} WMO: ${product_id.wmo} PIL: ${product_id.pil} Channel: ${fromChannel}`
}
}
if (stanza.getChild("x").attrs.twitter_media) {
embed.image = {
url: stanza.getChild("x").attrs.twitter_media
}
}
// Run through the database, and find all channels that are linked to the iem channel
db.all(`SELECT channelid, custommessage FROM channels WHERE iemchannel = ?`, [fromChannel], (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(`Deleted channel ${row.channelid} from database`)
});
};
channel.send({ content: row.custommessage, embeds: [embed] }).then((msg) => {
if (msg.channel.type === Discord.ChannelType.GuildAnnouncement) msg.crosspost();
})
});
});
}
});
xmpp.on("online", async (address) => {
// Start listening on all channels, (dont ban me funny man)
// for (const channel in config.iem.channels) {
// console.log(`Joining ${channel.name}`)
// await xmpp.send(xml("presence", { to: `${channel.jud}/${channel.name}` }));
// }
// Join all channels
config.iem.channels.forEach((channel => {
console.log(`Joining ${channel.name}`)
xmpp.send(xml("presence", { to: `${channel.jid}/${channel.jid.split("@")[0]}` }));
}))
console.log("online as", address.toString());
setTimeout(() => {
startup = false;
}, 1000)
});
const start = () => {
xmpp.start().catch((err) => {
console.error(`start failed, ${err}\nGonna try again in 5 seconds...`);
xmpp.stop();
setTimeout(() => {
start();
}, 5000);
});
}
// END XMPP
// START DISCORD
discord.on('ready', async () => {
console.log(`Logged in as ${discord.user.tag}!`);
// Do slash command stuff
const commands = [
{
"name": "subscribe",
"description": "Subscribe to a weather.im room",
"default_member_permissions": 0,
"options": [
{
"name": "room",
"description": "The room you want to subscribe to",
"type": 3,
"required": true,
"autocomplete": false
},
{
"name": "message",
"description": "Custom message to send when alert is sent",
"type": 3,
"required": false
}
]
},
{
"name": "setmessage",
"description": "Set a custom message for a room",
"default_member_permissions": 0,
"options": [
{
"name": "room",
"description": "The room you want to set a message for",
"type": 3,
"required": true,
"autocomplete": false
},
{
"name": "message",
"description": "Custom message to send when alert is sent",
"type": 3,
"required": true
}
]
},
{
"name": "unsubscribe",
"description": "Unsubscribe from a weather.im room",
"default_member_permissions": 0,
"options": [
{
"name": "room",
"description": "The room you want to unsubscribe from",
"type": 3,
"required": true,
"autocomplete": false
},
]
},
{
"name": "list",
"description": "List all subscribed rooms for this channel",
"default_member_permissions": 0
},
{
"name": "about",
"description": "About this bot"
}
];
await (async () => {
try {
//Global
await rest.put(Routes.applicationCommands(discord.user.id), { body: commands })
} catch (error) {
console.error(error);
}
})();
start();
});
discord.on("interactionCreate", async (interaction) => {
switch(interaction.type) {
case Discord.InteractionType.ApplicationCommand:
if (!interaction.channel) return interaction.reply({ content: "This command can only be run in a text channel", ephemeral: true });
if (interaction.channel.type !== Discord.ChannelType.GuildText && interaction.channel.type !== Discord.ChannelType.GuildAnnouncement) {
interaction.reply({ content: "This command can only be run in a text channel", ephemeral: true });
return;
}
switch (interaction.commandName) {
case "subscribe":
room = interaction.options.getString("room");
if (!config.iem.channels.find((channel) => channel.jid.split("@")[0] === room)) {
interaction.reply({ content: "Invalid room", ephemeral: true });
return;
}
message = interaction.options.getString("message") || null;
db.run(`INSERT INTO channels (channelid, iemchannel, custommessage) VALUES (?, ?, ?)`, [interaction.channel.id, room, message], (err) => {
if (err) {
console.error(err.message);
interaction.reply({ content: "Failed to subscribe to room", ephemeral: true });
} else {
interaction.reply({ content: "Subscribed to room", ephemeral: true });
}
});
break;
case "unsubscribe":
// Check that the room is valid
room = interaction.options.getString("room");
if (!config.iem.channels.find((channel) => channel.jid.split("@")[0] === room)) {
interaction.reply({ content: "Invalid room", ephemeral: true });
return;
}
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 });
} else {
interaction.reply({ content: "Unsubscribed from room", ephemeral: true });
}
});
break;
case "list":
db.all(`SELECT iemchannel, custommessage FROM channels WHERE channelid = ?`, [interaction.channel.id], (err, rows) => {
if (err) {
console.error(err.message);
interaction.reply({ content: "Failed to list subscribed rooms", ephemeral: true });
} else {
let message = "";
rows.forEach((row) => {
message += `Room: \`${row.iemchannel}\` Custom Message: \`\`${row.custommessage}\`\`\n`;
});
if (message === "") {
message = "No subscribed rooms";
}
interaction.reply({ content: message, ephemeral: true });
}
});
break;
case "setmessage":
room = interaction.options.getString("room");
if (!config.iem.channels.find((channel) => channel.jid.split("@")[0] === room)) {
interaction.reply({ content: "Invalid room", ephemeral: true });
return;
}
message = interaction.options.getString("message");
db.run(`UPDATE channels SET custommessage = ? WHERE channelid = ? AND iemchannel = ?`, [message, interaction.channel.id, room], (err) => {
if (err) {
console.error(err.message);
interaction.reply({ content: "Failed to set message", ephemeral: true });
} else {
interaction.reply({ content: "Set message", 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;
await db.run(`SELECT COUNT(*) FROM channels`, (err, row) => {
if (err) {
console.error(err.message);
}
channels = row[0];
});
const embed = {
title: "About Me!",
thumbnail: {
url: discord.user.avatarURL()
},
description: `I am a bot that listens to weather.im alerts and sends them to discord channels.\nI am open source, you can find my code [here!](https://github.com/ChrisChrome/iembot-2.0)`,
fields: [
{
name: "Guilds",
value: guilds
},
{
name: "Subscribed Rooms",
value: channels
}
]
color: 0x00ff00
footer: {
text: "Made by @chrischrome with <3",
icon_url: discord.users.cache.get("289884287765839882").avatarURL()
}
}
}
break;
}
});
// Login to discord
discord.login(config.discord.token);