commit 974fa1660adac0c5046cfddbae60851adca55c83 Author: Marcus Noble Date: Sun Mar 22 13:51:40 2020 +0000 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9c5c92f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.env +Makefile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..992f45f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +node_modules +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1f6020 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:10-alpine + +RUN apk add --no-cache chromium nss freetype freetype-dev harfbuzz ca-certificates ttf-freefont + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +ENV CHROMIUM_PATH "/usr/bin/chromium-browser" + +WORKDIR /app + +ADD package.json . +RUN npm install + +ADD . . + +CMD npm start diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a78a3b --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.DEFAULT_GOAL := default + +IMAGE := docker.cloud.cluster.fun/averagemarcus/website-to-remarkable:latest + +.PHONY: docker-build # Build the docker image +docker-build: + @docker build -t $(IMAGE) . + +.PHONY: publish # Publish the docker image to the Artifactory registry +publish: + @docker push $(IMAGE) + +.PHONY: help # Show this list of commands +help: + @grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1: \2/' | expand -t20 + +default: format test build + diff --git a/failure.html b/failure.html new file mode 100644 index 0000000..d325eca --- /dev/null +++ b/failure.html @@ -0,0 +1,29 @@ + + + + + + + Website to reMarkable + + + +

⚠️

+

Failed to send to reMarkable

+ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ee539b --- /dev/null +++ b/index.html @@ -0,0 +1,54 @@ + + + + + + + Website to reMarkable + + + + +

🚀

+ + + + + + + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..df5bc3e --- /dev/null +++ b/index.js @@ -0,0 +1,100 @@ +require("dotenv").config(); +const http = require('http'); +const fs = require("fs"); +const puppeteer = require('puppeteer'); +const { Remarkable } = require('remarkable-typescript'); + +const client = new Remarkable({ token: process.env.REMARKABLE_TOKEN }); + +const server = http.createServer(async (req, res) => { + const incomingURL = new URL(`http://localhost:8000${req.url}`); + + if (incomingURL.searchParams.get("website")) { + const website = new URL(incomingURL.searchParams.get("website")); + console.log(`Fetching '${website.toString()}'`); + if (await sendPage(website)) { + fs.readFile(__dirname + "/success.html", function (err,data) { + if (err) { + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end(data); + }); + } else { + fs.readFile(__dirname + "/failure.html", function (err,data) { + if (err) { + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } + res.writeHead(500, {'Content-Type': 'text/html'}); + res.end(data); + }); + } + }else { + let url = req.url === "/" ? "/index.html": req.url; + fs.readFile(__dirname + url || "/index.html", function (err,data) { + if (err) { + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } + + if (url.endsWith(".js")) { + res.writeHead(200, {'Content-Type': 'application/javascript'}); + } else if (url.endsWith(".json")) { + res.writeHead(200, {'Content-Type': 'application/json'}); + } else if (url.endsWith(".png")) { + res.writeHead(200, {'Content-Type': 'image/png'}); + } else { + res.writeHead(200, {'Content-Type': 'text/html'}); + } + + res.end(data); + }); + } +}); + +server.listen(8000); + +async function sendPage(website, tries = 0) { + const browser = await puppeteer.launch({ + executablePath: process.env.CHROMIUM_PATH, + args: ['--disable-dev-shm-usage', '--no-sandbox'] + }); + try { + const page = await browser.newPage(); + await page.emulate(Object.assign({}, puppeteer.devices["iPad Pro"], { userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" })); + await page.goto(website.toString()); + const title = await page.title() + console.log("Page loaded. Title - " + title) + + await page.evaluate( function(){ + [...document.querySelectorAll('*')].forEach(node => { + const pos = window.getComputedStyle(node).getPropertyValue("position"); + if (pos == "fixed" || pos == "sticky") { + node.style.position = "unset"; + } + }) + } ); + + const myPDF = await page.pdf({ format: 'A3' }); + console.log("Saved to PDF") + + await client.uploadPDF(title, myPDF); + console.log("Uploaded to reMarkable"); + + return true; + } catch (ex) { + console.log(ex); + if (tries < 5) { + return await sendPage(website, ++tries); + } else { + return false; + } + } finally { + await browser.close(); + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..346f5f5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,26 @@ +{ + "name": "Website-to-reMarkable", + "short_name": "Website-to-reMarkable", + "description": "Send websites as PDFs to reMarkable", + "theme_color": "#999", + "background_color": "#999", + "display": "standalone", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "rocket.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "splash_pages": null, + "share_target": { + "action": "/", + "method": "GET", + "params": { + "url": "website", + "text": "website" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3013050 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "website-to-remarkable", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "crypto-random-string": "^3.2.0", + "dotenv": "^8.2.0", + "express": "^4.17.1", + "puppeteer": "^2.1.1", + "remarkable-typescript": "^1.0.5" + } +} diff --git a/rocket.png b/rocket.png new file mode 100644 index 0000000..3188cb8 Binary files /dev/null and b/rocket.png differ diff --git a/success.html b/success.html new file mode 100644 index 0000000..1514c43 --- /dev/null +++ b/success.html @@ -0,0 +1,29 @@ + + + + + + + Website to reMarkable + + + +

🚀

+

Sent to reMarkable

+ + diff --git a/sw-toolbox.js b/sw-toolbox.js new file mode 100644 index 0000000..dacb855 --- /dev/null +++ b/sw-toolbox.js @@ -0,0 +1,16 @@ +/* + Copyright 2016 Google Inc. All Rights Reserved. + + 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. +*/!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.toolbox=e()}}(function(){return function e(t,n,r){function o(c,s){if(!n[c]){if(!t[c]){var a="function"==typeof require&&require;if(!s&&a)return a(c,!0);if(i)return i(c,!0);var u=new Error("Cannot find module '"+c+"'");throw u.code="MODULE_NOT_FOUND",u}var f=n[c]={exports:{}};t[c][0].call(f.exports,function(e){var n=t[c][1][e];return o(n||e)},f,f.exports,e,t,n,r)}return n[c].exports}for(var i="function"==typeof require&&require,c=0;ct.value[l]){var r=t.value[p];c.push(r),a.delete(r),t.continue()}},s.oncomplete=function(){r(c)},s.onabort=o}):Promise.resolve([])}function s(e,t){return t?new Promise(function(n,r){var o=[],i=e.transaction(h,"readwrite"),c=i.objectStore(h),s=c.index(l),a=s.count();s.count().onsuccess=function(){var e=a.result;e>t&&(s.openCursor().onsuccess=function(n){var r=n.target.result;if(r){var i=r.value[p];o.push(i),c.delete(i),e-o.length>t&&r.continue()}})},i.oncomplete=function(){n(o)},i.onabort=r}):Promise.resolve([])}function a(e,t,n,r){return c(e,n,r).then(function(n){return s(e,t).then(function(e){return n.concat(e)})})}var u="sw-toolbox-",f=1,h="store",p="url",l="timestamp",d={};t.exports={getDb:o,setTimestampForUrl:i,expireEntries:a}},{}],3:[function(e,t,n){"use strict";function r(e){var t=a.match(e.request);t?e.respondWith(t(e.request)):a.default&&"GET"===e.request.method&&0===e.request.url.indexOf("http")&&e.respondWith(a.default(e.request))}function o(e){s.debug("activate event fired");var t=u.cache.name+"$$$inactive$$$";e.waitUntil(s.renameCache(t,u.cache.name))}function i(e){return e.reduce(function(e,t){return e.concat(t)},[])}function c(e){var t=u.cache.name+"$$$inactive$$$";s.debug("install event fired"),s.debug("creating cache ["+t+"]"),e.waitUntil(s.openCache({cache:{name:t}}).then(function(e){return Promise.all(u.preCacheItems).then(i).then(s.validatePrecacheInput).then(function(t){return s.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}e("serviceworker-cache-polyfill");var s=e("./helpers"),a=e("./router"),u=e("./options");t.exports={fetchListener:r,activateListener:o,installListener:c}},{"./helpers":1,"./options":4,"./router":6,"serviceworker-cache-polyfill":16}],4:[function(e,t,n){"use strict";var r;r=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,t.exports={cache:{name:"$$$toolbox-cache$$$"+r+"$$$",maxAgeSeconds:null,maxEntries:null,queryOptions:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/}},{}],5:[function(e,t,n){"use strict";var r=new URL("./",self.location),o=r.pathname,i=e("path-to-regexp"),c=function(e,t,n,r){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=o+t),this.keys=[],this.regexp=i(t,this.keys)),this.method=e,this.options=r,this.handler=n};c.prototype.makeHandler=function(e){var t;if(this.regexp){var n=this.regexp.exec(e);t={},this.keys.forEach(function(e,r){t[e.name]=n[r+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},t.exports=c},{"path-to-regexp":15}],6:[function(e,t,n){"use strict";function r(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var o=e("./route"),i=e("./helpers"),c=function(e,t){for(var n=e.entries(),r=n.next(),o=[];!r.done;){new RegExp(r.value[0]).test(t)&&o.push(r.value[1]),r=n.next()}return o},s=function(){this.routes=new Map,this.routes.set(RegExp,new Map),this.default=null};["get","post","put","delete","head","any"].forEach(function(e){s.prototype[e]=function(t,n,r){return this.add(e,t,n,r)}}),s.prototype.add=function(e,t,n,c){c=c||{};var s;t instanceof RegExp?s=RegExp:(s=c.origin||self.location.origin,s=s instanceof RegExp?s.source:r(s)),e=e.toLowerCase();var a=new o(e,t,n,c);this.routes.has(s)||this.routes.set(s,new Map);var u=this.routes.get(s);u.has(e)||u.set(e,new Map);var f=u.get(e),h=a.regexp||a.fullUrlRegExp;f.has(h.source)&&i.debug('"'+t+'" resolves to same regex as existing route.'),f.set(h.source,a)},s.prototype.matchMethod=function(e,t){var n=new URL(t),r=n.origin,o=n.pathname;return this._match(e,c(this.routes,r),o)||this._match(e,[this.routes.get(RegExp)],t)},s.prototype._match=function(e,t,n){if(0===t.length)return null;for(var r=0;r0)return s[0].makeHandler(n)}}return null},s.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},t.exports=new s},{"./helpers":1,"./route":5}],7:[function(e,t,n){"use strict";function r(e,t,n){n=n||{};var r=n.cache||o.cache,c=r.queryOptions;return i.debug("Strategy: cache first ["+e.url+"]",n),i.openCache(n).then(function(t){return t.match(e,c).then(function(t){var o=Date.now();return i.isResponseFresh(t,r.maxAgeSeconds,o)?t:i.fetchAndCache(e,n)})})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],8:[function(e,t,n){"use strict";function r(e,t,n){n=n||{};var r=n.cache||o.cache,c=r.queryOptions;return i.debug("Strategy: cache only ["+e.url+"]",n),i.openCache(n).then(function(t){return t.match(e,c).then(function(e){var t=Date.now();if(i.isResponseFresh(e,r.maxAgeSeconds,t))return e})})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],9:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: fastest ["+e.url+"]",n),new Promise(function(r,c){var s=!1,a=[],u=function(e){a.push(e.toString()),s?c(new Error('Both cache and network failed: "'+a.join('", "')+'"')):s=!0},f=function(e){e instanceof Response?r(e):u("No result returned")};o.fetchAndCache(e.clone(),n).then(f,u),i(e,t,n).then(f,u)})}var o=e("../helpers"),i=e("./cacheOnly");t.exports=r},{"../helpers":1,"./cacheOnly":8}],10:[function(e,t,n){t.exports={networkOnly:e("./networkOnly"),networkFirst:e("./networkFirst"),cacheOnly:e("./cacheOnly"),cacheFirst:e("./cacheFirst"),fastest:e("./fastest")}},{"./cacheFirst":7,"./cacheOnly":8,"./fastest":9,"./networkFirst":11,"./networkOnly":12}],11:[function(e,t,n){"use strict";function r(e,t,n){n=n||{};var r=n.cache||o.cache,c=r.queryOptions,s=n.successResponses||o.successResponses,a=n.networkTimeoutSeconds||o.networkTimeoutSeconds;return i.debug("Strategy: network first ["+e.url+"]",n),i.openCache(n).then(function(t){var o,u,f=[];if(a){var h=new Promise(function(n){o=setTimeout(function(){t.match(e,c).then(function(e){var t=Date.now(),o=r.maxAgeSeconds;i.isResponseFresh(e,o,t)&&n(e)})},1e3*a)});f.push(h)}var p=i.fetchAndCache(e,n).then(function(e){if(o&&clearTimeout(o),s.test(e.status))return e;throw i.debug("Response was an HTTP error: "+e.statusText,n),u=e,new Error("Bad response")}).catch(function(r){return i.debug("Network or response error, fallback to cache ["+e.url+"]",n),t.match(e,c).then(function(e){if(e)return e;if(u)return u;throw r})});return f.push(p),Promise.race(f)})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],12:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: network only ["+e.url+"]",n),fetch(e)}var o=e("../helpers");t.exports=r},{"../helpers":1}],13:[function(e,t,n){"use strict";var r=e("./options"),o=e("./router"),i=e("./helpers"),c=e("./strategies"),s=e("./listeners");i.debug("Service Worker Toolbox is loading"),self.addEventListener("install",s.installListener),self.addEventListener("activate",s.activateListener),self.addEventListener("fetch",s.fetchListener),t.exports={networkOnly:c.networkOnly,networkFirst:c.networkFirst,cacheOnly:c.cacheOnly,cacheFirst:c.cacheFirst,fastest:c.fastest,router:o,options:r,cache:i.cache,uncache:i.uncache,precache:i.precache}},{"./helpers":1,"./listeners":3,"./options":4,"./router":6,"./strategies":10}],14:[function(e,t,n){t.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},{}],15:[function(e,t,n){function r(e,t){for(var n,r=[],o=0,i=0,c="",s=t&&t.delimiter||"/";null!=(n=x.exec(e));){var f=n[0],h=n[1],p=n.index;if(c+=e.slice(i,p),i=p+f.length,h)c+=h[1];else{var l=e[i],d=n[2],m=n[3],g=n[4],v=n[5],w=n[6],y=n[7];c&&(r.push(c),c="");var b=null!=d&&null!=l&&l!==d,E="+"===w||"*"===w,R="?"===w||"*"===w,k=n[2]||s,$=g||v;r.push({name:m||o++,prefix:d||"",delimiter:k,optional:R,repeat:E,partial:b,asterisk:!!y,pattern:$?u($):y?".*":"[^"+a(k)+"]+?"})}}return i=46||"Chrome"===n&&r>=50)||(Cache.prototype.addAll=function(e){function t(e){this.name="NetworkError",this.code=19,this.message=e}var n=this;return t.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return e=e.map(function(e){return e instanceof Request?e:String(e)}),Promise.all(e.map(function(e){"string"==typeof e&&(e=new Request(e));var n=new URL(e.url).protocol;if("http:"!==n&&"https:"!==n)throw new t("Invalid scheme");return fetch(e.clone())}))}).then(function(r){if(r.some(function(e){return!e.ok}))throw new t("Incorrect response status");return Promise.all(r.map(function(t,r){return n.put(e[r],t)}))}).then(function(){})},Cache.prototype.add=function(e){return this.addAll([e])})}()},{}]},{},[13])(13)}); +//# sourceMappingURL=sw-toolbox.js.map diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..de136d7 --- /dev/null +++ b/sw.js @@ -0,0 +1,3 @@ +importScripts('sw-toolbox.js'); +toolbox.precache([]); +toolbox.router.get('/*', toolbox.networkFirst, { networkTimeoutSeconds: 60 });