Skip to content

Commit

Permalink
Merge pull request #472 from GoogleChromeLabs/rss-api-fixes
Browse files Browse the repository at this point in the history
RSS feed and API changes
  • Loading branch information
juliantoledo authored Mar 22, 2018
2 parents b7fae4d + 43a3fc8 commit aa79116
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 98 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
// http://eslint.org/docs/user-guide/configuring#specifying-environments
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2017
}
}
145 changes: 90 additions & 55 deletions controllers/api/pwa.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,11 @@
const express = require('express');
require('express-csv');
const pwaLib = require('../../lib/pwa');
const libMetadata = require('../../lib/metadata');
const router = express.Router(); // eslint-disable-line new-cap
const CACHE_CONTROL_EXPIRES = 60 * 60 * 1; // 1 hour
const RSS = require('rss');

const config = require('../../config/config');
const apiKeyArray = config.get('API_TOKENS');

/**
* Checks for the presence of an API key from API_TOKENS in config.json
*
* Skip API key check of RSS feed
*/
function checkApiKey(req, res, next) {
if (req.query.key &&
(apiKeyArray === req.query.key ||
apiKeyArray.indexOf(req.query.key) !== -1) ||
req.query.format === 'rss') {
return next();
}
return res.sendStatus(403);
}

function getDate(date) {
return new Date(date).toISOString().split('T')[0];
}
Expand Down Expand Up @@ -88,34 +71,83 @@ class JsonWriter {
}
}

function render(res, view, options) {
return new Promise((resolve, reject) => {
res.render(view, options, (err, html) => {
if (err) {
console.log(err);
reject(err);
}
resolve(html);
});
});
}

function renderOnePwaRss(pwa, req, res) {
const url = req.originalUrl;
const contentOnly = false || req.query.contentOnly;
let arg = Object.assign(libMetadata.fromRequest(req, url), {
pwa: pwa,
title: 'PWA Directory: ' + pwa.name,
description: 'PWA Directory: ' + pwa.name + ' - ' + pwa.description,
backlink: true,
contentOnly: contentOnly
});
return render(res, 'pwas/view-rss.hbs', arg);
}

async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

