steamapi
Version:
A nice Steam API wrapper.
487 lines (486 loc) • 20.6 kB
JavaScript
import SteamID from 'steamid';
import querystring from 'node:querystring';
// https://stackoverflow.com/a/66726426/7504056
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const Package = require('../../package.json');
import { MemoryCacheMap } from './Cache.js';
import { fetch, assertApp, assertID } from './utils.js';
import UserStats from './structures/UserStats.js';
import NewsPost from './structures/NewsPost.js';
import Server from './structures/Server.js';
import Game from './structures/Game.js';
import GameDetails from './structures/GameDetails.js';
import GameInfo from './structures/GameInfo.js';
import GameInfoExtended from './structures/GameInfoExtended.js';
import GameInfoBasic from './structures/GameInfoBasic.js';
import GameServer from './structures/GameServer.js';
import GameSchema from './structures/GameSchema.js';
import UserAchievements from './structures/UserAchievements.js';
import UserBadges from './structures/UserBadges.js';
import UserPlaytime from './structures/UserPlaytime.js';
import UserBans from './structures/UserBans.js';
import UserFriend from './structures/UserFriend.js';
import UserServers from './structures/UserServers.js';
import UserSummary from './structures/UserSummary.js';
const defaultOptions = {
language: 'english',
currency: 'us',
headers: { 'User-Agent': `SteamAPI/${Package.version} (https://www.npmjs.com/package/${Package.name})` },
baseAPI: 'https://api.steampowered.com',
baseStore: 'https://store.steampowered.com/api',
baseActions: 'https://steamcommunity.com/actions',
inMemoryCacheEnabled: true,
gameDetailCacheEnabled: true,
gameDetailCacheTTL: 86400000,
userResolveCacheEnabled: true,
userResolveCacheTTL: 86400000,
};
export default class SteamAPI {
static reProfileBase = String.raw `(?:(?:(?:(?:https?)?:\/\/)?(?:www\.)?steamcommunity\.com)?)?\/?`;
static reCommunityID = RegExp(String.raw `^(\d{17})$`, 'i');
static reSteamID2 = RegExp(String.raw `^(STEAM_\d+:\d+:\d+)$`, 'i');
static reSteamID3 = RegExp(String.raw `^(\[U:\d+:\d+\])$`, 'i');
static reProfileURL = RegExp(String.raw `${this.reProfileBase}profiles\/(\d{17})`, 'i');
static reVanityURL = RegExp(String.raw `${this.reProfileBase}id\/([a-z0-9_-]{2,32})`, 'i');
static reVanityID = RegExp(String.raw `([a-z0-9_-]{2,32})`, 'i');
static SUCCESS_CODE = 1;
language;
currency;
headers;
baseAPI;
baseStore;
baseActions;
gameDetailCache;
userResolveCache;
key = '';
/**
* Make a new SteamAPI Client
* @param key Key to use for API calls. Key can be generated at https://steamcommunity.com/dev/apikey. If you want to make requests without a key, pass in false
* @param options Custom options for default language, HTTP parameters, and caching
*/
constructor(key, options = {}) {
if (key !== false) {
if (key) {
this.key = key;
}
else {
console.warn([
'no key provided',
'some methods won\'t work',
'get one from https://goo.gl/DfNy5s or initialize SteamAPI as new SteamAPI(false) to suppress this warning'
].join('\n'));
}
}
options = { ...defaultOptions, ...options };
if (options.inMemoryCacheEnabled) {
if (options.gameDetailCacheEnabled && options.gameDetailCacheTTL)
this.gameDetailCache = new MemoryCacheMap(options.gameDetailCacheTTL);
if (options.userResolveCacheEnabled && options.userResolveCacheTTL)
this.userResolveCache = new MemoryCacheMap(options.userResolveCacheTTL);
}
this.language = options.language;
this.currency = options.currency;
this.headers = options.headers;
this.baseAPI = options.baseAPI;
this.baseStore = options.baseStore;
this.baseActions = options.baseActions;
}
/**
* Used to make any GET request to the Steam API
* @param path Path to request e.g '/IPlayerService/GetOwnedGames/v1?steamid=76561198378422474'
* @param base Base API URL
* @returns Parse JSON
*/
get(path, params = {}, base = this.baseAPI) {
if (this.key)
params.key = this.key;
return fetch(`${base}${path}?${querystring.stringify(params)}`, this.headers);
}
/**
* Resolve runs through a couple different methods for finding a user's profile ID based on
* either their id, username, profile url, vanity url, steamID2, or steamID3.
* Rejects promise if a profile couldn't be resolved
* @param query Something to resolve like https://steamcommunity.com/id/xDim
* @returns Profile ID
*/
async resolve(query) {
// community id match, ex. 76561198378422474
const communityIDMatch = query.match(SteamAPI.reCommunityID);
if (communityIDMatch !== null)
return communityIDMatch[1];
// url, https://steamcommunity.com/profiles/76561198378422474
const urlMatch = query.match(SteamAPI.reProfileURL);
if (urlMatch !== null)
return urlMatch[1];
// Steam 2: STEAM_0:0:209078373
const steamID2Match = query.match(SteamAPI.reSteamID2);
if (steamID2Match !== null) {
const sid = new SteamID(steamID2Match[1]);
return sid.getSteamID64();
}
// Steam 3: [U:1:418156746]
const steamID3Match = query.match(SteamAPI.reSteamID3);
if (steamID3Match !== null) {
const sid = new SteamID(steamID3Match[1]);
return sid.getSteamID64();
}
// vanity id, https://steamcommunity.com/id/xDim
const vanityUrlMatch = query.match(SteamAPI.reVanityURL);
if (vanityUrlMatch !== null) {
const id = vanityUrlMatch[1];
const cachedID = this.userResolveCache?.get(id);
if (cachedID)
return cachedID;
const json = await this.get('/ISteamUser/ResolveVanityURL/v1', { vanityurl: id });
if (json.response.success !== SteamAPI.SUCCESS_CODE)
throw new Error(json.response.message);
if (this.userResolveCache)
this.userResolveCache.set(id, json.response.steamid);
return json.response.steamid;
}
const vanityIdMatch = query.match(SteamAPI.reVanityID);
if (vanityIdMatch !== null) {
const id = vanityIdMatch[0];
const cachedID = this.userResolveCache?.get(id);
if (cachedID)
return cachedID;
const json = await this.get('/ISteamUser/ResolveVanityURL/v1', { vanityurl: id });
if (json.response.success !== SteamAPI.SUCCESS_CODE)
throw new Error(json.response.message);
if (this.userResolveCache)
this.userResolveCache.set(id, json.response.steamid);
return json.response.steamid;
}
throw new TypeError('Invalid format');
}
/**
* Gets featured categories on Steam store
*
* <warn>undocumented endpoint -- may be unstable</warn>
* @param options More options
* @param options.language The language
* @param options.currency The currency
*/
getFeaturedCategories({ language = this.language, currency = this.currency } = {}) {
// TODO: make class for this
return this.get('/featuredcategories', { l: language, cc: currency }, this.baseStore);
}
/**
* Gets featured games on Steam store
*
* <warn>undocumented endpoint -- may be unstable</warn>
* @param options More options
* @param options.language The language
* @param options.currency The currency
*/
getFeaturedGames({ language = this.language, currency = this.currency } = {}) {
// TODO: make class for this
return this.get('/featured', { l: language, cc: currency }, this.baseStore);
}
async getGameDetails(app, { language = this.language, currency = this.currency, filters = [] } = {}) {
assertApp(app);
const isArr = Array.isArray(app);
const appIDs = isArr ? app : [app];
const cached = appIDs.map(a => this.gameDetailCache?.get(`${a}-${currency}-${language}`)).filter(g => g !== undefined);
const remainingAppIDs = appIDs.filter((_, i) => cached[i] === undefined);
// If some appIDs were missing from the cache, fetch from Steam and update the cache
if (remainingAppIDs.length !== 0) {
const details = await this
.get('/appdetails', {
appids: remainingAppIDs.join(','),
cc: currency,
l: language,
filters: isArr && appIDs.length > 1 ? 'price_overview' : filters.join(','),
}, this.baseStore)
.then(json => {
if (json === null)
throw new Error('Failed to find app ID');
const filtered = {};
for (const [k, v] of Object.entries(json))
if (v.success) {
const d = v.data;
// Convert empty arrays to empty objects for consistency
filtered[k] = Array.isArray(d) && d.length === 0 ? {} : d;
}
if (Object.keys(filtered).length === 0)
throw new Error('Failed to find app ID');
return filtered;
});
for (const [appID, data] of Object.entries(details)) {
const game = new GameDetails(data);
this.gameDetailCache?.set(`${appID}-${currency}-${language}`, game);
cached.push(game);
}
}
if (!isArr)
return cached[0];
const cachedObject = {};
for (const details of cached)
cachedObject[details.id.toString()] = details;
return cachedObject;
}
/**
* Get every single app on steam
*
* Note: Original JSON names are being preserved instead of converting
* each element to a class here because there are 186311+ games
* that would have to be made into a class.
* @returns Array of very basic app info (ID + name)
*/
async getAppList() {
// TODO: allow a parameter to be passed in to convert these to a class?
return (await this.get('/ISteamApps/GetAppList/v2')).applist.apps;
}
/**
* Get every server associated with a particular host
* @param host Host to query (IPv4 or IPv4:queryport)
* @returns Info of servers
*/
async getServers(host) {
const { response } = await this.get('/ISteamApps/GetServersAtAddress/v1', { addr: host });
if (!response.success)
throw new Error(response.message);
return response.servers.map((server) => new Server(server));
}
/**
* Get number of current players for app ID
* @param app App ID to get number of current players for
* @returns Number of current players
*/
async getGamePlayers(app) {
assertApp(app);
const json = await this.get('/ISteamUserStats/GetNumberOfCurrentPlayers/v1', { appid: app });
if (json.response.result !== SteamAPI.SUCCESS_CODE)
throw new Error('No app found');
return json.response.player_count;
}
/**
* Get schema for app ID
* @param app App ID to get schema for
* @param language Language to return strings for (note: does not seem to affect stats; only achievements)
* @returns Schema
*/
async getGameSchema(app, language = this.language) {
assertApp(app);
return new GameSchema((await this.get('/ISteamUserStats/GetSchemaForGame/v2', { appid: app, l: language })).game);
}
/**
* Get a user's achievements for app ID
* @param id Steam ID of user
* @param app App ID to get achievements for
* @param language Language to return strings for
* @returns Achievements
*/
async getUserAchievements(id, app, language = this.language) {
assertID(id);
assertApp(app);
const json = await this.get('/ISteamUserStats/GetPlayerAchievements/v1', { steamid: id, appid: app, l: language });
if (!json.playerstats.success)
throw new Error(json.playerstats.message);
return new UserAchievements(json.playerstats);
}
/**
* Get achievement percentages for app ID
*
* If a game does not have any achievements, this will error
* @param app App ID to get achievement progress for
* @returns Array of object with achievement name and percentage for app ID
*/
async getGameAchievementPercentages(app) {
assertApp(app);
const json = await this.get('/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2', { gameid: app });
return json.achievementpercentages.achievements;
}
/**
* Get a user's stats for app ID
* @param id Steam ID of user
* @param app App ID to get user stats for
* @returns Stats for app ID
*/
async getUserStats(id, app) {
assertID(id);
assertApp(app);
return new UserStats((await this.get('/ISteamUserStats/GetUserStatsForGame/v2', { steamid: id, appid: app })).playerstats);
}
/**
* Get news for app ID
* @param app App ID
* @param options Additional options for filtering posts
* @returns App news for ID
*/
async getGameNews(app, options = {}) {
assertApp(app);
const params = {
appid: app,
maxlength: options.maxContentLength,
enddate: options.endDate?.getTime(),
count: options.count,
feeds: options.feeds?.join(','),
tags: options.tags?.join(','),
};
// Filter out options that weren't supplied
for (const [k, v] of Object.entries(params))
if (v === undefined)
delete params[k];
return (await this.get('/ISteamNews/GetNewsForApp/v2', params)).appnews.newsitems.map((item) => new NewsPost(item));
}
/**
* Get a user's badges
* @param id User ID
* @returns User level info and badges
*/
async getUserBadges(id) {
assertID(id);
return new UserBadges((await this.get('/IPlayerService/GetBadges/v1', { steamid: id })).response);
}
/**
* Get a user's level
* @param id User ID
* @returns The user's Steam level
*/
async getUserLevel(id) {
assertID(id);
return (await this.get('/IPlayerService/GetSteamLevel/v1', { steamid: id })).response.player_level;
}
/**
* Get users owned games.
* @param id User ID
* @param opts Additional options for filtering
* @returns Owned games
*/
async getUserOwnedGames(id, opts = {}) {
assertID(id);
// Same behavior as v3
if (opts.includeFreeGames === undefined)
opts.includeFreeGames = true;
if (opts.language === undefined)
opts.language = this.language;
if (opts.includeExtendedAppInfo)
opts.includeAppInfo = true;
const params = {
steamid: id,
include_appinfo: opts.includeAppInfo,
include_played_free_games: opts.includeFreeGames,
include_free_sub: opts.includeFreeSubGames,
skip_unvetted_apps: opts.includeUnvettedApps === undefined ? undefined : !opts.includeUnvettedApps,
include_extended_appinfo: opts.includeExtendedAppInfo,
appids_filter: opts.filterApps,
language: opts.language,
};
// Filter out options that weren't supplied
for (const [k, v] of Object.entries(params))
if (v === undefined)
delete params[k];
const json = await this.get('/IPlayerService/GetOwnedGames/v1', params);
return json.response.games.map((data) => {
let game;
if (opts.includeExtendedAppInfo)
game = new GameInfoExtended(data);
else if (opts.includeAppInfo)
game = new GameInfo(data);
else
game = new Game(data);
return new UserPlaytime(data, game);
});
}
/**
* Get a user's recently played games. Note: <UserPlaytime>.game is GameInfo not just Game
*
* Like getUserOwnedGames() but only returns games played in the last 2 weeks
* @param id User ID
* @param count Number of results to limit the request to (0 means no limit)
* @returns Recently played games and their play times
*/
async getUserRecentGames(id, count = 0) {
assertID(id);
const json = await this.get('/IPlayerService/GetRecentlyPlayedGames/v1', { steamid: id, count });
return (json.response.games || []).map((data) => new UserPlaytime(data, new GameInfoBasic(data)));
}
async getUserBans(id) {
assertID(id);
const arr = Array.isArray(id);
const json = await this.get('/ISteamUser/GetPlayerBans/v1', {
steamids: arr ? id.join(',') : id,
});
const bans = json.players.map((player) => new UserBans(player));
return arr ? bans : bans[0];
}
/**
* Get a user's friends
* @param id User ID
* @returns The provided user's friends
*/
async getUserFriends(id) {
assertID(id);
const json = await this.get('/ISteamUser/GetFriendList/v1', { steamid: id });
return json.friendslist.friends.map((friend) => new UserFriend(friend));
}
/**
* Get the groups the user is a member of
* @param id User ID
* @returns Group IDs
*/
async getUserGroups(id) {
assertID(id);
const json = await this.get('/ISteamUser/GetUserGroupList/v1', { steamid: id });
if (!json.response.success)
throw new Error(json.response.message);
return json.response.groups.map((group) => group.gid);
}
/**
* Gets servers on steamcommunity.com/dev/managegameservers using your key
* @returns Your server
*/
async getUserServers() {
return new UserServers((await this.get('/IGameServersService/GetAccountList/v1')).response);
}
async getUserSummary(id) {
assertID(id);
const arr = Array.isArray(id);
const json = await this.get('/ISteamUser/GetPlayerSummaries/v2', { steamids: arr ? id.join(',') : id });
if (!json.response.players.length)
throw new Error('No players found');
const summaries = json.response.players.map((player) => new UserSummary(player));
return arr ? summaries : summaries[0];
}
/**
* Gets the Steam server's time
* @returns Date object from the server
*/
async getServerTime() {
const json = await this.get('/ISteamWebAPIUtil/GetServerInfo/v1');
return new Date(json.servertime * 1000);
}
/**
* Gets all the countries
* @returns Array of country objects with fields countrycode, hasstates, and countryname
*/
async getCountries() {
return (await this.get('/QueryLocations', {}, this.baseActions));
}
/**
* Gets all the states for a particular country
* @returns Array of state objects with fields countrycode, statecode, and statename
*/
async getStates(countryCode) {
return (await this.get(`/QueryLocations/${countryCode}`, {}, this.baseActions));
}
/**
* Gets all the cities for a particular state
* @returns Array of city objects with fields countrycode, statecode, cityname and cityid
*/
async getCities(countryCode, stateCode) {
return (await this.get(`/QueryLocations/${countryCode}/${stateCode}`, {}, this.baseActions));
}
/**
* Gets servers using Master Server Query Protocol filtering
* @param filter Filter as defined by the [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter).
* Although a filter is not strictly required, you probably want to at least use something like \appid\[appid] to filter by app
* @param limit Number of results to return. 100 by default
*/
async getServerList(filter = '', limit = 100) {
const json = await this.get('/IGameServersService/GetServerList/v1', { filter, limit });
return json.response.servers.map((server) => new GameServer(server));
}
}