diff --git a/lib/ics.js b/lib/ics.js new file mode 100644 index 0000000..b576f33 --- /dev/null +++ b/lib/ics.js @@ -0,0 +1,21 @@ +const ics = require('ics') + +const generateICS = async(data) => { + return new Promise((resolve, reject) => { + ics.createEvent({ + title: data.title || '', + start: data.start || [], + duration: data.duration || { hours: 2 }, + location: data.location || '', + }, (err, value) => { + if (err) { + reject(err) + return + } + + resolve(value) + }) + }) +} + +module.exports = generateICS diff --git a/lib/index.js b/lib/index.js index 4ab716d..f1ce976 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ const path = require('path') const crawl = require('./crawler') const parseHTML = require('./parser') +const generateICS = require('./ics') const port = process.env.PORT || 3000 const app = express() @@ -17,19 +18,36 @@ app.get('/', (req, res) => { }) app.get('*', (req, res) => { - res.render('404') + res.status(400).render('404') }) app.post('/download', async (req, res) => { const { url } = req.body + if (!/facebook/.test(url)) { + return res + .status(500) + .render( + 'error', + { error: 'Not Facebook URL!' } + ) + } + try { const html = await crawl(url) const data = parseHTML(html) - console.log(data) + const ics = await generateICS(data) + + if (ics) { + return res + .contentType('text/calendar') + .send(200, new Buffer(ics, 'utf8')) + } } catch (err) { console.error(err) - return res.render('error', { error: err.toString() }) + return res + .status(500) + .render('error', { error: err.toString() }) } return res.render('download', { url }) diff --git a/lib/parser.js b/lib/parser.js index fb2e905..93c1a00 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,22 +1,67 @@ -const { parse } = require('node-html-parser') +const cheerio = require('cheerio') +const dayjs = require('dayjs') const parseHTML = (html) => { let data = {} - const root = parse(html) - const eventSummary = root.querySelector('#event_summary') + const root = cheerio.load(html) - const timeInfo = eventSummary.querySelector('#event_time_info') - const timeContent = timeInfo.querySelector('[content]') + const $title = root('#event_header_primary h1') + const titleContent = $title ? $title.text() : null - if ( - timeContent && - timeContent.attributes && - timeContent.attributes.content - ) { + if (titleContent) { data = { ...data, - time: timeContent.attributes.content.value, + title: titleContent, + } + } + + const $timeInfo = root('#event_time_info') + const $time = $timeInfo.find('[content]') + + const timeContent = $time ? $time.attr('content') : null + + if (timeContent) { + const parsedTimeContent = timeContent + .split('to') + .map(part => part.trim()) + const startDate = dayjs(parsedTimeContent[0]) || dayjs() + const endDate = dayjs(parsedTimeContent[1]) || dayjs() + + const start = [ + startDate.year(), + startDate.month(), + startDate.date(), + startDate.hour(), + startDate.minute(), + ] + + const end = [ + endDate.year(), + endDate.month(), + endDate.date(), + endDate.hour(), + endDate.minute() + ] + + data = { + ...data, + start, + end, + // TODO: Create duration from end date + duration: { hours: 1 }, + } + } + + const $locations = $timeInfo.next().find('table tr td').get(1) + const $location = $locations.children[0].children[0] + + const locationContent = $location ? root($location).text() : null + + if (locationContent) { + data = { + ...data, + location: locationContent, } } diff --git a/package-lock.json b/package-lock.json index 853372e..2bb17e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,45 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@hapi/address": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.2.tgz", + "integrity": "sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==" + }, + "@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==" + }, + "@hapi/hoek": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.3.1.tgz", + "integrity": "sha512-75ocgnI7HG/I01iGA3/rs0y6PXydUA/kxhFZM0HoT8NLSTnt/J8Gq03iKl4a4B/2A3iMG0ctXtxr5Hg9SGr1gw==" + }, + "@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, + "@types/node": { + "version": "12.7.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", + "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -207,6 +246,11 @@ "type-is": "~1.6.17" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -310,6 +354,19 @@ "supports-color": "^5.3.0" } }, + "cheerio": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.1", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + } + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -486,6 +543,27 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + }, + "dayjs": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.16.tgz", + "integrity": "sha512-XPmqzWz/EJiaRHjBqSJ2s6hE/BUoCIHKgdS2QPtTQtKcS9E4/Qn0WomoH1lXanWCzri+g7zPcuNV4aTZ8PMORQ==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -557,6 +635,37 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -587,6 +696,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -1541,10 +1655,30 @@ } } }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } }, "http-errors": { "version": "1.7.2", @@ -1590,6 +1724,17 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ics": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/ics/-/ics-2.18.0.tgz", + "integrity": "sha512-QEhHNWk8Bh/ukjnhXIDGoaphnPA6yiCUiym6k9slqv60SBfMVM7uM2jzLOpqQzgq+YArde3Qyf0+fz7XxLmPDQ==", + "requires": { + "@hapi/joi": "^15.1.1", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "uuid": "^3.3.3" + } + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1859,6 +2004,11 @@ "package-json": "^4.0.0" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -1995,6 +2145,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2031,14 +2186,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, - "node-html-parser": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.16.tgz", - "integrity": "sha512-cfqTZIYDdp5cGh3NvCD5dcEDP7hfyni7WgyFacmDynLlIZaF3GVlRk8yMARhWp/PobWt1KaCV8VKdP5LKWiVbg==", - "requires": { - "he": "1.1.1" - } - }, "nodemon": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.3.tgz", @@ -2098,6 +2245,14 @@ "path-key": "^2.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -2181,6 +2336,14 @@ "semver": "^5.1.0" } }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "requires": { + "@types/node": "*" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3004,6 +3167,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 8e70dd9..ecdb996 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "PORT=80 node lib/index.js", "start:dev": "nodemon lib/index.js", + "start:dev:inspect": "nodemon --inspect lib/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ @@ -19,9 +20,11 @@ "license": "ISC", "dependencies": { "body-parser": "^1.19.0", + "cheerio": "^1.0.0-rc.3", + "dayjs": "^1.8.16", "ejs": "^2.7.1", "express": "^4.17.1", - "node-html-parser": "^1.1.16", + "ics": "^2.18.0", "puppeteer": "^1.20.0" }, "devDependencies": {