├── .gitignore
├── LICENSE
├── README.md
├── index.js
├── lib
└── mediumClient.js
├── package.json
└── test
└── mediumClient_test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Warning:** This sdk is no longer supported or maintained by Medium.
2 |
3 |
4 | # Medium SDK for NodeJS
5 |
6 | This repository contains the open source SDK for integrating [Medium](https://medium.com)'s OAuth2 API into your NodeJs app.
7 |
8 | View the full [documentation here](https://github.com/Medium/medium-api-docs).
9 |
10 | Install
11 | -------
12 |
13 | npm install medium-sdk
14 |
15 | Usage
16 | -----
17 |
18 | Create a client, then call commands on it.
19 |
20 | ```javascript
21 | var medium = require('medium-sdk')
22 |
23 | var client = new medium.MediumClient({
24 | clientId: 'YOUR_CLIENT_ID',
25 | clientSecret: 'YOUR_CLIENT_SECRET'
26 | })
27 |
28 | var redirectURL = 'https://yoursite.com/callback/medium';
29 |
30 | var url = client.getAuthorizationUrl('secretState', redirectURL, [
31 | medium.Scope.BASIC_PROFILE, medium.Scope.PUBLISH_POST
32 | ])
33 |
34 | // (Send the user to the authorization URL to obtain an authorization code.)
35 |
36 | client.exchangeAuthorizationCode('YOUR_AUTHORIZATION_CODE', redirectURL, function (err, token) {
37 | client.getUser(function (err, user) {
38 | client.createPost({
39 | userId: user.id,
40 | title: 'A new post',
41 | contentFormat: medium.PostContentFormat.HTML,
42 | content: '
A New Post
This is my new post.
',
43 | publishStatus: medium.PostPublishStatus.DRAFT
44 | }, function (err, post) {
45 | console.log(token, user, post)
46 | })
47 | })
48 | })
49 | ```
50 |
51 | Contributing
52 | ------------
53 |
54 | Questions, comments, bug reports, and pull requests are all welcomed. If you haven't contributed to a Medium project before please head over to the [Open Source Project](https://github.com/Medium/opensource#note-to-external-contributors) and fill out an OCLA (it should be pretty painless).
55 |
56 | Authors
57 | -------
58 |
59 | [Jamie Talbot](https://github.com/majelbstoat)
60 |
61 | License
62 | -------
63 |
64 | Copyright 2015 [A Medium Corporation](https://medium.com)
65 |
66 | Licensed under Apache License Version 2.0. Details in the attached LICENSE
67 | file.
68 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./lib/mediumClient.js")
2 |
--------------------------------------------------------------------------------
/lib/mediumClient.js:
--------------------------------------------------------------------------------
1 | // Copright 2015 A Medium Corporation
2 |
3 | var https = require('https')
4 | var qs = require('querystring')
5 | var url = require('url')
6 | var util = require('util')
7 |
8 |
9 | var DEFAULT_ERROR_CODE = -1
10 | var DEFAULT_TIMEOUT_MS = 5000
11 |
12 |
13 | /**
14 | * Valid scope options.
15 | * @enum {string}
16 | */
17 | var Scope = {
18 | BASIC_PROFILE: 'basicProfile',
19 | LIST_PUBLICATIONS: 'listPublications',
20 | PUBLISH_POST: 'publishPost'
21 | }
22 |
23 |
24 | /**
25 | * The publish status when creating a post.
26 | * @enum {string}
27 | */
28 | var PostPublishStatus = {
29 | DRAFT: 'draft',
30 | UNLISTED: 'unlisted',
31 | PUBLIC: 'public'
32 | }
33 |
34 |
35 | /**
36 | * The content format to use when creating a post.
37 | * @enum {string}
38 | */
39 | var PostContentFormat = {
40 | HTML: 'html',
41 | MARKDOWN: 'markdown'
42 | }
43 |
44 |
45 | /**
46 | * The license to use when creating a post.
47 | * @enum {string}
48 | */
49 | var PostLicense = {
50 | ALL_RIGHTS_RESERVED: 'all-rights-reserved',
51 | CC_40_BY: 'cc-40-by',
52 | CC_40_BY_ND: 'cc-40-by-nd',
53 | CC_40_BY_SA: 'cc-40-by-sa',
54 | CC_40_BY_NC: 'cc-40-by-nc',
55 | CC_40_BY_NC_ND: 'cc-40-by-nc-nd',
56 | CC_40_BY_NC_SA: 'cc-40-by-nc-sa',
57 | CC_40_ZERO: 'cc-40-zero',
58 | PUBLIC_DOMAIN: 'public-domain'
59 | }
60 |
61 |
62 | /**
63 | * An error with a code.
64 | *
65 | * @param {string} message
66 | * @param {number} code
67 | * @constructor
68 | */
69 | function MediumError(message, code) {
70 | this.message = message
71 | this.code = code
72 | }
73 | util.inherits(MediumError, Error)
74 |
75 |
76 | /**
77 | * The core client.
78 | *
79 | * @param {{
80 | * clientId: string,
81 | * clientSecret: string
82 | * }} options
83 | * @constructor
84 | */
85 | function MediumClient(options) {
86 | this._enforce(options, ['clientId', 'clientSecret'])
87 | this._clientId = options.clientId
88 | this._clientSecret = options.clientSecret
89 | this._accessToken = ""
90 | }
91 |
92 |
93 | /**
94 | * Sets an access token on the client used for making requests.
95 | *
96 | * @param {string} accessToken
97 | * @return {MediumClient}
98 | */
99 | MediumClient.prototype.setAccessToken = function (accessToken) {
100 | this._accessToken = accessToken
101 | return this
102 | }
103 |
104 |
105 | /**
106 | * Builds a URL at which you may request authorization from the user.
107 | *
108 | * @param {string} state
109 | * @param {string} redirectUrl
110 | * @param {Array.} requestedScope
111 | * @return {string}
112 | */
113 | MediumClient.prototype.getAuthorizationUrl = function (state, redirectUrl, requestedScope) {
114 | return url.format({
115 | protocol: 'https',
116 | host: 'medium.com',
117 | pathname: '/m/oauth/authorize',
118 | query: {
119 | client_id: this._clientId,
120 | scope: requestedScope.join(','),
121 | response_type: 'code',
122 | state: state,
123 | redirect_uri: redirectUrl
124 | }
125 | })
126 | }
127 |
128 |
129 | /**
130 | * Exchanges an authorization code for an access token and a refresh token.
131 | *
132 | * @param {string} code
133 | * @param {string} redirectUrl
134 | * @param {NodeCallback} callback
135 | */
136 | MediumClient.prototype.exchangeAuthorizationCode = function (code, redirectUrl, callback) {
137 | this._acquireAccessToken({
138 | code: code,
139 | client_id: this._clientId,
140 | client_secret: this._clientSecret,
141 | grant_type: 'authorization_code',
142 | redirect_uri: redirectUrl
143 | }, callback)
144 | }
145 |
146 |
147 | /**
148 | * Exchanges a refresh token for an access token and a refresh token.
149 | *
150 | * @param {string} refreshToken
151 | * @param {NodeCallback} callback
152 | */
153 | MediumClient.prototype.exchangeRefreshToken = function (refreshToken, callback) {
154 | this._acquireAccessToken({
155 | refresh_token: refreshToken,
156 | client_id: this._clientId,
157 | client_secret: this._clientSecret,
158 | grant_type: 'refresh_token'
159 | }, callback)
160 | }
161 |
162 |
163 | /**
164 | * Returns the details of the user associated with the current
165 | * access token.
166 | *
167 | * Requires the current access token to have the basicProfile scope.
168 | *
169 | * @param {NodeCallback} callback
170 | */
171 | MediumClient.prototype.getUser = function (callback) {
172 | this._makeRequest({
173 | method: 'GET',
174 | path: '/v1/me'
175 | }, callback)
176 | }
177 |
178 |
179 | /**
180 | * Returns the publications related to the current user. Notice that
181 | * the userId needs to be passed in as an option. It can be acquired
182 | * with a call to getUser().
183 | *
184 | * Requires the current access token to have the
185 | * listPublications scope.
186 | *
187 | * @param {{
188 | * userId: string
189 | * }} options
190 | * @param {NodeCallback} callback
191 | */
192 | MediumClient.prototype.getPublicationsForUser = function (options, callback) {
193 | this._enforce(options, ['userId'])
194 | this._makeRequest({
195 | method: 'GET',
196 | path: '/v1/users/' + options.userId + '/publications'
197 | }, callback)
198 | }
199 |
200 |
201 | /**
202 | * Returns the contributors for a chosen publication. The publication is identified
203 | * by the publication ID included in the options argument. IDs for publications
204 | * can be acquired by getUsersPublications.
205 | *
206 | * Requires the current access token to have the basicProfile scope.
207 | *
208 | * @param {{
209 | * publicationId: string
210 | * }} options
211 | * @param {NodeCallback} callback
212 | */
213 | MediumClient.prototype.getContributorsForPublication = function (options, callback) {
214 | this._enforce(options, ['publicationId'])
215 | this._makeRequest({
216 | method: 'GET',
217 | path: '/v1/publications/' + options.publicationId + '/contributors'
218 | }, callback)
219 | }
220 |
221 |
222 | /**
223 | * Creates a post on Medium.
224 | *
225 | * Requires the current access token to have the publishPost scope.
226 | *
227 | * @param {{
228 | * userId: string,
229 | * title: string,
230 | * contentFormat: PostContentFormat,
231 | * content: string,
232 | * tags: Array.,
233 | * canonicalUrl: string,
234 | * publishStatus: PostPublishStatus,
235 | * license: PostLicense
236 | * }} options
237 | * @param {NodeCallback} callback
238 | */
239 | MediumClient.prototype.createPost = function (options, callback) {
240 | this._enforce(options, ['userId'])
241 | this._makeRequest({
242 | method: 'POST',
243 | path: '/v1/users/' + options.userId + '/posts',
244 | data: {
245 | title: options.title,
246 | content: options.content,
247 | contentFormat: options.contentFormat,
248 | tags: options.tags,
249 | canonicalUrl: options.canonicalUrl,
250 | publishedAt: options.publishedAt,
251 | publishStatus: options.publishStatus,
252 | license: options.license
253 | }
254 | }, callback)
255 | }
256 |
257 |
258 | /**
259 | * Creates a post on Medium and places it under specified publication.
260 | * Please refer to the API documentation for rules around publishing in
261 | * a publication: https://github.com/Medium/medium-api-docs
262 | *
263 | * Requires the current access token to have the publishPost scope.
264 | *
265 | * @param {{
266 | * userId: string,
267 | * publicationId: string,
268 | * title: string,
269 | * contentFormat: PostContentFormat,
270 | * content: string,
271 | * tags: Array.,
272 | * canonicalUrl: string,
273 | * publishStatus: PostPublishStatus,
274 | * license: PostLicense
275 | * }} options
276 | * @param {NodeCallback} callback
277 | */
278 | MediumClient.prototype.createPostInPublication = function (options, callback) {
279 | this._enforce(options, ['publicationId'])
280 | this._makeRequest({
281 | method: 'POST',
282 | path: '/v1/publications/' + options.publicationId + '/posts',
283 | data: {
284 | title: options.title,
285 | content: options.content,
286 | contentFormat: options.contentFormat,
287 | tags: options.tags,
288 | canonicalUrl: options.canonicalUrl,
289 | publishedAt: options.publishedAt,
290 | publishStatus: options.publishStatus,
291 | license: options.license
292 | }
293 | }, callback)
294 | }
295 |
296 |
297 | /**
298 | * Acquires an access token for the Medium API.
299 | *
300 | * Sets the access token on the client on success.
301 | *
302 | * @param {Object} params
303 | * @param {NodeCallback} callback
304 | */
305 | MediumClient.prototype._acquireAccessToken = function (params, callback) {
306 | this._makeRequest({
307 | method: 'POST',
308 | path: '/v1/tokens',
309 | contentType: 'application/x-www-form-urlencoded',
310 | data: qs.stringify(params)
311 | }, function (err, data) {
312 | if (!err) {
313 | this._accessToken = data.access_token
314 | }
315 | callback(err, data)
316 | }.bind(this))
317 | }
318 |
319 |
320 | /**
321 | * Enforces that given options object (first param) defines
322 | * all keys requested (second param). Raises an error if any
323 | * is missing.
324 | *
325 | * @param {Object} options
326 | * @param {keys} requiredKeys
327 | */
328 | MediumClient.prototype._enforce = function (options, requiredKeys) {
329 | if (!options) {
330 | throw new MediumError('Parameters for this call are undefined', DEFAULT_ERROR_CODE)
331 | }
332 | requiredKeys.forEach(function (requiredKey) {
333 | if (!options[requiredKey]) throw new MediumError('Missing required parameter "' + requiredKey + '"', DEFAULT_ERROR_CODE)
334 | })
335 | }
336 |
337 |
338 |
339 | /**
340 | * Makes a request to the Medium API.
341 | *
342 | * @param {Object} options
343 | * @param {NodeCallback} callback
344 | */
345 | MediumClient.prototype._makeRequest = function (options, callback) {
346 | var requestParams = {
347 | host: 'api.medium.com',
348 | port: 443,
349 | method: options.method,
350 | path: options.path
351 | }
352 | var req = https.request(requestParams, function (res) {
353 | var body = []
354 |
355 | res.setEncoding('utf-8')
356 | res.on('data', function (data) {
357 | body.push(data)
358 | })
359 | res.on('end', function () {
360 | var payload
361 | var responseText = body.join('')
362 | try {
363 | payload = JSON.parse(responseText)
364 | } catch (err) {
365 | callback(new MediumError('Failed to parse response', DEFAULT_ERROR_CODE), null)
366 | return
367 | }
368 |
369 | var statusCode = res.statusCode
370 | var statusType = Math.floor(res.statusCode / 100)
371 |
372 | if (statusType == 4 || statusType == 5) {
373 | var err = payload.errors[0]
374 | callback(new MediumError(err.message, err.code), null)
375 | } else if (statusType == 2) {
376 | callback(null, payload.data || payload)
377 | } else {
378 | callback(new MediumError('Unexpected response', DEFAULT_ERROR_CODE), null)
379 | }
380 | })
381 | }).on('error', function (err) {
382 | callback(new MediumError(err.message, DEFAULT_ERROR_CODE), null)
383 | })
384 |
385 | req.setHeader('Content-Type', options.contentType || 'application/json')
386 | req.setHeader('Authorization', 'Bearer ' + this._accessToken)
387 | req.setHeader('Accept', 'application/json')
388 | req.setHeader('Accept-Charset', 'utf-8')
389 |
390 | req.setTimeout(DEFAULT_TIMEOUT_MS, function () {
391 | // Aborting a request triggers the 'error' event.
392 | req.abort()
393 | })
394 |
395 | if (options.data) {
396 | var data = options.data
397 | if (typeof data == 'object') {
398 | data = JSON.stringify(data)
399 | }
400 | req.write(data)
401 | }
402 | req.end()
403 | }
404 |
405 | // Exports
406 |
407 | module.exports = {
408 | MediumClient: MediumClient,
409 | MediumError: MediumError,
410 | Scope: Scope,
411 | PostPublishStatus: PostPublishStatus,
412 | PostLicense: PostLicense,
413 | PostContentFormat: PostContentFormat
414 | }
415 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medium-sdk"
3 | , "description": "NodeJS client for the Medium app"
4 | , "version": "0.0.4"
5 | , "homepage": "https://github.com/medium/medium-sdk-nodejs"
6 | , "author": "Jamie Talbot (https://github.com/majelbstoat)"
7 | , "keywords": ["medium", "api", "writing"]
8 | , "main": "index.js"
9 | , "repository": {
10 | "type": "git"
11 | , "url": "https://github.com/medium/medium-sdk-nodejs.git"
12 | }
13 | , "devDependencies": {
14 | "mocha": "^2.2.5"
15 | , "should": "^7.1"
16 | , "nock": "^2.17"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/test/mediumClient_test.js:
--------------------------------------------------------------------------------
1 | var medium = require("../")
2 | var nock = require("nock")
3 | var qs = require('querystring')
4 | var should = require("should")
5 | var url = require('url')
6 |
7 |
8 | describe('MediumClient - constructor', function () {
9 |
10 | it('should throw a MediumError when options are undefined', function (done) {
11 | (function () { new medium.MediumClient() }).should.throw(medium.MediumError)
12 | done()
13 | })
14 |
15 | it('should throw a MediumError when options are empty', function (done) {
16 | (function () { new medium.MediumClient({}) }).should.throw(medium.MediumError)
17 | done()
18 | })
19 |
20 | it('should throw a MediumError when only clientId is provided', function (done) {
21 | (function () { new medium.MediumClient({clientId: 'xxx'}) }).should.throw(medium.MediumError)
22 | done()
23 | })
24 |
25 | it('should throw a MediumError when only clientSecret is provided', function (done) {
26 | (function () { new medium.MediumClient({clientSecret: 'yyy'}) }).should.throw(medium.MediumError)
27 | done()
28 | })
29 |
30 | it('should succeed when both clientId and clientSecret are provided', function (done) {
31 | var client = new medium.MediumClient({clientId: 'xxx', clientSecret: 'yyy'})
32 | done()
33 | })
34 | })
35 |
36 |
37 | describe('MediumClient - methods', function () {
38 |
39 | var clientId = 'xxx'
40 | var clientSecret = 'yyy'
41 | var client
42 |
43 | beforeEach(function () {
44 | client = new medium.MediumClient({clientId: clientId, clientSecret: clientSecret})
45 | nock.disableNetConnect()
46 | })
47 |
48 | afterEach(function () {
49 | nock.enableNetConnect();
50 | delete client
51 | })
52 |
53 | describe('#setAccessToken', function () {
54 |
55 | it ('sets the access token', function (done) {
56 | var token = "new token"
57 | client.setAccessToken(token)
58 | client._accessToken.should.be.String().and.equal(token)
59 | done()
60 | })
61 | })
62 |
63 | describe('#getAuthorizationUrl', function () {
64 |
65 | it ('returns a valid URL for fetching', function (done) {
66 | var state = "state"
67 | var redirectUrl = "https://example.com/callback"
68 | var scope = [medium.Scope.BASIC_PROFILE, medium.Scope.LIST_PUBLICATIONS, medium.Scope.PUBLISH_POST]
69 | var authUrlStr = client.getAuthorizationUrl(state, redirectUrl, scope)
70 | var authUrl = url.parse(authUrlStr, true)
71 | authUrl.protocol.should.equal('https:')
72 | authUrl.hostname.should.equal('medium.com')
73 | authUrl.pathname.should.equal('/m/oauth/authorize')
74 | authUrl.query.should.deepEqual({
75 | client_id: clientId,
76 | scope: scope.join(','),
77 | response_type: 'code',
78 | state: state,
79 | redirect_uri: redirectUrl
80 | })
81 | done()
82 | })
83 | })
84 |
85 | describe('#exchangeAuthorizationCode', function () {
86 |
87 | it ('makes a request for authorization_code and sets the access token from response', function (done) {
88 | var code = '12345'
89 | var grantType = 'authorization_code'
90 | var redirectUrl = 'https://example.com/callback'
91 |
92 | var requestBody = qs.stringify({
93 | code: code,
94 | client_id: clientId,
95 | client_secret: clientSecret,
96 | grant_type: grantType,
97 | redirect_uri: redirectUrl
98 | })
99 | // the response might have other parameters. this test only considers the ones called out
100 | // in the Medium Node SDK documentation
101 | var accessToken = 'abcdef'
102 | var refreshToken = 'ghijkl'
103 | var responseBody = {
104 | access_token: accessToken,
105 | refresh_token: refreshToken
106 | }
107 | var request = nock('https://api.medium.com/', {
108 | 'Content-Type': 'application/x-www-form-urlencoded'
109 | })
110 | .post('/v1/tokens', requestBody)
111 | .reply(201, responseBody)
112 |
113 | client.exchangeAuthorizationCode(code, redirectUrl, function (err, data) {
114 | if (err) throw err
115 | data.access_token.should.equal(accessToken)
116 | data.refresh_token.should.equal(refreshToken)
117 | done()
118 | })
119 | request.done()
120 | })
121 | })
122 |
123 | describe('#exchangeRefreshToken', function () {
124 |
125 | it ('makes a request for authorization_code and sets the access token from response', function (done) {
126 | var refreshToken = 'fedcba'
127 | var accessToken = 'lkjihg'
128 |
129 | var requestBody = qs.stringify({
130 | refresh_token: refreshToken,
131 | client_id: clientId,
132 | client_secret: clientSecret,
133 | grant_type: 'refresh_token'
134 | })
135 | // the response might have other parameters. this test only considers the ones called out
136 | // in the Medium Node SDK documentation
137 | var responseBody = {
138 | access_token: accessToken,
139 | refresh_token: refreshToken
140 | }
141 | var request = nock('https://api.medium.com/', {
142 | 'Content-Type': 'application/x-www-form-urlencoded'
143 | })
144 | .post('/v1/tokens', requestBody)
145 | .reply(201, responseBody)
146 |
147 | client.exchangeRefreshToken(refreshToken, function (err, data) {
148 | if (err) throw err
149 | data.access_token.should.equal(accessToken)
150 | data.refresh_token.should.equal(refreshToken)
151 | done()
152 | })
153 | request.done()
154 | })
155 | })
156 |
157 | describe('#getUser', function () {
158 | it ('gets the information from expected URL and returns contents of data envelope', function (done) {
159 | var response = { data: 'response data' }
160 |
161 | var request = nock('https://api.medium.com')
162 | .get('/v1/me')
163 | .reply(200, response)
164 |
165 | client.getUser(function (err, data) {
166 | if (err) throw err
167 | data.should.deepEqual(response['data'])
168 | done()
169 | })
170 | request.done()
171 | })
172 | })
173 |
174 | describe('#getPublicationsForUser', function () {
175 |
176 | it ('throws a MediumError when no user ID is provided', function (done) {
177 | (function () { client.getPublicationsForUser({}) }).should.throw(medium.MediumError)
178 | done()
179 | })
180 |
181 | it ('makes a proper GET request to the Medium API and returns contents of data envelope when valid options are provided', function (done) {
182 | var userId = '123456'
183 | var response = { data: 'response data' }
184 |
185 | var request = nock('https://api.medium.com/')
186 | .get('/v1/users/' + userId + '/publications')
187 | .reply(200, response)
188 |
189 | client.getPublicationsForUser({userId: userId}, function (err, data) {
190 | if (err) throw err
191 | data.should.deepEqual(response['data'])
192 | done()
193 | })
194 | request.done()
195 | })
196 | })
197 |
198 | describe('#getContributorsForPublication', function () {
199 |
200 | it ('throws a MediumError when no publication ID is provided', function (done) {
201 | (function () { client.getContributorsForPublication({}) }).should.throw(medium.MediumError)
202 | done()
203 | })
204 |
205 | it ('makes a proper GET request to the Medium API and returns contents of data envelope', function (done) {
206 | var options = { publicationId: 'abcdef' }
207 | var response = { data: 'response data' }
208 | var request = nock('https://api.medium.com/')
209 | .get('/v1/publications/' + options.publicationId + '/contributors')
210 | .reply(200, response)
211 |
212 | client.getContributorsForPublication(options, function (err, data) {
213 | if (err) throw err
214 | data.should.deepEqual(response['data'])
215 | done()
216 | })
217 | request.done()
218 | })
219 | })
220 |
221 | describe('#createPost', function () {
222 |
223 | it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) {
224 | var options = {
225 | userId: '123456',
226 | title: 'new post title',
227 | content: 'New Post!
',
228 | contentFormat: 'html',
229 | tags: ['js', 'unit tests'],
230 | canonicalUrl: 'http://example.com/new-post',
231 | publishedAt: '2004-02-12T15:19:21+00:00',
232 | publishStatus: 'draft',
233 | license: 'all-rights-reserved'
234 | }
235 | var response = { data: 'response data' }
236 | var request = nock('https://api.medium.com/')
237 | .post('/v1/users/' + options.userId + '/posts', {
238 | title: options.title,
239 | content: options.content,
240 | contentFormat: options.contentFormat,
241 | tags: options.tags,
242 | canonicalUrl: options.canonicalUrl,
243 | publishedAt: options.publishedAt,
244 | publishStatus: options.publishStatus,
245 | license: options.license
246 | })
247 | .reply(200, response)
248 |
249 | client.createPost(options, function (err, data) {
250 | if (err) throw err
251 | data.should.deepEqual(response['data'])
252 | done()
253 | })
254 | request.done()
255 | })
256 | })
257 |
258 | describe('#createPostInPublication', function () {
259 |
260 | it ('should throw an error when no publication ID is provided', function (done) {
261 | (function () { client.createPostInPublication({}) }).should.throw(medium.MediumError)
262 | done()
263 | })
264 |
265 | it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) {
266 | var options = {
267 | publicationId: 'abcdef',
268 | title: 'new post title',
269 | content: 'New Post!
',
270 | contentFormat: 'html',
271 | tags: ['js', 'unit tests'],
272 | canonicalUrl: 'http://example.com/new-post',
273 | publishedAt: '2004-02-12T15:19:21+00:00',
274 | publishStatus: 'draft',
275 | license: 'all-rights-reserved'
276 | }
277 | var response = { data: 'response data' }
278 | var request = nock('https://api.medium.com/')
279 | .post('/v1/publications/' + options.publicationId + '/posts', {
280 | title: options.title,
281 | content: options.content,
282 | contentFormat: options.contentFormat,
283 | tags: options.tags,
284 | canonicalUrl: options.canonicalUrl,
285 | publishedAt: options.publishedAt,
286 | publishStatus: options.publishStatus,
287 | license: options.license
288 | })
289 | .reply(200, response)
290 |
291 | client.createPostInPublication(options, function (err, data) {
292 | if (err) throw err
293 | data.should.deepEqual(response['data'])
294 | done()
295 | })
296 | request.done()
297 | })
298 | })
299 | })
300 |
--------------------------------------------------------------------------------