From d7be67e8b1df12e6f52097dc98a213d9ae074064 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sun, 23 Apr 2023 13:13:20 +0700 Subject: [PATCH] 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"