diff --git a/.gitignore b/.gitignore index 8fa17e5..7f0326f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules dist .idea typings +test # files package-lock.json diff --git a/package.json b/package.json index c27fb5a..2baca97 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "repository": "MenuDocs/erela.js", "bugs": "https://github.com/MenuDocs/erela.js", "devDependencies": { - "@types/axios": "^0.14.0", "@types/node": "^14.6.0", "@types/ws": "^6.0.4", "@typescript-eslint/eslint-plugin": "^3.10.1", @@ -48,7 +47,7 @@ }, "dependencies": { "@discordjs/collection": "^0.1.6", - "axios": "^0.21.0", + "petitio": "^1.1.0", "ws": "^7.3.1" }, "eslintConfig": { diff --git a/src/structures/Manager.ts b/src/structures/Manager.ts index e83678d..c03953b 100644 --- a/src/structures/Manager.ts +++ b/src/structures/Manager.ts @@ -1,6 +1,5 @@ /* eslint-disable no-async-promise-executor */ import Collection from "@discordjs/collection"; -import Axios from "axios"; import { EventEmitter } from "events"; import { Node, NodeOptions } from "./Node"; import { Player, PlayerOptions, Track, UnresolvedTrack } from "./Player"; @@ -61,6 +60,12 @@ function check(options: ManagerOptions) { !Array.isArray(options.trackPartial) ) throw new TypeError('Manager option "trackPartial" must be a string array.'); + + if ( + typeof options.clientName !== "undefined" && + typeof options.clientName !== "string" + ) + throw new TypeError('Manager option "clientName" must be a string.'); } export interface Manager { @@ -251,6 +256,7 @@ export class Manager extends EventEmitter { nodes: [{ identifier: "default", host: "localhost" }], shards: 1, autoPlay: true, + clientName: "erela.js", ...options, }; @@ -316,39 +322,30 @@ export class Manager extends EventEmitter { search = `${source}search:${search}`; } - const url = `http${node.options.secure ? "s" : ""}://${ - node.options.host - }:${node.options.port}/loadtracks`; - - const res = await Axios.get(url, { - headers: { Authorization: node.options.password }, - params: { identifier: search }, - timeout: 10000, - timeoutErrorMessage: `Node ${node.options.identifier} search timed out.`, - }).catch((err) => { - return reject(err); - }); - - node.calls++; + const res = await node.makeRequest(`/loadtracks?identifier=${encodeURIComponent(search)}`, r => { + if (node.options.requestTimeout) { + r.timeout(node.options.requestTimeout) + } + }).catch(err => reject(err)); - if (!res || !res.data) { + if (!res) { return reject(new Error("Query not found.")); } const result: SearchResult = { - loadType: res.data.loadType, - exception: res.data.exception ?? null, - tracks: res.data.tracks.map((track: TrackData) => + loadType: res.loadType, + exception: res.exception ?? null, + tracks: res.tracks.map((track: TrackData) => TrackUtils.build(track, requester) ), }; if (result.loadType === "PLAYLIST_LOADED") { result.playlist = { - name: res.data.playlistInfo.name, - selectedTrack: res.data.playlistInfo.selectedTrack === -1 ? null : + name: res.playlistInfo.name, + selectedTrack: res.playlistInfo.selectedTrack === -1 ? null : TrackUtils.build( - res.data.tracks[res.data.playlistInfo.selectedTrack], + res.tracks[res.playlistInfo.selectedTrack], requester ), duration: result.tracks @@ -368,23 +365,16 @@ export class Manager extends EventEmitter { return new Promise(async (resolve, reject) => { const node = this.nodes.first(); if (!node) throw new Error("No available nodes."); - const url = `http${node.options.secure ? "s" : ""}://${ - node.options.host - }:${node.options.port}/decodetracks`; - - const res = await Axios.post(url, tracks, { - headers: { Authorization: node.options.password }, - }).catch((err) => { - return reject(err); - }); - node.calls++; + const res = await node.makeRequest(`/decodetracks`, r => r + .body(tracks, "json")) + .catch(err => reject(err)); - if (!res || !res.data) { + if (!res) { return reject(new Error("No data returned from query.")); } - return resolve(res.data); + return resolve(res); }); } @@ -392,15 +382,9 @@ export class Manager extends EventEmitter { * Decodes the base64 encoded track and returns a TrackData. * @param track */ - public decodeTrack(track: string): Promise { - return new Promise(async (resolve, reject) => { - try { - const res = await this.decodeTracks([track]); - return resolve(res[0]); - } catch (e) { - return reject(e); - } - }); + public async decodeTrack(track: string): Promise { + const res = await this.decodeTracks([ track ]); + return res[0]; } /** @@ -505,6 +489,8 @@ export interface ManagerOptions { nodes?: NodeOptions[]; /** The client ID to use. */ clientId?: string; + /** Value to use for the `Client-Name` header. */ + clientName?: string; /** The shard count. */ shards?: number; /** A array of plugins to use. */ @@ -513,7 +499,6 @@ export interface ManagerOptions { autoPlay?: boolean; /** An array of track properties to keep. `track` will always be present. */ trackPartial?: string[]; - /** * Function to send data to the websocket. * @param id diff --git a/src/structures/Node.ts b/src/structures/Node.ts index bc89226..d2be282 100644 --- a/src/structures/Node.ts +++ b/src/structures/Node.ts @@ -1,5 +1,6 @@ /* eslint-disable no-case-declarations */ import WebSocket from "ws"; +import fetch from "petitio"; import { Manager } from "./Manager"; import { Player, Track, UnresolvedTrack } from "./Player"; import { @@ -13,6 +14,8 @@ import { WebSocketClosedEvent, } from "./Utils"; +import type { PetitioRequest } from "petitio/dist/lib/PetitioRequest"; + function check(options: NodeOptions) { if (!options) throw new TypeError("NodeOptions must not be empty."); @@ -58,6 +61,12 @@ function check(options: NodeOptions) { typeof options.retryDelay !== "number" ) throw new TypeError('Node option "retryDelay" must be a number.'); + + if ( + typeof options.requestTimeout !== "undefined" && + typeof options.requestTimeout !== "number" + ) + throw new TypeError('Node option "requestTimeout" must be a number.'); } export class Node { @@ -141,6 +150,7 @@ export class Node { Authorization: this.options.password, "Num-Shards": String(this.manager.options.shards), "User-Id": this.manager.options.clientId, + "Client-Name": this.manager.options.clientName, }; this.socket = new WebSocket( @@ -173,6 +183,26 @@ export class Node { this.manager.destroyNode(this.options.identifier); } + /** + * Makes an API call to the Node + * @param endpoint The endpoint that we will make the call to + * @param modify Used to modify the request before being sent + * @returns The returned data + */ + public async makeRequest(endpoint: string, modify?: ModifyRequest): Promise { + endpoint = endpoint.replace(/^\//gm, ""); + + const request = fetch(`http${this.options.secure ? "s" : ""}://${this.options.host}:${this.options.port}/${endpoint}`) + .header("Authorization", this.options.password); + + if (modify) { + await modify(request); + } + + this.calls++; + return await request.json(); + } + /** * Sends data to the Node. * @param data @@ -375,6 +405,9 @@ export class Node { } } +/** Modifies any outgoing REST requests. */ +export type ModifyRequest = (request: PetitioRequest) => any | Promise; + export interface NodeOptions { /** The host for the node. */ host: string; @@ -390,6 +423,8 @@ export interface NodeOptions { retryAmount?: number; /** The retryDelay for the node. */ retryDelay?: number; + /** The timeout used for api calls */ + requestTimeout?: number; } export interface NodeStats { diff --git a/tsconfig.json b/tsconfig.json index e374fc8..ac3e5a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "declaration": true, "outDir": "./dist", "rootDir": "./src", + "skipLibCheck": true, /* Strict Type-Checking Options */ // "strict": true,