lib/Api.js
const request = require('superagent');
const userAgent = `node-osu v${require('../package.json').version} (https://github.com/brussell98/node-osu)`;
const Beatmap = require('./base/Beatmap.js');
const Score = require('./base/Score.js');
const Match = require('./base/Match.js');
const User = require('./base/User.js');
class Api {
/**
* Creates a new node-osu object
* @param {String} apiKey your osu api key
* @param {Object} [options]
* @param {String} [options.baseUrl="https://osu.ppy.sh/api"] Sets the base api url
* @param {Boolean} [options.notFoundAsError=true] Throw an error on not found instead of returning nothing
* @param {Boolean} [options.completeScores=false] When fetching scores also fetch the beatmap they are for (Allows getting accuracy)
* @param {Boolean} [options.parseNumeric=false] Parse numeric properties into numbers. May have overflow
*/
constructor(apiKey, options = { }) {
this.apiKey = apiKey;
this.baseUrl = options.baseUrl || 'https://osu.ppy.sh/api';
this.notFoundAsError = options.notFoundAsError === undefined ? true : !!options.notFoundAsError;
this.completeScores = !!options.completeScores;
this.parseNumeric = !!options.parseNumeric;
}
get config() {
return {
notFoundAsError: this.notFoundAsError,
completeScores: this.completeScores,
parseNumeric: this.parseNumeric
};
}
/**
* Makes an api call
* @param {String} endpoint
* @param {Object} options
* @param {Date} [options.since] Return all beatmaps ranked or loved since this date
* @param {String} [options.s] Specify a beatmapSetId to return metadata from
* @param {String} [options.b] Specify a beatmapId to return metadata from
* @param {String} [options.u] Specify a userId or a username to return metadata from
* @param {"string"|"id"} [options.type] Specify if `u` is a userId or a username
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {0|1} [options.a] Specify whether converted beatmaps are included
* @param {String} [options.h] The beatmap hash
* @param {Number} [options.limit] Amount of results. Default and maximum are 500
* @param {Number} [options.mods] Mods that apply to the beatmap requested. Default is 0
* @param {Number} [options.event_days] Max number of days between now and last event date. Range of 1-31. Default value is 1
* @param {String} [options.mp] Match id to get information from
* @returns {Promise<Object>} The response body
*/
async apiCall(endpoint, options) {
if (!this.apiKey)
throw new Error('apiKey not set');
options.k = this.apiKey;
try {
const resp = await request.get(this.baseUrl + endpoint)
.set('User-Agent', userAgent)
.query(options);
return resp.body;
} catch (error) {
throw new Error(error.response || error);
}
}
/**
* Returns a not found error or the response, depending on the config
* @param {Object} response
* @returns {Object}
*/
notFound(response) {
if (this.notFoundAsError)
throw new Error('Not found');
return response;
}
/**
* Returns an array of Beatmap objects
* @param {Object} options
* @param {String} options.b Specify a beatmapId to return metadata from
* @param {Date} [options.since] Return all beatmaps ranked or loved since this date
* @param {String} [options.s] Specify a beatmapSetId to return metadata from
* @param {String} [options.u] Specify a userId or a username to return metadata from
* @param {"string"|"id"} [options.type] Specify if `u` is a userId or a username
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {0|1} [options.a] Specify whether converted beatmaps are included
* @param {String} [options.h] The beatmap hash
* @param {Number} [options.limit] Amount of results. Default and maximum are 500
* @param {Number} [options.mods] Mods that apply to the beatmap requested. Default is 0
* @returns {Promise<Beatmap[]>}
*/
async getBeatmaps(options) {
const resp = await this.apiCall('/get_beatmaps', options);
if (resp.length === 0)
return this.notFound(resp);
return resp.map(bm => new Beatmap(this.config, bm));
}
/**
* Returns a User object
* @param {Object} options
* @param {String} options.u Specify a userId or a username to return metadata from
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
* @param {Number} [options.event_days] Max number of days between now and last event date. Range of 1-31. Default value is 1
* @returns {Promise<User>}
*/
async getUser(options) {
const resp = await this.apiCall('/get_user', options);
if (resp.length === 0)
return this.notFound(resp);
return new User(this.config, resp[0]);
}
/**
* Returns an array of Score objects
* @param {Object} options
* @param {String} options.b Specify a beatmapId to return score information from
* @param {String} [options.u] Specify a userId or a username to return information for
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
* @param {Number} [options.limit] Amount of results from the top (range between 1 and 100 - defaults to 50)
* @returns {Promise<Score[]>}
*/
async getScores(options) {
const resp = await this.apiCall('/get_scores', options);
if (resp.length === 0)
return this.notFound(resp);
if (!this.completeScores)
return resp.map(sc => new Score(this.config, sc));
const beatmaps = await this.getBeatmaps({ b: options.b });
return resp.map(sc => new Score(this.config, sc, beatmaps[0]));
}
/**
* Returns an array of Score objects
* @param {Object} options
* @param {String} options.u Specify a userId or a username to return best scores from
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
* @param {Number} [options.limit] Amount of results (range between 1 and 100 - defaults to 10)
* @returns {Promise<Score[]>}
*/
async getUserBest(options) {
const resp = await this.apiCall('/get_user_best', options);
if (resp.length === 0)
return this.notFound(resp);
if (!this.completeScores)
return resp.map(sc => new Score(this.config, sc));
const scores = resp.map(sc => new Score(this.config, sc));
for (const score of scores)
score.beatmap = (await this.getBeatmaps({ b: score.beatmapId }))[0];
return scores;
}
/**
* Returns an array of Score objects.
* Will return not found if the user has not submitted any scores in the past 24 hours
* @param {Object} options
* @param {String} options.u Specify a userId or a username to return recent plays from
* @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {"string"|"id"} [options.type] Specify if `u` is a user_id or a username
* @param {Number} [options.limit] Amount of results (range between 1 and 50 - defaults to 10)
* @returns {Promise<Score[]>}
*/
async getUserRecent(options) {
const resp = await this.apiCall('/get_user_recent', options);
if (resp.length === 0)
return this.notFound(resp);
if (!this.completeScores)
return resp.map(sc => new Score(this.config, sc));
const scores = resp.map(sc => new Score(this.config, sc));
for (const score of scores)
score.beatmap = (await this.getBeatmaps({ b: score.beatmapId }))[0];
return scores;
}
/**
* Returns a Match object.
* @param {Object} options
* @param {String} options.mp Match id to get information from
* @returns {Promise<Match>}
*/
async getMatch(options) {
const resp = await this.apiCall('/get_match', options);
if (resp.match === 0)
return this.notFound(resp);
return new Match(this.config, resp);
}
/**
* Returns a replay object. **Do not spam this endpoint.**
* @param {Object} options
* @param {0|1|2|3} options.m Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
* @param {String} options.b The beatmapId in which the replay was played
* @param {String} options.u The user that has played the beatmap (required)
* @param {"string"|"id"} [options.type] Specify if u is a userId or a username
* @param {Number} [options.mods] Specify a mod or mod combination
*
*/
async getReplay(options) {
return await this.apiCall('/get_replay', options);
}
}
module.exports = Api;