UNPKG

blapi

Version:

BLAPI is a package to handle posting your discord stats to botlists. It's intended to be used with discord.js, though you can also manually post your stats.

324 lines 14.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.setBotblock = exports.setLogging = exports.manualPost = exports.handle = exports.LogLevel = void 0; const requests_1 = require("./requests"); const fallbackListData_1 = __importDefault(require("./fallbackListData")); const legacyIdsFallbackData_1 = __importDefault(require("./legacyIdsFallbackData")); // eslint doesnt like enums // eslint-disable-next-line no-shadow var LogLevel; (function (LogLevel) { LogLevel[LogLevel["None"] = 0] = "None"; LogLevel[LogLevel["ErrorOnly"] = 16] = "ErrorOnly"; LogLevel[LogLevel["WarnAndErrorOnly"] = 32] = "WarnAndErrorOnly"; LogLevel[LogLevel["All"] = 48] = "All"; })(LogLevel = exports.LogLevel || (exports.LogLevel = {})); let listData = fallbackListData_1.default; let legacyIds = legacyIdsFallbackData_1.default; const lastUpdatedListAt = new Date(1999); // some date that's definitely past let useBotblockAPI = true; let logLevel = LogLevel.WarnAndErrorOnly; /** * the userLogger variable will later be defined with the * logger supplied by the user if they supplied any */ // eslint-disable-next-line max-len let userLogger; function createBlapiMessage(message) { return `BLAPI: ${message}`; } const logger = { info: (msg) => { if (logLevel < LogLevel.All) { return; } if (userLogger) { userLogger.info(createBlapiMessage(msg)); } else { // eslint-disable-next-line no-console console.info(createBlapiMessage(msg)); } }, warn: (msg) => { if (logLevel < LogLevel.WarnAndErrorOnly) { return; } if (userLogger) { userLogger.warn(createBlapiMessage(msg)); } else { // eslint-disable-next-line no-console console.warn(createBlapiMessage(msg)); } }, error: (msg) => { if (logLevel < LogLevel.ErrorOnly) { return; } if (userLogger) { userLogger.error(createBlapiMessage(msg)); } else { // eslint-disable-next-line no-console console.error(createBlapiMessage(msg)); } }, }; function convertLegacyIds(apiKeys) { const newApiKeys = { ...apiKeys }; Object.entries(legacyIds).forEach(([list, newlist]) => { if (newApiKeys[list]) { newApiKeys[newlist] = newApiKeys[list]; delete newApiKeys[list]; } }); return newApiKeys; } function buildBotblockData(apiKeys, bot_id, server_count, shard_id, shard_count, shards) { return { ...convertLegacyIds(apiKeys), bot_id, server_count, shard_id, shard_count, shards, }; } /** * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.} ; * it also includes other metadata including sharddata */ async function postToAllLists(apiKeys, client_id, server_count, shard_id, shard_count, shards) { // make sure we have all lists we can post to and their apis const currentDate = new Date(); if (!listData || lastUpdatedListAt < currentDate) { // we try to update the listdata every day // in case new lists are added but the code is not restarted lastUpdatedListAt.setDate(currentDate.getDate() + 1); try { const tmpListData = await (0, requests_1.get)('https://botblock.org/api/lists?filter=true', logger); // make sure we only save it if nothing goes wrong if (tmpListData) { listData = tmpListData; logger.info('Updated list endpoints.'); } else { logger.error('Got empty list of endpoints from botblock.'); } } catch (e) { logger.error(String(e)); logger.error("Something went wrong when contacting BotBlock for the API of the lists, so we're using an older preset. Some lists might not be available because of this."); } try { const tmpLegacyIdsData = await (0, requests_1.get)('https://botblock.org/api/legacy-ids', logger); // make sure we only save it if nothing goes wrong if (tmpLegacyIdsData) { legacyIds = tmpLegacyIdsData; logger.info('Updated legacy Ids.'); } else { logger.error('Got empty list of legacy Ids from botblock.'); } } catch (e) { logger.error(String(e)); logger.error("Something went wrong when contacting BotBlock for legacy Ids, so we're using an older preset. Some lists might not be available because of this."); } } const posts = []; const updatedApiKeys = convertLegacyIds(apiKeys); Object.entries(listData).forEach(([listname]) => { if (updatedApiKeys[listname] && listData[listname].api_post) { const list = listData[listname]; if (!list.api_post || !list.api_field) { return; } const apiPath = list.api_post.replace(':id', client_id); const sendObj = {}; sendObj[list.api_field] = server_count; if (shard_id && list.api_shard_id) { sendObj[list.api_shard_id] = shard_id; } if (shard_count && list.api_shard_count) { sendObj[list.api_shard_count] = shard_count; } if (shards && list.api_shards) { sendObj[list.api_shards] = shards; } posts.push((0, requests_1.post)(apiPath, updatedApiKeys[listname], sendObj, logger)); } }); return Promise.all(posts); } async function runHandleInternalInSeconds(client, apiKeys, repeatInterval, seconds) { setTimeout( /* eslint-disable-next-line no-use-before-define */ () => handleInternal(client, apiKeys, repeatInterval), 1000 * seconds); } /** * @param client Discord.js client * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.} * @param repeatInterval Number of minutes between each repetition */ async function handleInternal(client, apiKeys, repeatInterval, isFirstRun = false) { // call this function again in the next interval runHandleInternalInSeconds(client, apiKeys, repeatInterval, 60 * repeatInterval); if (client.user) { const client_id = client.user.id; let unchanged; let shard_count; let shards; let server_count = 0; let shard_id; // Checks if bot is sharded // Only run posting from shard 0 if (client.shard?.ids.includes(0)) { shard_count = client.shard.count; shard_id = client.shard.ids.at(0); // this should always only be a single number // This will get as much info as it can, without erroring try { const guildSizes = await client.shard.broadcastEval((broadcastedClient) => broadcastedClient.guilds.cache.size); const shardCounts = guildSizes.filter((count) => count !== 0); if (shardCounts.length !== client.shard.count) { // If not all shards are up yet, we skip this run of handleInternal return; } server_count = shardCounts.reduce((prev, val) => prev + val, 0); } catch (e) { logger.error(String(e)); logger.error('Error while fetching shard server counts:'); } // Checks if bot is sharded with internal sharding } else if (client.ws.shards.size > 1) { shard_count = client.ws.shards.size; // Get array of shards shards = client.ws.shards.map((shard) => client.guilds.cache.filter((guild) => guild.shardId === shard.id).size); if (shards.length !== client.ws.shards.size) { // If not all shards are up yet, we skip this run of handleInternal and try again later if (isFirstRun) { const secondsToWait = 10; logger.info(`Not all shards are up yet, so we're trying again in ${secondsToWait} seconds.`); runHandleInternalInSeconds(client, apiKeys, repeatInterval, secondsToWait); return; } logger.error('Not all shards are up yet, but this is not the first time we\'re trying so we will wait for the entire interval.'); return; } server_count = shards.reduce((prev, val) => prev + val, 0); // Check if bot is not sharded at all, but still wants to send server count // (it's recommended to shard your bot, even if it's only one shard) } else if (!client.shard) { server_count = client.guilds.cache.size; } else { unchanged = true; } // nothing has changed, therefore we don't send any data if (!unchanged) { if (repeatInterval > 2 && useBotblockAPI) { // if the interval isnt below the BotBlock ratelimit, use their API await (0, requests_1.post)('https://botblock.org/api/count', 'no key needed for this', buildBotblockData(apiKeys, client_id, server_count, shard_id, shard_count, shards), logger); // they blacklisted botblock, so we need to do this, posting their stats manually if (apiKeys['top.gg']) { await postToAllLists({ 'top.gg': apiKeys['top.gg'] }, client_id, server_count, shard_id, shard_count, shards); } } else { await postToAllLists(apiKeys, client_id, server_count, shard_id, shard_count, shards); } } } else { if (isFirstRun) { const secondsToWait = 10; logger.info(`Discord client seems to not be connected yet, so we're trying again in ${secondsToWait} seconds.`); runHandleInternalInSeconds(client, apiKeys, repeatInterval, secondsToWait); return; } logger.error('Discord client seems to not be connected yet, but this is not the first time we\'re trying so we will wait for the entire interval.'); } } /** * This function is for automated use with discord.js * @param discordClient Client via wich your code is connected to Discord * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.} * @param repeatInterval Number of minutes until you want to post again, leave out to use 30 */ function handle(discordClient, apiKeys, repeatInterval) { return handleInternal(discordClient, apiKeys, !repeatInterval || repeatInterval < 1 ? 30 : repeatInterval, true); } exports.handle = handle; /** * For when you don't use discord.js or just want to post to manual times * @param guildCount Integer value of guilds your bot is serving * @param botId Snowflake of the ID the user your bot is using * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.} * @param shardId (optional) The shard ID, which will be used to identify the * shards valid for posting * (and for super efficient posting with BLAPIs own distributer when not using botBlock) * @param shardCount (optional) The number of shards the bot has, which is posted to the lists * @param shards (optional) An array of guild counts of each single shard * (this should be a complete list, and only a single shard will post it) */ async function manualPost(guildCount, botID, apiKeys, shard_id, shard_count, shards) { const updatedApiKeys = convertLegacyIds(apiKeys); const client_id = botID; let server_count = guildCount; // check if we want to use sharding if (shard_id === 0 || (shard_id && !shards)) { // if we don't have all the shard info in one place well try to post every shard itself if (shards) { if (shards.length !== shard_count) { throw new Error(`BLAPI: Shardcount (${shard_count}) does not equal the length of the shards array (${shards.length}).`); } server_count = shards.reduce((prev, val) => prev + val, 0); } } const responses = []; if (useBotblockAPI) { responses.push(await (0, requests_1.post)('https://botblock.org/api/count', 'no key needed for this', buildBotblockData(updatedApiKeys, client_id, server_count, shard_id, shard_count, shards), logger)); if (updatedApiKeys['top.gg']) { responses.concat(await postToAllLists({ 'top.gg': updatedApiKeys['top.gg'] }, client_id, server_count, shard_id, shard_count, shards)); } } else { responses.concat(await postToAllLists(updatedApiKeys, client_id, server_count, shard_id, shard_count, shards)); } return responses; } exports.manualPost = manualPost; function setLogging(logOptions) { if (typeof logOptions.logLevel === 'number') { logLevel = logOptions.logLevel; } else if ( // backwards compatibility typeof logOptions.extended === 'boolean') { logLevel = logOptions.extended ? LogLevel.All : LogLevel.WarnAndErrorOnly; } // no logger supplied by user if (!Object.prototype.hasOwnProperty.call(logOptions, 'logger')) { return; } const passedLogger = logOptions.logger; // we checked that it exists beforehand // making sure the logger supplied by the user has our required log levels (info, warn, error) if (typeof passedLogger.info !== 'function' || typeof passedLogger.warn !== 'function' || typeof passedLogger.error !== 'function') { throw new Error('Your supplied logger does not seem to expose the log levels BLAPI needs to work. Make sure your logger offers the following methods: info() warn() error()'); } userLogger = passedLogger; } exports.setLogging = setLogging; function setBotblock(useBotblock) { useBotblockAPI = useBotblock; } exports.setBotblock = setBotblock; //# sourceMappingURL=main.js.map