diff --git a/src/lib/utils.js b/src/lib/utils.js index 8f288ec..e4162fb 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -401,6 +401,14 @@ function getUploadLimit(guild) { return UPLOAD_LIMIT; } +const TWITTER_EPOCH = 1288834974657; +const DISCORD_EPOCH = 1420070400000; +function snowflakeToTimestamp(snowflake, twitter = false) { + return Math.floor(Number(snowflake) / Math.pow(2, 22)) + twitter + ? TWITTER_EPOCH + : DISCORD_EPOCH; +} + module.exports = { pastelize, formatUsername, @@ -416,4 +424,5 @@ module.exports = { lookupUser, parseHtmlEntities, getUploadLimit, + snowflakeToTimestamp, }; diff --git a/src/modules/utility.js b/src/modules/utility.js index a5b050c..c9e47ec 100644 --- a/src/modules/utility.js +++ b/src/modules/utility.js @@ -11,6 +11,7 @@ const { lookupUser, formatUsername, safeString, + snowflakeToTimestamp, } = require("../lib/utils.js"); const {getNamesFromString} = require("../lib/unicode.js"); @@ -33,6 +34,8 @@ const SPLASH_BASE = CDN + "splashes/"; const BANNER_BASE = CDN + "banners/"; const EMOTE_BASE = CDN + "emojis/"; const CHANNEL_ICON_BASE = CDN + "channel-icons/"; +const APP_ICON_BASE = CDN + "app-icons/"; +const APP_ASSET_BASE = CDN + "app-assets/"; const DEFAULT_GROUP_DM_AVATARS = [ "/assets/ee9275c5a437f7dc7f9430ba95f12ebd.png", @@ -46,6 +49,8 @@ const DEFAULT_GROUP_DM_AVATARS = [ ]; const CUSTOM_EMOTE_REGEX = /<(?:\u200b|&)?(a)?:(\w+):(\d+)>/; +const POMELO_REGEX = /^[a-z0-9._]{1,32}$/; +const SNOWFLAKE_REGEX = /[0-9]{17,21}/; const NOWPLAYING_BAR_LENGTH = 30; @@ -71,6 +76,11 @@ const PRESENCE_ICONS = { dnd: "<:embedded_dnd:1104972134964543518>", }, }; +const OS_ICONS = { + darwin: "\u{1f34e}", + win32: "\u{1fa9f}", + linux: "\u{1f427}", +}; const PRESENCE_TYPES = [ "Playing", @@ -136,6 +146,43 @@ const USER_FLAGS = [ "RESTRICTED_COLLABORATOR", ]; +const APPLICATION_FLAGS = [ + "Embedded Release (deprecated)", + "Create Managed Emoji", + "Embedded In-App Purchaces", + "Create Group DMs", + "RPC Private Beta", + "Uses AutoMod", + undefined, + "Allow Assets (deprecated)", + "Allow Spectate (deprecated)", + "Allow Join Requests (deprecated)", + "Has Used RPC (deprecated)", + "Presence Intent", + "Presence Intent (Unverified)", + "Guild Members Intent", + "Guild Members Intent (Unverified)", + "Suspicious Growth", + "Embedded", + "Message Content Intent", + "Message Content Intent (Unverified)", + "Embedded (First Party)", + undefined, + undefined, + "Supports Commands", + "Active", + undefined, + "iframe Modals", +]; + +const APPLICATION_TYPES = [ + undefined, + "Game", + "Music", + "Ticketed Event", + "Creator Monetization", +]; + // https://discord-userdoccers.vercel.app/resources/guild#guild-features const GUILD_FEATURES = { ACTIVITIES_ALPHA: {icon: "\u{1f680}"}, @@ -316,6 +363,74 @@ EMOJI_SETS.fb = EMOJI_SETS.facebook; // }}} +// {{{ helpers + +function flagsFromInt(int, flags, withRaw = true) { + const bits = int.toString(2); + const splitBits = bits.split("").reverse(); + + const reassignedBits = {}; + + for (const shift in splitBits) { + reassignedBits[shift] = splitBits[shift]; + } + + const assignedFlags = Object.keys(reassignedBits).filter( + (bit) => reassignedBits[bit] == 1 + ); + + const out = []; + + for (const flag of assignedFlags) { + out.push( + (flags[flag] || "") + withRaw + ? ` (1 << ${flag}, ${1n << BigInt(flag)})` + : "" + ); + } + + return out.join("\n"); +} + +async function getGuild(id) { + if (hf.bot.guilds.has(id)) { + return {source: "local", data: hf.bot.guilds.get(id)}; + } + + try { + const preview = await hf.bot.requestHandler.request( + "GET", + `/guilds/${id}/preview`, + true + ); + if (preview) return {source: "preview", data: preview}; + } catch { + try { + const discovery = await hf.bot.requestHandler.request( + "GET", + `/discovery/${id}`, + false + ); + if (discovery) return {source: "discovery", data: discovery}; + } catch { + try { + const widget = await hf.bot.requestHandler.request( + "GET", + `/guilds/${id}/widget.json`, + false + ); + if (widget) return {source: "widget", data: widget}; + } catch { + return null; + } + } + } + + return null; +} + +// }}} + // {{{ commands const avatar = new Command("avatar"); @@ -796,16 +911,10 @@ snowflake.category = CATEGORY; snowflake.helpText = "Converts a snowflake ID into readable time."; snowflake.usage = "<--twitter> [snowflake]"; snowflake.callback = function (msg, line, [snowflake], {twitter}) { - const num = parseInt(snowflake); + const num = Number(snowflake); if (!isNaN(num)) { - let binary = num.toString(2); - binary = "0".repeat(64 - binary.length) + binary; - const timestamp = - parseInt(binary.substr(0, 42), 2) + - (twitter ? 1288834974657 : 1420070400000); - return `The timestamp for \`${snowflake}\` is `; } else { return "Argument provided is not a number."; @@ -813,31 +922,6 @@ snowflake.callback = function (msg, line, [snowflake], {twitter}) { }; hf.registerCommand(snowflake); -function flagFromInt(int) { - const bits = int.toString(2); - const splitBits = bits.split("").reverse(); - - const reassignedBits = {}; - - for (const shift in splitBits) { - reassignedBits[shift] = splitBits[shift]; - } - - const flags = Object.keys(reassignedBits).filter( - (bit) => reassignedBits[bit] == 1 - ); - - let out = ""; - - for (const flag of flags) { - out += - (USER_FLAGS[flag] || "") + - ` (1 << ${flag}, ${1n << BigInt(flag)})\n`; - } - - return out; -} - const flagdump = new Command("flagdump"); flagdump.category = CATEGORY; flagdump.helpText = "Dumps Discord user flags."; @@ -850,7 +934,7 @@ flagdump.callback = async function (msg, line, [numOrMention], {id, list}) { if (USER_FLAGS[index] == undefined) continue; allFlags += 1n << BigInt(index); } - return `All flags:\n\`\`\`${flagFromInt(allFlags)}\`\`\``; + return `All flags:\n\`\`\`${flagsFromInt(allFlags, USER_FLAGS)}\`\`\``; } else if (/<@!?(\d+)>/.test(numOrMention) || !isNaN(id)) { const targetId = id || numOrMention.match(/<@!?(\d+)>/)?.[1]; if (!targetId) return "Got null ID."; @@ -865,16 +949,20 @@ flagdump.callback = async function (msg, line, [numOrMention], {id, list}) { if (!user) { return "User not cached."; } else { - return `\`${formatUsername(user)}\`'s public flags:\n\`\`\`${flagFromInt( - user.publicFlags + return `\`${formatUsername(user)}\`'s public flags:\n\`\`\`${flagsFromInt( + user.publicFlags, + USER_FLAGS )}\`\`\``; } } else if (!isNaN(num)) { - return `\`\`\`\n${flagFromInt(num)}\`\`\``; + return `\`\`\`\n${flagsFromInt(num, USER_FLAGS)}\`\`\``; } else { return `\`${formatUsername( msg.author - )}\`'s public flags:\n\`\`\`${flagFromInt(msg.author.publicFlags)}\`\`\``; + )}\`'s public flags:\n\`\`\`${flagsFromInt( + msg.author.publicFlags, + USER_FLAGS + )}\`\`\``; } }; hf.registerCommand(flagdump); @@ -1262,7 +1350,6 @@ presence.callback = async function (msg, line) { }; hf.registerCommand(presence); -const POMELO_REGEX = /^[a-z0-9._]{1,32}$/; const pomelo = new Command("pomelo"); pomelo.category = CATEGORY; pomelo.helpText = "Check to see if a username is taken or not"; @@ -1310,4 +1397,279 @@ pomelo.callback = async function (msg, line) { }; hf.registerCommand(pomelo); +const appinfo = new Command("appinfo"); +appinfo.category = CATEGORY; +appinfo.helpText = "Get information on an application"; +appinfo.usage = "[application id]"; +appinfo.addAlias("ainfo"); +appinfo.addAlias("ai"); +appinfo.callback = async function (msg, line) { + if (!line || line === "") return "Arguments required."; + if (!SNOWFLAKE_REGEX.test(line)) return "Not a snowflake."; + + try { + const _app = await hf.bot.requestHandler.request( + "GET", + `/applications/${line}/rpc`, + false + ); + + let app = _app; + const game = GameData.find((game) => game.id == app.id); + if (game) { + app = Object.assign(app, game); + } + + const assets = await hf.bot.requestHandler.request( + "GET", + `/oauth2/applications/${app.id}/assets`, + false + ); + + const embed = { + title: `${app.name}`, + description: + app.description.length > 0 ? app.description : "*No description*.", + fields: [ + { + name: "Created", + value: ``, + inline: true, + }, + ], + }; + + if (app.icon) { + embed.thumbnail = { + url: `${APP_ICON_BASE}/app-icons/${app.id}/${app.icon}.png?size=1024`, + }; + } + + if (app.type) { + embed.fields.push({ + name: "Type", + value: `${APPLICATION_TYPES[app.type] ?? ""} (\`${ + app.type + }\`)`, + inline: true, + }); + } + + if (app.guild_id) { + const guild = await getGuild(app.guild_id); + if (guild) { + embed.fields.push({ + name: "Guild", + value: `${guild.data.name} (\`${app.guild_id}\`)`, + inline: true, + }); + } else { + embed.fields.push({ + name: "Guild ID", + value: `\`${app.guild_id}\``, + inline: true, + }); + } + } + + if (app.tags) { + embed.fields.push({ + name: "Tags", + value: app.tags.join(", "), + inline: true, + }); + } + + if (app.publishers || app.developers) { + embed.fields.push({ + name: "Game Companies", + value: `**Developers:** ${ + app.developers?.length > 0 + ? app.developers.map((x) => x.name).join(", ") + : "" + }\n**Publishers:** ${ + app.publishers?.length > 0 + ? app.publishers.map((x) => x.name).join(", ") + : "" + }`, + inline: true, + }); + } + + if (app.executables) { + embed.fields.push({ + name: "Game Executables", + value: app.executables + .map( + (exe) => + `${OS_ICONS[exe.os] ?? "\u2753"} \`${exe.name}\`${ + exe.is_launcher ? " (launcher)" : "" + }` + ) + .join("\n"), + inline: true, + }); + } + + if (app.third_party_skus) { + embed.fields.push({ + name: "Game Distributors", + value: app.third_party_skus + .map((sku) => + sku.distributor == "steam" + ? `[Steam](https://steamdb.info/app/${sku.id})` + : sku.distributor == "discord" + ? `[Discord](https://discord.com/store/skus/${sku.id})` + : `${sku.distributor + .split("_") + .map( + (x) => + x[0].substring(1).toUpperCase() + + x.substring(1).toLowerCase() + ) + .join(" ") + .replace(" Net", ".net")}: \`${sku.id}\`` + ) + .join("\n"), + inline: true, + }); + } + + if ( + (app.bot_public != null && !app.bot_require_code_grant) || + (app.integration_public != null && !app.integration_require_code_grant) + ) { + let scope = "bot"; + let permissions = ""; + if (app.install_params) { + if (app.install_params.scopes) { + scope = app.install_params.scopes.join("+"); + } + if (app.install_params.permissions) { + permissions = "&permissions=" + app.install_params.permissions; + } + } + embed.url = `https://discord.com/oauth2/authorize?client_id=${app.id}&scope=${scope}${permissions}`; + + try { + const bot = await hf.bot.requestHandler.request( + "GET", + `/users/${app.id}`, + true + ); + + embed.fields.push({ + name: "Bot", + value: formatUsername(bot), + inline: false, + }); + } catch { + embed.fields.push({ + name: "Bot", + value: "", + inline: false, + }); + } + } + + if (app.custom_install_url) { + embed.url = app.custom_install_url; + } + + if (app.flags > 0) { + const flags = flagsFromInt(app.flags, APPLICATION_FLAGS, false).split( + "\n" + ); + + embed.fields.push({ + name: "Flags", + value: "- " + flags.slice(0, Math.ceil(flags.length / 2)).join("\n- "), + inline: true, + }); + if (flags.length > 1) + embed.fields.push({ + name: "\u200b", + value: + "- " + + flags.slice(Math.ceil(flags.length / 2), flags.length).join("\n- "), + inline: true, + }); + } + + const images = []; + if (app.icon) { + images.push(`[Icon](${embed.thumbnail.url})`); + } + if (app.cover_image) { + images.push( + `[Cover](${APP_ICON_BASE}${app.id}/${app.cover_image}.png?size=2048)` + ); + } + if (app.splash) { + images.push( + `[Splash](${APP_ICON_BASE}${app.id}/${app.splash}.png?size=2048)` + ); + } + + const links = []; + if (app.terms_of_service_url) { + links.push(`[Terms of Service](${app.terms_of_service_url})`); + } + if (app.privacy_policy_url) { + links.push(`[Privacy Policy](${app.privacy_policy_url})`); + } + + if (images.length > 0 || links.length > 0) { + embed.fields.push({ + name: "\u200b", + value: (images.join(" | ") + "\n" + links.join(" | ")).trim(), + inline: false, + }); + } + + if (assets.length > 0) { + if (images.length == 0 && links.length == 0) { + embed.fields.push({ + name: "\u200b", + value: "\u200b", + inline: false, + }); + } + + const mappedAssets = assets.map( + (asset) => `[${asset.name}](${APP_ASSET_BASE}${app.id}/${asset.id}.png)` + ); + + embed.fields.push({ + name: "Assets", + value: + "- " + + mappedAssets + .slice(0, Math.ceil(mappedAssets.length / 2)) + .join("\n- "), + inline: true, + }); + if (mappedAssets.length > 1) + embed.fields.push({ + name: "\u200b", + value: + "- " + + mappedAssets + .slice(Math.ceil(mappedAssets.length / 2), mappedAssets.length) + .join("\n- "), + inline: true, + }); + } + + return {embed}; + } catch (error) { + if (error.message === "Unknown Application") { + return "ID provided does not point to a valid application."; + } else { + return `:warning: Got error \`${safeString(error)}\``; + } + } +}; +hf.registerCommand(appinfo); + // }}}