diff --git a/bot/package.json b/bot/package.json index b62774260..ead7f7fc5 100644 --- a/bot/package.json +++ b/bot/package.json @@ -35,10 +35,11 @@ "express": "4.17.1", "json5": "2.2.0", "lodash": "4.17.21", + "monk": "^7.3.4", "node-fetch": "3.2.0", "prudence": "0.9.6", "tachi-common": "0.6.11", "ts-node": "10.2.0", "winston": "3.3.3" } -} \ No newline at end of file +} diff --git a/bot/src/config.ts b/bot/src/config.ts index c4e062a88..b464ab582 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -14,6 +14,8 @@ export enum LoggerLayers { chartSearch = "chartSearch", buildChartEmbed = "buildChartEmbed", tachiAuth = "tachiAuth", + database = "database", + databaseQuery = "databaseQuery", } export const theFunny = [ diff --git a/bot/src/database/mongo.ts b/bot/src/database/mongo.ts index 7fbc719d3..cc4d1b599 100644 --- a/bot/src/database/mongo.ts +++ b/bot/src/database/mongo.ts @@ -1,9 +1,26 @@ import { Snowflake } from "discord.js/typings/index.js"; -// import monk from "monk"; +import monk from "monk"; import { integer } from "tachi-common"; -// import { ProcessEnv } from "../setup"; +import { LoggerLayers } from "../config"; +import { ProcessEnv } from "../setup"; +import { createLayeredLogger } from "../utils/logger"; -// const monkDB = monk(ProcessEnv.MONGO_URL); +const logger = createLayeredLogger(LoggerLayers.database); + +logger.info(`Connecting to ${ProcessEnv.MONGO_URL}...`); + +const monkDB = monk(ProcessEnv.MONGO_URL); + +monkDB + .then(() => { + logger.info(`Database connection successful.`, { + bootInfo: true, + }); + }) + .catch((err) => { + logger.crit(err); + process.exit(1); + }); export interface DiscordUserMapDocument { userID: integer; @@ -12,10 +29,7 @@ export interface DiscordUserMapDocument { } const db = { - discordUserMap: (() => { - console.error("DATABASE HAS BEEN STUBBED OUT TEMPORARILY. SIGTERMING."); - process.exit(1); - }) as any, // monkDB.get("discordUserMap") + discordUserMap: monkDB.get("discord-user-map"), }; export default db; diff --git a/bot/src/database/queries.ts b/bot/src/database/queries.ts new file mode 100644 index 000000000..665b6197f --- /dev/null +++ b/bot/src/database/queries.ts @@ -0,0 +1,11 @@ +import { LoggerLayers } from "../config"; +import { createLayeredLogger } from "../utils/logger"; +import db, { DiscordUserMapDocument } from "./mongo"; + +const logger = createLayeredLogger(LoggerLayers.databaseQuery); + +export function GetUserForDiscordID(discordID: string): Promise { + logger.verbose(`Fetching linked user for DiscordID: ${discordID}`); + + return db.discordUserMap.findOne({ discordID: discordID }); +} diff --git a/bot/src/interactionHandlers/command/handleIsCommand.ts b/bot/src/interactionHandlers/command/handleIsCommand.ts index 5360eca0d..9b3c4f879 100644 --- a/bot/src/interactionHandlers/command/handleIsCommand.ts +++ b/bot/src/interactionHandlers/command/handleIsCommand.ts @@ -1,26 +1,38 @@ import { Interaction } from "discord.js"; import { LoggerLayers } from "../../config"; -import { SlashCommand, slashCommands } from "../../slashCommands/register"; +import { GetUserForDiscordID } from "../../database/queries"; +import { slashCommands } from "../../slashCommands/register"; import { createLayeredLogger } from "../../utils/logger"; const logger = createLayeredLogger(LoggerLayers.slashCommands); -export const handleIsCommand = async (interaction: Interaction): Promise => { +// For performance's sake, create a lookup table here and use that to refer +// to commands. +const COMMAND_LOOKUP_TABLE = new Map(); + +for (const command of slashCommands) { + COMMAND_LOOKUP_TABLE.set(command.info.name, command); +} + +export async function handleIsCommand(interaction: Interaction) { try { - /** Rechecking required to enforce types */ - if (interaction.isCommand()) { - const command = slashCommands.find( - (command: SlashCommand) => command.info.name === interaction.commandName - ); + if (!interaction.isCommand()) { + throw new Error(`Can't handle command -- interaction was not a command?`); + } - const requestingUserInfo = GetRequestingUserInfo(interaction); + const command = COMMAND_LOOKUP_TABLE.get(interaction.commandName); - if (command && command.exec) { - logger.info(`Running ${command.info.name} interaction`); - command.exec(interaction); - } + if (!command) { + throw new Error(`A command was requested that does not exist.`); + } + + const requestingUser = await GetUserForDiscordID(interaction.id); + + if (command) { + logger.info(`Running ${command.info.name} interaction.`); + command.exec(interaction, requestingUser); } } catch (e) { - logger.error("Failed to handle isCommand interaction"); + logger.error("Failed to handle isCommand interaction", { error: e }); } -}; +} diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index 7cb8bb0eb..5565800b3 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -10,6 +10,7 @@ import { LoggerLayers } from "../config"; import { help } from "../commands/help/help"; import { createLayeredLogger } from "../utils/logger"; import { gamesToChoicesObject } from "../utils/utils"; +import { Command } from "./types"; const logger = createLayeredLogger(LoggerLayers.slashCommands); @@ -19,7 +20,7 @@ export interface SlashCommand { description: string; options: APIApplicationCommandOption[]; }; - exec(interaction: CommandInteraction): Promise; + exec: Command; } export const slashCommands: SlashCommand[] = [ diff --git a/bot/src/slashCommands/types.ts b/bot/src/slashCommands/types.ts new file mode 100644 index 000000000..be668a498 --- /dev/null +++ b/bot/src/slashCommands/types.ts @@ -0,0 +1,7 @@ +import { CommandInteraction } from "discord.js"; +import { DiscordUserMapDocument } from "../database/mongo"; + +export type Command = ( + interaction: CommandInteraction, + requestingUser: DiscordUserMapDocument | null +) => Promise; diff --git a/bot/src/utils/discord-to-tachi.ts b/bot/src/utils/discord-to-tachi.ts deleted file mode 100644 index b4bf35cff..000000000 --- a/bot/src/utils/discord-to-tachi.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LoggerLayers } from "../config"; -import { createLayeredLogger } from "./logger"; -import db, { DiscordUserMapDocument } from "../mongo/mongo"; -import { Interaction } from "discord.js"; - -const logger = createLayeredLogger(LoggerLayers.tachiAuth); - -export async function GetRequestingUserInfo( - interaction: Interaction -): Promise { - const discordID = interaction.id; - - logger.verbose(`Fetching linked ID for ${discordID}`); - - return db.discordUserMap.findOne({ discordID: discordID }); -} diff --git a/bot/yarn.lock b/bot/yarn.lock index a7e1374f0..e615573eb 100644 --- a/bot/yarn.lock +++ b/bot/yarn.lock @@ -1018,6 +1018,14 @@ "@types/bson" "*" "@types/node" "*" +"@types/mongodb@^3.5.25": + version "3.6.20" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" + integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + "@types/node@*", "@types/node@16.7.10": version "16.7.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc" @@ -1404,6 +1412,14 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1476,6 +1492,11 @@ bson@*: dependencies: buffer "^5.6.0" +bson@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" + integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1727,6 +1748,13 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +debug@*, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1748,13 +1776,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -1792,6 +1813,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3498,6 +3524,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3560,6 +3591,65 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mongodb@^3.2.3: + version "3.7.3" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.3.tgz#b7949cfd0adc4cc7d32d3f2034214d4475f175a5" + integrity sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + optional-require "^1.1.8" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +monk-middleware-cast-ids@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/monk-middleware-cast-ids/-/monk-middleware-cast-ids-0.2.1.tgz#40c40e5a6cb33ccedc289220943275ee8861c529" + integrity sha1-QMQOWmyzPM7cKJIglDJ17ohhxSk= + +monk-middleware-fields@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-fields/-/monk-middleware-fields-0.2.0.tgz#ff637af35f5948879ccb2be15a91360911bea6c1" + integrity sha1-/2N6819ZSIecyyvhWpE2CRG+psE= + +monk-middleware-handle-callback@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/monk-middleware-handle-callback/-/monk-middleware-handle-callback-0.2.2.tgz#47de6cc1248726c72a2be0c81bc4e68310c32146" + integrity sha512-5hBynb7asZ2uw9XVze7C3XH0zXT51yFDvYydk/5HnWWzh2NLglDSiKDcX0yLKPHzFgiq+5Z4Laq5fFVnFsmm8w== + +monk-middleware-options@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/monk-middleware-options/-/monk-middleware-options-0.2.1.tgz#58dae1c518d46636ebdff506fadfc773bb442886" + integrity sha1-WNrhxRjUZjbr3/UG+t/Hc7tEKIY= + +monk-middleware-query@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-query/-/monk-middleware-query-0.2.0.tgz#a926c677d4a5620c62151b0a56d0c0c151675874" + integrity sha1-qSbGd9SlYgxiFRsKVtDAwVFnWHQ= + +monk-middleware-wait-for-connection@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-wait-for-connection/-/monk-middleware-wait-for-connection-0.2.0.tgz#312958d30e588b57d09754dd7c97b4843316835a" + integrity sha1-MSlY0w5Yi1fQl1TdfJe0hDMWg1o= + +monk@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/monk/-/monk-7.3.4.tgz#50ccd7daebb4c16ace58d45b2c28c29112f8a85e" + integrity sha512-PkPNiElwroVyKQj01usyziOvwiKYBUVSq7YU1FB4KFr0J3v0GeXW0TebYsLR4u33WB8JGqPiAcuzDspfdujqQg== + dependencies: + "@types/mongodb" "^3.5.25" + debug "*" + mongodb "^3.2.3" + monk-middleware-cast-ids "^0.2.1" + monk-middleware-fields "^0.2.0" + monk-middleware-handle-callback "^0.2.0" + monk-middleware-options "^0.2.1" + monk-middleware-query "^0.2.0" + monk-middleware-wait-for-connection "^0.2.0" + object-assign "^4.1.1" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3641,6 +3731,11 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + object-inspect@^1.11.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -3698,6 +3793,13 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +optional-require@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" + integrity sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA== + dependencies: + require-at "^1.0.6" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -3973,7 +4075,7 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -readable-stream@^2.3.7: +readable-stream@^2.3.5, readable-stream@^2.3.7: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4000,6 +4102,11 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +require-at@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" + integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4059,7 +4166,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: +safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4069,6 +4176,13 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" @@ -4188,6 +4302,13 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"