diff options
-rw-r--r-- | build/cleaners/rank.js | 2 | ||||
-rw-r--r-- | build/database.js | 41 | ||||
-rw-r--r-- | build/discord.js | 42 | ||||
-rw-r--r-- | build/hypixel.js | 28 | ||||
-rw-r--r-- | build/hypixelApi.js | 2 | ||||
-rw-r--r-- | build/index.js | 50 | ||||
-rw-r--r-- | package-lock.json | 25 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | src/cleaners/rank.ts | 2 | ||||
-rw-r--r-- | src/cleaners/skyblock/member.ts | 2 | ||||
-rw-r--r-- | src/cleaners/skyblock/slayers.ts | 3 | ||||
-rw-r--r-- | src/database.ts | 65 | ||||
-rw-r--r-- | src/discord.ts | 64 | ||||
-rw-r--r-- | src/hypixel.ts | 34 | ||||
-rw-r--r-- | src/hypixelApi.ts | 2 | ||||
-rw-r--r-- | src/index.ts | 55 | ||||
-rw-r--r-- | test/test.js | 2 |
17 files changed, 379 insertions, 45 deletions
diff --git a/build/cleaners/rank.js b/build/cleaners/rank.js index de2309e..38965e2 100644 --- a/build/cleaners/rank.js +++ b/build/cleaners/rank.js @@ -33,8 +33,8 @@ function cleanRank({ packageRank, newPackageRank, monthlyPackageRank, rankPlusCo name = rank; else name = (_a = newPackageRank === null || newPackageRank === void 0 ? void 0 : newPackageRank.replace('_PLUS', '+')) !== null && _a !== void 0 ? _a : packageRank === null || packageRank === void 0 ? void 0 : packageRank.replace('_PLUS', '+'); - // MVP++ is called Superstar for some reason switch (name) { + // MVP++ is called Superstar for some reason case 'SUPERSTAR': name = 'MVP++'; break; diff --git a/build/database.js b/build/database.js index 55867ce..2d1f5e9 100644 --- a/build/database.js +++ b/build/database.js @@ -25,8 +25,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.queueUpdateDatabaseProfile = exports.queueUpdateDatabaseMember = exports.updateDatabaseProfile = exports.updateDatabaseMember = exports.fetchMemberLeaderboardSpots = exports.fetchLeaderboard = exports.fetchProfileLeaderboard = exports.fetchMemberLeaderboard = exports.fetchAllMemberLeaderboardAttributes = exports.fetchSlayerLeaderboards = exports.fetchAllLeaderboardsCategorized = exports.cachedRawLeaderboards = void 0; +exports.updateAccount = exports.fetchAccountFromDiscord = exports.fetchAccount = exports.fetchSession = exports.createSession = exports.queueUpdateDatabaseProfile = exports.queueUpdateDatabaseMember = exports.updateDatabaseProfile = exports.updateDatabaseMember = exports.fetchMemberLeaderboardSpots = exports.fetchLeaderboard = exports.fetchProfileLeaderboard = exports.fetchMemberLeaderboard = exports.fetchAllMemberLeaderboardAttributes = exports.fetchSlayerLeaderboards = exports.fetchAllLeaderboardsCategorized = exports.cachedRawLeaderboards = void 0; const stats_1 = require("./cleaners/skyblock/stats"); +const slayers_1 = require("./cleaners/skyblock/slayers"); const mongodb_1 = require("mongodb"); const cached = __importStar(require("./hypixelCached")); const constants = __importStar(require("./constants")); @@ -34,7 +35,7 @@ const util_1 = require("./util"); const node_cache_1 = __importDefault(require("node-cache")); const queue_promise_1 = __importDefault(require("queue-promise")); const _1 = require("."); -const slayers_1 = require("./cleaners/skyblock/slayers"); +const uuid_1 = require("uuid"); // don't update the user for 3 minutes const recentlyUpdated = new node_cache_1.default({ stdTTL: 60 * 3, @@ -51,6 +52,8 @@ let client; let database; let memberLeaderboardsCollection; let profileLeaderboardsCollection; +let sessionsCollection; +let accountsCollection; async function connect() { if (!process.env.db_uri) return console.warn('Warning: db_uri was not found in .env. Features that utilize the database such as leaderboards won\'t work.'); @@ -60,6 +63,8 @@ async function connect() { database = client.db(process.env.db_name); memberLeaderboardsCollection = database.collection('member-leaderboards'); profileLeaderboardsCollection = database.collection('profile-leaderboards'); + sessionsCollection = database.collection('sessions'); + accountsCollection = database.collection('accounts'); } function getMemberCollectionAttributes(member) { const collectionAttributes = {}; @@ -512,6 +517,38 @@ async function fetchAllLeaderboards(fast) { if (_1.debug) console.debug('Finished caching leaderboards!'); } +async function createSession(refreshToken, userData) { + const sessionId = uuid_1.v4(); + await sessionsCollection.insertOne({ + _id: sessionId, + refresh_token: refreshToken, + discord_user: { + id: userData.id, + name: userData.username + '#' + userData.discriminator + }, + lastUpdated: new Date() + }); + return sessionId; +} +exports.createSession = createSession; +async function fetchSession(sessionId) { + return await sessionsCollection.findOne({ _id: sessionId }); +} +exports.fetchSession = fetchSession; +async function fetchAccount(minecraftUuid) { + return await accountsCollection.findOne({ minecraftUuid }); +} +exports.fetchAccount = fetchAccount; +async function fetchAccountFromDiscord(discordId) { + return await accountsCollection.findOne({ discordId }); +} +exports.fetchAccountFromDiscord = fetchAccountFromDiscord; +async function updateAccount(discordId, schema) { + await accountsCollection.updateOne({ + discordId + }, { $set: schema }, { upsert: true }); +} +exports.updateAccount = updateAccount; // make sure it's not in a test if (!globalThis.isTest) { connect().then(() => { diff --git a/build/discord.js b/build/discord.js new file mode 100644 index 0000000..be085bb --- /dev/null +++ b/build/discord.js @@ -0,0 +1,42 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getUser = exports.exchangeCode = void 0; +const node_fetch_1 = __importDefault(require("node-fetch")); +const https_1 = require("https"); +const DISCORD_CLIENT_ID = '656634948148527107'; +const httpsAgent = new https_1.Agent({ + keepAlive: true +}); +async function exchangeCode(redirectUri, code) { + const API_ENDPOINT = 'https://discord.com/api/v6'; + const CLIENT_SECRET = process.env.discord_client_secret; + const data = { + 'client_id': DISCORD_CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirectUri, + 'scope': 'identify' + }; + console.log(new URLSearchParams(data).toString()); + const fetchResponse = await node_fetch_1.default(API_ENDPOINT + '/oauth2/token', { + method: 'POST', + agent: () => httpsAgent, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(data).toString() + }); + return await fetchResponse.json(); +} +exports.exchangeCode = exchangeCode; +async function getUser(accessToken) { + const API_ENDPOINT = 'https://discord.com/api/v6'; + const response = await node_fetch_1.default(API_ENDPOINT + '/users/@me', { + headers: { 'Authorization': 'Bearer ' + accessToken }, + agent: () => httpsAgent, + }); + return response.json(); +} +exports.getUser = getUser; diff --git a/build/hypixel.js b/build/hypixel.js index ea61e4e..8e58ffc 100644 --- a/build/hypixel.js +++ b/build/hypixel.js @@ -23,13 +23,13 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.fetchMemberProfilesUncached = exports.fetchBasicProfileFromUuidUncached = exports.fetchMemberProfileUncached = exports.fetchMemberProfile = exports.fetchUser = exports.sendCleanApiRequest = exports.maxMinion = exports.saveInterval = void 0; -const player_1 = require("./cleaners/player"); -const hypixelApi_1 = require("./hypixelApi"); -const cached = __importStar(require("./hypixelCached")); const profile_1 = require("./cleaners/skyblock/profile"); +const database_1 = require("./database"); +const hypixelApi_1 = require("./hypixelApi"); const profiles_1 = require("./cleaners/skyblock/profiles"); +const player_1 = require("./cleaners/player"); +const cached = __importStar(require("./hypixelCached")); const _1 = require("."); -const database_1 = require("./database"); // the interval at which the "last_save" parameter updates in the hypixel api, this is 3 minutes exports.saveInterval = 60 * 3 * 1000; // the highest level a minion can be @@ -61,11 +61,12 @@ async function cleanResponse({ path, data }, options) { * @param included lets you choose what is returned, so there's less processing required on the backend * used inclusions: player, profiles */ -async function fetchUser({ user, uuid, username }, included = ['player']) { +async function fetchUser({ user, uuid, username }, included = ['player'], customization) { if (!uuid) { // If the uuid isn't provided, get it uuid = await cached.uuidFromUser(user || username); } + const websiteAccountPromise = customization ? database_1.fetchAccount(uuid) : null; if (!uuid) { // the user doesn't exist. if (_1.debug) @@ -99,11 +100,15 @@ async function fetchUser({ user, uuid, username }, included = ['player']) { } } } + let websiteAccount = undefined; + if (websiteAccountPromise) + websiteAccount = await websiteAccountPromise; return { player: playerData !== null && playerData !== void 0 ? playerData : null, profiles: profilesData !== null && profilesData !== void 0 ? profilesData : basicProfilesData, activeProfile: includeProfiles ? activeProfile === null || activeProfile === void 0 ? void 0 : activeProfile.uuid : undefined, - online: includeProfiles ? lastOnline > (Date.now() - exports.saveInterval) : undefined + online: includeProfiles ? lastOnline > (Date.now() - exports.saveInterval) : undefined, + customization: websiteAccount === null || websiteAccount === void 0 ? void 0 : websiteAccount.customization }; } exports.fetchUser = fetchUser; @@ -112,9 +117,12 @@ exports.fetchUser = fetchUser; * This is safe to use many times as the results are cached! * @param user A username or uuid * @param profile A profile name or profile uuid + * @param customization Whether stuff like the user's custom background will be returned */ -async function fetchMemberProfile(user, profile) { +async function fetchMemberProfile(user, profile, customization) { const playerUuid = await cached.uuidFromUser(user); + // we don't await the promise immediately so it can load while we do other stuff + const websiteAccountPromise = customization ? database_1.fetchAccount(playerUuid) : null; const profileUuid = await cached.fetchProfileUuid(user, profile); // if the profile doesn't have an id, just return if (!profileUuid) @@ -133,6 +141,9 @@ async function fetchMemberProfile(user, profile) { }; }); cleanProfile.members = simpleMembers; + let websiteAccount = undefined; + if (websiteAccountPromise) + websiteAccount = await websiteAccountPromise; return { member: { // the profile name is in member rather than profile since they sometimes differ for each member @@ -142,7 +153,8 @@ async function fetchMemberProfile(user, profile) { // add all other data relating to the hypixel player, such as username, rank, etc ...player }, - profile: cleanProfile + profile: cleanProfile, + customization: websiteAccount === null || websiteAccount === void 0 ? void 0 : websiteAccount.customization }; } exports.fetchMemberProfile = fetchMemberProfile; diff --git a/build/hypixelApi.js b/build/hypixelApi.js index ccae09b..9f54ef2 100644 --- a/build/hypixelApi.js +++ b/build/hypixelApi.js @@ -36,7 +36,7 @@ function chooseApiKey() { if (Date.now() > keyUsage.reset) keyUsage.remaining = keyUsage.limit; // if this key has more uses remaining than the current known best one, save it - if (!bestKeyUsage || keyUsage.remaining > bestKeyUsage.remaining) { + if (bestKeyUsage === null || keyUsage.remaining > bestKeyUsage.remaining) { bestKeyUsage = keyUsage; bestKey = key; } diff --git a/build/index.js b/build/index.js index c7f1385..09ca7d7 100644 --- a/build/index.js +++ b/build/index.js @@ -27,9 +27,11 @@ const database_1 = require("./database"); const hypixel_1 = require("./hypixel"); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const constants = __importStar(require("./constants")); +const discord = __importStar(require("./discord")); const express_1 = __importDefault(require("express")); const app = express_1.default(); exports.debug = false; +const mainSiteUrl = 'http://localhost:8081'; // 200 requests over 5 minutes const limiter = express_rate_limit_1.default({ windowMs: 60 * 1000 * 5, @@ -43,6 +45,7 @@ const limiter = express_rate_limit_1.default({ } }); app.use(limiter); +app.use(express_1.default.json()); app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); next(); @@ -51,10 +54,13 @@ app.get('/', async (req, res) => { res.json({ ok: true }); }); app.get('/player/:user', async (req, res) => { - res.json(await hypixel_1.fetchUser({ user: req.params.user }, [req.query.basic === 'true' ? undefined : 'profiles', 'player'])); + res.json(await hypixel_1.fetchUser({ user: req.params.user }, [req.query.basic === 'true' ? undefined : 'profiles', 'player'], req.query.customization === 'true')); +}); +app.get('/discord/:id', async (req, res) => { + res.json(await database_1.fetchAccountFromDiscord(req.params.id)); }); app.get('/player/:user/:profile', async (req, res) => { - res.json(await hypixel_1.fetchMemberProfile(req.params.user, req.params.profile)); + res.json(await hypixel_1.fetchMemberProfile(req.params.user, req.params.profile, req.query.customization === 'true')); }); app.get('/player/:user/:profile/leaderboards', async (req, res) => { res.json(await database_1.fetchMemberLeaderboardSpots(req.params.user, req.params.profile)); @@ -74,6 +80,46 @@ app.get('/leaderboards', async (req, res) => { app.get('/constants', async (req, res) => { res.json(await constants.fetchConstantValues()); }); +app.post('/accounts/createsession', async (req, res) => { + try { + const { code } = req.body; + const { access_token: accessToken, refresh_token: refreshToken } = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code); + if (!accessToken) + // access token is invalid :( + return res.json({ ok: false }); + const userData = await discord.getUser(accessToken); + const sessionId = await database_1.createSession(refreshToken, userData); + res.json({ ok: true, session_id: sessionId }); + } + catch (err) { + res.json({ ok: false }); + } +}); +app.post('/accounts/session', async (req, res) => { + try { + const { uuid } = req.body; + const session = await database_1.fetchSession(uuid); + const account = await database_1.fetchAccountFromDiscord(session.discord_user.id); + res.json({ session, account }); + } + catch (err) { + console.error(err); + res.json({ ok: false }); + } +}); +app.post('/accounts/update', async (req, res) => { + // it checks against the key, so it's kind of secure + if (req.headers.key !== process.env.key) + return console.log('bad key!'); + try { + await database_1.updateAccount(req.body.discordId, req.body); + res.json({ ok: true }); + } + catch (err) { + console.error(err); + res.json({ ok: false }); + } +}); // only run the server if it's not doing tests if (!globalThis.isTest) app.listen(8080, () => console.log('App started :)')); diff --git a/package-lock.json b/package-lock.json index c996b6d..eaf9338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1305,16 +1305,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1330,6 +1320,16 @@ } } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -1428,6 +1428,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 922df81..2ddd4fd 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,16 @@ "node-cache": "^5.1.2", "node-fetch": "^2.6.1", "prismarine-nbt": "^1.5.0", + "uuid": "^8.3.2", "queue-promise": "^2.2.1" }, "devDependencies": { "@types/express": "^4.17.11", "@types/express-rate-limit": "^5.1.1", - "@types/lru-cache": "^5.1.0", "@types/mocha": "^8.2.2", - "@types/mongodb": "^3.6.15", "@types/node": "^15.3.1", + "@types/mongodb": "^3.6.15", + "@types/lru-cache": "^5.1.0", "@types/node-fetch": "^2.5.10", "dotenv": "^9.0.2", "mocha": "^8.4.0", diff --git a/src/cleaners/rank.ts b/src/cleaners/rank.ts index 997888c..2d80e31 100644 --- a/src/cleaners/rank.ts +++ b/src/cleaners/rank.ts @@ -46,8 +46,8 @@ export function cleanRank({ name = newPackageRank?.replace('_PLUS', '+') ?? packageRank?.replace('_PLUS', '+') - // MVP++ is called Superstar for some reason switch (name) { + // MVP++ is called Superstar for some reason case 'SUPERSTAR': name = 'MVP++' break diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts index 7a57975..0ec2c0a 100644 --- a/src/cleaners/skyblock/member.ts +++ b/src/cleaners/skyblock/member.ts @@ -5,6 +5,7 @@ import { cleanObjectives, Objective } from './objectives' import { CleanFullProfileBasicMembers } from './profile' import { cleanProfileStats, StatItem } from './stats' import { CleanMinion, cleanMinions } from './minions' +import { AccountCustomization } from '../../database' import { cleanSlayers, SlayerData } from './slayers' import { cleanVisitedZones, Zone } from './zones' import { cleanSkills, Skill } from './skills' @@ -108,4 +109,5 @@ export interface CleanMemberProfilePlayer extends CleanPlayer { export interface CleanMemberProfile { member: CleanMemberProfilePlayer profile: CleanFullProfileBasicMembers + customization: AccountCustomization } diff --git a/src/cleaners/skyblock/slayers.ts b/src/cleaners/skyblock/slayers.ts index e9c5cb6..7892eaf 100644 --- a/src/cleaners/skyblock/slayers.ts +++ b/src/cleaners/skyblock/slayers.ts @@ -6,8 +6,7 @@ const SLAYER_NAMES = { wolf: 'sven' } as const -type ApiSlayerName = keyof typeof SLAYER_NAMES -type SlayerName = (typeof SLAYER_NAMES)[ApiSlayerName] +type SlayerName = (typeof SLAYER_NAMES)[keyof typeof SLAYER_NAMES] interface SlayerTier { tier: number, diff --git a/src/database.ts b/src/database.ts index 42fb569..2c679b6 100644 --- a/src/database.ts +++ b/src/database.ts @@ -3,17 +3,19 @@ */ import { categorizeStat, getStatUnit } from './cleaners/skyblock/stats' -import { CleanBasicProfile, CleanFullProfile, CleanProfile } from './cleaners/skyblock/profile' +import { CleanFullProfile } from './cleaners/skyblock/profile' +import { slayerLevels } from './cleaners/skyblock/slayers' import { CleanMember } from './cleaners/skyblock/member' import { Collection, Db, MongoClient } from 'mongodb' import { CleanPlayer } from './cleaners/player' import * as cached from './hypixelCached' import * as constants from './constants' import { shuffle, sleep } from './util' +import * as discord from './discord' import NodeCache from 'node-cache' import Queue from 'queue-promise' import { debug } from '.' -import { slayerLevels } from './cleaners/skyblock/slayers' +import { v4 as uuid4 } from 'uuid' // don't update the user for 3 minutes const recentlyUpdated = new NodeCache({ @@ -57,8 +59,33 @@ const reversedLeaderboards = [ let client: MongoClient let database: Db + +interface SessionSchema { + _id?: string + refresh_token: string + discord_user: { + id: string + name: string + } + lastUpdated: Date +} + +export interface AccountCustomization { + backgroundUrl?: string + pack?: string +} + +export interface AccountSchema { + _id?: string + discordId: string + minecraftUuid?: string + customization?: AccountCustomization +} + let memberLeaderboardsCollection: Collection<any> let profileLeaderboardsCollection: Collection<any> +let sessionsCollection: Collection<SessionSchema> +let accountsCollection: Collection<AccountSchema> async function connect(): Promise<void> { if (!process.env.db_uri) @@ -69,6 +96,8 @@ async function connect(): Promise<void> { database = client.db(process.env.db_name) memberLeaderboardsCollection = database.collection('member-leaderboards') profileLeaderboardsCollection = database.collection('profile-leaderboards') + sessionsCollection = database.collection('sessions') + accountsCollection = database.collection('accounts') } interface StringNumber { @@ -620,6 +649,38 @@ async function fetchAllLeaderboards(fast?: boolean): Promise<void> { if (debug) console.debug('Finished caching leaderboards!') } +export async function createSession(refreshToken: string, userData: discord.DiscordUser): Promise<string> { + const sessionId = uuid4() + await sessionsCollection.insertOne({ + _id: sessionId, + refresh_token: refreshToken, + discord_user: { + id: userData.id, + name: userData.username + '#' + userData.discriminator + }, + lastUpdated: new Date() + }) + return sessionId +} + +export async function fetchSession(sessionId: string): Promise<SessionSchema> { + return await sessionsCollection.findOne({ _id: sessionId }) +} + +export async function fetchAccount(minecraftUuid: string): Promise<AccountSchema> { + return await accountsCollection.findOne({ minecraftUuid }) +} + +export async function fetchAccountFromDiscord(discordId: string): Promise<AccountSchema> { + return await accountsCollection.findOne({ discordId }) +} + +export async function updateAccount(discordId: string, schema: AccountSchema) { + await accountsCollection.updateOne({ + discordId + }, { $set: schema }, { upsert: true }) +} + // make sure it's not in a test if (!globalThis.isTest) { connect().then(() => { diff --git a/src/discord.ts b/src/discord.ts new file mode 100644 index 0000000..41f4c6e --- /dev/null +++ b/src/discord.ts @@ -0,0 +1,64 @@ +import fetch from 'node-fetch' +import { Agent } from 'https' + +const DISCORD_CLIENT_ID = '656634948148527107' + +const httpsAgent = new Agent({ + keepAlive: true +}) + +export interface TokenResponse { + access_token: string + expires_in: number + refresh_token: string + scope: string + token_type: string +} + +export interface DiscordUser { + id: string + username: string + avatar: string + discriminator: string + public_flags: number + flags: number + locale: string + mfa_enabled: boolean +} + +export async function exchangeCode(redirectUri: string, code: string): Promise<TokenResponse> { + const API_ENDPOINT = 'https://discord.com/api/v6' + const CLIENT_SECRET = process.env.discord_client_secret + const data = { + 'client_id': DISCORD_CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirectUri, + 'scope': 'identify' + } + console.log(new URLSearchParams(data).toString()) + const fetchResponse = await fetch( + API_ENDPOINT + '/oauth2/token', + { + method: 'POST', + agent: () => httpsAgent, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(data).toString() + } + ) + return await fetchResponse.json() +} + + +export async function getUser(accessToken: string): Promise<DiscordUser> { + const API_ENDPOINT = 'https://discord.com/api/v6' + const response = await fetch( + API_ENDPOINT + '/users/@me', + { + headers: {'Authorization': 'Bearer ' + accessToken}, + agent: () => httpsAgent, + } + ) + return response.json() +} diff --git a/src/hypixel.ts b/src/hypixel.ts index 4d3e692..99bc671 100644 --- a/src/hypixel.ts +++ b/src/hypixel.ts @@ -2,14 +2,14 @@ * Fetch the clean Hypixel API */ -import { CleanPlayer, cleanPlayerResponse } from './cleaners/player' -import { chooseApiKey, HypixelResponse, sendApiRequest } from './hypixelApi' -import * as cached from './hypixelCached' -import { CleanBasicMember, CleanMemberProfile } from './cleaners/skyblock/member' import { cleanSkyblockProfileResponse, CleanProfile, CleanBasicProfile, CleanFullProfile, CleanFullProfileBasicMembers } from './cleaners/skyblock/profile' +import { AccountCustomization, AccountSchema, fetchAccount, queueUpdateDatabaseMember, queueUpdateDatabaseProfile } from './database' +import { CleanBasicMember, CleanMemberProfile } from './cleaners/skyblock/member' +import { chooseApiKey, HypixelResponse, sendApiRequest } from './hypixelApi' import { cleanSkyblockProfilesResponse } from './cleaners/skyblock/profiles' +import { CleanPlayer, cleanPlayerResponse } from './cleaners/player' +import * as cached from './hypixelCached' import { debug } from '.' -import { queueUpdateDatabaseMember, queueUpdateDatabaseProfile } from './database' export type Included = 'profiles' | 'player' | 'stats' | 'inventories' @@ -67,6 +67,7 @@ export interface CleanUser { profiles?: CleanProfile[] activeProfile?: string online?: boolean + customization?: AccountCustomization } @@ -76,11 +77,12 @@ export interface CleanUser { * @param included lets you choose what is returned, so there's less processing required on the backend * used inclusions: player, profiles */ -export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player']): Promise<CleanUser> { +export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player'], customization?: boolean): Promise<CleanUser> { if (!uuid) { // If the uuid isn't provided, get it uuid = await cached.uuidFromUser(user || username) } + const websiteAccountPromise = customization ? fetchAccount(uuid) : null if (!uuid) { // the user doesn't exist. if (debug) console.debug('error:', user, 'doesnt exist') @@ -118,11 +120,16 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc } } } + let websiteAccount: AccountSchema = undefined + + if (websiteAccountPromise) + websiteAccount = await websiteAccountPromise return { player: playerData ?? null, profiles: profilesData ?? basicProfilesData, activeProfile: includeProfiles ? activeProfile?.uuid : undefined, - online: includeProfiles ? lastOnline > (Date.now() - saveInterval): undefined + online: includeProfiles ? lastOnline > (Date.now() - saveInterval): undefined, + customization: websiteAccount?.customization } } @@ -131,9 +138,12 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc * This is safe to use many times as the results are cached! * @param user A username or uuid * @param profile A profile name or profile uuid + * @param customization Whether stuff like the user's custom background will be returned */ -export async function fetchMemberProfile(user: string, profile: string): Promise<CleanMemberProfile> { +export async function fetchMemberProfile(user: string, profile: string, customization: boolean): Promise<CleanMemberProfile> { const playerUuid = await cached.uuidFromUser(user) + // we don't await the promise immediately so it can load while we do other stuff + const websiteAccountPromise = customization ? fetchAccount(playerUuid) : null const profileUuid = await cached.fetchProfileUuid(user, profile) // if the profile doesn't have an id, just return @@ -158,6 +168,11 @@ export async function fetchMemberProfile(user: string, profile: string): Promise cleanProfile.members = simpleMembers + let websiteAccount: AccountSchema = undefined + + if (websiteAccountPromise) + websiteAccount = await websiteAccountPromise + return { member: { // the profile name is in member rather than profile since they sometimes differ for each member @@ -167,7 +182,8 @@ export async function fetchMemberProfile(user: string, profile: string): Promise // add all other data relating to the hypixel player, such as username, rank, etc ...player }, - profile: cleanProfile + profile: cleanProfile, + customization: websiteAccount?.customization } } diff --git a/src/hypixelApi.ts b/src/hypixelApi.ts index fdb4535..72af1af 100644 --- a/src/hypixelApi.ts +++ b/src/hypixelApi.ts @@ -47,7 +47,7 @@ export function chooseApiKey(): string { keyUsage.remaining = keyUsage.limit // if this key has more uses remaining than the current known best one, save it - if (!bestKeyUsage || keyUsage.remaining > bestKeyUsage.remaining) { + if (bestKeyUsage === null || keyUsage.remaining > bestKeyUsage.remaining) { bestKeyUsage = keyUsage bestKey = key } diff --git a/src/index.ts b/src/index.ts index 1652428..3d6bf6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,15 @@ -import { fetchAllLeaderboardsCategorized, fetchLeaderboard, fetchMemberLeaderboardSpots } from './database' +import { createSession, fetchAccount, fetchAccountFromDiscord, fetchAllLeaderboardsCategorized, fetchLeaderboard, fetchMemberLeaderboardSpots, fetchSession, updateAccount } from './database' import { fetchMemberProfile, fetchUser } from './hypixel' import rateLimit from 'express-rate-limit' import * as constants from './constants' +import * as discord from './discord' import express from 'express' const app = express() export const debug = false +const mainSiteUrl = 'http://localhost:8081' // 200 requests over 5 minutes const limiter = rateLimit({ @@ -22,6 +24,7 @@ const limiter = rateLimit({ }) app.use(limiter) +app.use(express.json()) app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*') next() @@ -35,14 +38,21 @@ app.get('/player/:user', async(req, res) => { res.json( await fetchUser( { user: req.params.user }, - [req.query.basic as string === 'true' ? undefined : 'profiles', 'player'] + [req.query.basic as string === 'true' ? undefined : 'profiles', 'player'], + req.query.customization as string === 'true' ) ) }) +app.get('/discord/:id', async(req, res) => { + res.json( + await fetchAccountFromDiscord(req.params.id) + ) +}) + app.get('/player/:user/:profile', async(req, res) => { res.json( - await fetchMemberProfile(req.params.user, req.params.profile) + await fetchMemberProfile(req.params.user, req.params.profile, req.query.customization as string === 'true') ) }) @@ -75,6 +85,45 @@ app.get('/constants', async(req, res) => { ) }) +app.post('/accounts/createsession', async(req, res) => { + try { + const { code } = req.body + const { access_token: accessToken, refresh_token: refreshToken } = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code) + if (!accessToken) + // access token is invalid :( + return res.json({ ok: false }) + const userData = await discord.getUser(accessToken) + const sessionId = await createSession(refreshToken, userData) + res.json({ ok: true, session_id: sessionId }) + } catch (err) { + res.json({ ok: false }) + } +}) + +app.post('/accounts/session', async(req, res) => { + try { + const { uuid } = req.body + const session = await fetchSession(uuid) + const account = await fetchAccountFromDiscord(session.discord_user.id) + res.json({ session, account }) + } catch (err) { + console.error(err) + res.json({ ok: false }) + } +}) + + +app.post('/accounts/update', async(req, res) => { + // it checks against the key, so it's kind of secure + if (req.headers.key !== process.env.key) return console.log('bad key!') + try { + await updateAccount(req.body.discordId, req.body) + res.json({ ok: true }) + } catch (err) { + console.error(err) + res.json({ ok: false }) + } +}) // only run the server if it's not doing tests if (!globalThis.isTest) diff --git a/test/test.js b/test/test.js index fff3f15..ad86ff7 100644 --- a/test/test.js +++ b/test/test.js @@ -69,7 +69,7 @@ function resetState() { hypixelCached.usernameCache.flushAll() hypixelCached.basicProfilesCache.flushAll() hypixelCached.playerCache.flushAll() - hypixelCached.basicPlayerCache.flushAll() + hypixelCached.basicPlayerCache.reset() hypixelCached.profileCache.flushAll() hypixelCached.profileNameCache.flushAll() requestsSent = 0 |