balaclava-utils
Version:
Functions and dependencies of the @controlado plugins.
458 lines (403 loc) • 12.8 kB
JavaScript
import axios from "axios";
/**
* @author balaclava
* @name controlado-utils
* @link https://github.com/controlado/pengu-plugins
* @description Functions and dependencies of the @controlado plugins
*/
const clientAuthRegex = /^--riotclient-auth-token=(.+)$/;
const clientPortRegex = /^--riotclient-app-port=(\d+)$/;
/**
* Rotinas chamadas periodicamente.
*
* @constant
* @type {function[]}
* @see {@link watchRoutines} que executa as rotinas.
*/
export const routines = [];
/**
* Credenciais do client que são atualizadas posteriormente.
*
* @constant
* @type {Object}
* @see {@link fetchClientCredentials} a função que atualiza essas credenciais.
*/
export const credentials = { auth: null, port: null };
/**
* Fase em que o jogo se encontra.
*
*
* @var
* @type {"None" | "Lobby" | "Matchmaking" | "ReadyCheck" | "ChampSelect" | "InProgress" | "GameStart"}
* @see {@link linkEndpoint} - retorna essa informação usando "/lol-gameflow/v1/gameflow-phase".
*/
export let gamePhase;
/**
* Elemento que contém alguns elementos da interface do usuário.
*
* @var
* @type {HTMLElement}
* @see {@link setLayerManager} - define esse elemento.
*/
export let layerManager;
/**
* ID da conversa da seleção de campeões.
*
* @var
* @type {string}
* @see {@link getChampionSelectCid} - define esse ID.
* @see {@link sendChatNotification} - usa esse ID.
*/
export let championSelectCid;
/**
* Modo de testes do módulo.
*
* @var
* @type {boolean}
*/
export let debug = false;
/**
* Uma classe usada para identificar um campeão.
*/
export class Champion {
/**
* @param {number} id
* @param {number} ipCost
*/
constructor(id, ipCost) {
this.id = id;
this.ipCost = ipCost;
}
}
/**
* Possibilita requisições a loja do usuário autenticado.
*/
export class StoreBase {
constructor() {
this.url = null;
this.token = null;
this.expiry = null;
this.summoner = null;
this.session = axios.create();
this.auth();
}
/**
* Compra os campeões requisitados.
*
* @param {...Champion} champions - Campeões que vão ser comprados.
* @return {Promise<Response>} Resposta da requisição.
*/
buyChampions(...champions) {
const items = champions.map(
champion => (
{
inventoryType: "CHAMPION",
itemId: champion.id,
ipCost: champion.ipCost,
quantity: 1,
}
),
);
const body = { accountId: this.summoner.accountId, items: items };
return this.request("POST", "/storefront/v3/purchase", body);
}
/**
* Faz uma requisição para a loja.
*
* @async
* @function
* @summary Deve ser chamada após a conclusão do {@link auth}.
* @param {"GET" | "POST" | "PUT" | "DELETE"} method - Método HTTP da requisição.
* @param {string} endpoint - Endpoint da requisição para a loja.
* @param {Object} [body] - Parâmetro opcional, corpo da requisição.
* @return {Promise<Response>} Resposta da requisição.
*/
async request(method, endpoint, body = undefined) {
const requestParams = {
method,
headers: {
Authorization: `Bearer ${this.token}`,
},
...body && { data: body },
};
if (new Date() > this.expiry) {
await this.refresh();
}
return this.session.request(this.url + endpoint, requestParams);
}
/**
* Espera até que a autenticação seja concluída.
*
* @async
* @function
* @summary Útil para casos onde a instância da loja pode ser usada muito cedo.
* @param {number} [delay=500] - Milissegundos que a função vai dormir entre as verificações.
* @see {@link auth} - função responsável pela autenticação.
*/
async wait(delay = 500) {
while (!this.url || !this.token || !this.summoner) {
await sleep(delay);
if (debug) {
console.log("store: waiting");
}
}
}
/**
* Autentica a classe, definindo os atributos da instância.
*
* @async
* @function
* @summary Essa função deve ser chamada antes de utilizar a classe.
*/
async auth() {
while (await this.isReady() === false) {
await sleep(500);
}
const promises = [this.getStoreUrl(), this.getSummonerToken(), this.getSummonerData()];
[this.url, this.token, this.summoner] = await Promise.all(promises);
if (debug) {
console.log("store: authenticated", this.url, this.token, this.summoner, this.expiry);
}
}
async refresh() {
this.token = await this.getSummonerToken();
if (debug) {
console.log("store: token expired", this.token, this.expiry);
}
}
async getStoreUrl() {
const response = await request("GET", "/lol-store/v1/getStoreUrl");
return await response.json();
}
async getSummonerToken() {
const response = await request("GET", "/lol-rso-auth/v1/authorization/access-token");
const responseData = await response.json();
this.expiry = new Date(responseData.expiry * 1000);
return responseData.token;
}
async getSummonerData() {
const response = await request("GET", "/lol-summoner/v1/current-summoner");
return await response.json();
}
async isReady() {
const response = await request("GET", "/lol-store/v1/store-ready");
return await response.json();
}
}
/**
* Função que realiza uma requisição utilizando a função fetch.
*
* @async
* @function
* @param {"GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD"} method - Método HTTP da requisição.
* @param {string} endpoint - Endpoint da requisição para a API.
* @param {Object} [options] - As opções da requisição.
* @param {Object} [options.headers] - Os cabeçalhos da requisição.
* @param {Object} [options.body] - O corpo da requisição.
* @param {Object} [options.params] - Parâmetros da requisição.
* @returns {Promise<Response>} Resposta da requisição.
*/
export function request(method, endpoint, { headers, body, params } = {}) {
const requestOptions = {
method,
headers: {
...headers,
"accept": "application/json",
"content-type": "application/json",
},
...body && { body: JSON.stringify(body) },
};
if (params) { // query parameters
endpoint += "?" + new URLSearchParams(params).toString();
}
return fetch(endpoint, requestOptions);
}
/**
* Adiciona um arquivo CSS ao documento.
*
* @function
* @param {string} url - URL do arquivo CSS.
* @see {@link https://pengu.lol/guide/css-theme#remote-theme}
*/
export function addCssLink(url) {
const link = document.createElement("link");
link.href = url;
link.type = "text/css";
link.rel = "stylesheet";
document.head.appendChild(link);
}
/**
* Aguarda até que o elemento tenha um shadow root.
*
* @async
* @function
* @param {HTMLElement} element - Elemento que vai ser verificado.
* @returns {Promise<ShadowRoot>} Shadow root do elemento.
*/
export async function waitShadowRoot(element) {
while (!element.shadowRoot) {
await sleep(200);
}
return element.shadowRoot;
}
/**
* Envia uma notificação no chat da seleção de campeões.
*
* @async
* @function
* @param {string} notification - Notificação que vai ser enviada.
* @return {Promise<Response>} Resposta da requisição.
*/
export async function sendChatNotification(notification) {
const cid = championSelectCid || await getChampionSelectCid();
const body = { body: notification, type: "celebration" };
const endpoint = `/lol-chat/v1/conversations/${cid}/messages`;
return request("POST", endpoint, { body });
}
/**
* Retorna os dados dos participantes da seleção de campeões.
*
* @async
* @function
* @returns {Promise<Object[]>} Dados dos participantes da seleção de campeões.
*/
export async function getChampionSelectParticipants() {
const response = await request("GET", "//riotclient/chat/v5/participants/champ-select");
const { participants } = await response.json();
return participants || [];
}
async function getChampionSelectCid() {
const participants = await getChampionSelectParticipants();
championSelectCid = participants[0]?.cid;
return championSelectCid;
}
/**
* Faz o código dormir por um tempo específico.
*
* @async
* @function
* @param {number} ms - Milissegundos que a função vai dormir.
* @return {Promise<number>} Retorno do uso de setTimeout.
*/
export const sleep = ms => new Promise(r => setTimeout(r, ms));
/**
* Adiciona funções que vão ser chamadas periodicamente.
*
* @async
* @function
* @param {...function} callbacks - Funções que vão ser adicionadas.
* @return {number} Quantidade atual de rotinas carregadas.
*/
export function addRoutines(...callbacks) { return routines.push(...callbacks); }
/**
* @typedef {Object} ParsedEventData
* @property {any} data - Os dados do evento recebido.
* @property {string} uri - A URI do evento recebido.
* @property {string} eventType - O tipo de evento recebido.
*/
/**
* Conecta um endpoint a uma função.
*
* @async
* @function
* @summary Quando o endpoint for atualizado, {@link callback} será chamado recebendo seus dados.
* @example
* function phaseHandler(parsedEvent) {
* if (parsedEvent.data === "ChampSelect") {
* console.log("Yep! Estamos na seleção de campeão!");
* }
* }
*
* linkEndpoint("/lol-gameflow/v1/gameflow-phase", phaseHandler);
* @param {string} rawEndpoint - Endpoint que será conectado à função.
* @param {function} callback - Função que será chamada no evento do endpoint.
* @param {ParsedEventData} callback.parsedEvent - Evento analisado recebido do endpoint.
* @param {MessageEvent} callback.rawEvent - Objeto MessageEvent contendo os detalhes do evento recebido.
*/
export function linkEndpoint(rawEndpoint, callback) {
const riotWebsocket = document.querySelector(`link[rel="riot:plugins:websocket"]`);
const webSocket = new WebSocket(riotWebsocket.href, "wamp");
const endpoint = rawEndpoint.replaceAll("/", "_");
webSocket.onopen = () => webSocket.send(JSON.stringify([5, "OnJsonApiEvent" + endpoint]));
webSocket.onmessage = messageEvent => callback(JSON.parse(messageEvent.data)[2], messageEvent);
}
/**
* Retorna o elemento HTML que corresponde ao XPath especificado.
*
* @param {string} path - XPath do elemento.
* @return {HTMLElement} Elemento HTML que corresponde ao XPath especificado.
*/
export function getElementByXpath(path) {
const result = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue;
}
async function fetchClientCredentials() {
const response = await request("GET", "/riotclient/command-line-args");
const responseData = await response.json();
for (const element of responseData) {
const authMatch = element.match(clientAuthRegex);
const portMatch = element.match(clientPortRegex);
if (authMatch) {
credentials.auth = authMatch[1];
}
if (portMatch) {
credentials.port = parseInt(portMatch[1], 10);
}
}
if (debug) {
console.log("credentials:", credentials);
}
}
async function watchRoutines() {
for (const routine of routines) {
routine(); // ideal que seja síncrono
}
setTimeout(watchRoutines, 1000);
}
/**
* Inicializa a variável global {@link gamePhase} com o valor atual da fase do jogo.
*
* @async
* @function
* @summary Útil para casos onde o script é carregado após o jogo já ter iniciado.
*/
async function setGamePhase() {
while (gamePhase === undefined) {
await sleep(500);
const response = await request("GET", "/lol-gameflow/v1/gameflow-phase");
gamePhase = await response.json(); // pode ser: None, Lobby, Matchmaking, ReadyCheck, ChampSelect...
if (debug) {
console.log("setGamePhase:", gamePhase);
}
}
}
async function setLayerManager() {
while (!layerManager) {
layerManager = document.querySelector("#lol-uikit-layer-manager-wrapper");
await sleep(500);
if (debug) {
console.log("setLayerManager:", layerManager);
}
}
}
function init() {
console.debug("controlado-utils(init): Report bugs to Balaclava#1912");
setGamePhase();
setLayerManager();
fetchClientCredentials();
linkEndpoint("/lol-gameflow/v1/gameflow-phase", parsedEvent => gamePhase = parsedEvent.data);
linkEndpoint("/lol-gameflow/v1/gameflow-phase", async parsedEvent => {
if (parsedEvent.data === "ChampSelect") {
await sleep(1000);
getChampionSelectCid();
}
});
if (debug) {
linkEndpoint("", parsedEvent => console.log(parsedEvent.uri, parsedEvent.data));
window.getGamePhase = () => gamePhase;
window.LCURequest = request;
window.LCUSleep = sleep;
}
watchRoutines();
}
window.addEventListener("load", init);