class RssWriter {
write(result, pwas) {
write(req, res, pwas) {
const feed = new RSS({
/* eslint-disable camelcase */
title: 'PWA Directory',
description: 'A Directory of Progressive Web Apps',
feed_url: 'https://pwa-directory.appspot.com/api/pwa?format=rss',
feed_url: 'https://pwa-directory.appspot.com/api/pwa/?format=rss',
site_url: 'https://pwa-directory.appspot.com/',
image_url: 'https://pwa-directory.appspot.com/favicons/android-chrome-144x144.png',
pubDate: new Date(),
custom_namespaces: {
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
l: 'http://purl.org/rss/1.0/modules/link/',
media: 'http://search.yahoo.com/mrss/',
content: 'http://purl.org/rss/1.0/modules/content/'
}
});

pwas.forEach(pwa => {
feed.item({
title: pwa.displayName,
description: pwa.description,
url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id,
guid: pwa.id,
date: pwa.created,
custom_elements: [{'content:encoded': JSON.stringify(pwa)}]
const start = async _ => {
await asyncForEach(pwas, async pwa => {
let html = await renderOnePwaRss(pwa, req, res);

const customElements = [];
customElements.push({'content:encoded': html});
customElements.push({'l:link': {_attr: {'l:rel': 'http://purl.org/rss/1.0/modules/link/#alternate',
'l:type': 'application/json',
'rdf:resource': 'https://pwa-directory.appspot.com/api/pwa/' + pwa.id}}});
if (pwa.iconUrl128) {
customElements.push({'media:thumbnail': {_attr: {url: pwa.iconUrl128,
height: '128', width: '128'}}});
}

feed.item({
title: pwa.displayName,
url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id,
description: html,
guid: pwa.id,
date: pwa.created,
custom_elements: customElements
});
});
});
res.setHeader('Content-Type', 'application/rss+xml');
res.status(200).send(feed.xml());
};
start();
/* eslint-enable camelcase */
result.setHeader('Content-Type', 'application/rss+xml');
result.status(200).send(feed.xml());
}
}

Expand All @@ -126,36 +158,39 @@ const rssWriter = new RssWriter();
/**
* GET /api/pwa
*
* Returns all PWAs as JSON or ?format=csv for CSV.
* Returns all PWAs as JSON, ?format=csv for CSV or ?format=rss for RSS feed
*/
router.get('/', checkApiKey, (req, res) => {
router.get('/:id*?', (req, res) => {
let format = req.query.format || 'json';
let sort = req.query.sort || 'newest';
let skip = parseInt(req.query.skip, 10);
let limit = parseInt(req.query.limit, 10);

let limit = parseInt(req.query.limit, 10) || 100;
res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);
pwaLib.list(skip, limit, sort)
.then(result => {
switch (format) {
case 'csv': {
csvWriter.write(res, result.pwas);
break;
}
case 'rss': {
rssWriter.write(res, result.pwas);
break;
}
default: {
jsonWriter.write(res, result.pwas);
}

let queryPromise = req.params.id ? pwaLib.find(req.params.id) : pwaLib.list(skip, limit, sort);
queryPromise
.then(result => {
result = result.pwas ? result : {pwas: [result]};
switch (format) {
case 'csv': {
csvWriter.write(res, result.pwas);
break;
}
})
.catch(err => {
console.log(err);
res.status(500);
res.json(err);
});
case 'rss': {
rssWriter.write(req, res, result.pwas);
break;
}
default: {
jsonWriter.write(res, result.pwas);
}
}
})
.catch(err => {
console.log(err);
let code = err.code || 500;
res.status(code);
res.json(err);
});
});

module.exports = router;
8 changes: 4 additions & 4 deletions lighthouse_machine/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ runtime: custom
env: flex
service: lighthouse-machine
automatic_scaling:
min_num_instances: 4
max_num_instances: 12
min_num_instances: 2
max_num_instances: 8
cool_down_period_sec: 60
cpu_utilization:
target_utilization: 0.6

resources:
cpu: 2
memory_gb: 8
cpu: 1
memory_gb: 6
disk_size_gb: 10

handlers:
Expand Down
Binary file added public/img/feed-icon-24px.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/feed-icon-48px.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 1 addition & 38 deletions test/app/controllers/api/pwa.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,7 @@ describe('controllers.api.pwa', () => {
result.pwas = [pwa];

afterEach(() => {
// simpleMock.restore();
});

it('respond with 400 without API key', done => {
simpleMock.mock(libPwa, 'list');
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa/')
.expect(400)
.expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => {
assert.equal(libPwa.list.callCount, 0);
done();
});
});

it('respond with 400 with wrong API key', done => {
simpleMock.mock(libPwa, 'list');
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa?key=xxxxxxx')
.expect(400)
.expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => {
assert.equal(libPwa.list.callCount, 0);
done();
});
simpleMock.restore();
});

it('respond with 200 and json', done => {
Expand All @@ -103,18 +79,5 @@ describe('controllers.api.pwa', () => {
done();
});
});

it('respond with 200 and rss, without key', done => {
simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result));
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa?format=rss')
.expect(200)
.expect('Content-Type', 'application/rss+xml; charset=utf-8')
.should.be.fulfilled.then(_ => {
assert.equal(libPwa.list.callCount, 1);
done();
});
});
});
});
6 changes: 5 additions & 1 deletion views/includes/footer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
<li>
<a href="https://github.com/GoogleChrome/gulliver" target="_blank" rel="noopener">
<img id="github-logo" class="github-logo" srcset="/img/GitHub-Mark-Light-24px.png 1x, /img/GitHub-Mark-Light-48px.png 2x" width="24" height="24" alt="Github" attribution="github">
<span>View on Github</span>
</a>
</li>
<li><a href="https://github.com/GoogleChrome/gulliver/issues/new">feedback</a></li>
<li><a href="https://www.google.com/intl/en/policies/privacy/">privacy</a></li>
<li>
<a href="http://feeds.feedburner.com/PwaDirectory" target="_blank" rel="noopener">
<img id="rss-logo" class="rss-logo" srcset="/img/feed-icon-24px.png 1x, /img/feed-icon-48px.png 2x" width="24" height="24" alt="RSS">
</a>
</li>
</a>
</ul>
<footer>
Expand Down
14 changes: 14 additions & 0 deletions views/pwas/view-rss.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div style="text-align: center; vertical-align: middle; background-color:{{pwa.backgroundColor}}; color:{{contrastColor pwa.backgroundColor}}; padding: 16px;">
<a href="{{pwa.absoluteStartUrl}}" target="_blank" rel="noopener">
{{#if pwa.iconUrl128}}
<img src="{{pwa.iconUrl128}}" width="128" height="128"/>
{{/if}}
</a>
<a href="{{pwa.absoluteStartUrl}}" target="_blank" rel="noopener" style="color:{{contrastColor pwa.backgroundColor}}; text-decoration: none;"><h2>{{pwa.displayName}}</h2></a>
<div>{{pwa.description}}</div>
</div>

<a href="https://pwa-directory.appspot.com/pwas/{{pwa.id}}" target="_blank" rel="noopener">
<img style="vertical-align:middle" src="https://pwa-directory.appspot.com/favicons/android-chrome-36x36.png">
<span style="">Open in PWA Directory</span>
</a>

0 comments on commit aa79116

Please sign in to comment.