UNPKG

bot18

Version:

A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f

540 lines 25.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = require("path"); const objectEntries = require("object.entries"); // tslint:disable-line:no-require-imports const urlJoin = require("url-join"); // tslint:disable-line:no-require-imports const isStream = require("is-stream"); // tslint:disable-line:no-require-imports const EventEmitter = require("eventemitter3"); // tslint:disable-line:import-name no-require-imports const PQueue = require("p-queue"); // tslint:disable-line:import-name no-require-imports const pRetry = require("p-retry"); // tslint:disable-line:no-require-imports const delay = require("delay"); // tslint:disable-line:no-require-imports // NOTE: to reduce depedency size, consider https://www.npmjs.com/package/got-lite const got = require("got"); // tslint:disable-line:no-require-imports const FormData = require("form-data"); // tslint:disable-line:no-require-imports import-name const util_1 = require("./util"); const errors_1 = require("./errors"); const logger_1 = require("./logger"); const retry_policies_1 = require("./retry-policies"); const pkg = require('../package.json'); // tslint:disable-line:no-require-imports no-var-requires /** * A client for Slack's Web API * * This client provides an alias for each {@link https://api.slack.com/methods|Web API method}. Each method is * a convenience wrapper for calling the {@link WebClient#apiCall} method using the method name as the first parameter. */ class WebClient extends EventEmitter { /** * @param token - An API token to authenticate/authorize with Slack (usually start with `xoxp`, `xoxb`, or `xoxa`) */ constructor(token, { slackApiUrl = 'https://slack.com/api/', logger = undefined, logLevel = logger_1.LogLevel.INFO, maxRequestConcurrency = 3, retryConfig = retry_policies_1.default.retryForeverExponentialCappedRandom, agent = undefined, tls = undefined, } = {}) { super(); /** * api method family */ this.api = { test: (this.apiCall.bind(this, 'api.test')), }; /** * apps method family */ this.apps = { permissions: { info: (this.apiCall.bind(this, 'apps.permissions.info')), request: (this.apiCall.bind(this, 'apps.permissions.request')), }, }; /** * auth method family */ this.auth = { revoke: (this.apiCall.bind(this, 'auth.revoke')), test: (this.apiCall.bind(this, 'auth.test')), }; /** * bots method family */ this.bots = { info: (this.apiCall.bind(this, 'bots.info')), }; /** * channels method family */ this.channels = { archive: (this.apiCall.bind(this, 'channels.archive')), create: (this.apiCall.bind(this, 'channels.create')), history: (this.apiCall.bind(this, 'channels.history')), info: (this.apiCall.bind(this, 'channels.info')), invite: (this.apiCall.bind(this, 'channels.invite')), join: (this.apiCall.bind(this, 'channels.join')), kick: (this.apiCall.bind(this, 'channels.kick')), leave: (this.apiCall.bind(this, 'channels.leave')), list: (this.apiCall.bind(this, 'channels.list')), mark: (this.apiCall.bind(this, 'channels.mark')), rename: (this.apiCall.bind(this, 'channels.rename')), replies: (this.apiCall.bind(this, 'channels.replies')), setPurpose: (this.apiCall.bind(this, 'channels.setPurpose')), setTopic: (this.apiCall.bind(this, 'channels.setTopic')), unarchive: (this.apiCall.bind(this, 'channels.unarchive')), }; /** * chat method family */ this.chat = { delete: (this.apiCall.bind(this, 'chat.delete')), getPermalink: (this.apiCall.bind(this, 'chat.getPermalink')), meMessage: (this.apiCall.bind(this, 'chat.meMessage')), postEphemeral: (this.apiCall.bind(this, 'chat.postEphemeral')), postMessage: (this.apiCall.bind(this, 'chat.postMessage')), unfurl: (this.apiCall.bind(this, 'chat.unfurl')), update: (this.apiCall.bind(this, 'chat.update')), }; /** * conversations method family */ this.conversations = { archive: (this.apiCall.bind(this, 'conversations.archive')), close: (this.apiCall.bind(this, 'conversations.close')), create: (this.apiCall.bind(this, 'conversations.create')), history: (this.apiCall.bind(this, 'conversations.history')), info: (this.apiCall.bind(this, 'conversations.info')), invite: (this.apiCall.bind(this, 'conversations.invite')), join: (this.apiCall.bind(this, 'conversations.join')), kick: (this.apiCall.bind(this, 'conversations.kick')), leave: (this.apiCall.bind(this, 'conversations.leave')), list: (this.apiCall.bind(this, 'conversations.list')), members: (this.apiCall.bind(this, 'conversations.members')), open: (this.apiCall.bind(this, 'conversations.open')), rename: (this.apiCall.bind(this, 'conversations.rename')), replies: (this.apiCall.bind(this, 'conversations.replies')), setPurpose: (this.apiCall.bind(this, 'conversations.setPurpose')), setTopic: (this.apiCall.bind(this, 'conversations.setTopic')), unarchive: (this.apiCall.bind(this, 'conversations.unarchive')), }; /** * dialog method family */ this.dialog = { open: (this.apiCall.bind(this, 'dialog.open')), }; /** * dnd method family */ this.dnd = { endDnd: (this.apiCall.bind(this, 'dnd.endDnd')), endSnooze: (this.apiCall.bind(this, 'dnd.endSnooze')), info: (this.apiCall.bind(this, 'dnd.info')), setSnooze: (this.apiCall.bind(this, 'dnd.setSnooze')), teamInfo: (this.apiCall.bind(this, 'dnd.teamInfo')), }; /** * emoji method family */ this.emoji = { list: (this.apiCall.bind(this, 'emoji.list')), }; /** * files method family */ this.files = { delete: (this.apiCall.bind(this, 'files.delete')), info: (this.apiCall.bind(this, 'files.info')), list: (this.apiCall.bind(this, 'files.list')), revokePublicURL: (this.apiCall.bind(this, 'files.revokePublicURL')), sharedPublicURL: (this.apiCall.bind(this, 'files.sharedPublicURL')), upload: (this.apiCall.bind(this, 'files.upload')), comments: { add: (this.apiCall.bind(this, 'files.comments.add')), delete: (this.apiCall.bind(this, 'files.comments.delete')), edit: (this.apiCall.bind(this, 'files.comments.edit')), }, }; /** * groups method family */ this.groups = { archive: (this.apiCall.bind(this, 'groups.archive')), create: (this.apiCall.bind(this, 'groups.create')), createChild: (this.apiCall.bind(this, 'groups.createChild')), history: (this.apiCall.bind(this, 'groups.history')), info: (this.apiCall.bind(this, 'groups.info')), invite: (this.apiCall.bind(this, 'groups.invite')), kick: (this.apiCall.bind(this, 'groups.kick')), leave: (this.apiCall.bind(this, 'groups.leave')), list: (this.apiCall.bind(this, 'groups.list')), mark: (this.apiCall.bind(this, 'groups.mark')), open: (this.apiCall.bind(this, 'groups.open')), rename: (this.apiCall.bind(this, 'groups.rename')), replies: (this.apiCall.bind(this, 'groups.replies')), setPurpose: (this.apiCall.bind(this, 'groups.setPurpose')), setTopic: (this.apiCall.bind(this, 'groups.setTopic')), unarchive: (this.apiCall.bind(this, 'groups.unarchive')), }; /** * im method family */ this.im = { close: (this.apiCall.bind(this, 'im.close')), history: (this.apiCall.bind(this, 'im.history')), list: (this.apiCall.bind(this, 'im.list')), mark: (this.apiCall.bind(this, 'im.mark')), open: (this.apiCall.bind(this, 'im.open')), replies: (this.apiCall.bind(this, 'im.replies')), }; /** * migration method family */ this.migration = { exchange: (this.apiCall.bind(this, 'migration.exchange')), }; /** * mpim method family */ this.mpim = { close: (this.apiCall.bind(this, 'mpim.close')), history: (this.apiCall.bind(this, 'mpim.history')), list: (this.apiCall.bind(this, 'mpim.list')), mark: (this.apiCall.bind(this, 'mpim.mark')), open: (this.apiCall.bind(this, 'mpim.open')), replies: (this.apiCall.bind(this, 'mpim.replies')), }; /** * oauth method family */ this.oauth = { access: (this.apiCall.bind(this, 'oauth.access')), token: (this.apiCall.bind(this, 'oauth.token')), }; /** * pins method family */ this.pins = { add: (this.apiCall.bind(this, 'pins.add')), list: (this.apiCall.bind(this, 'pins.list')), remove: (this.apiCall.bind(this, 'pins.remove')), }; /** * reactions method family */ this.reactions = { add: (this.apiCall.bind(this, 'reactions.add')), get: (this.apiCall.bind(this, 'reactions.get')), list: (this.apiCall.bind(this, 'reactions.list')), remove: (this.apiCall.bind(this, 'reactions.remove')), }; /** * reminders method family */ this.reminders = { add: (this.apiCall.bind(this, 'reminders.add')), complete: (this.apiCall.bind(this, 'reminders.complete')), delete: (this.apiCall.bind(this, 'reminders.delete')), info: (this.apiCall.bind(this, 'reminders.info')), list: (this.apiCall.bind(this, 'reminders.list')), }; /** * rtm method family */ this.rtm = { connect: (this.apiCall.bind(this, 'rtm.connect')), start: (this.apiCall.bind(this, 'rtm.start')), }; /** * search method family */ this.search = { all: (this.apiCall.bind(this, 'search.all')), files: (this.apiCall.bind(this, 'search.files')), messages: (this.apiCall.bind(this, 'search.messages')), }; /** * stars method family */ this.stars = { add: (this.apiCall.bind(this, 'stars.add')), list: (this.apiCall.bind(this, 'stars.list')), remove: (this.apiCall.bind(this, 'stars.remove')), }; /** * team method family */ this.team = { accessLogs: (this.apiCall.bind(this, 'team.accessLogs')), billableInfo: (this.apiCall.bind(this, 'team.billableInfo')), info: (this.apiCall.bind(this, 'team.info')), integrationLogs: (this.apiCall.bind(this, 'team.integrationLogs')), profile: { get: (this.apiCall.bind(this, 'team.profile.get')), }, }; /** * usergroups method family */ this.usergroups = { create: (this.apiCall.bind(this, 'usergroups.create')), disable: (this.apiCall.bind(this, 'usergroups.disable')), enable: (this.apiCall.bind(this, 'usergroups.enable')), list: (this.apiCall.bind(this, 'usergroups.list')), update: (this.apiCall.bind(this, 'usergroups.update')), users: { list: (this.apiCall.bind(this, 'usergroups.users.list')), update: (this.apiCall.bind(this, 'usergroups.users.update')), }, }; /** * users method family */ this.users = { conversations: (this.apiCall.bind(this, 'users.conversations')), deletePhoto: (this.apiCall.bind(this, 'users.deletePhoto')), getPresence: (this.apiCall.bind(this, 'users.getPresence')), identity: (this.apiCall.bind(this, 'users.identity')), info: (this.apiCall.bind(this, 'users.info')), list: (this.apiCall.bind(this, 'users.list')), lookupByEmail: (this.apiCall.bind(this, 'users.lookupByEmail')), setActive: (this.apiCall.bind(this, 'users.setActive')), setPhoto: (this.apiCall.bind(this, 'users.setPhoto')), setPresence: (this.apiCall.bind(this, 'users.setPresence')), profile: { get: (this.apiCall.bind(this, 'users.profile.get')), set: (this.apiCall.bind(this, 'users.profile.set')), }, }; this.token = token; this.slackApiUrl = slackApiUrl; this.retryConfig = retryConfig; this.requestQueue = new PQueue({ concurrency: maxRequestConcurrency }); this.agentConfig = agent; // NOTE: may want to filter the keys to only those acceptable for TLS options this.tlsConfig = tls !== undefined ? tls : {}; // Logging if (logger !== undefined) { this.logger = logger_1.loggerFromLoggingFunc(WebClient.loggerName, logger); } else { this.logger = logger_1.getLogger(WebClient.loggerName); } this.logger.setLevel(logLevel); this.userAgent = util_1.getUserAgent(); this.logger.debug('initialized'); } apiCall(method, options, callback) { this.logger.debug('apiCall() start'); // The following thunk is the actual implementation for this method. It is wrapped so that it can be adapted for // different executions below. const implementation = () => __awaiter(this, void 0, void 0, function* () { if (typeof options === 'string' || typeof options === 'number' || typeof options === 'boolean') { throw new TypeError(`Expected an options argument but instead received a ${typeof options}`); } const requestBody = this.serializeApiCallOptions(Object.assign({ token: this.token }, options)); // The following thunk encapsulates the task so that it can be coordinated for retries const task = () => { this.logger.debug('request attempt'); return got.post(urlJoin(this.slackApiUrl, method), // @ts-ignore using older definitions for package `got`, can remove when type `@types/got` is updated for v8 Object.assign({ form: !canBodyBeFormMultipart(requestBody), body: requestBody, retries: 0, headers: { 'user-agent': this.userAgent, }, agent: this.agentConfig, }, this.tlsConfig)) .catch((error) => { // Wrap errors in this packages own error types (abstract the implementation details' types) if (error.name === 'RequestError') { throw requestErrorWithOriginal(error); } else if (error.name === 'ReadError') { throw readErrorWithOriginal(error); } else if (error.name === 'HTTPError') { throw httpErrorWithOriginal(error); } else { throw error; } }) .then((response) => { const result = this.buildResult(response); // log warnings in response metadata if (result.response_metadata !== undefined && result.response_metadata.warnings !== undefined) { result.response_metadata.warnings.forEach(this.logger.warn); } // handle rate-limiting if (response.statusCode !== undefined && response.statusCode === 429) { const retryAfterMs = result.retryAfter !== undefined ? result.retryAfter : (60 * 1000); // NOTE: the following event could have more information regarding the api call that is being delayed this.emit('rate_limited', retryAfterMs / 1000); this.logger.info(`API Call failed due to rate limiting. Will retry in ${retryAfterMs / 1000} seconds.`); // wait and return the result from calling `task` again after the specified number of seconds return delay(retryAfterMs).then(task); } // For any error in the API response, treat them as irrecoverable by throwing an AbortError to end retries. if (!result.ok) { const error = errors_1.errorWithCode(new Error(`An API error occurred: ${result.error}`), errors_1.ErrorCode.PlatformError); error.data = result; throw new pRetry.AbortError(error); } return result; }); }; // The following thunk encapsulates the retried task so that it can be coordinated for request queuing const taskAfterRetries = () => pRetry(task, this.retryConfig); // The final return value is the resolution of the task after being retried and queued return this.requestQueue.add(taskAfterRetries); }); // Adapt the interface for callback-based execution or Promise-based execution if (callback !== undefined) { util_1.callbackify(implementation)(callback); return; } return implementation(); } /** * Transforms options into an object suitable for got to use as a body. This can be one of two things: * - A FormCanBeURLEncoded object, which is just a key-value object where the values have been flattened and * got can serialize it into application/x-www-form-urlencoded content type. * - A BodyCanBeFormMultipart: when the options includes a file, and got must use multipart/form-data content type. * * @param options arguments for the Web API method */ serializeApiCallOptions(options) { // The following operation both flattens complex objects into a JSON-encoded strings and searches the values for // binary content let containsBinaryData = false; const flattened = objectEntries(options) .map(([key, value]) => { if (value === undefined || value === null) { return []; } let serializedValue = value; if (Buffer.isBuffer(value) || isStream(value)) { containsBinaryData = true; } else if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { // if value is anything other than string, number, boolean, binary data, a Stream, or a Buffer, then encode it // as a JSON string. serializedValue = JSON.stringify(value); } return [key, serializedValue]; }); // A body with binary content should be serialized as multipart/form-data if (containsBinaryData) { this.logger.debug('request arguments contain binary data'); return flattened.reduce((form, [key, value]) => { if (Buffer.isBuffer(value) || isStream(value)) { const options = {}; options.filename = (() => { // attempt to find filename from `value`. adapted from: // tslint:disable-next-line:max-line-length // https://github.com/form-data/form-data/blob/028c21e0f93c5fefa46a7bbf1ba753e4f627ab7a/lib/form_data.js#L227-L230 // formidable and the browser add a name property // fs- and request- streams have path property const streamOrBuffer = value; if (typeof streamOrBuffer.name === 'string') { return path_1.basename(streamOrBuffer.name); } if (typeof streamOrBuffer.path === 'string') { return path_1.basename(streamOrBuffer.path); } return defaultFilename; })(); form.append(key, value, options); } else { form.append(key, value); } return form; }, new FormData()); } // Otherwise, a simple key-value object is returned return flattened.reduce((accumulator, [key, value]) => { if (key !== undefined && value !== undefined) { accumulator[key] = value; } return accumulator; }, {}); } /** * Processes an HTTP response into a WebAPICallResult by performing JSON parsing on the body and merging relevent * HTTP headers into the object. * @param response */ buildResult(response) { const data = JSON.parse(response.body); // add scopes metadata from headers if (response.headers['x-oauth-scopes'] !== undefined) { data.scopes = response.headers['x-oauth-scopes'].trim().split(/\s*,\s*/); } if (response.headers['x-accepted-oauth-scopes'] !== undefined) { data.acceptedScopes = response.headers['x-accepted-oauth-scopes'].trim().split(/\s*,\s*/); } // add retry metadata from headers if (response.headers['retry-after'] !== undefined) { data.retryAfter = parseInt(response.headers['retry-after'], 10) * 1000; } return data; } } /** * The name used to prefix all logging generated from this object */ WebClient.loggerName = `${pkg.name}:WebClient`; exports.WebClient = WebClient; exports.default = WebClient; /* * Helpers */ const defaultFilename = 'Untitled'; /** * Determines whether a request body object should be treated as FormData-encodable (Content-Type=multipart/form-data). * @param body a request body */ function canBodyBeFormMultipart(body) { // tried using `isStream.readable(body)` but that failes because the object doesn't have a `_read()` method or a // `_readableState` property return isStream(body); } /** * A factory to create WebAPIRequestError objects * @param original */ function requestErrorWithOriginal(original) { const error = errors_1.errorWithCode( // any cast is used because the got definition file doesn't export the got.RequestError type new Error(`A request error occurred: ${original.code}`), errors_1.ErrorCode.RequestError); error.original = original; return error; } /** * A factory to create WebAPIReadError objects * @param original */ function readErrorWithOriginal(original) { const error = errors_1.errorWithCode( // any cast is used because the got definition file doesn't export the got.ReadError type new Error('A response read error occurred'), errors_1.ErrorCode.ReadError); error.original = original; return error; } /** * A factory to create WebAPIHTTPError objects * @param original */ function httpErrorWithOriginal(original) { const error = errors_1.errorWithCode( // any cast is used because the got definition file doesn't export the got.HTTPError type new Error(`An HTTP protocol error occurred: statusCode = ${original.statusCode}`), errors_1.ErrorCode.HTTPError); error.original = original; return error; } //# sourceMappingURL=WebClient.js.map