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 {}