├── .gitignore ├── README.md ├── cli.js ├── package-lock.json ├── package.json ├── parser.js └── resourceManager.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thread-reader-reader 2 | 3 | Helps converting Twitter threads to stand-alone articles by extracting them to simple HTML. Relies on the [Thread reader](https://threadreaderapp.com) third-party app. 4 | 5 | ## Installation 6 | 7 | 1. Make sure you have Node and NPM installed. 8 | 2. Install the command globally 9 | ``` 10 | npm i -g thread-reader-reader 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Submit the first tweet of a Twitter thread to [Thread reader](https://threadreaderapp.com) 16 | 2. Get the resulting URL (Should look like https://threadreaderapp.com/thread/1241364682084093953.html) 17 | 3. Run the CLI program with the following command: 18 | 19 | ``` 20 | thread-reader-reader 21 | ``` 22 | 4. The result will be written to the standard output. 23 | 24 | Tweets will be output in `

` divs, and images and videos will be wrapped in a `

` tag following the paragraph of the tweet they appear in. 25 | 26 | ### Downloading resources 27 | 28 | By default, original image and video urls will be kept, meaning links will still use the resources stored on Twitter servers. 29 | 30 | Instead, you can download these resources locally by adding a path to a local directory as second parameter. 31 | 32 | ``` 33 | thread-reader-reader [directory_to_store_files] 34 | ``` 35 | 36 | In that case, the urls to images and videos will be rewritten with the same path as relative url, replacing Twitter urls. 37 | 38 | You can change this relative url by specifying a third parameter. 39 | 40 | ``` 41 | thread-reader-reader [directory_to_store_files] [relative_url_path] 42 | ``` 43 | 44 | That way, it's easy to adapt to how your destination website stores content. 45 | 46 | ### Full example 47 | 48 | Command: 49 | 50 | ``` 51 | thread-reader-reader https://threadreaderapp.com/thread/1241364682084093953.html aquatint-files /public/images/aquatint-files > aquatint-article.html 52 | ``` 53 | 54 | Result: 55 | 56 | - The extracted thread HTML will be written to the `aquatint-article.html` file. 57 | - In this HTML, urls to images and videos will use `/public/images/aquatint-files` as prefix. 58 | - Image and video files will be downloaded and stored in the `aquatint-files` directory, ready to be uploaded on your server, in the directory corresponding to `/public/images/aquatint-files`. 59 | 60 | 61 | ## As a lib 62 | 63 | You can also use the `parser.js` file as a separate lib. 64 | 65 | The `getTweets` function takes a DOM element as input (JSDom or real DOM will work), basically the document of a Thread Reader page and will return an array of objects: 66 | 67 | ```js 68 | { 69 | tweetHTML, // inner markup of the tweet text, including links 70 | images, // array of { url } objects 71 | videos // array of { poster, sources: [{ type, src }] } objects 72 | } 73 | ``` -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fetch = require('node-fetch'); 3 | const emojis = require('node-emoji'); 4 | const getTweets = require('./parser').getTweets; 5 | const JSDOM = require('jsdom').JSDOM; 6 | const { rewriteUrl, downloadResources } = require('./resourceManager'); 7 | 8 | const [,, threadReaderUrl, rsrcDirPath, rsrcUrlPath] = process.argv; 9 | 10 | function processDocument(document, { extractResources = false, resourcesUrlPath = '.' }) { 11 | const resources = []; 12 | const urlTransformer = extractResources ? rewriteUrl(resourcesUrlPath) : x => x; 13 | const html = getTweets(document) 14 | .map(({ tweetHTML, images, videos }) => { 15 | const imagesHTML = images.map(img => { 16 | if (extractResources) { 17 | resources.push(img.url); 18 | } 19 | return imageToHtml(urlTransformer)(img); 20 | }); 21 | const videosHTML = videos.map(video => { 22 | if (extractResources) { 23 | resources.push(video.poster); 24 | video.sources.forEach(source => resources.push(source.src)); 25 | } 26 | return videoToHtml(urlTransformer)(video); 27 | }); 28 | return { tweetHTML: emojis.strip(tweetHTML), imagesHTML, videosHTML }; 29 | }) 30 | .map(({ tweetHTML, imagesHTML, videosHTML }) => { 31 | const tags = [ `

${tweetHTML}

` ]; 32 | if (imagesHTML.length > 0) { tags.push(`
${imagesHTML.join('')}
`); } 33 | if (videosHTML.length > 0) { tags.push(`
${videosHTML.join('')}
`); } 34 | return tags; 35 | }) 36 | .reduce((acc, tweetParts) => acc.concat(tweetParts), []) // alternative for missing .flat 37 | .join(''); 38 | 39 | return { html, resources }; 40 | } 41 | 42 | const imageToHtml = urlTransformer => ({ url }) => 43 | ` 44 | 45 | `; 46 | 47 | const videoToHtml = urlTransformer => ({ poster, sources }) => { 48 | const types = sources.map(({ src, type }) => ``); 49 | return ``; 53 | } 54 | 55 | fetch(threadReaderUrl) 56 | .then(response => response.text()) 57 | .then(html => new JSDOM(html).window.document) 58 | .then(doc => processDocument(doc, { 59 | extractResources: !!rsrcDirPath, 60 | resourcesUrlPath: rsrcUrlPath || rsrcDirPath 61 | })) 62 | .then(async ({ html, resources }) => { 63 | await downloadResources(resources, rsrcDirPath); 64 | return html; 65 | }) 66 | .then(console.log); 67 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thread-reader-reader", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abab": { 8 | "version": "2.0.3", 9 | "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", 10 | "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==" 11 | }, 12 | "acorn": { 13 | "version": "7.2.0", 14 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", 15 | "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==" 16 | }, 17 | "acorn-globals": { 18 | "version": "6.0.0", 19 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", 20 | "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", 21 | "requires": { 22 | "acorn": "^7.1.1", 23 | "acorn-walk": "^7.1.1" 24 | } 25 | }, 26 | "acorn-walk": { 27 | "version": "7.1.1", 28 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", 29 | "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==" 30 | }, 31 | "ajv": { 32 | "version": "6.12.2", 33 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", 34 | "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", 35 | "requires": { 36 | "fast-deep-equal": "^3.1.1", 37 | "fast-json-stable-stringify": "^2.0.0", 38 | "json-schema-traverse": "^0.4.1", 39 | "uri-js": "^4.2.2" 40 | } 41 | }, 42 | "asn1": { 43 | "version": "0.2.4", 44 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 45 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 46 | "requires": { 47 | "safer-buffer": "~2.1.0" 48 | } 49 | }, 50 | "assert-plus": { 51 | "version": "1.0.0", 52 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 53 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 54 | }, 55 | "asynckit": { 56 | "version": "0.4.0", 57 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 58 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 59 | }, 60 | "at-least-node": { 61 | "version": "1.0.0", 62 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 63 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" 64 | }, 65 | "aws-sign2": { 66 | "version": "0.7.0", 67 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 68 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 69 | }, 70 | "aws4": { 71 | "version": "1.9.1", 72 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 73 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 74 | }, 75 | "bcrypt-pbkdf": { 76 | "version": "1.0.2", 77 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 78 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 79 | "requires": { 80 | "tweetnacl": "^0.14.3" 81 | } 82 | }, 83 | "browser-process-hrtime": { 84 | "version": "1.0.0", 85 | "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", 86 | "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" 87 | }, 88 | "caseless": { 89 | "version": "0.12.0", 90 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 91 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 92 | }, 93 | "combined-stream": { 94 | "version": "1.0.8", 95 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 96 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 97 | "requires": { 98 | "delayed-stream": "~1.0.0" 99 | } 100 | }, 101 | "core-util-is": { 102 | "version": "1.0.2", 103 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 104 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 105 | }, 106 | "cssom": { 107 | "version": "0.4.4", 108 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", 109 | "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" 110 | }, 111 | "cssstyle": { 112 | "version": "2.3.0", 113 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", 114 | "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", 115 | "requires": { 116 | "cssom": "~0.3.6" 117 | }, 118 | "dependencies": { 119 | "cssom": { 120 | "version": "0.3.8", 121 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", 122 | "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" 123 | } 124 | } 125 | }, 126 | "dashdash": { 127 | "version": "1.14.1", 128 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 129 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 130 | "requires": { 131 | "assert-plus": "^1.0.0" 132 | } 133 | }, 134 | "data-urls": { 135 | "version": "2.0.0", 136 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", 137 | "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", 138 | "requires": { 139 | "abab": "^2.0.3", 140 | "whatwg-mimetype": "^2.3.0", 141 | "whatwg-url": "^8.0.0" 142 | } 143 | }, 144 | "decimal.js": { 145 | "version": "10.2.0", 146 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", 147 | "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==" 148 | }, 149 | "deep-is": { 150 | "version": "0.1.3", 151 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 152 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" 153 | }, 154 | "delayed-stream": { 155 | "version": "1.0.0", 156 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 157 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 158 | }, 159 | "domexception": { 160 | "version": "2.0.1", 161 | "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", 162 | "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", 163 | "requires": { 164 | "webidl-conversions": "^5.0.0" 165 | }, 166 | "dependencies": { 167 | "webidl-conversions": { 168 | "version": "5.0.0", 169 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", 170 | "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" 171 | } 172 | } 173 | }, 174 | "ecc-jsbn": { 175 | "version": "0.1.2", 176 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 177 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 178 | "requires": { 179 | "jsbn": "~0.1.0", 180 | "safer-buffer": "^2.1.0" 181 | } 182 | }, 183 | "escodegen": { 184 | "version": "1.14.1", 185 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", 186 | "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", 187 | "requires": { 188 | "esprima": "^4.0.1", 189 | "estraverse": "^4.2.0", 190 | "esutils": "^2.0.2", 191 | "optionator": "^0.8.1", 192 | "source-map": "~0.6.1" 193 | } 194 | }, 195 | "esprima": { 196 | "version": "4.0.1", 197 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 198 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 199 | }, 200 | "estraverse": { 201 | "version": "4.3.0", 202 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 203 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" 204 | }, 205 | "esutils": { 206 | "version": "2.0.3", 207 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 208 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" 209 | }, 210 | "extend": { 211 | "version": "3.0.2", 212 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 213 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 214 | }, 215 | "extsprintf": { 216 | "version": "1.3.0", 217 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 218 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 219 | }, 220 | "fast-deep-equal": { 221 | "version": "3.1.1", 222 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 223 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 224 | }, 225 | "fast-json-stable-stringify": { 226 | "version": "2.1.0", 227 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 228 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 229 | }, 230 | "fast-levenshtein": { 231 | "version": "2.0.6", 232 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 233 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" 234 | }, 235 | "forever-agent": { 236 | "version": "0.6.1", 237 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 238 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 239 | }, 240 | "form-data": { 241 | "version": "2.3.3", 242 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 243 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 244 | "requires": { 245 | "asynckit": "^0.4.0", 246 | "combined-stream": "^1.0.6", 247 | "mime-types": "^2.1.12" 248 | } 249 | }, 250 | "fs-extra": { 251 | "version": "9.0.0", 252 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", 253 | "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", 254 | "requires": { 255 | "at-least-node": "^1.0.0", 256 | "graceful-fs": "^4.2.0", 257 | "jsonfile": "^6.0.1", 258 | "universalify": "^1.0.0" 259 | } 260 | }, 261 | "getpass": { 262 | "version": "0.1.7", 263 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 264 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 265 | "requires": { 266 | "assert-plus": "^1.0.0" 267 | } 268 | }, 269 | "graceful-fs": { 270 | "version": "4.2.4", 271 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 272 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" 273 | }, 274 | "har-schema": { 275 | "version": "2.0.0", 276 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 277 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 278 | }, 279 | "har-validator": { 280 | "version": "5.1.3", 281 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 282 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 283 | "requires": { 284 | "ajv": "^6.5.5", 285 | "har-schema": "^2.0.0" 286 | } 287 | }, 288 | "html-encoding-sniffer": { 289 | "version": "2.0.1", 290 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", 291 | "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", 292 | "requires": { 293 | "whatwg-encoding": "^1.0.5" 294 | } 295 | }, 296 | "http-signature": { 297 | "version": "1.2.0", 298 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 299 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 300 | "requires": { 301 | "assert-plus": "^1.0.0", 302 | "jsprim": "^1.2.2", 303 | "sshpk": "^1.7.0" 304 | } 305 | }, 306 | "iconv-lite": { 307 | "version": "0.4.24", 308 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 309 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 310 | "requires": { 311 | "safer-buffer": ">= 2.1.2 < 3" 312 | } 313 | }, 314 | "ip-regex": { 315 | "version": "2.1.0", 316 | "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", 317 | "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" 318 | }, 319 | "is-potential-custom-element-name": { 320 | "version": "1.0.0", 321 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", 322 | "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=" 323 | }, 324 | "is-typedarray": { 325 | "version": "1.0.0", 326 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 327 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 328 | }, 329 | "isstream": { 330 | "version": "0.1.2", 331 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 332 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 333 | }, 334 | "jsbn": { 335 | "version": "0.1.1", 336 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 337 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 338 | }, 339 | "jsdom": { 340 | "version": "16.2.2", 341 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.2.tgz", 342 | "integrity": "sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg==", 343 | "requires": { 344 | "abab": "^2.0.3", 345 | "acorn": "^7.1.1", 346 | "acorn-globals": "^6.0.0", 347 | "cssom": "^0.4.4", 348 | "cssstyle": "^2.2.0", 349 | "data-urls": "^2.0.0", 350 | "decimal.js": "^10.2.0", 351 | "domexception": "^2.0.1", 352 | "escodegen": "^1.14.1", 353 | "html-encoding-sniffer": "^2.0.1", 354 | "is-potential-custom-element-name": "^1.0.0", 355 | "nwsapi": "^2.2.0", 356 | "parse5": "5.1.1", 357 | "request": "^2.88.2", 358 | "request-promise-native": "^1.0.8", 359 | "saxes": "^5.0.0", 360 | "symbol-tree": "^3.2.4", 361 | "tough-cookie": "^3.0.1", 362 | "w3c-hr-time": "^1.0.2", 363 | "w3c-xmlserializer": "^2.0.0", 364 | "webidl-conversions": "^6.0.0", 365 | "whatwg-encoding": "^1.0.5", 366 | "whatwg-mimetype": "^2.3.0", 367 | "whatwg-url": "^8.0.0", 368 | "ws": "^7.2.3", 369 | "xml-name-validator": "^3.0.0" 370 | }, 371 | "dependencies": { 372 | "parse5": { 373 | "version": "5.1.1", 374 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", 375 | "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" 376 | } 377 | } 378 | }, 379 | "json-schema": { 380 | "version": "0.2.3", 381 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 382 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 383 | }, 384 | "json-schema-traverse": { 385 | "version": "0.4.1", 386 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 387 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 388 | }, 389 | "json-stringify-safe": { 390 | "version": "5.0.1", 391 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 392 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 393 | }, 394 | "jsonfile": { 395 | "version": "6.0.1", 396 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", 397 | "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", 398 | "requires": { 399 | "graceful-fs": "^4.1.6", 400 | "universalify": "^1.0.0" 401 | } 402 | }, 403 | "jsprim": { 404 | "version": "1.4.1", 405 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 406 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 407 | "requires": { 408 | "assert-plus": "1.0.0", 409 | "extsprintf": "1.3.0", 410 | "json-schema": "0.2.3", 411 | "verror": "1.10.0" 412 | } 413 | }, 414 | "levn": { 415 | "version": "0.3.0", 416 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 417 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 418 | "requires": { 419 | "prelude-ls": "~1.1.2", 420 | "type-check": "~0.3.2" 421 | } 422 | }, 423 | "lodash": { 424 | "version": "4.17.15", 425 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 426 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 427 | }, 428 | "lodash.sortby": { 429 | "version": "4.7.0", 430 | "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", 431 | "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" 432 | }, 433 | "lodash.toarray": { 434 | "version": "4.4.0", 435 | "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", 436 | "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" 437 | }, 438 | "mime-db": { 439 | "version": "1.44.0", 440 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 441 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 442 | }, 443 | "mime-types": { 444 | "version": "2.1.27", 445 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 446 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 447 | "requires": { 448 | "mime-db": "1.44.0" 449 | } 450 | }, 451 | "node-emoji": { 452 | "version": "1.10.0", 453 | "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", 454 | "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", 455 | "requires": { 456 | "lodash.toarray": "^4.4.0" 457 | } 458 | }, 459 | "node-fetch": { 460 | "version": "2.6.0", 461 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 462 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 463 | }, 464 | "nwsapi": { 465 | "version": "2.2.0", 466 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", 467 | "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" 468 | }, 469 | "oauth-sign": { 470 | "version": "0.9.0", 471 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 472 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 473 | }, 474 | "optionator": { 475 | "version": "0.8.3", 476 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 477 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 478 | "requires": { 479 | "deep-is": "~0.1.3", 480 | "fast-levenshtein": "~2.0.6", 481 | "levn": "~0.3.0", 482 | "prelude-ls": "~1.1.2", 483 | "type-check": "~0.3.2", 484 | "word-wrap": "~1.2.3" 485 | } 486 | }, 487 | "performance-now": { 488 | "version": "2.1.0", 489 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 490 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 491 | }, 492 | "prelude-ls": { 493 | "version": "1.1.2", 494 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 495 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" 496 | }, 497 | "psl": { 498 | "version": "1.8.0", 499 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 500 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 501 | }, 502 | "punycode": { 503 | "version": "2.1.1", 504 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 505 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 506 | }, 507 | "qs": { 508 | "version": "6.5.2", 509 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 510 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 511 | }, 512 | "request": { 513 | "version": "2.88.2", 514 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 515 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 516 | "requires": { 517 | "aws-sign2": "~0.7.0", 518 | "aws4": "^1.8.0", 519 | "caseless": "~0.12.0", 520 | "combined-stream": "~1.0.6", 521 | "extend": "~3.0.2", 522 | "forever-agent": "~0.6.1", 523 | "form-data": "~2.3.2", 524 | "har-validator": "~5.1.3", 525 | "http-signature": "~1.2.0", 526 | "is-typedarray": "~1.0.0", 527 | "isstream": "~0.1.2", 528 | "json-stringify-safe": "~5.0.1", 529 | "mime-types": "~2.1.19", 530 | "oauth-sign": "~0.9.0", 531 | "performance-now": "^2.1.0", 532 | "qs": "~6.5.2", 533 | "safe-buffer": "^5.1.2", 534 | "tough-cookie": "~2.5.0", 535 | "tunnel-agent": "^0.6.0", 536 | "uuid": "^3.3.2" 537 | }, 538 | "dependencies": { 539 | "tough-cookie": { 540 | "version": "2.5.0", 541 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 542 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 543 | "requires": { 544 | "psl": "^1.1.28", 545 | "punycode": "^2.1.1" 546 | } 547 | } 548 | } 549 | }, 550 | "request-promise-core": { 551 | "version": "1.1.3", 552 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", 553 | "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", 554 | "requires": { 555 | "lodash": "^4.17.15" 556 | } 557 | }, 558 | "request-promise-native": { 559 | "version": "1.0.8", 560 | "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", 561 | "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", 562 | "requires": { 563 | "request-promise-core": "1.1.3", 564 | "stealthy-require": "^1.1.1", 565 | "tough-cookie": "^2.3.3" 566 | }, 567 | "dependencies": { 568 | "tough-cookie": { 569 | "version": "2.5.0", 570 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 571 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 572 | "requires": { 573 | "psl": "^1.1.28", 574 | "punycode": "^2.1.1" 575 | } 576 | } 577 | } 578 | }, 579 | "safe-buffer": { 580 | "version": "5.2.0", 581 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 582 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 583 | }, 584 | "safer-buffer": { 585 | "version": "2.1.2", 586 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 587 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 588 | }, 589 | "saxes": { 590 | "version": "5.0.1", 591 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", 592 | "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", 593 | "requires": { 594 | "xmlchars": "^2.2.0" 595 | } 596 | }, 597 | "source-map": { 598 | "version": "0.6.1", 599 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 600 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 601 | "optional": true 602 | }, 603 | "sshpk": { 604 | "version": "1.16.1", 605 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 606 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 607 | "requires": { 608 | "asn1": "~0.2.3", 609 | "assert-plus": "^1.0.0", 610 | "bcrypt-pbkdf": "^1.0.0", 611 | "dashdash": "^1.12.0", 612 | "ecc-jsbn": "~0.1.1", 613 | "getpass": "^0.1.1", 614 | "jsbn": "~0.1.0", 615 | "safer-buffer": "^2.0.2", 616 | "tweetnacl": "~0.14.0" 617 | } 618 | }, 619 | "stealthy-require": { 620 | "version": "1.1.1", 621 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", 622 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" 623 | }, 624 | "symbol-tree": { 625 | "version": "3.2.4", 626 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", 627 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" 628 | }, 629 | "tough-cookie": { 630 | "version": "3.0.1", 631 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", 632 | "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", 633 | "requires": { 634 | "ip-regex": "^2.1.0", 635 | "psl": "^1.1.28", 636 | "punycode": "^2.1.1" 637 | } 638 | }, 639 | "tr46": { 640 | "version": "2.0.2", 641 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", 642 | "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", 643 | "requires": { 644 | "punycode": "^2.1.1" 645 | } 646 | }, 647 | "tunnel-agent": { 648 | "version": "0.6.0", 649 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 650 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 651 | "requires": { 652 | "safe-buffer": "^5.0.1" 653 | } 654 | }, 655 | "tweetnacl": { 656 | "version": "0.14.5", 657 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 658 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 659 | }, 660 | "type-check": { 661 | "version": "0.3.2", 662 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 663 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 664 | "requires": { 665 | "prelude-ls": "~1.1.2" 666 | } 667 | }, 668 | "universalify": { 669 | "version": "1.0.0", 670 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", 671 | "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" 672 | }, 673 | "uri-js": { 674 | "version": "4.2.2", 675 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 676 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 677 | "requires": { 678 | "punycode": "^2.1.0" 679 | } 680 | }, 681 | "uuid": { 682 | "version": "3.4.0", 683 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 684 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 685 | }, 686 | "verror": { 687 | "version": "1.10.0", 688 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 689 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 690 | "requires": { 691 | "assert-plus": "^1.0.0", 692 | "core-util-is": "1.0.2", 693 | "extsprintf": "^1.2.0" 694 | } 695 | }, 696 | "w3c-hr-time": { 697 | "version": "1.0.2", 698 | "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", 699 | "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", 700 | "requires": { 701 | "browser-process-hrtime": "^1.0.0" 702 | } 703 | }, 704 | "w3c-xmlserializer": { 705 | "version": "2.0.0", 706 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", 707 | "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", 708 | "requires": { 709 | "xml-name-validator": "^3.0.0" 710 | } 711 | }, 712 | "webidl-conversions": { 713 | "version": "6.1.0", 714 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", 715 | "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" 716 | }, 717 | "whatwg-encoding": { 718 | "version": "1.0.5", 719 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", 720 | "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", 721 | "requires": { 722 | "iconv-lite": "0.4.24" 723 | } 724 | }, 725 | "whatwg-mimetype": { 726 | "version": "2.3.0", 727 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", 728 | "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" 729 | }, 730 | "whatwg-url": { 731 | "version": "8.1.0", 732 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", 733 | "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", 734 | "requires": { 735 | "lodash.sortby": "^4.7.0", 736 | "tr46": "^2.0.2", 737 | "webidl-conversions": "^5.0.0" 738 | }, 739 | "dependencies": { 740 | "webidl-conversions": { 741 | "version": "5.0.0", 742 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", 743 | "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" 744 | } 745 | } 746 | }, 747 | "word-wrap": { 748 | "version": "1.2.3", 749 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 750 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" 751 | }, 752 | "ws": { 753 | "version": "7.3.0", 754 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", 755 | "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" 756 | }, 757 | "xml-name-validator": { 758 | "version": "3.0.0", 759 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", 760 | "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" 761 | }, 762 | "xmlchars": { 763 | "version": "2.2.0", 764 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 765 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" 766 | } 767 | } 768 | } 769 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thread-reader-reader", 3 | "version": "0.1.0", 4 | "description": "Parse Twitter thread already simplified by third-party Thread Reader app, and produce dead simple HTML.", 5 | "main": "cli.js", 6 | "bin": { 7 | "thread-reader-reader": "./cli.js" 8 | }, 9 | "dependencies": { 10 | "fs-extra": "^9.0.0", 11 | "jsdom": "^16.2.2", 12 | "node-emoji": "^1.10.0", 13 | "node-fetch": "^2.6.0" 14 | }, 15 | "devDependencies": {}, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "Benjamin Becquet (https://bbecquet.net)", 20 | "license": "WTFPL" 21 | } 22 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | const queryDOM = element => selector => Array.from(element.querySelectorAll(selector)); 2 | 3 | const getImageData = img => ({ 4 | url: queryDOM(img)('img')[0].getAttribute('data-src') 5 | }); 6 | 7 | const getVideoData = video => { 8 | const sources = queryDOM(video)('source'); 9 | return { 10 | poster: queryDOM(video)('video')[0].getAttribute('poster'), 11 | sources: sources.map(source => ({ 12 | src: source.getAttribute('src'), 13 | type: source.getAttribute('type'), 14 | })) 15 | } 16 | }; 17 | 18 | function parseTweet(tweet) { 19 | const $tweet = queryDOM(tweet); 20 | 21 | const images = $tweet('.entity-image').map(getImageData); 22 | const videos = $tweet('.entity-video').map(getVideoData); 23 | 24 | // Clean stuff. /!\ Modify the original node. 25 | const ignoreSelector = ['.row', '.entity-image', '.entity-video', '.tw-permalink'].join(','); 26 | $tweet(ignoreSelector).forEach(child => child.remove()); 27 | $tweet('a.entity-url').forEach(child => { 28 | child.removeAttribute('data-preview'); 29 | child.removeAttribute('class'); 30 | }); 31 | const tweetHTML = tweet.innerHTML; 32 | 33 | return { tweetHTML, images, videos }; 34 | } 35 | 36 | function getTweets(threadReaderDoc) { 37 | return queryDOM(threadReaderDoc)('.t-main .content-tweet').map(parseTweet); 38 | } 39 | 40 | if (typeof exports === 'object' && typeof module !== 'undefined') { 41 | exports.getTweets = getTweets; 42 | } 43 | -------------------------------------------------------------------------------- /resourceManager.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | const resourceBaseName = url => path.basename(new URL(url).pathname); 6 | const rewriteUrl = urlPath => url => path.join(urlPath, resourceBaseName(url)); 7 | 8 | function download(url, destPath) { 9 | return fetch(url) 10 | .then(response => response.buffer()) 11 | .then(buffer => fs.outputFile(destPath, buffer)); 12 | } 13 | 14 | function downloadResources(urls, localPath) { 15 | return Promise.all(urls.map(async url => 16 | download(url, path.join(localPath, resourceBaseName(url))) 17 | )); 18 | } 19 | 20 | if (typeof exports === 'object' && typeof module !== 'undefined') { 21 | exports.downloadResources = downloadResources; 22 | exports.rewriteUrl = rewriteUrl; 23 | } --------------------------------------------------------------------------------