steamapi
Version:
A nice Steam API wrapper.
746 lines (624 loc) • 25.9 kB
text/typescript
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 { CacheMap, MemoryCacheMap } from './Cache.js';
import { fetch, assertApp, assertID } from './utils.js';
import { City, Country, State } from './structures/Locations.js';
import AppBase from './structures/AppBase.js';
import AchievementPercentage from './structures/AchievementPercentage.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';
export interface SteamAPIOptions {
/**
* Default language to use for the API when a language is not explicitly provided
*
* 'english' by default
*/
language?: Language;
/**
* Default currency to use for the API when a currency is not explicitly provided
*
* 'us' by default
*/
currency?: Currency;
/**
* Custom headers to send for all API requests
*
* By default, User-Agent is "SteamAPI/<VERSION> (https://www.npmjs.com/package/steamapi)"
*/
headers?: { [key: string]: string };
/**
* URL to use for Steam API requests
*
* 'https://api.steampowered.com' by default
*/
baseAPI?: string,
/**
* URL to use for Steam Store API requests
*
* 'https://store.steampowered.com/api' by default
*/
baseStore?: string,
/**
* URL to use for Steam action requests (only used for getLocations)
*
* 'https://steamcommunity.com/actions' by default
*/
baseActions?: string,
/**
* Whether to use built-in in-memory caching for gameDetailCache and userResolveCache
*/
inMemoryCacheEnabled?: boolean;
/**
* If `inMemoryCacheEnabled` is true, this decides whether to cache API requests for getGameDetails()
*/
gameDetailCacheEnabled?: boolean;
/**
* How long to cache getGameDetails() in milliseconds
*/
gameDetailCacheTTL?: number;
/**
* If `inMemoryCacheEnabled` is true, this decides whether to cache API requests for resolve()
*/
userResolveCacheEnabled?: boolean;
/**
* How long to cache resolve() in milliseconds
*/
userResolveCacheTTL?: number;
}
const defaultOptions: SteamAPIOptions = {
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 interface GetGameNewsOptions {
/** Maximum length for the content to return, if this is 0 the full content is returned, if it's less then a blurb is generated to fit */
maxContentLength?: number;
/** Retrieve posts earlier than this date */
endDate?: Date;
/** Number of posts to retrieve (default 20) */
count?: number;
/** List of feed names to return news for */
feeds?: string[];
/** List of tags to filter by (e.g. 'patchnotes') */
tags?: string[];
}
export interface GetUserOwnedGamesOptions {
/** Include additional details (name, icon) about each game */
includeAppInfo?: boolean;
/** Include free games the user has played */
includeFreeGames?: boolean;
/** Includes games in the free sub (defaults to false) */
includeFreeSubGames?: boolean;
/** Include unvetted store apps (defaults to false) */
includeUnvettedApps?: boolean;
/** Include even more app details. If true, `includeAppInfo` will also be set to true */
includeExtendedAppInfo?: boolean;
/** If set, restricts results to the passed in apps. (note: does not seem to actually work) */
filterApps?: number[];
/** Language to return app info in. (note: does not seem to actually work) */
language?: Language;
}
// Currencies are used for requests with a currency parameter (found on https://steamdb.info/)
export type Currency = 'us' | 'uk' | 'eu' | 'ru' | 'br' | 'jp' | 'id' | 'my'
| 'ph' | 'sg' | 'th' | 'vn' | 'kr' | 'ua' | 'mx' | 'ca' | 'au' | 'nz'
| 'no' | 'pl' | 'ch' | 'cn' | 'in' | 'cl' | 'pe' | 'co' | 'za' | 'hk' | 'tw'
| 'sa' | 'ae' | 'il' | 'kz' | 'kw' | 'qa' | 'cr' | 'uy' | 'az' | 'ar' | 'tr' | 'pk';
// Languages supported by Steam (found on https://steamcommunity.com/)
export type Language = 'arabic' | 'bulgarian' | 'schinese' | 'tchinese'
| 'czech' | 'danish' | 'dutch' | 'english' | 'finnish' | 'french'
| 'german' | 'greek' | 'hungarian' | 'italian' | 'japanese' | 'koreana'
| 'norwegian' | 'polish' | 'brazilian' | 'portuguese' | 'romanian' | 'russian'
| 'latam' | 'spanish' | 'swedish' | 'thai' | 'turkish' | 'ukrainian' | 'vietnamese';
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?: CacheMap<string, GameDetails>;
userResolveCache?: CacheMap<string, string>;
private 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: string | false, options: SteamAPIOptions = {}) {
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 as Language;
this.currency = options.currency as Currency;
this.headers = options.headers as { [key: string]: string };
this.baseAPI = options.baseAPI as string;
this.baseStore = options.baseStore as string;
this.baseActions = options.baseActions as string;
}
/**
* 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: string, params: querystring.ParsedUrlQueryInput = {}, base = this.baseAPI): Promise<any> {
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: string): Promise<string> {
// 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 } = {}): Promise<{ [key: string]: any }> {
// 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 } = {}): Promise<{ [key: string]: any }> {
// TODO: make class for this
return this.get('/featured', { l: language, cc: currency }, this.baseStore);
}
/**
* Get details for app ID. If an array of more than one app ID is passed in, the parameter &filters=price_overview
* will be added to the request since otherwise the server would respond with null
*
* Note: a game will not have a price_overview field if it is F2P
*
* <warn>If the array contains invalid app IDs, they will be filtered out</warn>
*
* <warn>Requests for this endpoint are limited to 200 every 5 minutes</warn>
*
* <warn>Not every `currency` is supported. Only the following are valid: `us, ca, cc, es, de, fr, ru, nz, au, uk`.</warn>
*
* <warn>Not every `language` is supported. A list of available languages can be found [here](https://www.ibabbleon.com/Steam-Supported-Languages-API-Codes.html).</warn>
* @param app App ID or array of App IDs
* @param options More options
* @param options.language The language
* @param options.currency The currency
* @param options.filters Fields to restrict the return results to
* @returns If app is number, returns single object. If app is array, returns a mapping of app IDs to objects
*/
async getGameDetails(app: Number, options?: { language: Language, currency: Currency, filters: string[] }): Promise<GameDetails>
async getGameDetails<T extends number>(app: T[], options?: { language: Language, currency: Currency, filters: string[] }): Promise<{ [key: string]: GameDetails }>
async getGameDetails<T extends number>(
app: T | T[],
{ language = this.language, currency = this.currency, filters = [] as string[] } = {}
) {
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: { [key: string]: any } = {};
for (const [k, v] of Object.entries(json))
if ((v as any).success) {
const d = (v as any).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: { [key: string]: GameDetails } = {};
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(): Promise<AppBase[]> {
// 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: string): Promise<Server[]> {
const { response } = await this.get('/ISteamApps/GetServersAtAddress/v1', { addr: host });
if (!response.success)
throw new Error(response.message);
return response.servers.map((server: any) => 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: number): Promise<number> {
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: number, language = this.language): Promise<GameSchema> {
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: string, app: number, language = this.language): Promise<UserAchievements> {
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: number): Promise<AchievementPercentage[]> {
assertApp(app);
const json = await this.get('/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2', { gameid: app });
return json.achievementpercentages.achievements as AchievementPercentage[];
}
/**
* 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: string, app: number): Promise<UserStats> {
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: number, options: GetGameNewsOptions = {}): Promise<NewsPost[]> {
assertApp(app);
const params: querystring.ParsedUrlQueryInput = {
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: any) => new NewsPost(item));
}
/**
* Get a user's badges
* @param id User ID
* @returns User level info and badges
*/
async getUserBadges(id: string): Promise<UserBadges> {
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: string): Promise<number> {
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: string, opts: GetUserOwnedGamesOptions = {}): Promise<UserPlaytime<Game | GameInfo | GameInfoExtended>[]> {
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: querystring.ParsedUrlQueryInput = {
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: any) => {
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: string, count = 0): Promise<UserPlaytime<GameInfoBasic>[]> {
assertID(id);
const json = await this.get('/IPlayerService/GetRecentlyPlayedGames/v1', { steamid: id, count });
return (json.response.games || []).map((data: any) => new UserPlaytime(data, new GameInfoBasic(data)));
}
/**
* Get a user's or multiple users' bans. If an array of IDs is passed in, this returns an array of UserBans
* @param id User ID(s)
* @returns Ban info
*/
async getUserBans(id: string): Promise<UserBans>
async getUserBans(id: string[]): Promise<UserBans[]>
async getUserBans(id: string | string[]): Promise<UserBans | UserBans[]> {
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: any) => 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: string): Promise<UserFriend[]> {
assertID(id);
const json = await this.get('/ISteamUser/GetFriendList/v1', { steamid: id });
return json.friendslist.friends.map((friend: any) => new UserFriend(friend));
}
/**
* Get the groups the user is a member of
* @param id User ID
* @returns Group IDs
*/
async getUserGroups(id: string): Promise<string[]> {
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: any) => group.gid);
}
/**
* Gets servers on steamcommunity.com/dev/managegameservers using your key
* @returns Your server
*/
async getUserServers(): Promise<UserServers> {
return new UserServers((await this.get('/IGameServersService/GetAccountList/v1')).response);
}
/**
* Get users summary. If an array of IDs is passed in, this returns an array of UserSummary
* @param id User ID(s)
* @returns Summary
*/
async getUserSummary(id: string): Promise<UserSummary>
async getUserSummary(id: string[]): Promise<UserSummary[]>
async getUserSummary(id: string | string[]): Promise<UserSummary | UserSummary[]> {
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: any) => new UserSummary(player));
return arr ? summaries : summaries[0];
}
/**
* Gets the Steam server's time
* @returns Date object from the server
*/
async getServerTime(): Promise<Date> {
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(): Promise<Country[]> {
return (await this.get('/QueryLocations', {}, this.baseActions)) as Country[];
}
/**
* Gets all the states for a particular country
* @returns Array of state objects with fields countrycode, statecode, and statename
*/
async getStates(countryCode: string): Promise<State[]> {
return (await this.get(`/QueryLocations/${countryCode}`, {}, this.baseActions)) as State[];
}
/**
* Gets all the cities for a particular state
* @returns Array of city objects with fields countrycode, statecode, cityname and cityid
*/
async getCities(countryCode: string, stateCode: string): Promise<City[]> {
return (await this.get(`/QueryLocations/${countryCode}/${stateCode}`, {}, this.baseActions)) as City[];
}
/**
* 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: string = '', limit: number = 100): Promise<GameServer[]> {
const json = await this.get('/IGameServersService/GetServerList/v1', { filter, limit });
return json.response.servers.map((server: any) => new GameServer(server));
}
/**
* * Leaving this here for future me
* Kinda useless/very similar to something already implemented, but worth considering:
*
* ResolveVanityURL for url_type=2 (group) and url_type=3 (official game group)
* GetFriendList relationship parameter? not sure if it does anything
* https://partner.steamgames.com/doc/webapi/ISteamApps#UpToDateCheck
* https://partner.steamgames.com/doc/webapi/ISteamWebAPIUtil#GetSupportedAPIList (wow)
* (undocumented) ISteamApps/GetSDRConfig?key={}&appid={}
* (undocumented) IStoreService/GetAppList
* https://partner.steamgames.com/doc/webapi/ISteamUserStats#GetGlobalStatsForGame
*/
}