From d7be67e8b1df12e6f52097dc98a213d9ae074064 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:13:20 +0700 Subject: [PATCH 1/8] Added VRChat commands --- package.json | 1 + src/discord/VRChat/User.ts | 302 +++++++++++++++++++++++++++++++++++ src/discord/VRChat/World.ts | 255 +++++++++++++++++++++++++++++ src/discord/VRChat/index.ts | 101 ++++++++++++ src/index.ts | 1 + src/providers/App.ts | 6 + src/providers/Discord.ts | 2 + src/providers/Environment.ts | 28 +++- src/providers/VRChatAPI.ts | 53 ++++++ yarn.lock | 88 ++++++++++ 10 files changed, 835 insertions(+), 2 deletions(-) create mode 100644 src/discord/VRChat/User.ts create mode 100644 src/discord/VRChat/World.ts create mode 100644 src/discord/VRChat/index.ts create mode 100644 src/providers/VRChatAPI.ts diff --git a/package.json b/package.json index 1ac3c00..888cd52 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "typescript": "^5.0.4", "utf-8-validate": "^6.0.3", "validator": "^13.9.0", + "vrchat": "^1.11.1", "winston": "^3.8.2", "zlib-sync": "^0.1.8" }, diff --git a/src/discord/VRChat/User.ts b/src/discord/VRChat/User.ts new file mode 100644 index 0000000..41f1f0d --- /dev/null +++ b/src/discord/VRChat/User.ts @@ -0,0 +1,302 @@ +import { Message, ActionRowBuilder, ButtonBuilder, CommandInteraction, BaseInteraction, ButtonStyle } from 'discord.js'; + +import Prisma from '../../providers/Prisma'; +import VRChatAPI from '../../providers/VRChatAPI'; +import validator from 'validator'; +import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; +import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; +import Environment from '../../providers/Environment'; +import ImagePorxy from '../../libs/ImageProxy'; + +const EMBEDS = { + NO_USER_FOUND: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `That user doesn't exists on VRChat`, + user: data.getUser() + }); + }, + NO_USER_MENTIONED: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `No VRChat username or user id provided`, + user: data.getUser() + }); + }, + ERROR: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `Something went wrong while connecting to VRChat`, + user: data.getUser() + }); + } +}; + +export default class VRChatUser { + public static async run(data: HybridInteractionMessage, args: any) { + let user; + if (data.isMessage()) { + if (typeof args[1] === 'undefined') + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_USER_MENTIONED(data)] + }); + const [removed, ...newArgs] = args; + user = newArgs.join(' '); + } else if (data.isApplicationCommand()) user = data.getSlashCommand().options.get('user')?.value?.toString(); + + let result; + + try { + if (user.startsWith('usr_')) result = await VRChatAPI.client!.UsersApi.getUser(user); + else { + let search_result = await VRChatAPI.client!.UsersApi.searchUsers(user, undefined, 100); + if (search_result.data.length === 0) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_USER_FOUND(data)] + }); + + let foundExact = false; + for (let i = 0; i < search_result.data.length; i++) { + if (search_result.data[i].displayName.toLowerCase() === user.toLowerCase()) { + result = await VRChatAPI.client!.UsersApi.getUser(search_result.data[i].id); + foundExact = true; + break; + } + } + + if (!foundExact) result = await VRChatAPI.client!.UsersApi.getUser(search_result.data[0].id); + } + } catch (err: any) { + // If 404 + if (err.response?.status === 404) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_USER_FOUND(data)] + }); + else + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.ERROR(data)] + }); + } + + if (!result) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_USER_FOUND(data)] + }); + + if (data.isApplicationCommand()) { + await data.getSlashCommand().deferReply(); + } + + const embed = makeInfoEmbed({ + icon: null, + title: `**${result.data.displayName}**`, + description: `**${ + result.data.isFriend ? `${this.getState(result.data.status, result.data.state)}` : `โšช Unknown` + }**\n\u200b`, + fields: [ + { + name: `${this.getTrustRankEmoji(result.data.tags)} ${this.getTrustRank( + result.data.tags + )}${this.getExtraRanks(result.data.tags)}`, + value: `\u200b`, + inline: false + }, + { + name: 'โœจ Status', + value: `${result.data.statusDescription || 'None'}\n\u200b`, + inline: true + }, + { + name: '๐ŸŒ Language', + value: `${this.listLanguages(result.data.tags)}\n\u200b`, + inline: true + }, + { + name: 'โœจ Bio', + value: `${result.data.bio}\n\u200b` + }, + { + name: `โค Account Information`, + value: `Joined: , + Avatar Cloning: ${result.data.allowAvatarCopying ? 'Enabled' : 'Disabled'} + User ID: \`\`${result.data.id}\`\`\n` + } + ], + user: data.getUser() + }); + embed.setAuthor({ + name: `VRChat Profile`, + url: `https://vrchat.com/home/user/${result.data.id}`, + iconURL: + 'https://www.theladders.com/s3proxy/company-photo.theladders.com/68949/5721f0dd-d662-4b5e-9c0b-236b8a41b0d3.png' + }); + + let userMainImage; + + if (result.data.userIcon) userMainImage = result.data.userIcon; + else if (result.data.currentAvatarImageUrl) userMainImage = result.data.currentAvatarImageUrl; + + if (userMainImage) + embed.setThumbnail( + ImagePorxy.signImageProxyURL(userMainImage, 'resize:fill:150:150:0') || + 'https://osu.ppy.sh/images/layout/avatar-guest.png' + ); + + if (result.data.profilePicOverride) + embed.setImage( + ImagePorxy.signImageProxyURL(result.data.profilePicOverride, 'resize:fill:960:540:0') || + 'https://osu.ppy.sh/images/layout/avatar-guest.png' + ); + + const row = new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setEmoji('๐Ÿ”—') + .setLabel(' Open Profile') + .setURL(`https://vrchat.com/home/user/${result.data.id}`) + .setStyle(ButtonStyle.Link) + ]); + + for (let link of result.data.bioLinks) { + this.parseBioLink(row, link); + } + + return await sendHybridInteractionMessageResponse(data, { + embeds: [embed], + components: [row] + }); + } + private static getState(status: string, state: string) { + if (state === 'offline') return 'โšช Offline'; + else if (state === 'active') return '๐ŸŸก Active (Web/API)'; + + switch (status) { + case 'join me': + return '๐Ÿ”ต Join Me'; + case 'active': + return '๐ŸŸข Online'; + case 'ask me': + return '๐ŸŸ  Ask Me'; + case 'busy': + return '๐Ÿ”ด Do Not Disturb'; + case 'offline': + return 'โšช Offline'; + default: + return 'โšช Unknown'; + } + } + + private static getTrustRank(tags: string[]) { + /* + Trusted user - system_trust_veteran + Known user - system_trust_trusted + User - system_trust_known + New user - system_trust_basic + Visitor - (No above tags) (or system_no_captcha?) + */ + if (tags.includes('system_trust_veteran')) return 'Trusted User'; + if (tags.includes('system_trust_trusted')) return 'Known User'; + if (tags.includes('system_trust_known')) return 'User'; + if (tags.includes('system_trust_basic')) return 'New User'; + return '๐Ÿค Visitor'; + } + + private static getExtraRanks(tags: string[]) { + let ranks = ''; + if (tags.includes('system_probable_troll')) ranks += ' [Nuisance]'; + if (tags.includes('admin_moderator') || tags.includes('admin_scripting_access')) ranks += ' [VRChat Team]'; + + return ranks; + } + + private static getTrustRankEmoji(tags: string[]) { + if (tags.includes('system_probable_troll')) return '๐Ÿšฉ'; + if (tags.includes('admin_moderator') || tags.includes('admin_scripting_access')) return 'โค๏ธ'; + + if (tags.includes('system_trust_veteran')) return '๐Ÿ’œ'; + if (tags.includes('system_trust_trusted')) return '๐Ÿงก'; + if (tags.includes('system_trust_known')) return '๐Ÿ’š'; + if (tags.includes('system_trust_basic')) return '๐Ÿ’™'; + return '๐Ÿค'; + } + + private static remapLanguageToISO3166(language: string) { + const map: any = { + eng: 'en', + kor: 'ko', + rus: 'ru', + spa: 'es', + por: 'pt', + zho: 'zh', + deu: 'de', + jpn: 'ja', + fra: 'fr', + swe: 'sv', + nld: 'nl', + pol: 'pl', + dan: 'da', + nor: 'no', + ita: 'it', + tha: 'th', + fin: 'fi', + hun: 'hu', + ces: 'cs', + tur: 'tr', + ara: 'ar', + ron: 'ro', + vie: 'vi', + ukr: 'uk' + }; + return map[language] || language; + } + + private static listLanguages(tags: string[]) { + const languages = tags.filter((tag) => tag.startsWith('language_')); + const languagesWithoutPrefix = languages.map((language) => language.replace('language_', '')); + + let finalLang = []; + for (let lang of languagesWithoutPrefix) { + //finalLang.push(`${countryCodeEmoji(this.remapLanguageToISO3166(lang))} ${lang}`); + finalLang.push(`${lang}`); + } + + return finalLang.join(', '); + } + + private static parseBioLink(row: any, link: string) { + const links: any = { + 'twitter.com': { emoji: '๐Ÿฆ', label: 'Twitter' }, + 'youtube.com': { emoji: '๐Ÿ“บ', label: 'YouTube' }, + 'twitch.tv': { emoji: '๐Ÿ“บ', label: 'Twitch' }, + 'instagram.com': { emoji: '๐Ÿ“ท', label: 'Instagram' }, + 'facebook.com': { emoji: '๐Ÿ“ท', label: 'Facebook' }, + 'discord.gg': { emoji: '๐Ÿ“ข', label: 'Discord' }, + 'discord.com': { emoji: '๐Ÿ“ข', label: 'Discord' }, + 'reddit.com': { emoji: '๐Ÿ“ฐ', label: 'Reddit' }, + 'github.com': { emoji: '๐Ÿ“ฆ', label: 'GitHub' }, + 'steamcommunity.com': { emoji: '๐ŸŽฎ', label: 'Steam' }, + 'tiktok.com': { emoji: '๐ŸŽฎ', label: 'TikTok' }, + 'patreon.com': { emoji: '๐Ÿ’ฐ', label: 'Patreon' }, + 'ko-fi.com': { emoji: 'โ˜•', label: 'Ko-Fi' }, + 'paypal.me': { emoji: '๐Ÿ’ณ', label: 'PayPal' }, + 'paypal.com': { emoji: '๐Ÿ’ณ', label: 'PayPal' } + }; + + // Check if valid url using validator + if (!validator.isURL(link)) return; + + const domain = new URL(link).hostname.toLowerCase(); + + if (links[domain]) { + row.addComponents([ + new ButtonBuilder() + .setEmoji(links[domain].emoji) + .setLabel(` ${links[domain].label}`) + .setStyle(ButtonStyle.Link) + .setURL(link) + ]); + } else { + row.addComponents([ + new ButtonBuilder().setEmoji('๐Ÿ”—').setLabel(' Bio Link').setURL(link).setStyle(ButtonStyle.Link) + ]); + } + } +} diff --git a/src/discord/VRChat/World.ts b/src/discord/VRChat/World.ts new file mode 100644 index 0000000..d52b849 --- /dev/null +++ b/src/discord/VRChat/World.ts @@ -0,0 +1,255 @@ +import { Message, ActionRowBuilder, ButtonBuilder, CommandInteraction, BaseInteraction, ButtonStyle } from 'discord.js'; + +import Prisma from '../../providers/Prisma'; +import VRChatAPI from '../../providers/VRChatAPI'; +import validator from 'validator'; +import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; +import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; +import Environment from '../../providers/Environment'; +import ImagePorxy from '../../libs/ImageProxy'; +import * as VRChat from 'vrchat'; + +const EMBEDS = { + NO_WORLD_FOUND: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `That world doesn't exists on VRChat`, + user: data.getUser() + }); + }, + NO_WORLD_MENTIONED: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `No VRChat world name or world id provided`, + user: data.getUser() + }); + }, + NOT_INITIALIZED: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `VRChat feature is disabled`, + user: data.getUser() + }); + }, + ERROR: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `Something went wrong while connecting to VRChat`, + user: data.getUser() + }); + } +}; + +export default class VRChatWorld { + public static async run(data: HybridInteractionMessage, args: any) { + let world; + if (data.isMessage()) { + if (typeof args[1] === 'undefined') + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_WORLD_MENTIONED(data)] + }); + const [removed, ...newArgs] = args; + world = newArgs.join(' '); + } else if (data.isApplicationCommand()) world = data.getSlashCommand().options.get('world')?.value?.toString(); + + let result; + + try { + if (world.startsWith('wrld_')) result = await VRChatAPI.client!.WorldsApi.getWorld(world); + else { + let search_result = await VRChatAPI.client!.WorldsApi.searchWorlds( + undefined, + VRChat.SortOption.Relevance, + undefined, + undefined, + 100, + undefined, //VRChat.OrderOption.Descending, + undefined, + world + ); + if (search_result.data.length === 0) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_WORLD_FOUND(data)] + }); + + let foundExact = false; + for (let i = 0; i < search_result.data.length; i++) { + if (search_result.data[i].name.toLowerCase() === world.toLowerCase()) { + result = await VRChatAPI.client!.WorldsApi.getWorld(search_result.data[i].id); + foundExact = true; + break; + } + } + + if (!foundExact) result = await VRChatAPI.client!.WorldsApi.getWorld(search_result.data[0].id); + } + } catch (err: any) { + if (err.response?.status === 404) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_WORLD_FOUND(data)] + }); + else + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.ERROR(data)] + }); + } + + if (!result) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NO_WORLD_FOUND(data)] + }); + + if (data.isApplicationCommand()) { + await data.getSlashCommand().deferReply(); + } + + let author = await VRChatAPI.client!.UsersApi.getUser(result.data.authorId); + + const embed = makeInfoEmbed({ + icon: null, + title: `**${result.data.name}**`, + description: `Created by **[${result.data.authorName}](https://vrchat.com/home/user/${author.data.id})**\n\u200b`, + fields: [ + { + name: '๐ŸŒŽ World Description', + value: `${result.data.description}\n\u200b`, + inline: false + }, + { + name: `${this.getReleaseStatusEmoji(result.data)} Visiblity`, + value: `${this.getReleaseStatus(result.data)}\n\u200b`, + inline: true + }, + { + name: 'โœจ Visits', + value: `${this.numberWithCommas(result.data.visits)}\n\u200b`, + inline: true + }, + { + name: '๐Ÿ’• Favorites', + value: `${result.data.favorites ? this.numberWithCommas(result.data.favorites) : 0}\n\u200b`, + inline: true + }, + { + name: '๐Ÿ‘ฅ Players', + value: `${result.data.occupants ? this.numberWithCommas(result.data.occupants) : 0} ${ + result.data.instances + ? `(${this.numberWithCommas( + result.data.publicOccupants || 0 + )} public, ${this.numberWithCommas(result.data.privateOccupants || 0)} private)` + : '' + }\n\u200b`, + inline: true + }, + { + name: '๐Ÿ’– Popularity', + value: `${this.numberWithCommas(result.data.popularity)}\n\u200b`, + inline: true + }, + { + name: '๐Ÿ”ฅ Heat', + value: `${result.data.heat}\n\u200b`, + inline: true + }, + { + name: '๐Ÿ“ฅ Capacity', + value: `${result.data.capacity}\n\u200b`, + inline: true + }, + { + name: '๐Ÿ“… Last updated', + value: `\n\n\u200b`, + inline: true + }, + { + name: '๐Ÿ“… Created at', + value: `\n\n\u200b`, + inline: true + }, + { + name: `โค World Information`, + value: `World ID: \`\`${result.data.id}\`\`\nCreator ID: \`\`${result.data.authorId}\`\`\n` + } + ], + user: data.getUser() + }); + + embed.setAuthor({ + name: `VRChat World`, + url: `https://vrchat.com/home/world/${result.data.id}`, + iconURL: + 'https://www.theladders.com/s3proxy/company-photo.theladders.com/68949/5721f0dd-d662-4b5e-9c0b-236b8a41b0d3.png' + }); + + let mainImage; + + if (result.data.thumbnailImageUrl) mainImage = result.data.thumbnailImageUrl; + + if (mainImage) + embed.setImage( + ImagePorxy.signImageProxyURL(mainImage, 'resize:fill:960:540:0') || + 'https://osu.ppy.sh/images/layout/avatar-guest.png' + ); + + let userMainImage; + + if (author.data.userIcon) userMainImage = author.data.userIcon; + else if (author.data.currentAvatarImageUrl) userMainImage = author.data.currentAvatarImageUrl; + + if (userMainImage) + embed.setThumbnail( + ImagePorxy.signImageProxyURL(userMainImage, 'resize:fill:150:150:0') || + 'https://osu.ppy.sh/images/layout/avatar-guest.png' + ); + + const row = new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setEmoji('๐Ÿ”—') + .setLabel(' Open World') + .setURL(`https://vrchat.com/home/world/${result.data.id}`) + .setStyle(ButtonStyle.Link), + new ButtonBuilder() + .setEmoji('๐Ÿ”—') + .setLabel(` ${author.data.displayName}'s Profile`) + .setURL(`https://vrchat.com/home/user/${author.data.id}`) + .setStyle(ButtonStyle.Link) + ]); + + return await sendHybridInteractionMessageResponse(data, { + embeds: [embed], + components: [row] + }); + } + + private static numberWithCommas(x: Number) { + try { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } catch (err) { + return x; + } + } + + private static getReleaseStatusEmoji(world: any) { + switch (world.releaseStatus) { + case 'public': + if (world.tags.includes('system_labs')) return '๐Ÿงช'; + else return '๐ŸŒŽ'; + case 'private': + return '๐Ÿ”’'; + default: + return '๐ŸŒŽ'; + } + } + + private static getReleaseStatus(world: any) { + switch (world.releaseStatus) { + case 'public': + if (world.tags.includes('system_labs')) return 'Labs'; + else return 'Public'; + case 'private': + return 'Private'; + default: + return world.releaseStatus; + } + } +} diff --git a/src/discord/VRChat/index.ts b/src/discord/VRChat/index.ts new file mode 100644 index 0000000..8b988d7 --- /dev/null +++ b/src/discord/VRChat/index.ts @@ -0,0 +1,101 @@ +import { Message, ActionRowBuilder, ButtonBuilder, CommandInteraction, BaseInteraction, ButtonStyle } from 'discord.js'; +import validator from 'validator'; +import { createHmac } from 'crypto'; +import countryLookup from 'country-code-lookup'; +import { countryCodeEmoji } from 'country-code-emoji'; + +import Prisma from '../../providers/Prisma'; +import VRChatAPI from '../../providers/VRChatAPI'; + +import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; +import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; +import Environment from '../../providers/Environment'; + +import VRChatUser from './User'; +import VRChatWorld from './World'; + +const EMBEDS = { + VRChat_INFO: (data: HybridInteractionMessage) => { + return makeInfoEmbed({ + title: 'VRChat', + description: `[VRChat](https://hello.vrchat.com/) is an online virtual world platform created by Graham Gaylor and Jesse Joudrey and operated by VRChat, Inc. The platform allows users to interact with others with user-created 3D avatars and worlds.`, + fields: [ + { + name: 'What can I do?', + value: 'You can view users stats and information' + }, + { + name: 'Available arguments', + value: '``user``' + } + ], + user: data.getUser() + }); + }, + NOT_INITIALIZED: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `VRChat feature is disabled`, + user: data.getUser() + }); + }, + ERROR: (data: HybridInteractionMessage) => { + return makeErrorEmbed({ + title: `Something went wrong while connecting to VRChat`, + user: data.getUser() + }); + } +}; + +export default class VRChat extends DiscordModule { + public id = 'Discord_VRChat'; + public commands = ['vrchat', 'vrc']; + public commandInteractionName = 'vrchat'; + + async GuildOnModuleCommand(args: any, message: Message) { + await this.run(new HybridInteractionMessage(message), args); + } + + async GuildModuleCommandInteractionCreate(interaction: CommandInteraction) { + await this.run(new HybridInteractionMessage(interaction), interaction.options); + } + + async run(data: HybridInteractionMessage, args: any) { + const Guild = await Prisma.client.guild.findFirst({ where: { id: data.getGuild()!.id } }); + if (!Guild) return; + + if (!VRChatAPI.client) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.NOT_INITIALIZED(data)] + }); + + let query; + + if (data.isMessage()) { + if (args.length === 0) { + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.VRChat_INFO(data)] + }); + } + query = args[0].toLowerCase(); + } else if (data.isApplicationCommand()) { + query = args.getSubcommand(); + } + + switch (query) { + case 'user': + case 'u': + return await VRChatUser.run(data, args); + case 'world': + case 'w': + return await VRChatWorld.run(data, args); + } + } + + private numberWithCommas(x: Number) { + try { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } catch (err) { + return x; + } + } +} diff --git a/src/index.ts b/src/index.ts index 5b4a1b6..be5d314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ App.loadPrisma(); App.loadLocale(); App.loadDiscord(); App.load_osu(); +App.loadVRChat(); App.loadExpress(); export default App; diff --git a/src/providers/App.ts b/src/providers/App.ts index 570f43c..8783de5 100644 --- a/src/providers/App.ts +++ b/src/providers/App.ts @@ -3,6 +3,7 @@ import Configuration from './Configuration'; import Prisma from './Prisma'; import Discord from './Discord'; import osu from './osuAPI'; +import VRChat from './VRChatAPI'; import Express from './Express'; import Logger from '../libs/Logger'; @@ -38,6 +39,11 @@ class App { Locale.init(); } + public loadVRChat(): void { + Logger.log('info', 'Loading VRChat'); + VRChat.init(); + } + public load_osu(): void { if (!Environment.get().OSU_API_KEY) { Logger.log('warn', 'OSU_API_KEY is not defined in .env, osu! features will be disabled'); diff --git a/src/providers/Discord.ts b/src/providers/Discord.ts index 1943227..9d54f12 100644 --- a/src/providers/Discord.ts +++ b/src/providers/Discord.ts @@ -25,6 +25,7 @@ import Discord_Say from '../discord/Say'; import Discord_InteractionManager from '../discord/InteractionManager'; import Discord_MembershipScreening from '../discord/MembershipScreening'; import Discord_osu from '../discord/osu'; +import Discord_VRChat from '../discord/VRChat'; import Discord_UserInfo from '../discord/UserInfo'; import Discord_Stats from '../discord/Stats'; import Discord_Support from '../discord/Support'; @@ -78,6 +79,7 @@ class Discord { new Discord_Say(), new Discord_InteractionManager(), new Discord_osu(), + new Discord_VRChat(), new Discord_Settings(), new Discord_Support(), diff --git a/src/providers/Environment.ts b/src/providers/Environment.ts index 27732fd..07fc37a 100644 --- a/src/providers/Environment.ts +++ b/src/providers/Environment.ts @@ -4,7 +4,15 @@ import validator from 'validator'; import Logger from '../libs/Logger'; -const requiredENV = ['NODE_ENV', 'DATABASE_URL', 'DISCORD_TOKEN', 'PRIVATE_BOT']; +const requiredENV = [ + 'NODE_ENV', + 'DATABASE_URL', + 'DISCORD_TOKEN', + 'PRIVATE_BOT', + 'IMGPROXY_HOST', + 'IMGPROXY_KEY', + 'IMGPROXY_SALT' +]; class Environment { public init(): void { @@ -76,6 +84,14 @@ class Environment { const SPOTIFY_REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN; const SPOTIFY_CLIENT_MARKET = process.env.SPOTIFY_CLIENT_MARKET; + const VRC_CONTACT_EMAIL = process.env.VRC_CONTACT_EMAIL; + const VRC_API_KEY = process.env.VRC_API_KEY; + const VRC_COOKIE = process.env.VRC_COOKIE; + + const IMGPROXY_HOST = process.env.IMGPROXY_HOST; + const IMGPROXY_KEY = process.env.IMGPROXY_KEY; + const IMGPROXY_SALT = process.env.IMGPROXY_SALT; + return { NODE_ENV, @@ -93,7 +109,15 @@ class Environment { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN, - SPOTIFY_CLIENT_MARKET + SPOTIFY_CLIENT_MARKET, + + VRC_CONTACT_EMAIL, + VRC_API_KEY, + VRC_COOKIE, + + IMGPROXY_HOST, + IMGPROXY_KEY, + IMGPROXY_SALT }; } diff --git a/src/providers/VRChatAPI.ts b/src/providers/VRChatAPI.ts new file mode 100644 index 0000000..4e57fc6 --- /dev/null +++ b/src/providers/VRChatAPI.ts @@ -0,0 +1,53 @@ +import { Configuration, AuthenticationApi, UsersApi, WorldsApi } from 'vrchat'; +import Environment from './Environment'; +import Logger from '../libs/Logger'; + +import App from './App'; + +interface VRChatAPIs { + AuthenticationApi: AuthenticationApi; + UsersApi: UsersApi; + WorldsApi: WorldsApi; +} + +class VRChatAPI { + public client: VRChatAPIs | null; + public configuration: Configuration | null; + + constructor() { + this.client = null; + this.configuration = null; + } + + public async init() { + this.configuration = new Configuration({ + //username: Environment.get().VRC_USERNAME, + //;password: Environment.get().VRC_PASSWORD, + apiKey: Environment.get().VRC_API_KEY, + baseOptions: { + headers: { + 'User-Agent': `Yumi/${App.version.replaceAll('/', '').replaceAll(' ', '-').replaceAll('--', '-')} ${ + Environment.get().VRC_CONTACT_EMAIL + }`, + 'Content-Type': 'application/json', + Cookie: Environment.get().VRC_COOKIE || '' + } + } + }); + + this.client = { + AuthenticationApi: new AuthenticationApi(this.configuration), + UsersApi: new UsersApi(this.configuration), + WorldsApi: new WorldsApi(this.configuration) + }; + + Logger.info('Logging in to VRChat'); + let res = await this.client.AuthenticationApi.getCurrentUser(); + + Logger.info('Logged in to VRChat as ' + res.data.displayName); + } + + public end(): void {} +} + +export default new VRChatAPI(); diff --git a/yarn.lock b/yarn.lock index cbe302a..985fb2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,11 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/tough-cookie@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/validator@^13.7.15": version "13.7.15" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.15.tgz#408c99d1b5f0eecc78109c11f896f72d1f026a10" @@ -434,6 +439,21 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +axios-cookiejar-support@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz#7b32af7d932508546c68b1fc5ba8f562884162e1" + integrity sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig== + dependencies: + is-redirect "^1.0.0" + pify "^5.0.0" + +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -922,6 +942,11 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +follow-redirects@^1.14.8: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -1135,6 +1160,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -1451,6 +1481,11 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + ping@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/ping/-/ping-0.4.4.tgz#f70c5c8ad7b476d903100c9d45ecd52cf83e1de3" @@ -1493,6 +1528,16 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + qs@6.11.0, qs@^6.9.4: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -1500,6 +1545,11 @@ qs@6.11.0, qs@^6.9.4: dependencies: side-channel "^1.0.4" +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -1566,6 +1616,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve@^1.0.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -1848,6 +1903,16 @@ token-types@^5.0.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +tough-cookie@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -1943,6 +2008,11 @@ undici@^5.21.0: dependencies: busboy "^1.6.0" +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -1953,6 +2023,14 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + utf-8-validate@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" @@ -1985,6 +2063,16 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vrchat@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/vrchat/-/vrchat-1.11.1.tgz#acf37426f717c9cfb60d37ef50131b0c94704c0c" + integrity sha512-zQ+wXN3fnf5y7oVbRQw99JKx8DIQ2xfLxSpR8Yw0SQBOHfgXLrdOQw9MofoHQbW1Rqmj+LnvRiNh9bZM/ffEcw== + dependencies: + "@types/tough-cookie" "^4.0.1" + axios "^0.26.1" + axios-cookiejar-support "^1.0.1" + tough-cookie "^4.0.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 2903f896cffc8b6a9af36da62ab347410c6a0fd3 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:13:24 +0700 Subject: [PATCH 2/8] Create ImageProxy.ts --- src/libs/ImageProxy.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/libs/ImageProxy.ts diff --git a/src/libs/ImageProxy.ts b/src/libs/ImageProxy.ts new file mode 100644 index 0000000..9489a0b --- /dev/null +++ b/src/libs/ImageProxy.ts @@ -0,0 +1,27 @@ +import { createHmac } from 'crypto'; +import Environment from '../providers/Environment'; + +export default class ImagePorxy { + public static signImageProxyURL(url: string, modifiers: string | undefined = undefined) { + const urlSafeBase64 = (string: Buffer) => { + return Buffer.from(string).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + }; + + const hexDecode = (hex: string) => Buffer.from(hex, 'hex'); + + const sign = (salt: string, target: string, secret: string) => { + const hmac = createHmac('sha256', hexDecode(secret)); + hmac.update(hexDecode(salt)); + hmac.update(target); + return urlSafeBase64(hmac.digest()); + }; + + let path; + if (modifiers) path = `/${modifiers}/plain/${url}`; + else path = `/plain/${url}`; + + const signature = sign(Environment.get().IMGPROXY_SALT, path, Environment.get().IMGPROXY_KEY); + const result = `${Environment.get().IMGPROXY_HOST}${signature}${path}`; + return result; + } +} From ce19c4a136b97194ccddc2ff7818992413ce6211 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:16:58 +0700 Subject: [PATCH 3/8] Added VRChat slash commands --- src/utils/DiscordInteraction.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/DiscordInteraction.ts b/src/utils/DiscordInteraction.ts index 758b8a8..a63e0fd 100644 --- a/src/utils/DiscordInteraction.ts +++ b/src/utils/DiscordInteraction.ts @@ -58,6 +58,22 @@ export const GLOBAL_COMMANDS: Object[] = [ .setName('beatmap') .setDescription('Get beatmap information on osu!') .addStringOption((user) => user.setName('beatmap').setDescription('Beatmap id').setRequired(true)) + ), + + new SlashCommandBuilder() + .setName('vrchat') + .setDescription('Interact with the VRChat') + .addSubcommand((user) => + user + .setName('user') + .setDescription('Get user information on VRChat') + .addStringOption((user) => user.setName('user').setDescription('Username or User id').setRequired(true)) + ) + .addSubcommand((world) => + world + .setName('world') + .setDescription('Get world information on VRChat') + .addStringOption((user) => user.setName('world').setDescription('World id').setRequired(true)) ) ]; export const GUILD_COMMANDS: Object[] = []; From 6ddbe3a0eedf80a534cd321d710c2c38aaadaf66 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:17:02 +0700 Subject: [PATCH 4/8] Typo --- src/discord/InteractionManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/discord/InteractionManager.ts b/src/discord/InteractionManager.ts index c28c87f..25239c6 100644 --- a/src/discord/InteractionManager.ts +++ b/src/discord/InteractionManager.ts @@ -25,7 +25,7 @@ const EMBEDS = { fields: [ { name: 'Available arguments', - value: '``reloadAll`` ``unloadAll`` ``reloadglobal``' + value: '``reloadAll`` ``unloadAll`` ``reloadGlobal``' } ], user: data.getUser() From be00264f1a6e8d7d88d61a40bc460be872b5330b Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:18:02 +0700 Subject: [PATCH 5/8] Remove extra emoji --- src/discord/VRChat/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/discord/VRChat/User.ts b/src/discord/VRChat/User.ts index 41f1f0d..fe3765c 100644 --- a/src/discord/VRChat/User.ts +++ b/src/discord/VRChat/User.ts @@ -196,7 +196,7 @@ export default class VRChatUser { if (tags.includes('system_trust_trusted')) return 'Known User'; if (tags.includes('system_trust_known')) return 'User'; if (tags.includes('system_trust_basic')) return 'New User'; - return '๐Ÿค Visitor'; + return 'Visitor'; } private static getExtraRanks(tags: string[]) { From 89c5b2fa8b6e287a58e55cb9fec68f51cdb9b023 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 14:18:21 +0700 Subject: [PATCH 6/8] Fixed broken log level check --- src/index.ts | 6 +++++- src/providers/Environment.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index be5d314..3c3fdfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,14 @@ +import path from 'path'; +import dotenv from 'dotenv'; +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + import App from './providers/App'; import NativeException from './exception/NativeException'; NativeException.process(); -App.loadConfig(); App.loadENV(); +App.loadConfig(); App.loadPrisma(); App.loadLocale(); App.loadDiscord(); diff --git a/src/providers/Environment.ts b/src/providers/Environment.ts index 07fc37a..87e50bf 100644 --- a/src/providers/Environment.ts +++ b/src/providers/Environment.ts @@ -1,5 +1,3 @@ -import * as path from 'path'; -import * as dotenv from 'dotenv'; import validator from 'validator'; import Logger from '../libs/Logger'; @@ -16,8 +14,6 @@ const requiredENV = [ class Environment { public init(): void { - dotenv.config({ path: path.resolve(__dirname, '../../.env') }); - for (let param of requiredENV) { if (this.isUndefinedOrEmpty(process.env[param])) throw new Error(`.env ${param} is undefined`); } From 59554b9fd8703b52c5d74c754cb0f584e9e23057 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 14:23:20 +0700 Subject: [PATCH 7/8] Implement basic caching --- src/discord/VRChat/User.ts | 62 +++++++++--------- src/discord/VRChat/World.ts | 76 +++++++++++----------- src/providers/VRChatAPI.ts | 124 ++++++++++++++++++++++++++++++++---- 3 files changed, 181 insertions(+), 81 deletions(-) diff --git a/src/discord/VRChat/User.ts b/src/discord/VRChat/User.ts index fe3765c..b6e541f 100644 --- a/src/discord/VRChat/User.ts +++ b/src/discord/VRChat/User.ts @@ -31,22 +31,22 @@ const EMBEDS = { export default class VRChatUser { public static async run(data: HybridInteractionMessage, args: any) { - let user; + let query; if (data.isMessage()) { if (typeof args[1] === 'undefined') return await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NO_USER_MENTIONED(data)] }); const [removed, ...newArgs] = args; - user = newArgs.join(' '); - } else if (data.isApplicationCommand()) user = data.getSlashCommand().options.get('user')?.value?.toString(); + query = newArgs.join(' '); + } else if (data.isApplicationCommand()) query = data.getSlashCommand().options.get('user')?.value?.toString(); - let result; + let user; try { - if (user.startsWith('usr_')) result = await VRChatAPI.client!.UsersApi.getUser(user); + if (query.startsWith('usr_')) user = await VRChatAPI.getCachedUserById(query); else { - let search_result = await VRChatAPI.client!.UsersApi.searchUsers(user, undefined, 100); + let search_result = await VRChatAPI.client!.UsersApi.searchUsers(query, undefined, 100); if (search_result.data.length === 0) return await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NO_USER_FOUND(data)] @@ -54,14 +54,14 @@ export default class VRChatUser { let foundExact = false; for (let i = 0; i < search_result.data.length; i++) { - if (search_result.data[i].displayName.toLowerCase() === user.toLowerCase()) { - result = await VRChatAPI.client!.UsersApi.getUser(search_result.data[i].id); + if (search_result.data[i].displayName.toLowerCase() === query.toLowerCase()) { + user = await VRChatAPI.getCachedUserById(search_result.data[i].id); foundExact = true; break; } } - if (!foundExact) result = await VRChatAPI.client!.UsersApi.getUser(search_result.data[0].id); + if (!foundExact) user = await VRChatAPI.getCachedUserById(search_result.data[0].id); } } catch (err: any) { // If 404 @@ -75,7 +75,7 @@ export default class VRChatUser { }); } - if (!result) + if (!user) return await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NO_USER_FOUND(data)] }); @@ -86,54 +86,52 @@ export default class VRChatUser { const embed = makeInfoEmbed({ icon: null, - title: `**${result.data.displayName}**`, - description: `**${ - result.data.isFriend ? `${this.getState(result.data.status, result.data.state)}` : `โšช Unknown` - }**\n\u200b`, + title: `**${user.displayName}**`, + description: `**${user.isFriend ? `${this.getState(user.status, user.state)}` : `โšช Unknown`}**\n\u200b`, fields: [ { - name: `${this.getTrustRankEmoji(result.data.tags)} ${this.getTrustRank( - result.data.tags - )}${this.getExtraRanks(result.data.tags)}`, + name: `${this.getTrustRankEmoji(user.tags)} ${this.getTrustRank(user.tags)}${this.getExtraRanks( + user.tags + )}`, value: `\u200b`, inline: false }, { name: 'โœจ Status', - value: `${result.data.statusDescription || 'None'}\n\u200b`, + value: `${user.statusDescription || 'None'}\n\u200b`, inline: true }, { name: '๐ŸŒ Language', - value: `${this.listLanguages(result.data.tags)}\n\u200b`, + value: `${this.listLanguages(user.tags)}\n\u200b`, inline: true }, { name: 'โœจ Bio', - value: `${result.data.bio}\n\u200b` + value: `${user.bio}\n\u200b` }, { name: `โค Account Information`, - value: `Joined: , - Avatar Cloning: ${result.data.allowAvatarCopying ? 'Enabled' : 'Disabled'} - User ID: \`\`${result.data.id}\`\`\n` + value: `Joined: , + Avatar Cloning: ${user.allowAvatarCopying ? 'Enabled' : 'Disabled'} + User ID: \`\`${user.id}\`\`\n` } ], user: data.getUser() }); embed.setAuthor({ name: `VRChat Profile`, - url: `https://vrchat.com/home/user/${result.data.id}`, + url: `https://vrchat.com/home/user/${user.id}`, iconURL: 'https://www.theladders.com/s3proxy/company-photo.theladders.com/68949/5721f0dd-d662-4b5e-9c0b-236b8a41b0d3.png' }); let userMainImage; - if (result.data.userIcon) userMainImage = result.data.userIcon; - else if (result.data.currentAvatarImageUrl) userMainImage = result.data.currentAvatarImageUrl; + if (user.userIcon) userMainImage = user.userIcon; + else if (user.currentAvatarImageUrl) userMainImage = user.currentAvatarImageUrl; if (userMainImage) embed.setThumbnail( @@ -141,9 +139,9 @@ export default class VRChatUser { 'https://osu.ppy.sh/images/layout/avatar-guest.png' ); - if (result.data.profilePicOverride) + if (user.profilePicOverride) embed.setImage( - ImagePorxy.signImageProxyURL(result.data.profilePicOverride, 'resize:fill:960:540:0') || + ImagePorxy.signImageProxyURL(user.profilePicOverride, 'resize:fill:960:540:0') || 'https://osu.ppy.sh/images/layout/avatar-guest.png' ); @@ -151,11 +149,11 @@ export default class VRChatUser { new ButtonBuilder() .setEmoji('๐Ÿ”—') .setLabel(' Open Profile') - .setURL(`https://vrchat.com/home/user/${result.data.id}`) + .setURL(`https://vrchat.com/home/user/${user.id}`) .setStyle(ButtonStyle.Link) ]); - for (let link of result.data.bioLinks) { + for (let link of user.bioLinks) { this.parseBioLink(row, link); } diff --git a/src/discord/VRChat/World.ts b/src/discord/VRChat/World.ts index d52b849..4ec89c5 100644 --- a/src/discord/VRChat/World.ts +++ b/src/discord/VRChat/World.ts @@ -38,20 +38,20 @@ const EMBEDS = { export default class VRChatWorld { public static async run(data: HybridInteractionMessage, args: any) { - let world; + let query; if (data.isMessage()) { if (typeof args[1] === 'undefined') return await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NO_WORLD_MENTIONED(data)] }); const [removed, ...newArgs] = args; - world = newArgs.join(' '); - } else if (data.isApplicationCommand()) world = data.getSlashCommand().options.get('world')?.value?.toString(); + query = newArgs.join(' '); + } else if (data.isApplicationCommand()) query = data.getSlashCommand().options.get('world')?.value?.toString(); let result; try { - if (world.startsWith('wrld_')) result = await VRChatAPI.client!.WorldsApi.getWorld(world); + if (query.startsWith('wrld_')) result = await VRChatAPI.getCachedWorldById(query); else { let search_result = await VRChatAPI.client!.WorldsApi.searchWorlds( undefined, @@ -61,7 +61,7 @@ export default class VRChatWorld { 100, undefined, //VRChat.OrderOption.Descending, undefined, - world + query ); if (search_result.data.length === 0) return await sendHybridInteractionMessageResponse(data, { @@ -70,14 +70,14 @@ export default class VRChatWorld { let foundExact = false; for (let i = 0; i < search_result.data.length; i++) { - if (search_result.data[i].name.toLowerCase() === world.toLowerCase()) { - result = await VRChatAPI.client!.WorldsApi.getWorld(search_result.data[i].id); + if (search_result.data[i].name.toLowerCase() === query.toLowerCase()) { + result = await VRChatAPI.getCachedWorldById(search_result.data[i].id); foundExact = true; break; } } - if (!foundExact) result = await VRChatAPI.client!.WorldsApi.getWorld(search_result.data[0].id); + if (!foundExact) result = await VRChatAPI.getCachedWorldById(search_result.data[0].id); } } catch (err: any) { if (err.response?.status === 404) @@ -99,76 +99,80 @@ export default class VRChatWorld { await data.getSlashCommand().deferReply(); } - let author = await VRChatAPI.client!.UsersApi.getUser(result.data.authorId); + let author = await VRChatAPI.getCachedUserById(result.authorId); + if (!author) + return await sendHybridInteractionMessageResponse(data, { + embeds: [EMBEDS.ERROR(data)] + }); const embed = makeInfoEmbed({ icon: null, - title: `**${result.data.name}**`, - description: `Created by **[${result.data.authorName}](https://vrchat.com/home/user/${author.data.id})**\n\u200b`, + title: `**${result.name}**`, + description: `Created by **[${result.authorName}](https://vrchat.com/home/user/${author.id})**\n\u200b`, fields: [ { name: '๐ŸŒŽ World Description', - value: `${result.data.description}\n\u200b`, + value: `${result.description}\n\u200b`, inline: false }, { - name: `${this.getReleaseStatusEmoji(result.data)} Visiblity`, - value: `${this.getReleaseStatus(result.data)}\n\u200b`, + name: `${this.getReleaseStatusEmoji(result)} Visiblity`, + value: `${this.getReleaseStatus(result)}\n\u200b`, inline: true }, { name: 'โœจ Visits', - value: `${this.numberWithCommas(result.data.visits)}\n\u200b`, + value: `${this.numberWithCommas(result.visits)}\n\u200b`, inline: true }, { name: '๐Ÿ’• Favorites', - value: `${result.data.favorites ? this.numberWithCommas(result.data.favorites) : 0}\n\u200b`, + value: `${result.favorites ? this.numberWithCommas(result.favorites) : 0}\n\u200b`, inline: true }, { name: '๐Ÿ‘ฅ Players', - value: `${result.data.occupants ? this.numberWithCommas(result.data.occupants) : 0} ${ - result.data.instances - ? `(${this.numberWithCommas( - result.data.publicOccupants || 0 - )} public, ${this.numberWithCommas(result.data.privateOccupants || 0)} private)` + value: `${result.occupants ? this.numberWithCommas(result.occupants) : 0} ${ + result.instances + ? `(${this.numberWithCommas(result.publicOccupants || 0)} public, ${this.numberWithCommas( + result.privateOccupants || 0 + )} private)` : '' }\n\u200b`, inline: true }, { name: '๐Ÿ’– Popularity', - value: `${this.numberWithCommas(result.data.popularity)}\n\u200b`, + value: `${this.numberWithCommas(result.popularity)}\n\u200b`, inline: true }, { name: '๐Ÿ”ฅ Heat', - value: `${result.data.heat}\n\u200b`, + value: `${result.heat}\n\u200b`, inline: true }, { name: '๐Ÿ“ฅ Capacity', - value: `${result.data.capacity}\n\u200b`, + value: `${result.capacity}\n\u200b`, inline: true }, { name: '๐Ÿ“… Last updated', - value: `\n\n\n\u200b`, inline: true }, { name: '๐Ÿ“… Created at', - value: `\n\n\n\u200b`, inline: true }, { name: `โค World Information`, - value: `World ID: \`\`${result.data.id}\`\`\nCreator ID: \`\`${result.data.authorId}\`\`\n` + value: `World ID: \`\`${result.id}\`\`\nCreator ID: \`\`${result.authorId}\`\`\n` } ], user: data.getUser() @@ -176,14 +180,14 @@ export default class VRChatWorld { embed.setAuthor({ name: `VRChat World`, - url: `https://vrchat.com/home/world/${result.data.id}`, + url: `https://vrchat.com/home/world/${result.id}`, iconURL: 'https://www.theladders.com/s3proxy/company-photo.theladders.com/68949/5721f0dd-d662-4b5e-9c0b-236b8a41b0d3.png' }); let mainImage; - if (result.data.thumbnailImageUrl) mainImage = result.data.thumbnailImageUrl; + if (result.thumbnailImageUrl) mainImage = result.thumbnailImageUrl; if (mainImage) embed.setImage( @@ -193,8 +197,8 @@ export default class VRChatWorld { let userMainImage; - if (author.data.userIcon) userMainImage = author.data.userIcon; - else if (author.data.currentAvatarImageUrl) userMainImage = author.data.currentAvatarImageUrl; + if (author.userIcon) userMainImage = author.userIcon; + else if (author.currentAvatarImageUrl) userMainImage = author.currentAvatarImageUrl; if (userMainImage) embed.setThumbnail( @@ -206,12 +210,12 @@ export default class VRChatWorld { new ButtonBuilder() .setEmoji('๐Ÿ”—') .setLabel(' Open World') - .setURL(`https://vrchat.com/home/world/${result.data.id}`) + .setURL(`https://vrchat.com/home/world/${result.id}`) .setStyle(ButtonStyle.Link), new ButtonBuilder() .setEmoji('๐Ÿ”—') - .setLabel(` ${author.data.displayName}'s Profile`) - .setURL(`https://vrchat.com/home/user/${author.data.id}`) + .setLabel(` ${author.displayName}'s Profile`) + .setURL(`https://vrchat.com/home/user/${author.id}`) .setStyle(ButtonStyle.Link) ]); diff --git a/src/providers/VRChatAPI.ts b/src/providers/VRChatAPI.ts index 4e57fc6..07612c4 100644 --- a/src/providers/VRChatAPI.ts +++ b/src/providers/VRChatAPI.ts @@ -1,34 +1,58 @@ -import { Configuration, AuthenticationApi, UsersApi, WorldsApi } from 'vrchat'; +import * as VRChat from 'vrchat'; import Environment from './Environment'; import Logger from '../libs/Logger'; - import App from './App'; +const VRC_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + interface VRChatAPIs { - AuthenticationApi: AuthenticationApi; - UsersApi: UsersApi; - WorldsApi: WorldsApi; + AuthenticationApi: VRChat.AuthenticationApi; + UsersApi: VRChat.UsersApi; + WorldsApi: VRChat.WorldsApi; +} + +interface UserCache { + user: VRChat.User; + expires: Date; +} + +interface WorldCache { + world: VRChat.World; + expires: Date; +} + +interface Cache { + Users: { [key: string]: UserCache }; + Worlds: { [key: string]: WorldCache }; } class VRChatAPI { public client: VRChatAPIs | null; - public configuration: Configuration | null; + public configuration: VRChat.Configuration | null; + private cache: Cache; + private useragent: string | null = null; constructor() { + this.cache = { + Users: {}, + Worlds: {} + }; this.client = null; this.configuration = null; } public async init() { - this.configuration = new Configuration({ + this.useragent = `Yumi/${App.version.replaceAll('/', '').replaceAll(' ', '-').replaceAll('--', '-')} ${ + Environment.get().VRC_CONTACT_EMAIL + }`; + Logger.debug(`[VRChat] Using user agent ${this.useragent}`); + this.configuration = new VRChat.Configuration({ //username: Environment.get().VRC_USERNAME, //;password: Environment.get().VRC_PASSWORD, apiKey: Environment.get().VRC_API_KEY, baseOptions: { headers: { - 'User-Agent': `Yumi/${App.version.replaceAll('/', '').replaceAll(' ', '-').replaceAll('--', '-')} ${ - Environment.get().VRC_CONTACT_EMAIL - }`, + 'User-Agent': this.useragent, 'Content-Type': 'application/json', Cookie: Environment.get().VRC_COOKIE || '' } @@ -36,15 +60,89 @@ class VRChatAPI { }); this.client = { - AuthenticationApi: new AuthenticationApi(this.configuration), - UsersApi: new UsersApi(this.configuration), - WorldsApi: new WorldsApi(this.configuration) + AuthenticationApi: new VRChat.AuthenticationApi(this.configuration), + UsersApi: new VRChat.UsersApi(this.configuration), + WorldsApi: new VRChat.WorldsApi(this.configuration) }; Logger.info('Logging in to VRChat'); let res = await this.client.AuthenticationApi.getCurrentUser(); Logger.info('Logged in to VRChat as ' + res.data.displayName); + + setInterval(() => { + for (let key in this.cache.Users) { + if (this.cache.Users[key].expires < new Date()) { + Logger.debug('[VRChat] [Cache] Removing user ' + key + ' from cache'); + delete this.cache.Users[key]; + } + } + for (let key in this.cache.Worlds) { + if (this.cache.Worlds[key].expires < new Date()) { + Logger.debug('[VRChat] [Cache] Removing world ' + key + ' from cache'); + delete this.cache.Worlds[key]; + } + } + }, VRC_CACHE_DURATION); + } + + public async getCachedUserById(id: string) { + if (typeof this.cache.Users[id] !== 'undefined') { + Logger.debug('[VRChat] [Cache] Cache hit for user ' + id); + if (this.cache.Users[id].expires > new Date()) { + return this.cache.Users[id].user; + } + } + + Logger.debug('[VRChat] [Cache] Cache miss for ' + id); + let user; + try { + user = await this.client!.UsersApi.getUser(id); + } catch (err: any) { + if (err.response?.status === 404) return null; + throw err; + } + + if (!user) { + return null; + } + + Logger.debug('[VRChat] [Cache] Caching user ' + id); + this.cache.Users[id] = { + user: user.data, + expires: new Date(Date.now() + VRC_CACHE_DURATION) + }; + return user.data; + } + + public async getCachedWorldById(id: string) { + if (typeof this.cache.Worlds[id] !== 'undefined') { + Logger.debug('[VRChat] [Cache] Cache hit for world ' + id); + if (this.cache.Worlds[id].expires > new Date()) { + return this.cache.Worlds[id].world; + } + } + + Logger.debug('[VRChat] [Cache] Cache miss for ' + id); + let world; + try { + world = await this.client!.WorldsApi.getWorld(id); + } catch (err: any) { + if (err.response?.status === 404) return null; + throw err; + } + + if (!world) { + return null; + } + + Logger.debug('[VRChat] [Cache] Caching world ' + id); + this.cache.Worlds[id] = { + world: world.data, + expires: new Date(Date.now() + VRC_CACHE_DURATION) + }; + + return world.data; } public end(): void {} From 7a90a70f3cb3411086ce731395f77d6eea950bfe Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 14:39:25 +0700 Subject: [PATCH 8/8] Fixed stuck check --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 50ea041..74155e6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'typescript' ] + language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support