'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const WebSocket = require('ws'); const URL = require('url'); const events = require('events'); const Debug = require('debug'); const zlib = require('zlib'); const axios = require('axios'); const util = require('util'); const fs = require('fs'); const YAML = require('yamljs'); const path = require('path'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } const WebSocket__default = /*#__PURE__*/_interopDefaultLegacy(WebSocket); const URL__default = /*#__PURE__*/_interopDefaultLegacy(URL); const Debug__default = /*#__PURE__*/_interopDefaultLegacy(Debug); const zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib); const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios); const util__default = /*#__PURE__*/_interopDefaultLegacy(util); const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); const YAML__default = /*#__PURE__*/_interopDefaultLegacy(YAML); const path__default = /*#__PURE__*/_interopDefaultLegacy(path); const debug = Debug__default['default']('screepsapi:socket'); const DEFAULTS$1 = { reconnect: true, resubscribe: true, keepAlive: true, maxRetries: 10, maxRetryDelay: 60 * 1000 // in milli-seconds }; class Socket extends events.EventEmitter { constructor (ScreepsAPI) { super(); this.api = ScreepsAPI; this.opts = Object.assign({}, DEFAULTS$1); this.on('error', () => {}); // catch to prevent unhandled-exception errors this.reset(); this.on('auth', ev => { if (ev.data.status === 'ok') { while (this.__queue.length) { this.emit(this.__queue.shift()); } clearInterval(this.keepAliveInter); if (this.opts.keepAlive) { this.keepAliveInter = setInterval(() => this.ws && this.ws.ping(1), 10000); } } }); } reset () { this.authed = false; this.connected = false; this.reconnecting = false; clearInterval(this.keepAliveInter); this.keepAliveInter = 0; this.__queue = []; // pending messages (to send once authenticated) this.__subQueue = []; // pending subscriptions (to request once authenticated) this.__subs = {}; // number of callbacks for each subscription } async connect (opts = {}) { Object.assign(this.opts, opts); if (!this.api.token) { throw new Error('No token! Call api.auth() before connecting the socket!') } return new Promise((resolve, reject) => { const baseURL = this.api.opts.url.replace('http', 'ws'); const wsurl = new URL.URL('socket/websocket', baseURL); this.ws = new WebSocket__default['default'](wsurl); this.ws.on('open', () => { this.connected = true; this.reconnecting = false; if (this.opts.resubscribe) { this.__subQueue.push(...Object.keys(this.__subs)); } debug('connected'); this.emit('connected'); resolve(this.auth(this.api.token)); }); this.ws.on('close', () => { clearInterval(this.keepAliveInter); this.authed = false; this.connected = false; debug('disconnected'); this.emit('disconnected'); if (this.opts.reconnect) { this.reconnect().catch(() => { /* error emitted in reconnect() */ }); } }); this.ws.on('error', (err) => { this.ws.terminate(); this.emit('error', err); debug(`error ${err}`); if (!this.connected) { reject(err); } }); this.ws.on('unexpected-response', (req, res) => { const err = new Error(`WS Unexpected Response: ${res.statusCode} ${res.statusMessage}`); this.emit('error', err); reject(err); }); this.ws.on('message', (data) => this.handleMessage(data)); }) } async reconnect () { if (this.reconnecting) { return } this.reconnecting = true; let retries = 0; let retry; do { let time = Math.pow(2, retries) * 100; if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay; await this.sleep(time); if (!this.reconnecting) return // reset() called in-between try { await this.connect(); retry = false; } catch (err) { retry = true; } retries++; debug(`reconnect ${retries}/${this.opts.maxRetries}`); } while (retry && retries < this.opts.maxRetries) if (retry) { const err = new Error(`Reconnection failed after ${this.opts.maxRetries} retries`); this.reconnecting = false; debug('reconnect failed'); this.emit('error', err); throw err } else { // Resume existing subscriptions on the new socket Object.keys(this.__subs).forEach(sub => this.subscribe(sub)); } } disconnect () { debug('disconnect'); clearInterval(this.keepAliveInter); this.ws.removeAllListeners(); // remove listeners first or we may trigger reconnection & Co. this.ws.terminate(); this.reset(); this.emit('disconnected'); } sleep (time) { return new Promise((resolve, reject) => { setTimeout(resolve, time); }) } handleMessage (msg) { msg = msg.data || msg; // Handle ws/browser difference if (msg.slice(0, 3) === 'gz:') { msg = this.api.inflate(msg); } debug(`message ${msg}`); if (msg[0] === '[') { msg = JSON.parse(msg); let [, type, id, channel] = msg[0].match(/^(.+):(.+?)(?:\/(.+))?$/); channel = channel || type; const event = { channel, id, type, data: msg[1] }; this.emit(msg[0], event); this.emit(event.channel, event); this.emit('message', event); } else { const [channel, ...data] = msg.split(' '); const event = { type: 'server', channel, data }; if (channel === 'auth') { event.data = { status: data[0], token: data[1] }; } if (['protocol', 'time', 'package'].includes(channel)) { event.data = { [channel]: data[0] }; } this.emit(channel, event); this.emit('message', event); } } async gzip (bool) { this.send(`gzip ${bool ? 'on' : 'off'}`); } async send (data) { if (!this.connected) { this.__queue.push(data); } else { this.ws.send(data); } } auth (token) { return new Promise((resolve, reject) => { this.send(`auth ${token}`); this.once('auth', (ev) => { const { data } = ev; if (data.status === 'ok') { this.authed = true; this.emit('token', data.token); this.emit('authed'); while (this.__subQueue.length) { this.send(this.__subQueue.shift()); } resolve(); } else { reject(new Error('socket auth failed')); } }); }) } async subscribe (path, cb) { if (!path) return const userID = await this.api.userID(); if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; } if (this.authed) { this.send(`subscribe ${path}`); } else { this.__subQueue.push(`subscribe ${path}`); } this.emit('subscribe', path); this.__subs[path] = this.__subs[path] || 0; this.__subs[path]++; if (cb) this.on(path, cb); } async unsubscribe (path) { if (!path) return const userID = await this.api.userID(); if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; } this.send(`unsubscribe ${path}`); this.emit('unsubscribe', path); if (this.__subs[path]) this.__subs[path]--; } } const debugHttp = Debug__default['default']('screepsapi:http'); const debugRateLimit = Debug__default['default']('screepsapi:ratelimit'); const { format } = URL__default['default']; const gunzipAsync = util__default['default'].promisify(zlib__default['default'].gunzip); const inflateAsync = util__default['default'].promisify(zlib__default['default'].inflate); const DEFAULT_SHARD = 'shard0'; const OFFICIAL_HISTORY_INTERVAL = 100; const PRIVATE_HISTORY_INTERVAL = 20; const sleep = ms => new Promise(resolve => setInterval(resolve, ms)); class RawAPI extends events.EventEmitter { constructor (opts = {}) { super(); this.setServer(opts); const self = this; this.raw = { /** * GET /api/version * @returns {{ * ok:1, package:number, protocol: number, * serverData: { * customObjectTypes, * historyChunkSize:number, * features, * shards: string[] * }, * users:number * }} */ version () { return self.req('GET', '/api/version') }, /** * GET /api/authmod * @returns {Object} */ authmod () { if (self.isOfficialServer()) { return Promise.resolve({ name: 'official' }) } return self.req('GET', '/api/authmod') }, /** * Official: * GET /room-history/${shard}/${room}/${tick}.json * Private: * GET /room-history * @param {string} room * @param {number} tick * @param {string} shard * @returns {Object} A json file with history data */ history (room, tick, shard = DEFAULT_SHARD) { if (self.isOfficialServer()) { tick -= tick % OFFICIAL_HISTORY_INTERVAL; return self.req('GET', `/room-history/${shard}/${room}/${tick}.json`) } else { tick -= tick % PRIVATE_HISTORY_INTERVAL; return self.req('GET', '/room-history', { room, time: tick }) } }, servers: { /** * POST /api/servers/list * A list of community servers * @returns {{ * ok:number, * servers:{ * _id:string, * settings:{ * host:string, * port:string, * pass:string * }, * name:string, * status:"active"|string * likeCount:number * }[] * }} */ list () { return self.req('POST', '/api/servers/list', {}) } }, auth: { /** * POST /api/auth/signin * @param {string} email * @param {string} password * @returns {{ok:number, token:string}} */ signin (email, password) { return self.req('POST', '/api/auth/signin', { email, password }) }, /** * POST /api/auth/steam-ticket * @param {*} ticket * @param {*} useNativeAuth * @returns {Object} */ steamTicket (ticket, useNativeAuth = false) { return self.req('POST', '/api/auth/steam-ticket', { ticket, useNativeAuth }) }, /** * GET /api/auth/me * @returns {{ * ok: number; * _id: string; * email: string; * username: string; * cpu: number; * badge: Badge; * password: string; * notifyPrefs: { sendOnline: any; errorsInterval: any; disabledOnMessages: any; disabled: any; interval: any }; * gcl: number; * credits: number; * lastChargeTime: any; * lastTweetTime: any; * github: { id: any; username: any }; * twitter: { username: string; followers_count: number }; *}} */ me () { return self.req('GET', '/api/auth/me') }, /** * GET /api/auth/query-token * @param {string} token * @returns {Object} */ queryToken (token) { return self.req('GET', '/api/auth/query-token', { token }) } }, register: { /** * GET /api/register/check-email * @param {string} email * @returns {Object} */ checkEmail (email) { return self.req('GET', '/api/register/check-email', { email }) }, /** * GET /api/register/check-username * @param {string} username * @returns {Object} */ checkUsername (username) { return self.req('GET', '/api/register/check-username', { username }) }, /** * POST /api/register/set-username * @param {string} username * @returns {Object} */ setUsername (username) { return self.req('POST', '/api/register/set-username', { username }) }, /** * POST /api/register/submit * @param {string} username * @param {string} email * @param {string} password * @param {*} modules * @returns {Object} */ submit (username, email, password, modules) { return self.req('POST', '/api/register/submit', { username, email, password, modules }) } }, userMessages: { /** * GET /api/user/messages/list?respondent={userId} * @param {string} respondent the long `_id` of the user, not the username * @returns {{ ok, messages: [ { _id, date, type, text, unread } ] }} */ list (respondent) { return self.req('GET', '/api/user/messages/list', { respondent }) }, /** * GET /api/user/messages/index * @returns {{ ok, messages: [ { _id, message: { _id, user, respondent, date, type, text, unread } } ], users: { : { _id, username, badge: Badge } } }} */ index () { return self.req('GET', '/api/user/messages/index') }, /** * GET /api/user/messages/unread-count * @returns {{ ok, count:number }} */ unreadCount () { return self.req('GET', '/api/user/messages/unread-count') }, /** * POST /api/user/messages/send * @param {string} respondent the long `_id` of the user, not the username * @param {string} text * @returns {{ ok }} */ send (respondent, text) { return self.req('POST', '/api/user/messages/send', { respondent, text }) }, /** * POST /api/user/messages/mark-read * @param {string} id * @returns {Object} */ markRead (id) { return self.req('POST', '/api/user/messages/mark-read', { id }) } }, game: { /** * @typedef {"creepsLost"|"creepsProduced"|"energyConstruction"|"energyControl"|"energyCreeps"|"energyHarvested"} stat * @param {string[]} rooms An array of room names * @param {"owner0"|"claim0"|stat} statName * @param {string} shard * @returns {{ * ok:number, * stats: { * [roomName:string]: { * status, * novice, * own: { user, level }, * : [ { user, value }] * } * } * , users: { [userId:string]: { _id, username, badge: Badge } } }} * The return type is not mapped correctly */ mapStats (rooms, statName, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/map-stats', { rooms, statName, shard }) }, /** * POST /api/game/gen-unique-object-name * @param {"flag"|"spawn"|string} type can be at least "flag" or "spawn" * @param {string} shard * @returns { ok, name:string } */ genUniqueObjectName (type, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/gen-unique-object-name', { type, shard }) }, /** * POST /api/game/check-unique-object-name * @param {string} type * @param {string} name * @param {string} shard * @returns {Object} */ checkUniqueObjectName (type, name, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/check-unique-object-name', { type, name, shard }) }, /** * @param {string} room * @param {number} x * @param {number} y * @param {string} name * @param {string?} shard */ placeSpawn (room, x, y, name, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/place-spawn', { name, room, x, y, shard }) }, /** * POST /api/game/create-flag * @param {string} room * @param {number} x * @param {number} y * @param {string} name * @param {FlagColor} color * @param {FlagColor} secondaryColor * @param {string} shard * @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }} * - if the name is new, result.upserted[0]._id is the game id of the created flag * - if not, this moves the flag and the response does not contain the id (but the id doesn't change) * - `connection` looks like some internal MongoDB thing that is irrelevant to us */ createFlag (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/create-flag', { name, room, x, y, color, secondaryColor, shard }) }, /** * POST/api/game/gen-unique-flag-name * @param {string} shard * @returns {Object} */ genUniqueFlagName (shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/gen-unique-flag-name', { shard }) }, /** * POST /api/game/check-unique-flag-name * @param {string} name * @param {string} shard * @returns {Object} */ checkUniqueFlagName (name, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/check-unique-flag-name', { name, shard }) }, /** * POST /api/game/change-flag-color * @param {FlagColor} color * @param {FlagColor} secondaryColor * @param {string} shard * @returns {{ ok, result: { nModified, ok, n }, connection: { host, id, port } }} */ changeFlagColor (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/change-flag-color', { color, secondaryColor, shard }) }, /** * POST /api/game/remove-flag * @param {string} room * @param {string} name * @param {string} shard * @returns {Object} */ removeFlag (room, name, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/remove-flag', { name, room, shard }) }, /** * POST /api/game/add-object-intent * [Missing parameter] _id is the game id of the object to affect (except for destroying structures), room is the name of the room it's in * this method is used for a variety of actions, depending on the `name` and `intent` parameters * @example remove flag: name = "remove", intent = {} * @example destroy structure: _id = "room", name = "destroyStructure", intent = [ {id: , roomName, , user: } ] can destroy multiple structures at once * @example suicide creep: name = "suicide", intent = {id: } * @example unclaim controller: name = "unclaim", intent = {id: } intent can be an empty object for suicide and unclaim, but the web interface sends the id in it, as described * @example remove construction site: name = "remove", intent = {} * @param {string} room * @param {string} name * @param {string} intent * @param {string} shard * @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }} */ addObjectIntent (room, name, intent, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/add-object-intent', { room, name, intent, shard }) }, /** * POST /api/game/create-construction * @param {string} room * @param {number} x * @param {number} y * @param {string} structureType the same value as one of the in-game STRUCTURE_* constants ('road', 'spawn', etc.) * @param {string} name * @param {string} shard * @returns {{ ok, result: { ok, n }, ops: [ { type, room, x, y, structureType, user, progress, progressTotal, _id } ], insertedCount, insertedIds }} */ createConstruction (room, x, y, structureType, name, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/create-construction', { room, x, y, structureType, name, shard }) }, /** * POST /api/game/set-notify-when-attacked * @param {string} _id * @param {bool} enabled is either true or false (literal values, not strings) * @param {string} shard * @returns {{ ok, result: { ok, nModified, n }, connection: { id, host, port } }} */ setNotifyWhenAttacked (_id, enabled = true, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/set-notify-when-attacked', { _id, enabled, shard }) }, /** * POST /api/game/create-invader * @param {string} room * @param {number} x * @param {number} y * @param {*} size * @param {*} type * @param {boolean} boosted * @param {string} shard * @returns {Object} */ createInvader (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/create-invader', { room, x, y, size, type, boosted, shard }) }, /** * POST /api/game/remove-invader * @param {string} _id * @param {string} shard * @returns {Object} */ removeInvader (_id, shard = DEFAULT_SHARD) { return self.req('POST', '/api/game/remove-invader', { _id, shard }) }, /** * GET /api/game/time * @param {string} shard * @returns {{ ok:number, time:number }} */ time (shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/time', { shard }) }, /** * GET /api/game/world-size * @param {string} shard * @returns {Object} */ worldSize (shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/world-size', { shard }) }, /** * GET /api/game/room-decorations * @param {string} room * @param {string} shard * @returns {Object} */ roomDecorations (room, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/room-decorations', { room, shard }) }, /** * GET /api/game/room-objects * @param {string} room * @param {string} shard * @returns {Object} */ roomObjects (room, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/room-objects', { room, shard }) }, /** * @param {string} room * @param {*} encoded can be anything non-empty * @param {string} shard * @returns {{ ok, terrain: [ { room:string, x:number, y:number, type:"wall"|"swamp" } ] } * | { ok, terrain: [ { _id,room:string, terrain:string, type:"wall"|"swamp" } ] }} * terrain is a string of digits, giving the terrain left-to-right and top-to-bottom * 0: plain, 1: wall, 2: swamp, 3: also wall */ roomTerrain (room, encoded = 1, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/room-terrain', { room, encoded, shard }) }, /** * @param {string} room * @param {string} shard * @returns {{ _id, status:"normal"|"out of borders"|string, novice:string }} * `status` can at least be "normal" or "out of borders" * if the room is in a novice area, novice will contain the Unix timestamp of the end of the protection (otherwise it is absent) */ roomStatus (room, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/room-status', { room, shard }) }, /** * GET /api/game/room-overview * @param {string} room * @param {number} interval * @param {string} shard * @returns {Object} */ roomOverview (room, interval = 8, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/room-overview', { room, interval, shard }) }, market: { /** * GET /api/game/market/orders-index * @param {string} shard * @returns {{ok:1,list:[{_id:string,count:number}]}} * - _id is the resource type, and there will only be one of each type. * - `count` is the number of orders. */ ordersIndex (shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/market/orders-index', { shard }) }, /** * GET /api/game/market/my-orders * @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }} * `resourceType` is one of the RESOURCE_* constants. */ myOrders () { return self.req('GET', '/api/game/market/my-orders').then(self.mapToShard) }, /** * GET /api/game/market/orders * @param {string} resourceType one of the RESOURCE_* constants. * @param {string} shard * @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }} * `resourceType` is one of the RESOURCE_* constants. */ orders (resourceType, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/market/orders', { resourceType, shard }) }, /** * GET /api/game/market/stats * @param {*} resourceType * @param {string} shard * @returns {Object} */ stats (resourceType, shard = DEFAULT_SHARD) { return self.req('GET', '/api/game/market/stats', { resourceType, shard }) } }, shards: { /** * GET /api/game/shards/info * @returns {{ok:number, shards:[{name:string,lastTicks:number[],cpuLimimt:number,rooms:number,users:number,tick:number}]}} */ info () { return self.req('GET', '/api/game/shards/info') } } }, leaderboard: { /** * GET /api/leaderboard/list * @param {number} limit * @param {"world"|"power"} mode * @param {number?} offset * @param {string?} season * @returns {{ ok, list: [ { _id, season, user, score, rank } ], count, users: { : { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } } }} */ list (limit = 10, mode = 'world', offset = 0, season) { if (mode !== 'world' && mode !== 'power') throw new Error('incorrect mode parameter') if (!season) season = self.currentSeason(); return self.req('GET', '/api/leaderboard/list', { limit, mode, offset, season }) }, /** * GET /api/leaderboard/find * @param {string} username * @param {"world"|string} mode * @param {string?} season An optional date in the format YYYY-MM, if not supplied all ranks in all seasons is returned. * @returns {{ ok, _id, season, user, score, rank }} * - `user` (not `_id`) is the user's _id, as returned by `me` and `user/find` * - `rank` is 0-based */ find (username, mode = 'world', season = '') { return self.req('GET', '/api/leaderboard/find', { season, mode, username }) }, /** * GET /api/leaderboard/seasons * @returns {{ ok, seasons: [ { _id, name, date } ] }} * The _id returned here is used for the season name in the other leaderboard calls */ seasons () { return self.req('GET', '/api/leaderboard/seasons') } }, user: { /** * @param {Badge} badge * @returns {{ ok?:number,error?:string}} */ badge (badge) { return self.req('POST', '/api/user/badge', { badge }) }, /** * POST /api/user/respawn * @returns {Object} */ respawn () { return self.req('POST', '/api/user/respawn') }, /** * POST /api/user/set-active-branch * @param {string} branch * @param {string} activeName * @returns {Object} */ setActiveBranch (branch, activeName) { return self.req('POST', '/api/user/set-active-branch', { branch, activeName }) }, /** * POST /api/user/clone-branch * @param {string} branch * @param {string} newName * @param {*} defaultModules * @returns {Object} */ cloneBranch (branch, newName, defaultModules) { return self.req('POST', '/api/user/clone-branch', { branch, newName, defaultModules }) }, /** * POST /api/user/delete-branch * @param {string} branch * @returns {Object} */ deleteBranch (branch) { return self.req('POST', '/api/user/delete-branch', { branch }) }, /** * POST /api/user/notify-prefs * @param {*} prefs * @returns {Object} */ notifyPrefs (prefs) { // disabled,disabledOnMessages,sendOnline,interval,errorsInterval return self.req('POST', '/api/user/notify-prefs', prefs) }, /** * POST /api/user/tutorial-done * @returns {Object} */ tutorialDone () { return self.req('POST', '/api/user/tutorial-done') }, /** * POST /api/user/email * @param {string} email * @returns {Object} */ email (email) { return self.req('POST', '/api/user/email', { email }) }, /** * GET /api/user/world-start-room * @param {string} shard * @returns {Object} */ worldStartRoom (shard) { return self.req('GET', '/api/user/world-start-room', { shard }) }, /** * returns a world status * - 'normal' * - 'lost' when you loose all your spawns * - 'empty' when you have respawned and not placed your spawn yet * @returns {{ ok: number; status: "normal" | "lost" | "empty" }} */ worldStatus () { return self.req('GET', '/api/user/world-status') }, /** * GET /api/user/branches * @returns {{ ok:number, list: [{ * _id: string; * branch: string; * activeWorld: boolean; * activeSim: boolean; * }]} * } */ branches () { return self.req('GET', '/api/user/branches') }, code: { /** * GET /api/user/code * for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612 * @param {string} branch * @returns code */ get (branch) { return self.req('GET', '/api/user/code', { branch }) }, /** * POST /api/user/code * for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612 * @param {string} branch * @param {*} modules * @param {*} _hash * @returns {Object} */ set (branch, modules, _hash) { if (!_hash) _hash = Date.now(); return self.req('POST', '/api/user/code', { branch, modules, _hash }) } }, decorations: { /** * GET /api/user/decorations/inventory * @returns {Object} */ inventory () { return self.req('GET', '/api/user/decorations/inventory') }, /** * GET /api/user/decorations/themes * @returns {Object} */ themes () { return self.req('GET', '/api/user/decorations/themes') }, /** * POST /api/user/decorations/convert * @param {*} decorations decorations is a string array of ids * @returns {Object} */ convert (decorations) { return self.req('POST', '/api/user/decorations/convert', { decorations }) }, /** * POST /api/user/decorations/pixelize * @param {number} count * @param {string} theme * @returns {Object} */ pixelize (count, theme = '') { return self.req('POST', '/api/user/decorations/pixelize', { count, theme }) }, /** * POST /api/user/decorations/activate * @param {string} _id * @param {*} active * @returns {Object} */ activate (_id, active) { return self.req('POST', '/api/user/decorations/activate', { _id, active }) }, /** * POST /api/user/decorations/deactivate * @param {*} decorations decorations is a string array of ids * @returns {Object} */ deactivate (decorations) { return self.req('POST', '/api/user/decorations/deactivate', { decorations }) } }, /** * GET /api/user/respawn-prohibited-rooms * @returns {{ ok, rooms: [ ] }} * - `room` is an array, but seems to always contain only one element */ respawnProhibitedRooms () { return self.req('GET', '/api/user/respawn-prohibited-rooms') }, memory: { /** * GET /api/user/memory?path={path} * @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1 * @param {string} shard * @returns {string} gz: followed by base64-encoded gzipped JSON encoding of the requested memory path */ get (path, shard = DEFAULT_SHARD) { return self.req('GET', '/api/user/memory', { path, shard }) }, /** * POST /api/user/memory * @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1 * @param {*} value * @param {string} shard * @returns {{ ok, result: { ok, n }, ops: [ { user, expression, hidden } ], data, insertedCount, insertedIds }} */ set (path, value, shard = DEFAULT_SHARD) { return self.req('POST', '/api/user/memory', { path, value, shard }) }, segment: { /** * GET /api/user/memory-segment?segment=[0-99] * @param {number} segment A number from 0-99 * @param {string} shard * @returns {{ ok, data: string }} */ get (segment, shard = DEFAULT_SHARD) { return self.req('GET', '/api/user/memory-segment', { segment, shard }) }, /** * POST /api/user/memory-segment * @param {number} segment A number from 0-99 * @param {*} data * @param {string} shard * @returns {Object} */ set (segment, data, shard = DEFAULT_SHARD) { return self.req('POST', '/api/user/memory-segment', { segment, data, shard }) } } }, /** * GET /api/user/find?username={username} * @param {string} username * @returns {{ ok, user: { _id, username, badge: Badge, gcl } }} */ find (username) { return self.req('GET', '/api/user/find', { username }) }, /** * GET /api/user/find?id={userId} * @param {string} id * @returns {{ ok, user: { _id, username, badge: Badge, gcl } }} */ findById (id) { return self.req('GET', '/api/user/find', { id }) }, /** * GET /api/user/stats * @param {number} interval * @returns {Object} */ stats (interval) { return self.req('GET', '/api/user/stats', { interval }) }, /** * GET /api/user/rooms * @param {string} id * @returns {Object} */ rooms (id) { return self.req('GET', '/api/user/rooms', { id }).then(self.mapToShard) }, /** * GET /api/user/overview?interval={interval}&statName={statName} * @param {number} interval * @param {string} statName energyControl * @returns {{{ ok, rooms: [ ], stats: { : [ { value, endTime } ] }, statsMax }}} */ overview (interval, statName) { return self.req('GET', '/api/user/overview', { interval, statName }) }, /** * GET /api/user/money-history * @param {number} page Used for pagination * @returns {{"ok":1,"page":0,"list":[ { _id, date, tick, user, type, balance, change, market: {} } ] }} * - page used for pagination. * - hasMore is true if there are more pages to view. * - market * - New Order- { order: { type, resourceType, price, totalAmount, roomName } } * - Extended Order- { extendOrder: { orderId, addAmount } } * - Fulfilled Order- { resourceType, roomName, targetRoomName, price, npc, amount } * - Price Change - { changeOrderPrice: { orderId, oldPrice, newPrice } } */ moneyHistory (page = 0) { return self.req('GET', '/api/user/money-history', { page }) }, /** * POST /api/user/console * @param {*} expression * @param {string} shard * @returns {{ ok, result: { ok, n }, ops: [ { user, expression, _id } ], insertedCount, insertedIds: [ ] }} */ console (expression, shard = DEFAULT_SHARD) { return self.req('POST', '/api/user/console', { expression, shard }) }, /** * GET /api/user/name * @returns {Object} */ name () { return self.req('GET', '/api/user/name') } }, experimental: { // https://screeps.com/api/experimental/pvp?start=14787157 seems to not be implemented in the api /** * @param {number} interval * @returns {{ ok, time, rooms: [ { _id, lastPvpTime } ] }} * time is the current server tick * _id contains the room name for each room, and lastPvpTime contains the last tick pvp occurred * if neither a valid interval nor a valid start argument is provided, the result of the call is still ok, but with an empty rooms array. */ pvp (interval = 100) { return self.req('GET', '/api/experimental/pvp', { interval }).then(self.mapToShard) }, /** * GET /api/experimental/nukes * @returns {Object} */ nukes () { return self.req('GET', '/api/experimental/nukes').then(self.mapToShard) } }, warpath: { /** * GET /api/warpath/battles * @param {number} interval * @returns {Object} */ battles (interval = 100) { return self.req('GET', '/api/warpath/battles', { interval }) } }, scoreboard: { /** * GET /api/scoreboard/list * @param {number} limit * @param {number} offset * @returns {Object} */ list (limit = 20, offset = 0) { return self.req('GET', '/api/scoreboard/list', { limit, offset }) } } }; } currentSeason () { const now = new Date(); const year = now.getFullYear(); let month = (now.getUTCMonth() + 1).toString(); if (month.length === 1) month = `0${month}`; return `${year}-${month}` } isOfficialServer () { return this.opts.url.match(/screeps\.com/) !== null } mapToShard (res) { if (!res.shards) { res.shards = { privSrv: res.list || res.rooms }; } return res } setServer (opts) { if (!this.opts) { this.opts = {}; } Object.assign(this.opts, opts); if (opts.path && !opts.pathname) { this.opts.pathname = this.opts.path; } if (!opts.url) { this.opts.url = format(this.opts); if (!this.opts.url.endsWith('/')) this.opts.url += '/'; } if (opts.token) { this.token = opts.token; } this.http = axios__default['default'].create({ baseURL: this.opts.url }); } async auth (email, password, opts = {}) { this.setServer(opts); if (email && password) { Object.assign(this.opts, { email, password }); } const res = await this.raw.auth.signin(this.opts.email, this.opts.password); this.emit('token', res.token); this.emit('auth'); this.__authed = true; return res } async req (method, path, body = {}) { const opts = { method, url: path, headers: {} }; debugHttp(`${method} ${path} ${JSON.stringify(body)}`); if (this.token) { Object.assign(opts.headers, { 'X-Token': this.token, 'X-Username': this.token }); } if (method === 'GET') { opts.params = body; } else { opts.data = body; } try { const res = await this.http(opts); const token = res.headers['x-token']; if (token) { this.emit('token', token); } const rateLimit = this.buildRateLimit(method, path, res); this.emit('rateLimit', rateLimit); debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`); if (typeof res.data.data === 'string' && res.data.data.slice(0, 3) === 'gz:') { res.data.data = await this.gz(res.data.data); } this.emit('response', res); return res.data } catch (err) { const res = err.response || {}; const rateLimit = this.buildRateLimit(method, path, res); this.emit('rateLimit', rateLimit); debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`); if (res.status === 401) { if (this.__authed && this.opts.email && this.opts.password) { this.__authed = false; await this.auth(this.opts.email, this.opts.password); return this.req(method, path, body) } else { throw new Error('Not Authorized') } } if (res.status === 429 && !res.headers['x-ratelimit-limit'] && this.opts.experimentalRetry429) { await sleep(Math.floor(Math.random() * 500) + 200); return this.req(method, path, body) } if (err.response) { throw new Error(res.data) } throw new Error(err.message) } } async gz (data) { const buf = Buffer.from(data.slice(3), 'base64'); const ret = await gunzipAsync(buf); return JSON.parse(ret.toString()) } async inflate (data) { // es const buf = Buffer.from(data.slice(3), 'base64'); const ret = await inflateAsync(buf); return JSON.parse(ret.toString()) } buildRateLimit (method, path, res) { const { headers: { 'x-ratelimit-limit': limit, 'x-ratelimit-remaining': remaining, 'x-ratelimit-reset': reset } = {} } = res; return { method, path, limit: +limit, remaining: +remaining, reset: +reset, toReset: reset - Math.floor(Date.now() / 1000) } } } /** * @typedef {{ * "color1": string; * "color2": string; * "color3": string; * "flip": boolean; * "param": number; * "type": number|{ path1:string, path2:string}; *}} Badge */ /** * @typedef {1|2|3|4|5|6|7|8|9|10} FlagColor * - Red = 1, * - Purple = 2, * - Blue = 3, * - Cyan = 4, * - Green = 5, * - Yellow = 6, * - Orange = 7, * - Brown = 8, * - Grey = 9, * - White = 10 */ const readFileAsync = util__default['default'].promisify(fs__default['default'].readFile); class ConfigManager { async refresh () { this._config = null; await this.getConfig(); } async getServers () { const conf = await this.getConfig(); return Object.keys(conf.servers) } async getConfig () { if (this._config) { return this._config } const paths = []; if (process.env.SCREEPS_CONFIG) { paths.push(process.env.SCREEPS_CONFIG); } const dirs = [__dirname, '']; for (const dir of dirs) { paths.push(path__default['default'].join(dir, '.screeps.yaml')); paths.push(path__default['default'].join(dir, '.screeps.yml')); } if (process.platform === 'win32') { paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yaml')); paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yml')); } else { if (process.env.XDG_CONFIG_PATH) { paths.push( path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yaml') ); paths.push( path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yml') ); } if (process.env.HOME) { paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yaml')); paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yml')); paths.push(path__default['default'].join(process.env.HOME, '.screeps.yaml')); paths.push(path__default['default'].join(process.env.HOME, '.screeps.yml')); } } for (const path of paths) { const data = await this.loadConfig(path); if (data) { if (!data.servers) { throw new Error( `Invalid config: 'servers' object does not exist in '${path}'` ) } this._config = data; this.path = path; return data } } return null } async loadConfig (file) { try { const contents = await readFileAsync(file, 'utf8'); return YAML__default['default'].parse(contents) } catch (e) { if (e.code === 'ENOENT') { return false } else { throw e } } } } const DEFAULTS = { protocol: 'https', hostname: 'screeps.com', port: 443, path: '/' }; const configManager = new ConfigManager(); class ScreepsAPI extends RawAPI { static async fromConfig (server = 'main', config = false, opts = {}) { const data = await configManager.getConfig(); if (data) { if (!data.servers[server]) { throw new Error(`Server '${server}' does not exist in '${configManager.path}'`) } const conf = data.servers[server]; if (conf.ptr) conf.path = '/ptr'; if (conf.season) conf.path = '/season'; const api = new ScreepsAPI( Object.assign( { hostname: conf.host, port: conf.port, protocol: conf.secure ? 'https' : 'http', token: conf.token, path: conf.path || '/' }, opts ) ); api.appConfig = (data.configs && data.configs[config]) || {}; if (!conf.token && conf.username && conf.password) { await api.auth(conf.username, conf.password); } return api } throw new Error('No valid config found') } constructor (opts) { opts = Object.assign({}, DEFAULTS, opts); super(opts); this.on('token', token => { this.token = token; this.raw.token = token; }); const defaultLimit = (limit, period) => ({ limit, period, remaining: limit, reset: 0, toReset: 0 }); this.rateLimits = { global: defaultLimit(120, 'minute'), GET: { '/api/game/room-terrain': defaultLimit(360, 'hour'), '/api/user/code': defaultLimit(60, 'hour'), '/api/user/memory': defaultLimit(1440, 'day'), '/api/user/memory-segment': defaultLimit(360, 'hour'), '/api/game/market/orders-index': defaultLimit(60, 'hour'), '/api/game/market/orders': defaultLimit(60, 'hour'), '/api/game/market/my-orders': defaultLimit(60, 'hour'), '/api/game/market/stats': defaultLimit(60, 'hour'), '/api/game/user/money-history': defaultLimit(60, 'hour') }, POST: { '/api/user/console': defaultLimit(360, 'hour'), '/api/game/map-stats': defaultLimit(60, 'hour'), '/api/user/code': defaultLimit(240, 'day'), '/api/user/set-active-branch': defaultLimit(240, 'day'), '/api/user/memory': defaultLimit(240, 'day'), '/api/user/memory-segment': defaultLimit(60, 'hour') } }; this.on('rateLimit', limits => { const rate = this.rateLimits[limits.method][limits.path] || this.rateLimits.global; const copy = Object.assign({}, limits); delete copy.path; delete copy.method; Object.assign(rate, copy); }); this.socket = new Socket(this); } getRateLimit (method, path) { return this.rateLimits[method][path] || this.rateLimits.global } get rateLimitResetUrl () { return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice( 0, 8 )}` } async me () { if (this._user) return this._user const tokenInfo = await this.tokenInfo(); if (tokenInfo.full) { this._user = await this.raw.auth.me(); } else { const { username } = await this.raw.user.name(); const { user } = await this.raw.user.find(username); this._user = user; } return this._user } async tokenInfo () { if (this._tokenInfo) { return this._tokenInfo } if (this.opts.token) { const { token } = await this.raw.auth.queryToken(this.token); this._tokenInfo = token; } else { this._tokenInfo = { full: true }; } return this._tokenInfo } async userID () { const user = await this.me(); return user._id } get history () { return this.raw.history } get authmod () { return this.raw.authmod } get version () { return this.raw.version } get time () { return this.raw.game.time } get leaderboard () { return this.raw.leaderboard } get market () { return this.raw.game.market } get registerUser () { return this.raw.register.submit } get code () { return this.raw.user.code } get memory () { return this.raw.user.memory } get segment () { return this.raw.user.memory.segment } get console () { return this.raw.user.console } } exports.ScreepsAPI = ScreepsAPI;