discord-gamestatus
Version:
Monitor game servers via discord
186 lines (153 loc) • 5.83 kB
JavaScript
;
/*
discord-gamestatus: Game server monitoring via discord API
Copyright (C) 2022 Douile
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
const { Client } = require("pg");
const path = require("path");
const { REST } = require("@discordjs/rest");
const { Routes } = require("discord-api-types/v10");
const { readJSONOrEmpty, throwForSchemaVersion } = require("../dist/utils");
const { REQUIRED_SCHEMA_VERSION } = require("../dist/constants");
// FIXME: This should be passed to the script
const defaultLimits = Object.freeze({
channelLimit: 3,
guildLimit: 3,
activationLimit: undefined,
});
const statusRemovedMessage = Object.freeze({
embeds: [
{
title: "Status has been removed",
description:
"This status message was removed because it exceeded our configured limits.\nThis is done to prevent our bandwidth limits from being exceeded.\n\nIf you self-host our bot you can set your own limits ([instructions on how to do that](https://gamestatus.douile.com/docs/admin)).\nOtherwise you are able to increase the limits on the public bot by donating to our patreon, for more info see our [support server](https://discord.gg/CUefWnZ)",
color: 0xff0000,
},
],
});
if (!process.env.DISCORD_API_KEY) {
console.error("You must provide a DISCORD_API_KEY");
process.exit(1);
}
const rest = new REST({ version: 10 }).setToken(process.env.DISCORD_API_KEY);
const userLimitCache = new Map();
const guildMemberCache = new Map();
let statusesChecked = 0;
let statusesRemoved = 0;
let serversAffected = 0;
let serversChecked = 0;
async function removeStatus(rest, pg, id) {
console.error("Removing", id);
const rowResult = await pg.query(
"SELECT channel_id, message_id FROM statuses WHERE id=$1",
[id]
);
const { channel_id, message_id } = rowResult.rows[0];
let editPromise = rest.patch(Routes.channelMessage(channel_id, message_id), {
body: statusRemovedMessage,
});
await pg.query("DELETE FROM statuses WHERE id=$1", [id]);
await editPromise;
}
async function enforceLimits(rest, pg, serverToCheck, limits) {
statusesChecked += serverToCheck.count;
serversChecked += 1;
// TODO: Check channel limits
// Exceeds the guild limit
if (!isNaN(limits.guildLimit) && serverToCheck.count > limits.guildLimit) {
for (const id of serverToCheck.ids.slice(limits.guildLimit)) {
await removeStatus(rest, pg, id);
}
statusesRemoved += serverToCheck.count - limits.guildLimit;
serversAffected += 1;
}
// Exceeds activation limit
limits.activations += 1;
if (
!isNaN(limits.activationLimit) &&
limits.activations > limits.activationLimit
) {
// TODO: Remove this activation, tricky will break limit check
}
}
async function getGuildMembers(guildId) {
const cachedMembers = guildMemberCache.get(guildId);
if (cachedMembers) return cachedMembers;
const membersObj = {};
const members = await rest.get(Routes.guildMembers(guildId));
for (const member of members) {
membersObj[member.user.id] = member.roles;
}
guildMemberCache.set(guildId, membersObj);
return membersObj;
}
async function resolveLimits(limitRules, userId) {
for (const guildId in limitRules) {
const members = await getGuildMembers(guildId);
if (userId in members) {
for (const roleId in limitRules[guildId]) {
if (members[userId].contains(roleId)) {
return Object.assign({ activations: 0 }, limitRules[guildId][roleId]);
}
}
}
}
return Object.assign({ activations: 0 }, defaultLimits);
}
function createServerChecker(rest, pg, limitRules) {
return async (serverToCheck) => {
console.log(serverToCheck);
if (!serverToCheck.user_id) {
const guildInfo = await rest.get(Routes.guild(serverToCheck.guild_id));
console.log(guildInfo);
serverToCheck.user_id = guildInfo.owner_id;
}
let limits = userLimitCache.get(serverToCheck.user_id);
if (!limits) {
limits = await resolveLimits(limitRules, serverToCheck.user_id);
userLimitCache.set(serverToCheck.user_id, limits);
}
await enforceLimits(rest, pg, serverToCheck, limits);
};
}
(async function () {
console.warn(
"It's a good idea to stop your scheduler and wait a few seconds before running this to prevent the status removed message from being overwritten by an actual update"
);
const limitRules = await readJSONOrEmpty(
path.join(__dirname, "../limit-rules.json")
);
if (Object.keys(limitRules).length === 0) {
console.log("There are no limit rules to enforce");
return;
}
console.error("Limits to enforce", limitRules);
const pg = new Client({
database: process.env.PG_DATABASE || "discord_gamestatus",
});
await pg.connect();
console.error("Connected to database...");
await throwForSchemaVersion(pg, REQUIRED_SCHEMA_VERSION);
const result = await pg.query(
"SELECT ids, count::INT, guild_id, user_id FROM status_counts_per_server_with_activation"
);
// Start transaction
await pg.query("BEGIN");
await Promise.all(result.rows.map(createServerChecker(rest, pg, limitRules)));
// Close transaction
await pg.query("COMMIT");
await pg.end();
// Print stats
console.log(
`Removed ${statusesRemoved}/${statusesChecked} statuses on ${serversAffected}/${serversChecked} guilds`
);
})().then(null, console.error);