forked from sam/fedifeed
allow people to filter out replies and/or boosts
This commit is contained in:
parent
e08008aaae
commit
0b567fe3a9
9 changed files with 1685 additions and 1379 deletions
142
index.js
142
index.js
|
@ -1,64 +1,78 @@
|
|||
var Express = require('express');
|
||||
var convert = require('./lib/convert');
|
||||
var serveStatic = require('serve-static');
|
||||
var request = require('request');
|
||||
var log = console.log;
|
||||
|
||||
var app = Express();
|
||||
|
||||
function logMiddleware(req,res,next){
|
||||
log(req.method.toUpperCase() +' '+ req.url);
|
||||
log( '\t'+ new Date().toISOString() );
|
||||
if(req.headers && req.headers.referer){
|
||||
log('\tReferer: '+req.headers.referer);
|
||||
}
|
||||
return next(null);
|
||||
}
|
||||
|
||||
app.use(logMiddleware);
|
||||
|
||||
app.use(
|
||||
serveStatic('static',{
|
||||
maxAge:'1d'
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/api/feed',function(req,res){
|
||||
|
||||
// get feed url
|
||||
var feedUrl = req.query.url;
|
||||
if (!feedUrl){
|
||||
res.status(400);
|
||||
res.send('You need to specify a feed URL');
|
||||
}
|
||||
|
||||
var opts = {};
|
||||
if (req.query.size){
|
||||
opts.size = req.query.size;
|
||||
}
|
||||
if (req.query.theme){
|
||||
opts.theme = req.query.theme;
|
||||
}
|
||||
if (req.query.header){
|
||||
if (req.query.header.toLowerCase() == 'no' || req.query.header.toLowerCase() == 'false'){
|
||||
opts.header = false;
|
||||
}else{
|
||||
opts.header = true;
|
||||
}
|
||||
}
|
||||
|
||||
var req = request.get(feedUrl);
|
||||
convert(req,opts,function(er,data){
|
||||
if (er){
|
||||
res.status(500);
|
||||
return res.send('Error fetching or parsing your feed.');
|
||||
}
|
||||
res.status(200);
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT || 8000,function(){
|
||||
log('listening on '+(process.env.PORT || 8000));
|
||||
});
|
||||
var Express = require('express');
|
||||
var convert = require('./lib/convert');
|
||||
var serveStatic = require('serve-static');
|
||||
var request = require('request');
|
||||
var log = console.log;
|
||||
|
||||
var app = Express();
|
||||
|
||||
function logMiddleware(req,res,next){
|
||||
log(req.method.toUpperCase() +' '+ req.url);
|
||||
log( '\t'+ new Date().toISOString() );
|
||||
if(req.headers && req.headers.referer){
|
||||
log('\tReferer: '+req.headers.referer);
|
||||
}
|
||||
return next(null);
|
||||
}
|
||||
|
||||
app.use(logMiddleware);
|
||||
|
||||
app.use(
|
||||
serveStatic('static',{
|
||||
maxAge:'1d'
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/api/feed',function(req,res){
|
||||
|
||||
// get feed url
|
||||
var feedUrl = req.query.url;
|
||||
if (!feedUrl){
|
||||
res.status(400);
|
||||
res.send('You need to specify a feed URL');
|
||||
}
|
||||
|
||||
var opts = {};
|
||||
if (req.query.size){
|
||||
opts.size = req.query.size;
|
||||
}
|
||||
if (req.query.theme){
|
||||
opts.theme = req.query.theme;
|
||||
}
|
||||
if (req.query.header){
|
||||
if (req.query.header.toLowerCase() == 'no' || req.query.header.toLowerCase() == 'false'){
|
||||
opts.header = false;
|
||||
}else{
|
||||
opts.header = true;
|
||||
}
|
||||
}
|
||||
if (req.query.boosts){
|
||||
if (req.query.boosts.toLowerCase() == 'no' || req.query.boosts.toLowerCase() == 'false'){
|
||||
opts.boosts = false;
|
||||
}else{
|
||||
opts.boosts = true;
|
||||
}
|
||||
}
|
||||
if (req.query.replies){
|
||||
if (req.query.replies.toLowerCase() == 'no' || req.query.replies.toLowerCase() == 'false'){
|
||||
opts.replies = false;
|
||||
}else{
|
||||
opts.replies = true;
|
||||
}
|
||||
}
|
||||
|
||||
var req = request.get(feedUrl);
|
||||
convert(req,opts,function(er,data){
|
||||
if (er){
|
||||
res.status(500);
|
||||
return res.send('Error fetching or parsing your feed.');
|
||||
}
|
||||
res.status(200);
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT || 8000,function(){
|
||||
log('listening on '+(process.env.PORT || 8000));
|
||||
});
|
||||
|
|
433
lib/convert.js
433
lib/convert.js
|
@ -1,204 +1,229 @@
|
|||
var pug = require('pug');
|
||||
var FeedParser = require('feedparser');
|
||||
var ejs = require('ejs');
|
||||
var fs = require('fs');
|
||||
var template = ejs.compile(fs.readFileSync('./lib/template.ejs','utf8'));
|
||||
var timeAgo = require('timeago.js');
|
||||
|
||||
function isArray(a){
|
||||
return Array.isArray(a);
|
||||
}
|
||||
|
||||
// accumulate a stream of XML into a html file
|
||||
|
||||
module.exports = function(stream,opts,callback){
|
||||
var callback = callback;
|
||||
var opts = opts;
|
||||
if (typeof opts == 'function'){
|
||||
callback = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
// convert s from atom feed to a full html page for rendering
|
||||
breakDown(stream,function(er,data){
|
||||
if (er) {
|
||||
return callback(er);
|
||||
}
|
||||
// try and build up
|
||||
try{
|
||||
var result = buildUp(data,opts)
|
||||
}catch(e){
|
||||
return callback(e);
|
||||
}
|
||||
return callback(null,result);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// break the xml into json
|
||||
function breakDown(stream,callback){
|
||||
|
||||
var spent = false;
|
||||
function cbOnce(er,data){
|
||||
if (!spent){
|
||||
callback(er,data);
|
||||
spent = true;
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('error',cbOnce);
|
||||
var feedparser = new FeedParser();
|
||||
feedparser.on('error', cbOnce);
|
||||
stream.pipe(feedparser)
|
||||
|
||||
|
||||
feedparser.items = [];
|
||||
feedparser.on('readable', function () {
|
||||
// This is where the action is!
|
||||
var stream = this; // `this` is `feedparser`, which is a stream
|
||||
var items = [];
|
||||
var item;
|
||||
|
||||
while (item = stream.read()) {
|
||||
feedparser.items.push(item);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
feedparser.on('end',function(er){cbOnce(null,feedparser)});
|
||||
|
||||
}
|
||||
|
||||
// hydrate the json to html
|
||||
function buildUp(jsonObj,opts){
|
||||
|
||||
// assign opts to the obj
|
||||
jsonObj.opts = opts||{};
|
||||
|
||||
// iterate through the items
|
||||
jsonObj.items = jsonObj.items.filter(function(item){
|
||||
|
||||
// get date
|
||||
item.stringDate = getTimeDisplay(item.date);
|
||||
|
||||
item.content = getH(item,'atom:content');
|
||||
if (!item.content ){// item was deleted
|
||||
return false;
|
||||
}
|
||||
|
||||
// make anchor tags have the "_top" target
|
||||
item.content = item.content.replace(/\<\s*a\s*/ig,'<a target="_top"');
|
||||
|
||||
// get enclosures
|
||||
item.enclosures = [];
|
||||
|
||||
function findEnclosure(link){
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'enclosure'){
|
||||
item.enclosures.push({
|
||||
url:href,
|
||||
type:link['@'].type||''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// find them in the item's links
|
||||
if (item['atom:link']){
|
||||
var links = item['atom:link'];
|
||||
if (!isArray(links) && typeof links == 'object'){
|
||||
links = [links];
|
||||
}else if (!isArray(links)){
|
||||
links = [];
|
||||
}
|
||||
links.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
// find them in activity:object
|
||||
if (item["activity:object"] && item["activity:object"].link){
|
||||
var enclosureLinks = item["activity:object"].link;
|
||||
if (!isArray(enclosureLinks) && typeof enclosureLinks == 'object'){
|
||||
enclosureLinks = [enclosureLinks];
|
||||
}else if (!isArray(enclosureLinks)){
|
||||
enclosureLinks = [];// not an object or array.
|
||||
}
|
||||
enclosureLinks.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
|
||||
// get author info
|
||||
|
||||
item.author = {};
|
||||
var _author = item.meta['atom:author'];
|
||||
if ( item['activity:object'] && item['activity:object'].author){
|
||||
_author = item['activity:object'].author;
|
||||
}
|
||||
|
||||
item.author.name = getH(_author,'name');
|
||||
item.author.uri = getH(_author,'uri');
|
||||
item.author.fullName = getH(_author,'email');
|
||||
item.author.displayName = getH(_author,'poco:displayname')||getH(_author,'poco:preferredUsername')||item.author.name;
|
||||
|
||||
var authorLinks = _author.link || [];
|
||||
|
||||
if (!isArray(authorLinks) && typeof authorLinks == 'object'){
|
||||
authorLinks = [authorLinks];
|
||||
}else if ( !isArray(authorLinks) ){
|
||||
authorLinks = [];// not an object or string.
|
||||
}
|
||||
|
||||
authorLinks.forEach(function(link){// todo: guard against authorLinks not being an array
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'avatar'){
|
||||
item.author.avatar = href;
|
||||
}else if(rel == 'alternate'){
|
||||
item.author.alternate = href;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
return template(jsonObj);
|
||||
}
|
||||
|
||||
|
||||
// get obj[key]['#'] or ''
|
||||
function getH(obj,key){
|
||||
if (!obj[key]){return ''}
|
||||
return obj[key]['#']||'';
|
||||
}
|
||||
|
||||
function getTimeDisplay(d){
|
||||
var d = d;
|
||||
if (typeof d !== 'object'){
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
dt = d.getTime();
|
||||
var now = Date.now();
|
||||
|
||||
var delta = now - dt;
|
||||
|
||||
// over 6 days ago
|
||||
if (delta > 1000*60*60*24*6){
|
||||
return isoDateToEnglish(d.toISOString());
|
||||
}else{
|
||||
return timeAgo().format(dt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isoDateToEnglish(d){
|
||||
|
||||
var dt = d.split(/[t\-]/ig);
|
||||
var months = [ "January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December" ];
|
||||
|
||||
return months[Number(dt[1])-1] +' '+dt[2]+ ', '+dt[0];
|
||||
}
|
||||
var pug = require('pug');
|
||||
var FeedParser = require('feedparser');
|
||||
var ejs = require('ejs');
|
||||
var fs = require('fs');
|
||||
var template = ejs.compile(fs.readFileSync('./lib/template.ejs','utf8'));
|
||||
var timeAgo = require('timeago.js');
|
||||
|
||||
function isArray(a){
|
||||
return Array.isArray(a);
|
||||
}
|
||||
|
||||
// accumulate a stream of XML into a html file
|
||||
|
||||
module.exports = function(stream,opts,callback){
|
||||
var callback = callback;
|
||||
var opts = opts;
|
||||
if (typeof opts == 'function'){
|
||||
callback = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
// convert s from atom feed to a full html page for rendering
|
||||
breakDown(stream,function(er,data){
|
||||
if (er) {
|
||||
return callback(er);
|
||||
}
|
||||
// try and build up
|
||||
try{
|
||||
var result = buildUp(data,opts)
|
||||
}catch(e){
|
||||
return callback(e);
|
||||
}
|
||||
return callback(null,result);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// break the xml into json
|
||||
function breakDown(stream,callback){
|
||||
|
||||
var spent = false;
|
||||
function cbOnce(er,data){
|
||||
if (!spent){
|
||||
callback(er,data);
|
||||
spent = true;
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('error',cbOnce);
|
||||
var feedparser = new FeedParser();
|
||||
feedparser.on('error', cbOnce);
|
||||
stream.pipe(feedparser)
|
||||
|
||||
|
||||
feedparser.items = [];
|
||||
feedparser.on('readable', function () {
|
||||
// This is where the action is!
|
||||
var stream = this; // `this` is `feedparser`, which is a stream
|
||||
var items = [];
|
||||
var item;
|
||||
|
||||
while (item = stream.read()) {
|
||||
feedparser.items.push(item);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
feedparser.on('end',function(er){cbOnce(null,feedparser)});
|
||||
|
||||
}
|
||||
|
||||
// hydrate the json to html
|
||||
function buildUp(jsonObj,opts){
|
||||
|
||||
// assign opts to the obj
|
||||
jsonObj.opts = opts||{};
|
||||
|
||||
// iterate through the items
|
||||
jsonObj.items = jsonObj.items.filter(function(item){
|
||||
|
||||
// get date
|
||||
item.stringDate = getTimeDisplay(item.date);
|
||||
|
||||
item.content = getH(item,'atom:content');
|
||||
if (!item.content ){// item was deleted
|
||||
return false;
|
||||
}
|
||||
|
||||
// make anchor tags have the "_top" target
|
||||
item.content = item.content.replace(/\<\s*a\s*/ig,'<a target="_top"');
|
||||
|
||||
// get enclosures
|
||||
item.enclosures = [];
|
||||
|
||||
function findEnclosure(link){
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'enclosure'){
|
||||
item.enclosures.push({
|
||||
url:href,
|
||||
type:link['@'].type||''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// find them in the item's links
|
||||
if (item['atom:link']){
|
||||
var links = item['atom:link'];
|
||||
if (!isArray(links) && typeof links == 'object'){
|
||||
links = [links];
|
||||
}else if (!isArray(links)){
|
||||
links = [];
|
||||
}
|
||||
links.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
// find them in activity:object
|
||||
if (item["activity:object"] && item["activity:object"].link){
|
||||
var enclosureLinks = item["activity:object"].link;
|
||||
if (!isArray(enclosureLinks) && typeof enclosureLinks == 'object'){
|
||||
enclosureLinks = [enclosureLinks];
|
||||
}else if (!isArray(enclosureLinks)){
|
||||
enclosureLinks = [];// not an object or array.
|
||||
}
|
||||
enclosureLinks.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
|
||||
// get author info
|
||||
|
||||
item.author = {};
|
||||
var _author = item.meta['atom:author'];
|
||||
if ( item['activity:object'] && item['activity:object'].author){
|
||||
_author = item['activity:object'].author;
|
||||
}
|
||||
|
||||
item.author.name = getH(_author,'name');
|
||||
item.author.uri = getH(_author,'uri');
|
||||
item.author.fullName = getH(_author,'email');
|
||||
item.author.displayName = getH(_author,'poco:displayname')||getH(_author,'poco:preferredUsername')||item.author.name;
|
||||
|
||||
var authorLinks = _author.link || [];
|
||||
|
||||
if (!isArray(authorLinks) && typeof authorLinks == 'object'){
|
||||
authorLinks = [authorLinks];
|
||||
}else if ( !isArray(authorLinks) ){
|
||||
authorLinks = [];// not an object or string.
|
||||
}
|
||||
|
||||
authorLinks.forEach(function(link){// todo: guard against authorLinks not being an array
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'avatar'){
|
||||
item.author.avatar = href;
|
||||
}else if(rel == 'alternate'){
|
||||
item.author.alternate = href;
|
||||
}
|
||||
});
|
||||
|
||||
// now detect if item is a reply or boost
|
||||
item.isBoost = false;// <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
|
||||
item.isReply = false;
|
||||
|
||||
var v = getH(item,'activity:verb')
|
||||
if (v.indexOf('share') > -1){
|
||||
item.isBoost = true;
|
||||
}
|
||||
|
||||
if (item['thr:in-reply-to']){
|
||||
item.isReply = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
if (jsonObj.meta && jsonObj.meta['atom:author'] && jsonObj.meta['atom:author'].link && Array.isArray(jsonObj.meta['atom:author'].link) ){
|
||||
jsonObj.meta['atom:author'].link.forEach(link=>{
|
||||
var l = link['@'];
|
||||
if (l.rel=="header"){
|
||||
jsonObj.meta.headerImage = l.href;
|
||||
}
|
||||
else if(l.rel=="avatar"){
|
||||
jsonObj.meta.avatar = l.href;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return template(jsonObj);
|
||||
}
|
||||
|
||||
|
||||
// get obj[key]['#'] or ''
|
||||
function getH(obj,key){
|
||||
if (!obj[key]){return ''}
|
||||
return obj[key]['#']||'';
|
||||
}
|
||||
|
||||
function getTimeDisplay(d){
|
||||
var d = d;
|
||||
if (typeof d !== 'object'){
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
dt = d.getTime();
|
||||
var now = Date.now();
|
||||
|
||||
var delta = now - dt;
|
||||
|
||||
// over 6 days ago
|
||||
if (delta > 1000*60*60*24*6){
|
||||
return isoDateToEnglish(d.toISOString());
|
||||
}else{
|
||||
return timeAgo().format(dt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isoDateToEnglish(d){
|
||||
|
||||
var dt = d.split(/[t\-]/ig);
|
||||
var months = [ "January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December" ];
|
||||
|
||||
return months[Number(dt[1])-1] +' '+dt[2]+ ', '+dt[0];
|
||||
}
|
||||
|
|
152
lib/template.ejs
152
lib/template.ejs
|
@ -1,68 +1,84 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"></meta>
|
||||
<style type="text/css"></style>
|
||||
|
||||
<% if (opts.theme && opts.theme.toLowerCase() == 'light'){ %>
|
||||
<link rel="stylesheet" href="/light.css"></link>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" href="/dark.css"></link>
|
||||
<% } %>
|
||||
|
||||
<% if (opts.size){ %>
|
||||
<style type="text/css">
|
||||
html,body{
|
||||
font-size: <%= opts.size.toString().slice(0,4) %>%;
|
||||
}
|
||||
</style>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<% if (opts.header !== false){ %>
|
||||
<div class="meta">
|
||||
<div class="title">
|
||||
<a target="_top" href="<%= meta.link %>">
|
||||
<%- meta.title %>'s Feed on Mastodon
|
||||
</a>
|
||||
</div>
|
||||
<div class="description"><%- meta.description %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="container">
|
||||
<% items.forEach(function(item){ %>
|
||||
<div class="item">
|
||||
<div class="item-title"> <%- item.title %> </div>
|
||||
<div class="author">
|
||||
<a target="_top" class="avatar" href="<%- item.author.uri %>">
|
||||
<img class="avatar" src="<%- item.author.avatar %>"/>
|
||||
</a>
|
||||
<div class="author-info">
|
||||
<a target="_top" class="author-displayname" href="<%- item.author.uri %>"> <%= item.author.displayName %> </a>
|
||||
<div class="author-fullname"> <%= item.author.fullName %> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<%- item.content %>
|
||||
</div>
|
||||
<% if (item.enclosures.length > 0){ %>
|
||||
<div class="enclosures">
|
||||
<% for (var i = 0; i < item.enclosures.length; i ++){ var e = item.enclosures[i] %>
|
||||
<a target="_top" class="enclosure" href="<%= e.url %>" >
|
||||
<% if (e.type.indexOf('image') > -1){ %>
|
||||
<img src="<%= e.url %>"/>
|
||||
<% }else if (e.type.indexOf('video') > -1){ %>
|
||||
<video autoplay loop muted src="<%= e.url %>"/>
|
||||
<% } else { %>
|
||||
<%= e.url %>
|
||||
<% } %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="date"><%= item.stringDate %></div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"></meta>
|
||||
<style type="text/css"></style>
|
||||
|
||||
<% if (opts.theme && opts.theme.toLowerCase() == 'light'){ %>
|
||||
<link rel="stylesheet" href="/light.css"></link>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" href="/dark.css"></link>
|
||||
<% } %>
|
||||
|
||||
<% if (opts.size){ %>
|
||||
<style type="text/css">
|
||||
html,body{
|
||||
font-size: <%= opts.size.toString().slice(0,4) %>%;
|
||||
}
|
||||
</style>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<% if (opts.header !== false){ %>
|
||||
<div class="meta">
|
||||
|
||||
<div class="header" style="<%= meta.headerImage?`background-image:url(${meta.headerImage})`:'' %>">
|
||||
<a class="header-left" target="_top" href="<%= meta.link %>">
|
||||
<% if (meta.avatar){ %>
|
||||
<img class="avatar circular" src="<%= meta.avatar %>"></img>
|
||||
<% } %>
|
||||
</a>
|
||||
<div class="description header-right">
|
||||
<a target="_top" href="<%= meta.link %>">
|
||||
<%= meta.title %>
|
||||
</a>
|
||||
<br>
|
||||
<%= meta.description %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="container">
|
||||
|
||||
<% var filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
|
||||
<% if (filtered.length < 2){
|
||||
filtered = items;// show all items if it was pared down too much
|
||||
} %>
|
||||
<% filtered.forEach(function(item){ %>
|
||||
<div class="item">
|
||||
<% if (item.isBoost) { %>
|
||||
<div class="item-title"> <%- item.title %> </div>
|
||||
<% } %>
|
||||
<div class="author">
|
||||
<a target="_top" class="avatar" href="<%- item.author.uri %>">
|
||||
<img class="avatar" src="<%- item.author.avatar %>"/>
|
||||
</a>
|
||||
<div class="author-info">
|
||||
<a target="_top" class="author-displayname" href="<%- item.author.uri %>"> <%= item.author.displayName %> </a>
|
||||
<div class="author-fullname"> <%= item.author.fullName %> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<%- item.content %>
|
||||
</div>
|
||||
<% if (item.enclosures.length > 0){ %>
|
||||
<div class="enclosures">
|
||||
<% for (var i = 0; i < item.enclosures.length; i ++){ var e = item.enclosures[i] %>
|
||||
<a target="_top" class="enclosure" href="<%= e.url %>" >
|
||||
<% if (e.type.indexOf('image') > -1){ %>
|
||||
<img src="<%= e.url %>"/>
|
||||
<% }else if (e.type.indexOf('video') > -1){ %>
|
||||
<video autoplay loop muted src="<%= e.url %>"/>
|
||||
<% } else { %>
|
||||
<%= e.url %>
|
||||
<% } %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="date"><%= item.stringDate %></div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -16,18 +16,35 @@ a * {
|
|||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
padding: 1rem;
|
||||
background-color: #39404d; }
|
||||
|
||||
.meta * {
|
||||
line-height: 2rem; }
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #ffffff; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(40, 44, 55, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(40, 44, 55, 0.85); }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
@ -38,8 +55,7 @@ a * {
|
|||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname,
|
||||
.description {
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
|
@ -67,6 +83,8 @@ a * {
|
|||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
|
|
|
@ -1,59 +1,69 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<title>Mastofeed - embeddable Mastodon feeds</title>
|
||||
<link rel="stylesheet" href="./stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<div>
|
||||
<h1>Mastofeed</h1>
|
||||
<h4>Embedded Mastodon feeds for blogs etc.</h4>
|
||||
<a href="https://github.com/fenwick67/mastofeed" class="cta button alt">Fork on Github <img class="link-logo after" src="github-logo.svg" alt="Github Logo" data-reactid="19"></a><br>
|
||||
<br><hr><br>
|
||||
<form action="javascript:genUrl()">
|
||||
<label>Instance URL:<input required type="text" id="urlin" placeholder="octodon.social" oninvalid="this.setCustomValidity('Insert your instance URL. Example: octodon.social')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Username:<input required type="text" id="usernamein" placeholder="fenwick67" oninvalid="this.setCustomValidity('Insert your username. Example: fenwick67')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Width (px):<input required type="number" id="width" value="400" oninvalid="this.setCustomValidity('Insert width of generated feed. Default: 400')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Height (px):<input required type="number" id="height" value="800" oninvalid="this.setCustomValidity('Insert height of generated feed. Default: 800')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>UI scale (percent):<input required type="number" id="size" value="100" oninvalid="this.setCustomValidity('Insert UI scale. Default: 100')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Theme:
|
||||
<select id="theme">
|
||||
<option value="dark">dark</option>
|
||||
<option value="light">light</option>
|
||||
</select>
|
||||
</label><br>
|
||||
<label>Show header?<input id="header" type="checkbox" checked="checked"></label><br>
|
||||
<button value="generate">Generate</button>
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
<label>Use this markup in your HTML: <br><textarea id="result" placeholder="result will go here"></textarea></label>
|
||||
<br>
|
||||
<h3>Live Preview:</h3>
|
||||
<iframe id="frame" allowfullscreen sandbox="allow-top-navigation allow-scripts" width="400" height="800" src="/api/feed?url=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67.atom&theme=dark&size=100&header=true"></iframe>
|
||||
<br>
|
||||
</div>
|
||||
<script>
|
||||
window.genUrl = function genUrl(){
|
||||
function val(id){
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
var inUrl = 'https://' + val('urlin') + '/users/'+val('usernamein')+'.atom';
|
||||
|
||||
var iframeUrl = window.location.protocol + '//'+ window.location.hostname +((window.location.port && window.location.port!=80)?(':'+window.location.port):'')
|
||||
+"/api/feed?url="+encodeURIComponent(inUrl)+"&theme="+val('theme')+'&size='+val('size')
|
||||
+ "&header="+(document.getElementById('header').checked.toString());
|
||||
|
||||
document.getElementById('result').value = '<iframe allowfullscreen sandbox="allow-top-navigation allow-scripts" width="'+val('width')+'" height="'+val('height')+'" src="'+iframeUrl+'"></iframe>';
|
||||
|
||||
var iframe = document.getElementById('frame');
|
||||
iframe.src = iframeUrl;
|
||||
iframe.width = val('width');
|
||||
iframe.height = val('height');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<title>Mastofeed - embeddable Mastodon feeds</title>
|
||||
<link rel="stylesheet" href="./stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<div>
|
||||
<h1>Mastofeed</h1>
|
||||
<h4>Embedded Mastodon feeds for blogs etc.</h4>
|
||||
<a href="https://github.com/fenwick67/mastofeed" class="cta button alt">Fork on Github <img class="link-logo after" src="github-logo.svg" alt="Github Logo" data-reactid="19"></a><br>
|
||||
<br><hr><br>
|
||||
<form action="javascript:genUrl()">
|
||||
<label>Instance URL:<input required type="text" id="urlin" placeholder="octodon.social" oninvalid="this.setCustomValidity('Insert your instance URL. Example: octodon.social')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Username:<input required type="text" id="usernamein" placeholder="fenwick67" oninvalid="this.setCustomValidity('Insert your username. Example: fenwick67')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Width (px):<input required type="number" id="width" value="400" oninvalid="this.setCustomValidity('Insert width of generated feed. Default: 400')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Height (px):<input required type="number" id="height" value="800" oninvalid="this.setCustomValidity('Insert height of generated feed. Default: 800')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>UI Scale (percent):<input required type="number" id="size" value="100" oninvalid="this.setCustomValidity('Insert UI scale. Default: 100')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Theme:
|
||||
<select id="theme">
|
||||
<option value="dark">dark</option>
|
||||
<option value="light">light</option>
|
||||
</select>
|
||||
</label><br>
|
||||
<label>Show Header?<input id="header" type="checkbox" checked="checked"></label><br>
|
||||
<label>Hide replies?<input type="checkbox" id="hidereplies"></label><br>
|
||||
<label>Hide boosts?<input type="checkbox" id="hideboosts"></label><br>
|
||||
|
||||
<button value="generate">Generate</button>
|
||||
</form>
|
||||
<div id="warnings"></div>
|
||||
<br>
|
||||
<br>
|
||||
<label>Use this markup in your HTML: <br><textarea id="result" placeholder="result will go here"></textarea></label>
|
||||
<br>
|
||||
<h3>Live Preview:</h3>
|
||||
<iframe id="frame" allowfullscreen sandbox="allow-top-navigation allow-scripts" width="400" height="800" src="/api/feed?url=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67.atom&theme=dark&size=100&header=true"></iframe>
|
||||
<br>
|
||||
</div>
|
||||
<script>
|
||||
window.genUrl = function genUrl(){
|
||||
function val(id){
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
var inUrl = 'https://' + val('urlin') + '/users/'+val('usernamein')+'.atom';
|
||||
|
||||
var showBoosts = (!document.getElementById('hideboosts').checked).toString();
|
||||
var showReplies = (!document.getElementById('hidereplies').checked).toString();
|
||||
|
||||
var iframeUrl = window.location.protocol + '//'+ window.location.hostname +((window.location.port && window.location.port!=80)?(':'+window.location.port):'')
|
||||
+"/api/feed?url="+encodeURIComponent(inUrl)+"&theme="+val('theme')+'&size='+val('size')
|
||||
+ "&header="+(document.getElementById('header').checked.toString())+'&replies='+showReplies+'&boosts='+showBoosts;
|
||||
|
||||
document.getElementById('result').value = '<iframe allowfullscreen sandbox="allow-top-navigation allow-scripts" width="'+val('width')+'" height="'+val('height')+'" src="'+iframeUrl+'"></iframe>';
|
||||
|
||||
var iframe = document.getElementById('frame');
|
||||
iframe.src = iframeUrl;
|
||||
iframe.width = val('width');
|
||||
iframe.height = val('height');
|
||||
|
||||
var warnings = (!showBoosts || !showReplies)?"<b>Friendly warning</b>: if you haven't posted lately, we may still show your boosts and/or replies":'';
|
||||
document.getElementById('warnings').innerHTML = warnings;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -16,18 +16,35 @@ a * {
|
|||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
padding: 1rem;
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.meta * {
|
||||
line-height: 2rem; }
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
@ -38,8 +55,7 @@ a * {
|
|||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname,
|
||||
.description {
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
|
@ -67,6 +83,8 @@ a * {
|
|||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
|
|
|
@ -1,118 +1,142 @@
|
|||
$bg: #fff !default;
|
||||
$bg2: darken($bg, 6) !default;
|
||||
$fg: #000 !default;
|
||||
$dim: lighten($fg, 50) !default;
|
||||
$dimmer: darken($bg2, 10) !default;
|
||||
$link: #09c !default;
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: $bg;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: $fg;
|
||||
font-weight: lighter;
|
||||
overflow-x:hidden;
|
||||
font-size:100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.meta {
|
||||
padding: 1rem;
|
||||
background-color: $bg2;
|
||||
}
|
||||
|
||||
.meta * {
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px $dimmer;
|
||||
}
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight:lighter;
|
||||
}
|
||||
|
||||
|
||||
.item-content * {
|
||||
margin: 1rem 0;
|
||||
line-height:1.4rem;
|
||||
}
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname,
|
||||
.description {
|
||||
color: $dim;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.date{
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
.author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%;
|
||||
}
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap:wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.enclosure{
|
||||
|
||||
display:flex;
|
||||
flex: 1 1 auto;
|
||||
width:50%;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
cursor: zoom-in;
|
||||
max-height: 12rem;
|
||||
}
|
||||
.enclosure>*{
|
||||
flex: 1 1 auto;
|
||||
width:100%;
|
||||
height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.meta .title{
|
||||
font-weight:bold;
|
||||
}
|
||||
$bg: #fff !default;
|
||||
$bg2: darken($bg, 6) !default;
|
||||
$fg: #000 !default;
|
||||
$dim: lighten($fg, 50) !default;
|
||||
$dimmer: darken($bg2, 10) !default;
|
||||
$link: #09c !default;
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: $bg;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: $fg;
|
||||
font-weight: lighter;
|
||||
overflow-x:hidden;
|
||||
font-size:100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.meta {
|
||||
//padding: 1rem;
|
||||
background-color: $bg2;
|
||||
}
|
||||
|
||||
.header{
|
||||
display:flex;
|
||||
background-size:cover;
|
||||
min-height:8rem;
|
||||
color:$fg;
|
||||
.header-left,.header-right{
|
||||
margin:0;
|
||||
}
|
||||
.header-left{
|
||||
min-width:8rem;
|
||||
position:relative;
|
||||
text-align:center;
|
||||
background:transparentize($bg,0.7);
|
||||
.avatar{
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position:relative;
|
||||
top:calc(50% - 3rem);
|
||||
}
|
||||
}
|
||||
.header-right{
|
||||
font-size:0.9rem;
|
||||
padding:0.9rem;
|
||||
background:transparentize($bg,0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px $dimmer;
|
||||
}
|
||||
|
||||
.item-content,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight:lighter;
|
||||
}
|
||||
|
||||
|
||||
.item-content * {
|
||||
margin: 1rem 0;
|
||||
line-height:1.4rem;
|
||||
}
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: $dim;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.date{
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
.author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%;
|
||||
&.circular{
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap:wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.enclosure{
|
||||
|
||||
display:flex;
|
||||
flex: 1 1 auto;
|
||||
width:50%;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
cursor: zoom-in;
|
||||
max-height: 12rem;
|
||||
}
|
||||
.enclosure>*{
|
||||
flex: 1 1 auto;
|
||||
width:100%;
|
||||
height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.meta .title{
|
||||
font-weight:bold;
|
||||
}
|
||||
|
|
1001
test/result.html
1001
test/result.html
File diff suppressed because it is too large
Load diff
888
test/sample.atom
888
test/sample.atom
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue