525 lines
15 KiB
JavaScript
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;
|
|
});
|
|
}
|
|
}
|