├── .gitignore ├── Readme.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # aml-gdoc-server 2 | 3 | This is a simple way to retrieve Google Docs written in the [ArchieML](http://archieml.org/) format as json for web apps. 4 | It was created for the Quartz things team, which uses it as part of our build process to streamline the editing of our work. 5 | 6 | ## Installation 7 | 8 | To install run 9 | 10 | npm install -g aml-gdoc-server 11 | 12 | in your terminal shell 13 | 14 | ## Usage 15 | 16 | To start the server run 17 | 18 | aml-gdoc-server 19 | 20 | in your terminal shell 21 | 22 | ### On the first run 23 | 24 | The first time you start the server it will prompt you to provide it with Google API credentials. You will be given instructions on securing those. These credentials and subsequent authorization tokens are saved in a hidden file at `~/.aml-gdoc-credentials` in json format 25 | 26 | ### On subsequent runs 27 | 28 | After the one time setup procedures are completed, all future instances of the server will start immediately upon running `aml-gdoc-server` in your shell 29 | 30 | ### On all runs 31 | 32 | The server defaults to port 6006 i.e. "Goog." 33 | 34 | ArchieML formatted Google Docs can can be retrieved using this url structure `http://127.0.0.1:6006/{google-doc-key}` 35 | 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const inquirer = require('inquirer'); 4 | const expandTilde = require('expand-tilde'); 5 | const express = require('express'); 6 | const { google } = require('googleapis'); 7 | const { docToArchieML } = require('@newswire/doc-to-archieml'); 8 | const fs = require("fs"); 9 | const opn = require('opn'); 10 | const url = require("url"); 11 | 12 | var questions = []; 13 | 14 | var config; 15 | var configpath = expandTilde("~/.aml-gdoc-credentials"); 16 | 17 | var HOST = "http://127.0.0.1"; 18 | var PORT = 6006; 19 | var BASE_URL = HOST + ":" + PORT; 20 | var REDIRECT_PATH = "/auth"; 21 | var LOGIN_PATH = "/login"; 22 | var REDIRECT_URL = BASE_URL + REDIRECT_PATH; 23 | 24 | var oAuth2Client; 25 | var drive; 26 | var app = express(); 27 | 28 | var TOKEN; 29 | var DOC_KEY; 30 | 31 | const SCOPES = ['https://www.googleapis.com/auth/documents.readonly']; 32 | 33 | var hasConfig = false; 34 | 35 | 36 | try { 37 | config = JSON.parse(fs.readFileSync(configpath,"utf8")); 38 | hasConfig = true; 39 | } 40 | catch (e) { 41 | //no config 42 | console.log("\x1b[01m") 43 | console.log("* You have no saved credentials.\n* Let's get you set up.","\x1b[00m") 44 | console.log(" 1. Go to https://console.developers.google.com"); 45 | console.log(" 2. Click \"Enable APIs and Services\"") 46 | console.log(" 3. Search for Google Docs API ang click it" ); 47 | console.log(" 4. Click \"Enable\""); 48 | console.log(" 5. In the menu on the left, click \"Credentials.\""); 49 | console.log(" 6. Click \"Create Credentials\" select \"OAuth client ID\""); 50 | console.log(" 7. Select \"Web Application\" from the list and click the create button."); 51 | console.log(" 8. Set an authorized JavaScript origin of: " + BASE_URL) 52 | console.log(" 9. Set an authorized redirect URI of " + REDIRECT_URL); 53 | console.log("10. Click create.\n\n"); 54 | 55 | questions.push({ 56 | type:"confirm", 57 | name:"got-credentials", 58 | message:"Have you completed the instructions above?\n", 59 | default: null 60 | }); 61 | 62 | config = { 63 | tokens: {}, 64 | credentials: { 65 | redirect_uris: [REDIRECT_URL] 66 | } 67 | }; 68 | } 69 | 70 | if(!config.credentials.client_id) { 71 | questions.push({ 72 | type:"input", 73 | name:"client_id", 74 | message: hasConfig ? "Your Google client id is not set. What is it?\n" : "What is your client id?\n", 75 | default: null 76 | }); 77 | } 78 | 79 | if(!config.credentials.client_secret) { 80 | questions.push({ 81 | type:"input", 82 | name:"client_secret", 83 | message: hasConfig ? "Your Google client secret is not set. What is it?\n" : "What is your client secret?\n", 84 | default: null 85 | }); 86 | } 87 | 88 | function saveConfig() { 89 | fs.writeFileSync(configpath, JSON.stringify(config, null, 4)) 90 | } 91 | 92 | 93 | function timestamp() { 94 | return "[" + new Date().toISOString().split("T")[1] + "]"; 95 | } 96 | 97 | async function updateToken(oA2C) { 98 | 99 | console.log(`${timestamp()} Updating access token`); 100 | 101 | oA2C.setCredentials({ 102 | refresh_token: config.tokens.refresh_token 103 | }); 104 | 105 | let t = await oA2C.getAccessToken(); 106 | config.tokens = Object.assign(config.tokens, t.res.data); 107 | 108 | saveConfig(); 109 | } 110 | 111 | function getNewToken(oA2C, callback) { 112 | const authUrl = oA2C.generateAuthUrl({ 113 | access_type: 'offline', 114 | scope: SCOPES, 115 | prompt: 'consent' 116 | }); 117 | 118 | console.log('Please authorize the app in your browser'); 119 | opn(authUrl, {wait: false}).then(cp => cp.unref()); 120 | 121 | } 122 | 123 | function authorize(credentials, callback) { 124 | const {client_secret, client_id, redirect_uris} = credentials; 125 | oAuth2Client = new google.auth.OAuth2( 126 | client_id, client_secret, redirect_uris[0]); 127 | 128 | if(!("refresh_token" in config.tokens)) { 129 | getNewToken(oAuth2Client); 130 | } 131 | 132 | if(Date.now() > config.tokens.expiry_date) updateToken(oAuth2Client) 133 | 134 | oAuth2Client.setCredentials(config.tokens); 135 | callback(oAuth2Client); 136 | } 137 | 138 | app.get(REDIRECT_PATH, function(req, res) { 139 | console.log(`${timestamp()} GET ${REDIRECT_PATH}`); 140 | var code = url.parse(req.url, true).query.code; 141 | 142 | 143 | oAuth2Client.getToken(code, (err, token) => { 144 | if (err) return res.send(`There was an error getting an access token\n{JSON.stringify(err, null, 4)}`); 145 | 146 | oAuth2Client.setCredentials(token); 147 | config.tokens = Object.assign(config.tokens, token); 148 | saveConfig(); 149 | 150 | console.log(`${timestamp()} The app is now authorized`); 151 | res.send("The app is now authorized!"); 152 | 153 | }); 154 | 155 | }); 156 | 157 | app.get(LOGIN_PATH, function(req, res) { 158 | console.log(`${timestamp()} GET ${LOGIN_PATH}`); 159 | 160 | var redirect_url = oAuth2Client.generateAuthUrl({ 161 | access_type: 'offline', 162 | scope: SCOPES, 163 | approval_prompt:'force' 164 | }); 165 | 166 | res.redirect(redirect_url); 167 | }); 168 | 169 | app.get("/favicon.ico",function(req,res) { 170 | res.status(404).send("Not found"); 171 | }); 172 | 173 | app.get('/:key', function (req, res) { 174 | console.log(`${timestamp()} GET /${DOC_KEY}`); 175 | 176 | if(Date.now() > config.tokens.expiry_date) updateToken(oAuth2Client); 177 | 178 | oAuth2Client.setCredentials(config.tokens); 179 | 180 | docToArchieML({ documentId: DOC_KEY, auth: oAuth2Client }) 181 | .then(r => res.send(r), e => res.status(e.code || 500 ).send(e.response ? e.response.data.error : e)) 182 | .catch(console.log); 183 | 184 | }); 185 | 186 | app.param('key', function (req, res, next, key) { 187 | DOC_KEY = key || DOC_KEY; 188 | next(); 189 | }); 190 | 191 | function run() { 192 | 193 | var server = app.listen(PORT, function () { 194 | console.log(`${timestamp()} The aml-gdoc-server is up and listening at ${HOST}:${PORT}`); 195 | }); 196 | 197 | authorize(config.credentials,()=>{}) 198 | 199 | } 200 | 201 | if(questions.length) { 202 | inquirer.prompt(questions) 203 | .then(function(answers){ 204 | config.credentials.client_id = config.credentials.id || answers.client_id; 205 | config.credentials.client_secret = config.credentials.client_secret || answers.client_secret; 206 | }) 207 | .then(saveConfig) 208 | .then(run); 209 | } 210 | else { 211 | run(); 212 | } 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aml-gdoc-server", 3 | "version": "2.1.3", 4 | "description": "A server to turn retrieve and return ArchieML formatted Google Docs as json", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Quartz/aml-gdoc-server.git" 11 | }, 12 | "keywords": [ 13 | "AML", 14 | "ArchieML" 15 | ], 16 | "author": "David Yanofsky ", 17 | "license": "Apache-2.0", 18 | "preferGlobal": true, 19 | "bin": { 20 | "aml-gdoc-server": "index.js" 21 | }, 22 | "dependencies": { 23 | "@newswire/doc-to-archieml": "^1.0.0", 24 | "expand-tilde": "^2.0.2", 25 | "express": "^4.17.1", 26 | "googleapis": "^40.0.0", 27 | "inquirer": "^6.3.1", 28 | "opn": "^6.0.0", 29 | "url": "^0.11.0" 30 | } 31 | } 32 | --------------------------------------------------------------------------------