utility: add appinfo

This commit is contained in:
Cynthia Foxwell 2023-10-07 22:15:55 -06:00
parent cabe10c230
commit 82106f0dd2
2 changed files with 410 additions and 39 deletions

View file

@ -401,6 +401,14 @@ function getUploadLimit(guild) {
return UPLOAD_LIMIT; 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 = { module.exports = {
pastelize, pastelize,
formatUsername, formatUsername,
@ -416,4 +424,5 @@ module.exports = {
lookupUser, lookupUser,
parseHtmlEntities, parseHtmlEntities,
getUploadLimit, getUploadLimit,
snowflakeToTimestamp,
}; };

View file

@ -11,6 +11,7 @@ const {
lookupUser, lookupUser,
formatUsername, formatUsername,
safeString, safeString,
snowflakeToTimestamp,
} = require("../lib/utils.js"); } = require("../lib/utils.js");
const {getNamesFromString} = require("../lib/unicode.js"); const {getNamesFromString} = require("../lib/unicode.js");
@ -33,6 +34,8 @@ const SPLASH_BASE = CDN + "splashes/";
const BANNER_BASE = CDN + "banners/"; const BANNER_BASE = CDN + "banners/";
const EMOTE_BASE = CDN + "emojis/"; const EMOTE_BASE = CDN + "emojis/";
const CHANNEL_ICON_BASE = CDN + "channel-icons/"; 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 = [ const DEFAULT_GROUP_DM_AVATARS = [
"/assets/ee9275c5a437f7dc7f9430ba95f12ebd.png", "/assets/ee9275c5a437f7dc7f9430ba95f12ebd.png",
@ -46,6 +49,8 @@ const DEFAULT_GROUP_DM_AVATARS = [
]; ];
const CUSTOM_EMOTE_REGEX = /<(?:\u200b|&)?(a)?:(\w+):(\d+)>/; 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; const NOWPLAYING_BAR_LENGTH = 30;
@ -71,6 +76,11 @@ const PRESENCE_ICONS = {
dnd: "<:embedded_dnd:1104972134964543518>", dnd: "<:embedded_dnd:1104972134964543518>",
}, },
}; };
const OS_ICONS = {
darwin: "\u{1f34e}",
win32: "\u{1fa9f}",
linux: "\u{1f427}",
};
const PRESENCE_TYPES = [ const PRESENCE_TYPES = [
"Playing", "Playing",
@ -136,6 +146,43 @@ const USER_FLAGS = [
"RESTRICTED_COLLABORATOR", "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 // https://discord-userdoccers.vercel.app/resources/guild#guild-features
const GUILD_FEATURES = { const GUILD_FEATURES = {
ACTIVITIES_ALPHA: {icon: "\u{1f680}"}, 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] || "<Undocumented 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 // {{{ commands
const avatar = new Command("avatar"); const avatar = new Command("avatar");
@ -796,16 +911,10 @@ snowflake.category = CATEGORY;
snowflake.helpText = "Converts a snowflake ID into readable time."; snowflake.helpText = "Converts a snowflake ID into readable time.";
snowflake.usage = "<--twitter> [snowflake]"; snowflake.usage = "<--twitter> [snowflake]";
snowflake.callback = function (msg, line, [snowflake], {twitter}) { snowflake.callback = function (msg, line, [snowflake], {twitter}) {
const num = parseInt(snowflake); const num = Number(snowflake);
if (!isNaN(num)) { 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 <t:${Math.floor( return `The timestamp for \`${snowflake}\` is <t:${Math.floor(
timestamp / 1000 snowflakeToTimestamp(num, twitter) / 1000
)}:F>`; )}:F>`;
} else { } else {
return "Argument provided is not a number."; return "Argument provided is not a number.";
@ -813,31 +922,6 @@ snowflake.callback = function (msg, line, [snowflake], {twitter}) {
}; };
hf.registerCommand(snowflake); 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] || "<Undocumented Flag>") +
` (1 << ${flag}, ${1n << BigInt(flag)})\n`;
}
return out;
}
const flagdump = new Command("flagdump"); const flagdump = new Command("flagdump");
flagdump.category = CATEGORY; flagdump.category = CATEGORY;
flagdump.helpText = "Dumps Discord user flags."; 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; if (USER_FLAGS[index] == undefined) continue;
allFlags += 1n << BigInt(index); 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)) { } else if (/<@!?(\d+)>/.test(numOrMention) || !isNaN(id)) {
const targetId = id || numOrMention.match(/<@!?(\d+)>/)?.[1]; const targetId = id || numOrMention.match(/<@!?(\d+)>/)?.[1];
if (!targetId) return "Got null ID."; if (!targetId) return "Got null ID.";
@ -865,16 +949,20 @@ flagdump.callback = async function (msg, line, [numOrMention], {id, list}) {
if (!user) { if (!user) {
return "User not cached."; return "User not cached.";
} else { } else {
return `\`${formatUsername(user)}\`'s public flags:\n\`\`\`${flagFromInt( return `\`${formatUsername(user)}\`'s public flags:\n\`\`\`${flagsFromInt(
user.publicFlags user.publicFlags,
USER_FLAGS
)}\`\`\``; )}\`\`\``;
} }
} else if (!isNaN(num)) { } else if (!isNaN(num)) {
return `\`\`\`\n${flagFromInt(num)}\`\`\``; return `\`\`\`\n${flagsFromInt(num, USER_FLAGS)}\`\`\``;
} else { } else {
return `\`${formatUsername( return `\`${formatUsername(
msg.author msg.author
)}\`'s public flags:\n\`\`\`${flagFromInt(msg.author.publicFlags)}\`\`\``; )}\`'s public flags:\n\`\`\`${flagsFromInt(
msg.author.publicFlags,
USER_FLAGS
)}\`\`\``;
} }
}; };
hf.registerCommand(flagdump); hf.registerCommand(flagdump);
@ -1262,7 +1350,6 @@ presence.callback = async function (msg, line) {
}; };
hf.registerCommand(presence); hf.registerCommand(presence);
const POMELO_REGEX = /^[a-z0-9._]{1,32}$/;
const pomelo = new Command("pomelo"); const pomelo = new Command("pomelo");
pomelo.category = CATEGORY; pomelo.category = CATEGORY;
pomelo.helpText = "Check to see if a username is taken or not"; 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); 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: `<t:${snowflakeToTimestamp(app.id)}:R>`,
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] ?? "<unknown 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(", ")
: "<unknown>"
}\n**Publishers:** ${
app.publishers?.length > 0
? app.publishers.map((x) => x.name).join(", ")
: "<unknown>"
}`,
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: "<app id and bot id mismatch or other error>",
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);
// }}} // }}}