aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/cleaners/rank.js2
-rw-r--r--build/database.js41
-rw-r--r--build/discord.js42
-rw-r--r--build/hypixel.js28
-rw-r--r--build/hypixelApi.js2
-rw-r--r--build/index.js50
-rw-r--r--package-lock.json25
-rw-r--r--package.json5
-rw-r--r--src/cleaners/rank.ts2
-rw-r--r--src/cleaners/skyblock/member.ts2
-rw-r--r--src/cleaners/skyblock/slayers.ts3
-rw-r--r--src/database.ts65
-rw-r--r--src/discord.ts64
-rw-r--r--src/hypixel.ts34
-rw-r--r--src/hypixelApi.ts2
-rw-r--r--src/index.ts55
-rw-r--r--test/test.js2
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