Files
hq_emu/src/classes/Client.js

525 lines
15 KiB
JavaScript

import request from 'request';
import WebSocket from 'ws';
import { promisify } from 'util';
import 'colors';
import faker from 'faker';
import delay from 'delay';
import { decode } from 'jsonwebtoken';
import Agent from 'https-proxy-agent';
import { discord, hq } from '../../config.json';
import { getTokens, updateTokens } from '../utils/tokens';
import { getPhone, getCode } from '../utils/sms';
import Logger from './Logger';
request.promise = promisify(request);
export default class Client {
constructor({ accessToken, loginToken, userId }, index, weights, show, proxy) {
this.index = index || 0;
if (index === 0 || !index) this.master = true;
this.accessToken = accessToken;
this.loginToken = loginToken;
this.userId = userId;
this.show = show;
this.proxy = proxy;
this.headers = {
'x-hq-client': 'Android/1.6.2',
'user-agent': 'okhttp/3.8.0',
authorization: `Bearer ${this.accessToken}`,
};
this.testWs = false;
this.testWsUrl = 'ws://localhost:8080';
this.logging = true;
this.debug = false;
this.inTheGame = true;
this.question = '';
this.answers = [];
this.broadcastID = 1337;
this.showID = 0;
this.questionID = 0;
this.roundID = 0;
this.checkpointID = 0;
this.erasedIndex = -1;
this.hasExtraLife = false;
this.hasEraser = false;
this.canCashout = false;
this.balance = 0;
this.letter = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[index % 26];
this.correctGuess = false;
this.puzzleState = [];
this.chatVisible = true;
this.friends = [];
this.weights = weights;
this.erasedIndex = null;
this.playersRemaining = 0;
this.questionCount = 0;
this.questionNumber = 0;
this.extraLivesUsed = 0;
this.remainingReferrals = 0;
this.logger = new Logger(this.index, this.userId);
}
async login() {
const { exp } = decode(this.accessToken);
if (exp * 1E3 > Date.now()) return this.accessToken;
const { body } = await request.promise('https://api-quiz.hype.space/tokens', {
method: 'POST',
json: { token: this.loginToken },
headers: this.headers,
});
const { error, accessToken, userId } = body;
if (error) throw new Error(`Error while getting access token: ${error}`);
this.accessToken = accessToken;
this.userId = userId;
this.headers = {
...this.headers,
authorization: `Bearer ${this.accessToken}`,
};
return this.accessToken;
}
async schedule() {
const { body } = await request.promise('https://api-quiz.hype.space/shows/now', {
headers: this.headers,
});
const json = JSON.parse(body);
if (!json) return this.schedule();
if (json.active) this.broadcastID = json.broadcast.broadcastId;
if (json.active && !json.broadcast) return this.schedule();
return json;
}
async connect(clients) {
if (this.testWs) {
this.ws = new WebSocket(this.testWsUrl, {
headers: this.headers,
});
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Using TestWs '.bgYellow.black}`);
} else {
const { broadcast, showId } = this.show;
this.showID = showId;
if (!broadcast) {
console.log(`${` Client ${this.index} `.bgBlue.black}${' Game Not Live, Rechecking... '.bgYellow.black}`);
await delay(2000);
return this.connect();
}
const options = { headers: this.headers };
if (this.proxy) options.agent = new Agent(this.proxy);
this.ws = new WebSocket(broadcast.socketUrl, options);
}
this.ws.sendAndLog = (data) => {
this.ws.send(data);
this.logger.sent(data);
};
this.ws.onopen = () => {
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Connected '.bgGreen.black}`);
const i = setInterval(() => {
if (this.ws.readyState !== this.ws.OPEN) return clearInterval(i);
this.ws.ping();
if (this.debug) console.log(`${` Client ${this.index} [DEBUG] `.bgBlue.black}${' PING '.bgGreen.black}`);
return true;
}, 5000);
this.toggleChat();
this.subscribe();
this.claimEraser();
};
this.ws.onclose = () => {
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Disconnected '.bgRed.black}`);
this.inTheGame = false;
this.logger.save();
};
this.ws.on('message', this.parse.bind(this));
// if (!clients) return true;
switch (this.index % 3) {
case 0:
this.friends = clients[this.index + 2] ? [
clients[this.index + 1].userId,
clients[this.index + 2].userId,
] : [];
break;
case 1:
this.friends = clients[this.index + 1] ? [
clients[this.index - 1].userId,
clients[this.index + 1].userId,
] : [];
break;
case 2:
this.friends = [
clients[this.index - 1].userId,
clients[this.index - 2].userId,
];
break;
default: this.friends = [];
}
return true;
}
disconnect() {
this.ws.close();
}
subscribe() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log('No WebSocket connection is active.');
this.ws.sendAndLog(JSON.stringify({
type: 'subscribe',
broadcastId: this.broadcastID,
}));
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Subscribed '.bgGreen.black}`);
return true;
}
toggleChat() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log('No WebSocket connection is active.');
this.ws.sendAndLog(JSON.stringify({
type: 'chatVisibilityToggled',
chatVisible: !this.chatVisible,
}));
return true;
}
parse(msg) {
const data = JSON.parse(msg);
if (data.type !== 'interaction') this.logger.incoming(msg);
switch (data.type) {
case 'question':
this.questionID = data.questionId;
this.answers = data.answers;
this.question = data.question;
this.questionNumber = data.questionNumber;
this.questionCount = data.questionCount;
if (this.master) console.log(`${' HQ '.bgBlue.black}${` Question ${data.questionNumber} `.bgGreen.black}`);
break;
case 'questionSummary':
if (this.master) console.log(`${' HQ '.bgBlue.black}${' Question Summary '.bgGreen.black}`);
if (!this.inTheGame) return;
this.inTheGame = data.youGotItRight;
if (!this.inTheGame && hq.autoLife && this.hasExtraLife) {
if (this.questionNumber > hq.autoLifeQuestionThreshold) this.useExtraLife();
}
if (!this.inTheGame && this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Eliminated '.bgRed.black}`);
break;
case 'gameStatus':
if (!data.extraLivesRemaining) data.extraLivesRemaining = 0;
if (!data.erase1sRemaining) data.erase1sRemaining = 0;
if (this.testWs) data.extraLivesRemaining = 1;
this.hasEraser = data.erase1sRemaining > 0;
this.hasExtraLife = data.extraLivesRemaining > 0;
break;
case 'broadcastStats':
this.playersRemaining = data.viewerCounts.playing;
break;
case 'erase1Answer':
this.erasedIndex = this.answers.findIndex(({ answerId }) => answerId === data.answerId);
this.weights[this.erasedIndex] = null;
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${` Erased choice ${this.erasedIndex + 1} `.bgGreen.black}`);
break;
case 'startRound':
if (this.master) console.log(`${' HQ Words '.bgBlue.black}${` Round ${data.roundNumber + 1} `.bgGreen.black}`);
this.roundID = data.roundId;
break;
case 'guessResponse':
this.correctGuess = data.correctGuess;
this.puzzleState = data.puzzleState;
break;
case 'checkpoint':
this.checkpointID = data.checkpointId;
break;
case 'checkpointSummary':
if (data.youWon) {
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${` Won ${data.prizeOffered}! `.bgGreen.black}`);
this.inTheGame = !data.youWon;
}
this.checkpointID = 0;
break;
default:
break;
}
}
submitAnswer(index) {
if (!this.inTheGame) return;
// eslint-disable-next-line consistent-return
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
if (index > 2 || index < 0) {
if (this.debug) console.log(`${` Client ${this.index} [DEBUG] `.bgBlue.black}${' Invalid index given to submitAnswer. '.bgRed.black}`);
return;
}
const { answerId } = this.answers[index];
if (!answerId) return;
this.ws.sendAndLog(JSON.stringify({
type: 'answer',
broadcastId: this.broadcastID,
questionId: this.questionID,
answerId,
}));
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${` Chose option ${index + 1} `.bgGreen.black}`);
}
useExtraLife() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
if (!this.hasExtraLife) return false;
if (this.extraLivesUsed >= hq.maxLivesPerGame) return false;
this.ws.sendAndLog(JSON.stringify({
type: 'useExtraLife',
broadcastId: this.broadcastID,
questionId: this.questionID,
}));
this.inTheGame = true;
this.extraLivesUsed += 1;
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Used Extra Life '.bgGreen.black}`);
return true;
}
useEraser() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
if (!this.hasEraser) return false;
this.ws.sendAndLog(JSON.stringify({ type: 'erase1', questionId: this.questionID, broadcastId: this.broadcastID }));
this.hasEraser = false;
return true;
}
claimEraser() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
this.ws.sendAndLog(JSON.stringify({ type: 'erase1Earned', broadcastId: this.broadcastID, friendIds: this.friends }));
return true;
}
checkpoint(winNow) {
if (!this.inTheGame) return false;
if (!this.checkpointID) return false;
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
this.ws.sendAndLog(JSON.stringify({ type: 'checkpointResponse', winNow, checkpointId: this.checkpointID }));
return true;
}
spinWheel(letter) {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log(`Client ${this.index} | No WebSocket connection is active.`);
this.ws.sendAndLog(JSON.stringify({
showId: this.showID,
type: 'spin',
nearbyIds: [],
letter,
}));
return true;
}
guess() {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return console.log('No WebSocket connection is active.');
if (!this.inTheGame) return false;
this.ws.sendAndLog(JSON.stringify({
type: 'guess',
letter: this.letter,
roundId: this.roundID,
showId: this.showID,
}));
return true;
}
async getBalance() {
const { body } = await request.promise('https://api-quiz.hype.space/users/me/payouts', {
headers: this.headers,
});
const { balance: { prizeTotal, available, frozen } } = JSON.parse(body);
this.balance = +prizeTotal.slice(1);
this.available = +available.slice(1);
this.frozen = +frozen.slice(1);
return { balance: this.balance, available: this.available, frozen: this.frozen };
}
async cashout(catchall) {
if (!catchall) throw new Error('No catchall was provided.');
if (catchall.includes('@')) this.email = catchall;
else this.email = `${faker.internet.userName()}@${catchall}`;
const { body } = await request.promise('https://api-quiz.hype.space/users/me/payouts', {
method: 'POST',
headers: this.headers,
form: { email: this.email },
});
if (!body) return console.log(`${` Client ${this.index} `.bgBlue.black}${' Error while cashing out: No response was recieved from HQ. '.bgRed.black}`);
if (body.includes('<html>') && this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${' Error while cashing out: HTML was returned. '.bgRed.black}`);
const { error } = JSON.parse(body);
if (error && this.logging) return console.log(`${` Client ${this.index} `.bgBlue.black}${` Error while cashing out: ${error}. `.bgRed.black}`);
if (this.logging) console.log(`${` Client ${this.index} `.bgBlue.black}${` Successfully cashed out $${this.balance.toFixed(2)} to ${this.email}! `.bgGreen.black}`);
return true;
}
async getUserData() {
const { body } = await request.promise('https://api-quiz.hype.space/users/me', { headers: this.headers });
return JSON.parse(body);
}
async signup() {
this.phone = await getPhone();
const phone = this.phone.result;
const { body } = await request.promise('https://api-quiz.hype.space/verifications', {
method: 'POST',
headers: {
'x-hq-client': 'Android/1.33.0',
},
json: { method: 'sms', phone },
});
const { verificationId, error } = body;
if (error) throw new Error(`Error while getting verification id: ${error}`);
this.code = await getCode(this.phone.tzid);
if (!this.code) return false;
const { body: verifBody } = await request.promise(`https://api-quiz.hype.space/verifications/${verificationId}`, {
method: 'POST',
json: { code: this.code },
});
const { error: verifError } = verifBody;
if (verifError) throw new Error(`Error while signing up: ${verifError}`);
const finishSignup = async () => {
const username = `${faker.name.firstName()}${faker.name.lastName()}${Math.floor(Math.random() * 100)}`;
console.log(`Signing up with username ${username}...`.green);
const { body: json } = await request.promise('https://api-quiz.hype.space/users', {
method: 'POST',
json: {
country: 'US',
language: 'en',
username,
verificationId,
},
});
if (json.error && json.error.includes('username')) {
console.log('Retrying signup because of username error...'.yellow);
return finishSignup();
}
return json;
};
let json = await finishSignup();
if (json.error) throw new Error(`Error while finishing signing up: ${json.error}`);
const tokens = getTokens(discord.defaultTokenSet);
console.log(`Created account with username ${json.username}!`.green);
json = {
...json,
instant: false,
used: false,
};
tokens.push(json);
updateTokens(discord.defaultTokenSet, tokens);
return json;
}
async validate() {
const res = await this.getUserData();
return res.error;
}
async checkReferrals() {
const { body } = await request.promise('https://api-quiz.hype.space/show-referrals', {
headers: this.headers,
json: true,
});
body.shows.forEach((s) => {
if (!s.referredByUsername) this.remainingReferrals += 1;
});
}
}