From c82bb58a9e78288a55f3f01ccda8544bcff2aee6 Mon Sep 17 00:00:00 2001 From: Jesse Ditson Date: Fri, 16 Mar 2018 17:30:29 -0700 Subject: [PATCH 1/2] Refactor quip API: add blob from URL/Path method, move to request lib so we can handle blob form data, add connectWebsocket function for using websockets (and ws dependency), make entire API promise based so it can be used with async/await --- .gitignore | 2 + nodejs/package-lock.json | 394 +++++++++++++++++++++++++++ nodejs/package.json | 26 ++ nodejs/quip.js | 556 ++++++++++++++++++++++----------------- 4 files changed, 733 insertions(+), 245 deletions(-) create mode 100644 nodejs/package-lock.json create mode 100644 nodejs/package.json diff --git a/.gitignore b/.gitignore index dccaa0b..b31d06f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *~ *.DS_Store TAGS + +nodejs/node_modules \ No newline at end of file diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json new file mode 100644 index 0000000..1b29226 --- /dev/null +++ b/nodejs/package-lock.json @@ -0,0 +1,394 @@ +{ + "name": "quip-api", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + } + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.0.0.tgz", + "integrity": "sha512-XXG4S0b771C68AeTHebBsJJBZMguxj7Em+D657RViuj6ppRd3tfuOhIK8eGwZGNb76C8MjQfCTfH2NN50rJN4w==", + "requires": { + "async-limiter": "1.0.0" + } + } + } +} diff --git a/nodejs/package.json b/nodejs/package.json new file mode 100644 index 0000000..1ef4450 --- /dev/null +++ b/nodejs/package.json @@ -0,0 +1,26 @@ +{ + "name": "quip-api", + "version": "0.0.1", + "description": "Node.js API Client for the quip automation API", + "main": "quip.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "ISC", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/quip/quip-api.git" + }, + "keywords": [ + "Quip", + "API" + ], + "bugs": { + "url": "https://github.com/quip/quip-api/issues" + }, + "homepage": "https://github.com/quip/quip-api#readme", + "dependencies": { + "request": "^2.85.0", + "ws": "^5.0.0" + } +} diff --git a/nodejs/quip.js b/nodejs/quip.js index 38dfe26..2601c1e 100644 --- a/nodejs/quip.js +++ b/nodejs/quip.js @@ -14,48 +14,58 @@ * under the License. */ -var https = require('https'); -var querystring = require('querystring'); +const https = require('https') +const http = require('http') +const querystring = require('querystring') +const url = require('url') +const WebSocket = require('ws') +const request = require('request') + +const isProd = process.env.NODE_ENV === 'production' +const baseURI = isProd ? 'platform.quip.com' : 'platform.docker.qa' +const baseURL = isProd ? `https://${baseURI}` : `http://${baseURI}` +const basePort = process.env.NODE_ENV === 'production' ? 443 : 10000 +const httpLib = isProd ? https : http /** * Folder colors * @enum {number} */ -var Color = { - MANILA: 0, - RED: 1, - ORANGE: 2, - GREEN: 3, - BLUE: 4 -}; +const Color = { + MANILA: 0, + RED: 1, + ORANGE: 2, + GREEN: 3, + BLUE: 4, +} /** * Edit operations * @enum {number} */ -var Operation = { - APPEND: 0, - PREPEND: 1, - AFTER_SECTION: 2, - BEFORE_SECTION: 3, - REPLACE_SECTION: 4, - DELETE_SECTION: 5 -}; +const Operation = { + APPEND: 0, + PREPEND: 1, + AFTER_SECTION: 2, + BEFORE_SECTION: 3, + REPLACE_SECTION: 4, + DELETE_SECTION: 5, +} /** * A Quip API client. * * To make API calls that access Quip data, initialize with an accessToken. * - * var quip = require('quip'); - * var client = quip.Client({accessToken: '...'}); - * client.getAuthenticatedUser(function(); + * const quip = require('quip'); + * const client = quip.Client({accessToken: '...'}); + * const user = await client.getAuthenticatedUser(); * * To generate authorization URLs, i.e., to implement OAuth login, initialize * with a clientId and and clientSecret. * - * var quip = require('quip'); - * var client = quip.Client({clientId: '...', clientSecret: '...'}); + * const quip = require('quip'); + * const client = quip.Client({clientId: '...', clientSecret: '...'}); * response.writeHead(302, { * 'Location': client.getAuthorizationUrl() * }); @@ -66,9 +76,9 @@ var Operation = { * @constructor */ function Client(options) { - this.accessToken = options.accessToken; - this.clientId = options.clientId; - this.clientSecret = options.clientSecret; + this.accessToken = options.accessToken + this.clientId = options.clientId + this.clientSecret = options.clientSecret } /** @@ -78,13 +88,16 @@ function Client(options) { * @param {string=} state */ Client.prototype.getAuthorizationUrl = function(redirectUri, state) { - return 'https://platform.quip.com/1/oauth/login?' + querystring.stringify({ - 'redirect_uri': redirectUri, - 'state': state, - 'response_type': 'code', - 'client_id': this.clientId - }); -}; + return ( + `${baseURL}:${basePort}/1/oauth/login?` + + querystring.stringify({ + redirect_uri: redirectUri, + state: state, + response_type: 'code', + client_id: this.clientId, + }) + ) +} /** * Exchanges a verification code for an access_token. @@ -95,209 +108,237 @@ Client.prototype.getAuthorizationUrl = function(redirectUri, state) { * * @param {string} redirectUri * @param {string} code - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getAccessToken = function(redirectUri, code, callback) { - this.call_('oauth/access_token?' + querystring.stringify({ - 'redirect_uri': redirectUri, - 'code': code, - 'grant_type': 'authorization_code', - 'client_id': this.clientId, - 'client_secret': this.clientSecret - }), callback); -}; +Client.prototype.getAccessToken = function(redirectUri, code) { + return this.call_( + 'oauth/access_token?' + + querystring.stringify({ + redirect_uri: redirectUri, + code: code, + grant_type: 'authorization_code', + client_id: this.clientId, + client_secret: this.clientSecret, + }), + ) +} /** - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getAuthenticatedUser = function(callback) { - this.call_('users/current', callback); -}; +Client.prototype.getAuthenticatedUser = function() { + return this.call_('users/current') +} /** * @param {string} id - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getUser = function(id, callback) { - this.getUsers([id], function(err, users) { - callback(err, err ? null : users[id]); - }); -}; +Client.prototype.getUser = async function(id) { + const users = await this.getUsers([id]) + return users[id] +} /** * @param {Array.} ids - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getUsers = function(ids, callback) { - this.call_('users/?' + querystring.stringify({ - 'ids': ids.join(',') - }), callback); -}; +Client.prototype.getUsers = function(ids) { + return this.call_( + 'users/?' + + querystring.stringify({ + ids: ids.join(','), + }), + ) +} /** - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getContacts = function(callback) { - this.call_('users/contacts', callback); -}; +Client.prototype.getContacts = function() { + return this.call_('users/contacts') +} /** * @param {string} id - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getFolder = function(id, callback) { - this.getFolders([id], function(err, folders) { - callback(err, err ? null : folders[id]); - }); -}; +Client.prototype.getFolder = async function(id) { + const folders = await this.getFolders([id]) + return folders[id] +} /** * @param {Array.} ids - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getFolders = function(ids, callback) { - this.call_('folders/?' + querystring.stringify({ - 'ids': ids.join(',') - }), callback); -}; +Client.prototype.getFolders = function(ids) { + return this.call_( + 'folders/?' + + querystring.stringify({ + ids: ids.join(','), + }), + ) +} /** * @param {{title: string, * parentId: (string|undefined), * color: (Color|undefined), * memberIds: (Array.|undefined)}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.newFolder = function(options, callback) { - var args = { - 'title': options.title, - 'parent_id': options.parentId, - 'color': options.color - }; - if (options.memberIds) { - args['member_ids'] = options.memberIds.join(','); - } - this.call_('folders/new', callback, args); -}; +Client.prototype.newFolder = function(options) { + const args = { + title: options.title, + parent_id: options.parentId, + color: options.color, + } + if (options.memberIds) { + args['member_ids'] = options.memberIds.join(',') + } + return this.call_('folders/new', args) +} /** * @param {{folderId: string, * title: (string|undefined), * color: (Color|undefined)}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.updateFolder = function(options, callback) { - var args = { - 'folder_id': options.folderId, - 'title': options.title, - 'color': options.color - }; - this.call_('folders/update', callback, args); -}; +Client.prototype.updateFolder = function(options) { + const args = { + folder_id: options.folderId, + title: options.title, + color: options.color, + } + return this.call_('folders/update', args) +} /** * @param {{folderId: string, * memberIds: Array.}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.addFolderMembers = function(options, callback) { - var args = { - 'folder_id': options.folderId, - 'member_ids': options.memberIds.join(',') - }; - this.call_('folders/add-members', callback, args); -}; +Client.prototype.addFolderMembers = function(options) { + const args = { + folder_id: options.folderId, + member_ids: options.memberIds.join(','), + } + return this.call_('folders/add-members', args) +} /** * @param {{folderId: string, * memberIds: Array.}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.removeFolderMembers = function(options, callback) { - var args = { - 'folder_id': options.folderId, - 'member_ids': options.memberIds.join(',') - }; - this.call_('folders/remove-members', callback, args); -}; +Client.prototype.removeFolderMembers = function(options) { + const args = { + folder_id: options.folderId, + member_ids: options.memberIds.join(','), + } + return this.call_('folders/remove-members', args) +} /** * @param {{threadId: string, * maxUpdatedUsec: (number|undefined), * count: (number|undefined)}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getMessages = function(options, callback) { - this.call_('messages/' + options.threadId + '?' + querystring.stringify({ - 'max_updated_usec': options.maxUpdatedUsec, - 'count': options.count - }), callback); -}; +Client.prototype.getMessages = function(options) { + return this.call_( + 'messages/' + + options.threadId + + '?' + + querystring.stringify({ + max_updated_usec: options.maxUpdatedUsec, + count: options.count, + }), + ) +} /** * @param {{threadId: string, * content: string, - * silent: (number|undefined)}} options - * @param {function(Error, Object)} callback + * silent: (boolean|undefined)}} options + * @return {Promise} */ -Client.prototype.newMessage = function(options, callback) { - var args = { - 'thread_id': options.threadId, - 'content': options.content, - 'silent': (options.silent ? 1 : undefined) - }; - this.call_('messages/new', callback, args); -}; +Client.prototype.newMessage = function(options) { + const args = { + thread_id: options.threadId, + frame: options.frame, + content: options.content, + parts: options.parts, + attachments: options.attachments, + silent: options.silent ? 1 : undefined, + annotation_id: options.annotationId, + section_id: options.sectionId, + suggested_responses: options.suggestedResponses, + } + return this.call_('messages/new', args) +} /** * @param {string} id - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getThread = function(id, callback) { - this.getThreads([id], function(err, threads) { - callback(err, err ? null : threads[id]); - }); -}; +Client.prototype.getThread = async function(id) { + const threads = await this.getThreads([id]) + return threads[id] +} /** * @param {Array.} ids - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getThreads = function(ids, callback) { - this.call_('threads/?' + querystring.stringify({ - 'ids': ids.join(',') - }), callback); -}; +Client.prototype.getThreads = function(ids) { + return this.call_( + 'threads/?' + + querystring.stringify({ + ids: ids.join(','), + }), + ) +} /** * @param {{maxUpdatedUsec: (number|undefined), * count: (number|undefined)}?} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.getRecentThreads = function(options, callback) { - this.call_('threads/recent?' + querystring.stringify(options ? { - 'max_updated_usec': options.maxUpdatedUsec, - 'count': options.count - } : {}), callback); -}; +Client.prototype.getRecentThreads = function(options) { + return this.call_( + 'threads/recent?' + + querystring.stringify( + options + ? { + max_updated_usec: options.maxUpdatedUsec, + count: options.count, + } + : {}, + ), + ) +} /** * @param {{content: string, * title: (string|undefined), * format: (string|undefined), * memberIds: (Array.|undefined)}} options + * @return {Promise} */ -Client.prototype.newDocument = function(options, callback) { - var args = { - 'content': options.content, - 'title': options.title, - 'format': options.format - }; - if (options.memberIds) { - args['member_ids'] = options.memberIds.join(','); - } - this.call_('threads/new-document', callback, args); -}; +Client.prototype.newDocument = function(options) { + const args = { + content: options.content, + title: options.title, + format: options.format, + } + if (options.memberIds) { + args['member_ids'] = options.memberIds.join(',') + } + return this.call_('threads/new-document', args) +} /** * @param {{threadId: string, @@ -305,104 +346,129 @@ Client.prototype.newDocument = function(options, callback) { * operation: (Operation|undefined), * format: (string|undefined), * sectionId: (string|undefined)}} options + * @return {Promise} */ -Client.prototype.editDocument = function(options, callback) { - var args = { - 'thread_id': options.threadId, - 'content': options.content, - 'location': options.operation, - 'format': options.format, - 'section_id': options.sectionId - }; - this.call_('threads/edit-document', callback, args); -}; +Client.prototype.editDocument = function(options) { + const args = { + thread_id: options.threadId, + content: options.content, + location: options.operation, + format: options.format, + section_id: options.sectionId, + } + return this.call_('threads/edit-document', args) +} /** * @param {{threadId: string, * memberIds: Array.}} options - * @param {function(Error, Object)} callback + * @return {Promise} */ -Client.prototype.addThreadMembers = function(options, callback) { - var args = { - 'thread_id': options.threadId, - 'member_ids': options.memberIds.join(',') - }; - this.call_('threads/add-members', callback, args); -}; +Client.prototype.addThreadMembers = function(options) { + const args = { + thread_id: options.threadId, + member_ids: options.memberIds.join(','), + } + return this.call_('threads/add-members', args) +} /** * @param {{threadId: string, * memberIds: Array.}} options - * @param {function(Error, Object)} callback + * @return {Promise} + */ +Client.prototype.removeThreadMembers = function(options) { + const args = { + thread_id: options.threadId, + member_ids: options.memberIds.join(','), + } + return this.call_('threads/remove-members', args) +} + +/** + * @param {{url: string, + * threadId: string}} options + * @return {Promise} + */ +Client.prototype.addBlobFromURL = async function(options) { + return this.call_(`blob/${options.threadId}`, { + blob: request(options.url), + }) +} + +/** + * @param {{path: string, + * threadId: string}} options + * @return {Promise} */ -Client.prototype.removeThreadMembers = function(options, callback) { - var args = { - 'thread_id': options.threadId, - 'member_ids': options.memberIds.join(',') - }; - this.call_('threads/remove-members', callback, args); -}; +Client.prototype.addBlobFromPath = async function(options) { + return this.call_(`blob/${options.threadId}`, { + blob: fs.createReadStream(options.path), + }) +} + +/** + * @return {Promise} + */ +Client.prototype.connectWebsocket = function() { + return this.call_('websockets/new').then(newSocket => { + if (!newSocket || !newSocket.url) { + throw new Error(newSocket ? newSocket.error : 'Request failed') + } + const urlInfo = url.parse(newSocket.url) + const ws = new WebSocket(newSocket.url, { + origin: `${urlInfo.protocol}//${urlInfo.hostname}`, + }) + return ws + }) +} /** * @param {string} path - * @param {function(Error, Object)} callback * @param {Object.=} postArguments + * @return {Promise} */ -Client.prototype.call_ = function(path, callback, postArguments) { - var requestOptions = { - hostname: 'platform.quip.com', - port: 443, - path: '/1/' + path, - headers: {} - }; - if (this.accessToken) { - requestOptions.headers['Authorization'] = 'Bearer ' + this.accessToken; +Client.prototype.call_ = function(path, postArguments) { + const requestOptions = { + uri: `${baseURL}:${basePort}/1/${path}`, + headers: {}, + } + if (this.accessToken) { + requestOptions.headers['Authorization'] = 'Bearer ' + this.accessToken + } + if (postArguments) { + const formData = {} + for (let name in postArguments) { + if (postArguments[name]) { + formData[name] = postArguments[name] + } } - var requestBody = null; - if (postArguments) { - for (var name in postArguments) { - if (!postArguments[name]) { - delete postArguments[name]; - } - } - requestOptions.method = 'POST'; - requestBody = querystring.stringify(postArguments); - requestOptions.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - requestOptions.headers['Content-Length'] = - Buffer.byteLength(requestBody); - } else { - requestOptions.method = 'GET'; + requestOptions.method = 'POST' + requestOptions.formData = formData + } else { + requestOptions.method = 'GET' + } + return new Promise((resolve, reject) => { + const callback = (err, res, body) => { + if (err) { + return reject(err) + } + let responseObject = null + try { + responseObject = /** @type {Object} */ (JSON.parse(body)) + } catch (err) { + reject(`Invalid response for ${path}: ${body}`) + return + } + if (res.statusCode !== 200) { + reject(new ClientError(res, responseObject)) + } else { + resolve(responseObject) + } } - var request = https.request(requestOptions, function(response) { - var data = []; - response.on('data', function(chunk) { - data.push(chunk); - }); - response.on('end', function() { - var responseObject = null; - try { - responseObject = /** @type {Object} */( - JSON.parse(data.join(''))); - } catch (err) { - callback(err, null); - return; - } - if (response.statusCode != 200) { - callback(new ClientError(response, responseObject), null); - } else { - callback(null, responseObject); - } - }); - }); - request.on('error', function(error) { - callback(error, null); - }); - if (requestBody) { - request.write(requestBody); - } - request.end(); -}; + request(requestOptions, callback) + }) +} /** * @param {http.IncomingMessage} httpResponse @@ -411,12 +477,12 @@ Client.prototype.call_ = function(path, callback, postArguments) { * @constructor */ function ClientError(httpResponse, info) { - this.httpResponse = httpResponse; - this.info = info; + this.httpResponse = httpResponse + this.info = info } -ClientError.prototype = Object.create(Error.prototype); +ClientError.prototype = Object.create(Error.prototype) -exports.Color = Color; -exports.Operation = Operation; -exports.Client = Client; -exports.ClientError = ClientError; +exports.Color = Color +exports.Operation = Operation +exports.Client = Client +exports.ClientError = ClientError From 6174c8bc2e38561de138e8ccb8e1186909884728 Mon Sep 17 00:00:00 2001 From: Jesse Ditson Date: Fri, 16 Mar 2018 17:37:41 -0700 Subject: [PATCH 2/2] remove suggested options, update type annotation for new message endpoint --- nodejs/quip.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nodejs/quip.js b/nodejs/quip.js index 2601c1e..40d62ab 100644 --- a/nodejs/quip.js +++ b/nodejs/quip.js @@ -261,8 +261,13 @@ Client.prototype.getMessages = function(options) { /** * @param {{threadId: string, + * frame: string, * content: string, - * silent: (boolean|undefined)}} options + * parts: Array> + * attachments: Array + * silent: (boolean|undefined) + * annotationId: string, + * sectionId: string}} options * @return {Promise} */ Client.prototype.newMessage = function(options) { @@ -275,7 +280,6 @@ Client.prototype.newMessage = function(options) { silent: options.silent ? 1 : undefined, annotation_id: options.annotationId, section_id: options.sectionId, - suggested_responses: options.suggestedResponses, } return this.call_('messages/new', args) }