diff --git a/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml b/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml
new file mode 100644
index 0000000..f0ee36b
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml
@@ -0,0 +1,34 @@
+# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
+# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
+
+name: Node.js Package
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ - run: npm ci
+ - run: npm test
+
+ publish-npm:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ registry-url: https://registry.npmjs.org/
+ - run: npm ci
+ - run: npm publish --access public
+ env:
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
+
diff --git a/node_modules/@redcode/medium-sdk/LICENSE b/node_modules/@redcode/medium-sdk/LICENSE
new file mode 100644
index 0000000..8f71f43
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/node_modules/@redcode/medium-sdk/README.md b/node_modules/@redcode/medium-sdk/README.md
new file mode 100644
index 0000000..a304442
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/README.md
@@ -0,0 +1,67 @@
+**Warning:** This sdk is no longer supported or maintained by Medium.
+
+
+# Medium SDK for NodeJS
+
+This repository contains the open source SDK for integrating [Medium](https://medium.com)'s OAuth2 API into your NodeJs app.
+
+View the full [documentation here](https://github.com/Medium/medium-api-docs).
+
+Install
+-------
+
+ npm install medium-sdk
+
+Usage
+-----
+
+Create a client, then call commands on it.
+
+```javascript
+var medium = require('medium-sdk')
+
+var client = new medium.MediumClient({
+ clientId: 'YOUR_CLIENT_ID',
+ clientSecret: 'YOUR_CLIENT_SECRET'
+})
+
+var redirectURL = 'https://yoursite.com/callback/medium';
+
+var url = client.getAuthorizationUrl('secretState', redirectURL, [
+ medium.Scope.BASIC_PROFILE, medium.Scope.PUBLISH_POST
+])
+
+// (Send the user to the authorization URL to obtain an authorization code.)
+
+client.exchangeAuthorizationCode('YOUR_AUTHORIZATION_CODE', redirectURL, function (err, token) {
+ client.getUser(function (err, user) {
+ client.createPost({
+ userId: user.id,
+ title: 'A new post',
+ contentFormat: medium.PostContentFormat.HTML,
+ content: '
A New Post
This is my new post.
',
+ publishStatus: medium.PostPublishStatus.DRAFT
+ }, function (err, post) {
+ console.log(token, user, post)
+ })
+ })
+})
+```
+
+Contributing
+------------
+
+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).
+
+Authors
+-------
+
+[Jamie Talbot](https://github.com/majelbstoat)
+
+License
+-------
+
+Copyright 2015 [A Medium Corporation](https://medium.com)
+
+Licensed under Apache License Version 2.0. Details in the attached LICENSE
+file.
diff --git a/node_modules/@redcode/medium-sdk/index.js b/node_modules/@redcode/medium-sdk/index.js
new file mode 100644
index 0000000..d7fd1f3
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/index.js
@@ -0,0 +1 @@
+module.exports = require("./lib/mediumClient.js")
diff --git a/node_modules/@redcode/medium-sdk/lib/mediumClient.js b/node_modules/@redcode/medium-sdk/lib/mediumClient.js
new file mode 100644
index 0000000..e469f81
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/lib/mediumClient.js
@@ -0,0 +1,413 @@
+// Copright 2015 A Medium Corporation
+
+var https = require('https')
+var qs = require('querystring')
+var url = require('url')
+var util = require('util')
+
+
+var DEFAULT_ERROR_CODE = -1
+var DEFAULT_TIMEOUT_MS = 5000
+
+
+/**
+ * Valid scope options.
+ * @enum {string}
+ */
+var Scope = {
+ BASIC_PROFILE: 'basicProfile',
+ LIST_PUBLICATIONS: 'listPublications',
+ PUBLISH_POST: 'publishPost'
+}
+
+
+/**
+ * The publish status when creating a post.
+ * @enum {string}
+ */
+var PostPublishStatus = {
+ DRAFT: 'draft',
+ UNLISTED: 'unlisted',
+ PUBLIC: 'public'
+}
+
+
+/**
+ * The content format to use when creating a post.
+ * @enum {string}
+ */
+var PostContentFormat = {
+ HTML: 'html',
+ MARKDOWN: 'markdown'
+}
+
+
+/**
+ * The license to use when creating a post.
+ * @enum {string}
+ */
+var PostLicense = {
+ ALL_RIGHTS_RESERVED: 'all-rights-reserved',
+ CC_40_BY: 'cc-40-by',
+ CC_40_BY_ND: 'cc-40-by-nd',
+ CC_40_BY_SA: 'cc-40-by-sa',
+ CC_40_BY_NC: 'cc-40-by-nc',
+ CC_40_BY_NC_ND: 'cc-40-by-nc-nd',
+ CC_40_BY_NC_SA: 'cc-40-by-nc-sa',
+ CC_40_ZERO: 'cc-40-zero',
+ PUBLIC_DOMAIN: 'public-domain'
+}
+
+
+/**
+ * An error with a code.
+ *
+ * @param {string} message
+ * @param {number} code
+ * @constructor
+ */
+function MediumError(message, code) {
+ this.message = message
+ this.code = code
+}
+util.inherits(MediumError, Error)
+
+
+/**
+ * The core client.
+ *
+ * @param {{
+ * clientId: string,
+ * clientSecret: string
+ * }} options
+ * @constructor
+ */
+function MediumClient(options = {}) {
+ this._clientId = options.clientId
+ this._clientSecret = options.clientSecret
+ this._accessToken = ""
+}
+
+
+/**
+ * Sets an access token on the client used for making requests.
+ *
+ * @param {string} accessToken
+ * @return {MediumClient}
+ */
+MediumClient.prototype.setAccessToken = function (accessToken) {
+ this._accessToken = accessToken
+ return this
+}
+
+
+/**
+ * Builds a URL at which you may request authorization from the user.
+ *
+ * @param {string} state
+ * @param {string} redirectUrl
+ * @param {Array.} requestedScope
+ * @return {string}
+ */
+MediumClient.prototype.getAuthorizationUrl = function (state, redirectUrl, requestedScope) {
+ return url.format({
+ protocol: 'https',
+ host: 'medium.com',
+ pathname: '/m/oauth/authorize',
+ query: {
+ client_id: this._clientId,
+ scope: requestedScope.join(','),
+ response_type: 'code',
+ state: state,
+ redirect_uri: redirectUrl
+ }
+ })
+}
+
+
+/**
+ * Exchanges an authorization code for an access token and a refresh token.
+ *
+ * @param {string} code
+ * @param {string} redirectUrl
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.exchangeAuthorizationCode = function (code, redirectUrl, callback) {
+ this._acquireAccessToken({
+ code: code,
+ client_id: this._clientId,
+ client_secret: this._clientSecret,
+ grant_type: 'authorization_code',
+ redirect_uri: redirectUrl
+ }, callback)
+}
+
+
+/**
+ * Exchanges a refresh token for an access token and a refresh token.
+ *
+ * @param {string} refreshToken
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.exchangeRefreshToken = function (refreshToken, callback) {
+ this._acquireAccessToken({
+ refresh_token: refreshToken,
+ client_id: this._clientId,
+ client_secret: this._clientSecret,
+ grant_type: 'refresh_token'
+ }, callback)
+}
+
+
+/**
+ * Returns the details of the user associated with the current
+ * access token.
+ *
+ * Requires the current access token to have the basicProfile scope.
+ *
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.getUser = function (callback) {
+ this._makeRequest({
+ method: 'GET',
+ path: '/v1/me'
+ }, callback)
+}
+
+
+/**
+ * Returns the publications related to the current user. Notice that
+ * the userId needs to be passed in as an option. It can be acquired
+ * with a call to getUser().
+ *
+ * Requires the current access token to have the
+ * listPublications scope.
+ *
+ * @param {{
+ * userId: string
+ * }} options
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.getPublicationsForUser = function (options, callback) {
+ this._enforce(options, ['userId'])
+ this._makeRequest({
+ method: 'GET',
+ path: '/v1/users/' + options.userId + '/publications'
+ }, callback)
+}
+
+
+/**
+ * Returns the contributors for a chosen publication. The publication is identified
+ * by the publication ID included in the options argument. IDs for publications
+ * can be acquired by getUsersPublications.
+ *
+ * Requires the current access token to have the basicProfile scope.
+ *
+ * @param {{
+ * publicationId: string
+ * }} options
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.getContributorsForPublication = function (options, callback) {
+ this._enforce(options, ['publicationId'])
+ this._makeRequest({
+ method: 'GET',
+ path: '/v1/publications/' + options.publicationId + '/contributors'
+ }, callback)
+}
+
+
+/**
+ * Creates a post on Medium.
+ *
+ * Requires the current access token to have the publishPost scope.
+ *
+ * @param {{
+ * userId: string,
+ * title: string,
+ * contentFormat: PostContentFormat,
+ * content: string,
+ * tags: Array.,
+ * canonicalUrl: string,
+ * publishStatus: PostPublishStatus,
+ * license: PostLicense
+ * }} options
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.createPost = function (options, callback) {
+ this._enforce(options, ['userId'])
+ this._makeRequest({
+ method: 'POST',
+ path: '/v1/users/' + options.userId + '/posts',
+ data: {
+ title: options.title,
+ content: options.content,
+ contentFormat: options.contentFormat,
+ tags: options.tags,
+ canonicalUrl: options.canonicalUrl,
+ publishedAt: options.publishedAt,
+ publishStatus: options.publishStatus,
+ license: options.license
+ }
+ }, callback)
+}
+
+
+/**
+ * Creates a post on Medium and places it under specified publication.
+ * Please refer to the API documentation for rules around publishing in
+ * a publication: https://github.com/Medium/medium-api-docs
+ *
+ * Requires the current access token to have the publishPost scope.
+ *
+ * @param {{
+ * userId: string,
+ * publicationId: string,
+ * title: string,
+ * contentFormat: PostContentFormat,
+ * content: string,
+ * tags: Array.,
+ * canonicalUrl: string,
+ * publishStatus: PostPublishStatus,
+ * license: PostLicense
+ * }} options
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype.createPostInPublication = function (options, callback) {
+ this._enforce(options, ['publicationId'])
+ this._makeRequest({
+ method: 'POST',
+ path: '/v1/publications/' + options.publicationId + '/posts',
+ data: {
+ title: options.title,
+ content: options.content,
+ contentFormat: options.contentFormat,
+ tags: options.tags,
+ canonicalUrl: options.canonicalUrl,
+ publishedAt: options.publishedAt,
+ publishStatus: options.publishStatus,
+ license: options.license
+ }
+ }, callback)
+}
+
+
+/**
+ * Acquires an access token for the Medium API.
+ *
+ * Sets the access token on the client on success.
+ *
+ * @param {Object} params
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype._acquireAccessToken = function (params, callback) {
+ this._makeRequest({
+ method: 'POST',
+ path: '/v1/tokens',
+ contentType: 'application/x-www-form-urlencoded',
+ data: qs.stringify(params)
+ }, function (err, data) {
+ if (!err) {
+ this._accessToken = data.access_token
+ }
+ callback(err, data)
+ }.bind(this))
+}
+
+
+/**
+ * Enforces that given options object (first param) defines
+ * all keys requested (second param). Raises an error if any
+ * is missing.
+ *
+ * @param {Object} options
+ * @param {keys} requiredKeys
+ */
+MediumClient.prototype._enforce = function (options, requiredKeys) {
+ if (!options) {
+ throw new MediumError('Parameters for this call are undefined', DEFAULT_ERROR_CODE)
+ }
+ requiredKeys.forEach(function (requiredKey) {
+ if (!options[requiredKey]) throw new MediumError('Missing required parameter "' + requiredKey + '"', DEFAULT_ERROR_CODE)
+ })
+}
+
+
+
+/**
+ * Makes a request to the Medium API.
+ *
+ * @param {Object} options
+ * @param {NodeCallback} callback
+ */
+MediumClient.prototype._makeRequest = function (options, callback) {
+ var requestParams = {
+ host: 'api.medium.com',
+ port: 443,
+ method: options.method,
+ path: options.path
+ }
+ var req = https.request(requestParams, function (res) {
+ var body = []
+
+ res.setEncoding('utf-8')
+ res.on('data', function (data) {
+ body.push(data)
+ })
+ res.on('end', function () {
+ var payload
+ var responseText = body.join('')
+ try {
+ payload = JSON.parse(responseText)
+ } catch (err) {
+ callback(new MediumError('Failed to parse response', DEFAULT_ERROR_CODE), null)
+ return
+ }
+
+ var statusCode = res.statusCode
+ var statusType = Math.floor(res.statusCode / 100)
+
+ if (statusType == 4 || statusType == 5) {
+ var err = payload.errors[0]
+ callback(new MediumError(err.message, err.code), null)
+ } else if (statusType == 2) {
+ callback(null, payload.data || payload)
+ } else {
+ callback(new MediumError('Unexpected response', DEFAULT_ERROR_CODE), null)
+ }
+ })
+ }).on('error', function (err) {
+ callback(new MediumError(err.message, DEFAULT_ERROR_CODE), null)
+ })
+
+ req.setHeader('Content-Type', options.contentType || 'application/json')
+ req.setHeader('Authorization', 'Bearer ' + this._accessToken)
+ req.setHeader('Accept', 'application/json')
+ req.setHeader('Accept-Charset', 'utf-8')
+
+ req.setTimeout(DEFAULT_TIMEOUT_MS, function () {
+ // Aborting a request triggers the 'error' event.
+ req.abort()
+ })
+
+ if (options.data) {
+ var data = options.data
+ if (typeof data == 'object') {
+ data = JSON.stringify(data)
+ }
+ req.write(data)
+ }
+ req.end()
+}
+
+// Exports
+
+module.exports = {
+ MediumClient: MediumClient,
+ MediumError: MediumError,
+ Scope: Scope,
+ PostPublishStatus: PostPublishStatus,
+ PostLicense: PostLicense,
+ PostContentFormat: PostContentFormat
+}
diff --git a/node_modules/@redcode/medium-sdk/package.json b/node_modules/@redcode/medium-sdk/package.json
new file mode 100644
index 0000000..b0e94f9
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/package.json
@@ -0,0 +1,56 @@
+{
+ "_from": "@redcode/medium-sdk",
+ "_id": "@redcode/medium-sdk@0.0.7",
+ "_inBundle": false,
+ "_integrity": "sha512-NFCBI15byP62gF0IUolRYL6jxQoZP5e4oNi7XZsLb1IaVqJJD+5BQaqgQy9mlK47e4Vi/hRUBOS4ksp1DZVD2Q==",
+ "_location": "/@redcode/medium-sdk",
+ "_phantomChildren": {},
+ "_requested": {
+ "type": "tag",
+ "registry": true,
+ "raw": "@redcode/medium-sdk",
+ "name": "@redcode/medium-sdk",
+ "escapedName": "@redcode%2fmedium-sdk",
+ "scope": "@redcode",
+ "rawSpec": "",
+ "saveSpec": null,
+ "fetchSpec": "latest"
+ },
+ "_requiredBy": [
+ "#USER",
+ "/"
+ ],
+ "_resolved": "https://registry.npmjs.org/@redcode/medium-sdk/-/medium-sdk-0.0.7.tgz",
+ "_shasum": "3b4eaaaf2c8c74f8982c376071eb84957c52f964",
+ "_spec": "@redcode/medium-sdk",
+ "_where": "/Users/maZahaca/projects/infraway/medium-post-markdown",
+ "author": {
+ "name": "Jamie Talbot",
+ "email": "jamie@jamietalbot.com",
+ "url": "https://github.com/majelbstoat"
+ },
+ "bugs": {
+ "url": "https://github.com/medium/medium-sdk-nodejs/issues"
+ },
+ "bundleDependencies": false,
+ "deprecated": false,
+ "description": "NodeJS client for the Medium app",
+ "devDependencies": {
+ "mocha": "^2.2.5",
+ "nock": "^2.17",
+ "should": "^7.1"
+ },
+ "homepage": "https://github.com/medium/medium-sdk-nodejs",
+ "keywords": [
+ "medium",
+ "api",
+ "writing"
+ ],
+ "main": "index.js",
+ "name": "@redcode/medium-sdk",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/medium/medium-sdk-nodejs.git"
+ },
+ "version": "0.0.7"
+}
diff --git a/node_modules/@redcode/medium-sdk/test/mediumClient_test.js b/node_modules/@redcode/medium-sdk/test/mediumClient_test.js
new file mode 100644
index 0000000..47e1e08
--- /dev/null
+++ b/node_modules/@redcode/medium-sdk/test/mediumClient_test.js
@@ -0,0 +1,299 @@
+var medium = require("../")
+var nock = require("nock")
+var qs = require('querystring')
+var should = require("should")
+var url = require('url')
+
+
+describe('MediumClient - constructor', function () {
+
+ it('should throw a MediumError when options are undefined', function (done) {
+ (function () { new medium.MediumClient() }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it('should throw a MediumError when options are empty', function (done) {
+ (function () { new medium.MediumClient({}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it('should throw a MediumError when only clientId is provided', function (done) {
+ (function () { new medium.MediumClient({clientId: 'xxx'}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it('should throw a MediumError when only clientSecret is provided', function (done) {
+ (function () { new medium.MediumClient({clientSecret: 'yyy'}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it('should succeed when both clientId and clientSecret are provided', function (done) {
+ var client = new medium.MediumClient({clientId: 'xxx', clientSecret: 'yyy'})
+ done()
+ })
+})
+
+
+describe('MediumClient - methods', function () {
+
+ var clientId = 'xxx'
+ var clientSecret = 'yyy'
+ var client
+
+ beforeEach(function () {
+ client = new medium.MediumClient({clientId: clientId, clientSecret: clientSecret})
+ nock.disableNetConnect()
+ })
+
+ afterEach(function () {
+ nock.enableNetConnect();
+ delete client
+ })
+
+ describe('#setAccessToken', function () {
+
+ it ('sets the access token', function (done) {
+ var token = "new token"
+ client.setAccessToken(token)
+ client._accessToken.should.be.String().and.equal(token)
+ done()
+ })
+ })
+
+ describe('#getAuthorizationUrl', function () {
+
+ it ('returns a valid URL for fetching', function (done) {
+ var state = "state"
+ var redirectUrl = "https://example.com/callback"
+ var scope = [medium.Scope.BASIC_PROFILE, medium.Scope.LIST_PUBLICATIONS, medium.Scope.PUBLISH_POST]
+ var authUrlStr = client.getAuthorizationUrl(state, redirectUrl, scope)
+ var authUrl = url.parse(authUrlStr, true)
+ authUrl.protocol.should.equal('https:')
+ authUrl.hostname.should.equal('medium.com')
+ authUrl.pathname.should.equal('/m/oauth/authorize')
+ authUrl.query.should.deepEqual({
+ client_id: clientId,
+ scope: scope.join(','),
+ response_type: 'code',
+ state: state,
+ redirect_uri: redirectUrl
+ })
+ done()
+ })
+ })
+
+ describe('#exchangeAuthorizationCode', function () {
+
+ it ('makes a request for authorization_code and sets the access token from response', function (done) {
+ var code = '12345'
+ var grantType = 'authorization_code'
+ var redirectUrl = 'https://example.com/callback'
+
+ var requestBody = qs.stringify({
+ code: code,
+ client_id: clientId,
+ client_secret: clientSecret,
+ grant_type: grantType,
+ redirect_uri: redirectUrl
+ })
+ // the response might have other parameters. this test only considers the ones called out
+ // in the Medium Node SDK documentation
+ var accessToken = 'abcdef'
+ var refreshToken = 'ghijkl'
+ var responseBody = {
+ access_token: accessToken,
+ refresh_token: refreshToken
+ }
+ var request = nock('https://api.medium.com/', {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ })
+ .post('/v1/tokens', requestBody)
+ .reply(201, responseBody)
+
+ client.exchangeAuthorizationCode(code, redirectUrl, function (err, data) {
+ if (err) throw err
+ data.access_token.should.equal(accessToken)
+ data.refresh_token.should.equal(refreshToken)
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#exchangeRefreshToken', function () {
+
+ it ('makes a request for authorization_code and sets the access token from response', function (done) {
+ var refreshToken = 'fedcba'
+ var accessToken = 'lkjihg'
+
+ var requestBody = qs.stringify({
+ refresh_token: refreshToken,
+ client_id: clientId,
+ client_secret: clientSecret,
+ grant_type: 'refresh_token'
+ })
+ // the response might have other parameters. this test only considers the ones called out
+ // in the Medium Node SDK documentation
+ var responseBody = {
+ access_token: accessToken,
+ refresh_token: refreshToken
+ }
+ var request = nock('https://api.medium.com/', {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ })
+ .post('/v1/tokens', requestBody)
+ .reply(201, responseBody)
+
+ client.exchangeRefreshToken(refreshToken, function (err, data) {
+ if (err) throw err
+ data.access_token.should.equal(accessToken)
+ data.refresh_token.should.equal(refreshToken)
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#getUser', function () {
+ it ('gets the information from expected URL and returns contents of data envelope', function (done) {
+ var response = { data: 'response data' }
+
+ var request = nock('https://api.medium.com')
+ .get('/v1/me')
+ .reply(200, response)
+
+ client.getUser(function (err, data) {
+ if (err) throw err
+ data.should.deepEqual(response['data'])
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#getPublicationsForUser', function () {
+
+ it ('throws a MediumError when no user ID is provided', function (done) {
+ (function () { client.getPublicationsForUser({}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it ('makes a proper GET request to the Medium API and returns contents of data envelope when valid options are provided', function (done) {
+ var userId = '123456'
+ var response = { data: 'response data' }
+
+ var request = nock('https://api.medium.com/')
+ .get('/v1/users/' + userId + '/publications')
+ .reply(200, response)
+
+ client.getPublicationsForUser({userId: userId}, function (err, data) {
+ if (err) throw err
+ data.should.deepEqual(response['data'])
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#getContributorsForPublication', function () {
+
+ it ('throws a MediumError when no publication ID is provided', function (done) {
+ (function () { client.getContributorsForPublication({}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it ('makes a proper GET request to the Medium API and returns contents of data envelope', function (done) {
+ var options = { publicationId: 'abcdef' }
+ var response = { data: 'response data' }
+ var request = nock('https://api.medium.com/')
+ .get('/v1/publications/' + options.publicationId + '/contributors')
+ .reply(200, response)
+
+ client.getContributorsForPublication(options, function (err, data) {
+ if (err) throw err
+ data.should.deepEqual(response['data'])
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#createPost', function () {
+
+ it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) {
+ var options = {
+ userId: '123456',
+ title: 'new post title',
+ content: 'New Post!
',
+ contentFormat: 'html',
+ tags: ['js', 'unit tests'],
+ canonicalUrl: 'http://example.com/new-post',
+ publishedAt: '2004-02-12T15:19:21+00:00',
+ publishStatus: 'draft',
+ license: 'all-rights-reserved'
+ }
+ var response = { data: 'response data' }
+ var request = nock('https://api.medium.com/')
+ .post('/v1/users/' + options.userId + '/posts', {
+ title: options.title,
+ content: options.content,
+ contentFormat: options.contentFormat,
+ tags: options.tags,
+ canonicalUrl: options.canonicalUrl,
+ publishedAt: options.publishedAt,
+ publishStatus: options.publishStatus,
+ license: options.license
+ })
+ .reply(200, response)
+
+ client.createPost(options, function (err, data) {
+ if (err) throw err
+ data.should.deepEqual(response['data'])
+ done()
+ })
+ request.done()
+ })
+ })
+
+ describe('#createPostInPublication', function () {
+
+ it ('should throw an error when no publication ID is provided', function (done) {
+ (function () { client.createPostInPublication({}) }).should.throw(medium.MediumError)
+ done()
+ })
+
+ it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) {
+ var options = {
+ publicationId: 'abcdef',
+ title: 'new post title',
+ content: 'New Post!
',
+ contentFormat: 'html',
+ tags: ['js', 'unit tests'],
+ canonicalUrl: 'http://example.com/new-post',
+ publishedAt: '2004-02-12T15:19:21+00:00',
+ publishStatus: 'draft',
+ license: 'all-rights-reserved'
+ }
+ var response = { data: 'response data' }
+ var request = nock('https://api.medium.com/')
+ .post('/v1/publications/' + options.publicationId + '/posts', {
+ title: options.title,
+ content: options.content,
+ contentFormat: options.contentFormat,
+ tags: options.tags,
+ canonicalUrl: options.canonicalUrl,
+ publishedAt: options.publishedAt,
+ publishStatus: options.publishStatus,
+ license: options.license
+ })
+ .reply(200, response)
+
+ client.createPostInPublication(options, function (err, data) {
+ if (err) throw err
+ data.should.deepEqual(response['data'])
+ done()
+ })
+ request.done()
+ })
+ })
+})