Compare commits

..

1 commit

Author SHA1 Message Date
fenwick67
c03766b565 add blocklist 2019-08-05 17:31:13 -04:00
43 changed files with 2890 additions and 13246 deletions

View file

@ -1,21 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: dependencies
image: node
commands:
- yarn
- name: lint
image: node
commands:
- yarn lint
depends: [dependencies]
- name: test
image: node
commands:
- yarn test
depends: [lint]

View file

@ -1 +0,0 @@
src/public/infinite-scroll.js

View file

@ -1,30 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

132
.gitignore vendored
View file

@ -1,130 +1,2 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
node_modules
*.atom

View file

@ -1,11 +1,8 @@
# Fedifeed
# Mastofeed
[![Build Status](https://ci.git.froth.zone/api/badges/Sam/fedifeed/status.svg)](https://ci.git.froth.zone/Sam/fedifeed)
Embed a mastodon feed in your blog et cetera.
Embed an activitypub feed in your blog et cetera. \
This is a fork of [mastofeed](https://github.com/fenwick67/mastofeed) that adds support for more themes and Misskey.
https://www.fedifeed.com
https://www.mastofeed.com
## User guide
@ -13,35 +10,46 @@ The homepage has a tool for generating iframe code for you, with a sensible `san
## API
### V1
### V2
#### GET `/api/v1/feed`
#### GET `/apiv2/feed`
> example: `/api/v1/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=masto-light`
> example: `/api/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=light`
Returns a html page which displays a feed for a user URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67')` ).
Returns a html page which displays a mastodon feed for a user URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67')` ).
Querystring options:
| option | required | description |
| ------ | -------- | ----------- |
| `userurl` | **\*** | Mastodon/Pleroma/Misskey account URL (usually `https://${instance}/users/${username}` for MastoAPI or `https://${instance}/@${username}` for Misskey) |
| `instance` | **\*\***| Mastodon/Pleroma/Misskey instance URL (usually `https://${instance}`) |
| `user` | **\*\*** | Mastodon/Pleroma/Misskey user ID (usually `${username}`) |
| `userurl` | **yes** | Mastodon/ActivityPub account URL (usually `https://${instance}/users/${username}`) |
| `feedurl` | no | a URL to a page of an ActivityPub post collection. Only used for pages beyond the first. |
| `theme` | no | either `masto-dark`, `masto-light` or `masto-auto`, to select the UI theme (default is `masto-dark`). `auto` will appear masto-light unless the user sets up masto-dark mode on their device. |
| `theme` | no | either `dark` or `light`, to select the UI theme (default is `dark`). |
| `boosts` | no | whether to show boosts or not |
| `replies` | no | whether to show replies or not |
| `size` | no | the scale of the UI in percent. |
\* `userurl` is required if `instance` and `user` are not specified.\*\*\* \
\*\* `instance` **and** `user` are required if `userurl` is not specified.\*\*\*
### V1 (deprecated, will now just redirect you to the v2 API)
#### GET `/api/feed`
> example: `/api/feed?url=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67.atom&scale=90&theme=light`
Returns a html page which displays a mastodon feed for an atom feed URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67.atom')` ).
Querystring options:
| option | required | description |
| ------ | -------- | ----------- |
| `url` | **yes** | Mastodon Atom feed URL |
| `theme` | no | either `dark` or `light`, to select the UI theme (default is `dark`). |
| `size` | no | the scale of the UI in percent. |
\*\*\* **`userurl` and `instance`/`user` are mutually exclusive.**
## Server Installation
This is a straightforward node project with zero databases or anything, you should just be able to run `yarn install` and then `yarn start` to get up and running. Set your `PORT` environment variable to change the port it listens on.
This is a straightforward node project with zero databases or anything, you should just be able to run `npm install` and then `npm start` to get up and running. Set your `PORT` environment variable to change the port it listens on.
## Improve me
Feel free to add a caching layer, improve the styles and add more features.
Feel free to add a chaching layer, improve the styles and add more features.

22
build-styles.js Normal file
View file

@ -0,0 +1,22 @@
// build the styles
var fs = require('fs');
var sass = require('node-sass');
var staticDir = './static/'
var srcDir = './stylesrc/';
var themes = ['light','dark'];
themes.forEach(function(s){
var sassFile = srcDir+s+'.scss'
var cssFile = staticDir+s+'.css'
var result = sass.renderSync({
data: fs.readFileSync(sassFile,'utf8'),
includePaths:[srcDir]
});
fs.writeFileSync(cssFile,result.css,'utf8')
});
console.log('ok');

166
index.js Normal file
View file

@ -0,0 +1,166 @@
var Express = require('express');
// v1 api
var convert = require('./lib/convert');
// v2 api
var convertv2 = require('./lib/convertv2');
var serveStatic = require('serve-static');
var request = require('request');
var cors = require('cors');
var errorPage = require('./lib/errorPage');
var morgan = require('morgan');
var app = Express();
var logger = morgan(':method :url :status via :referrer - :response-time ms')
app.use(
serveStatic('static',{
maxAge:'1d'
})
);
function doCache(res,durationSecs){
res.set({
"Cache-Control":"max-age="+durationSecs
})
}
// web.site:another.one.here => [ /web\.site/i , /another\.one/i ]
var blocklist = [];
if (process.env["BLOCKLIST"]){
blocklist = process.env["BLOCKLIST"].split(':').map((s)=>{
var dotsFixed = s.replace(/\./gi,'\\.');
return new RegExp(dotsFixed, 'i');
});
}
// this just redirects to the
app.options('/api/feed',cors());
app.get('/api/feed',cors(),logger,function(req,res){
// get feed url
var feedUrl = req.query.url;
if (!feedUrl){
res.status(400);
res.send(errorPage(400,'You need to specify a feed URL'));
return;
}
var userUrl = feedUrl.replace(/\.atom.*/i,'');
var redirectUrl = '/apiv2/feed?';
var qs = ['userurl='+encodeURIComponent(userUrl),"api=v1"];
(['size','theme','boosts','replies']).forEach(key=>{
if (typeof req.query[key] != 'undefined'){
qs.push(key+'='+encodeURIComponent(req.query[key]));
}
})
res.redirect(redirectUrl + qs.join('&'));
});
app.options('/apiv2/feed',cors());
// http://localhost:8000/apiv2/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67
app.get('/apiv2/feed',cors(),logger,function(req,res){
// get feed url
var userUrl = req.query.userurl;
if (!userUrl){
res.status(400);
res.send(errorPage(400,'You need to specify a user URL'));
return;
}
var feedUrl = req.query.feedurl;
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;
}
}
opts.boosts = true;
if (req.query.boosts){
if (req.query.boosts.toLowerCase() == 'no' || req.query.boosts.toLowerCase() == 'false'){
opts.boosts = false;
}else{
opts.boosts = true;
}
}
opts.replies = true;
if (req.query.replies){
if (req.query.replies.toLowerCase() == 'no' || req.query.replies.toLowerCase() == 'false'){
opts.replies = false;
}else{
opts.replies = true;
}
}
opts.userUrl = userUrl;
opts.feedUrl = feedUrl;
opts.mastofeedUrl = req.url;
var blocked = false;
function fakeFail(){
var t = 1000 + 1000 * Math.random() * Math.random();
blocked = true;
setTimeout(function(){
res.status(500);
res.send(errorPage(500,null,{theme:opts.theme,size:opts.size}));
},t);
}
// shall I block the user?
var base = new URL(userUrl).hostname;
for (var i = 0; i < blocklist.length; i++){
var re = blocklist[i];
if (re.test(base)){
fakeFail();
console.log("blocked domain: "+base+" (matches "+re.source+")");
return; // need to exit this function so feed isn't actually fetched
}
}
// block by referer
var ref = req.get("referer")
if (ref){
for (var i = 0; i < blocklist.length; i++){
var re = blocklist[i];
if (re.test(ref)){
fakeFail();
console.log("blocked domain via referer: "+base+" (matches "+re.source+")");
return; // need to exit this function so feed isn't actually fetched
}
}
}
if (!blocked){
convertv2(opts).then((data)=>{
res.status(200);
doCache(res,60*60);
res.send(data);
}).catch((er)=>{
res.status(500);
res.send(errorPage(500,null,{theme:opts.theme,size:opts.size}));
// TODO log the error
console.error(er,er.stack);
})
}
})
app.listen(process.env.PORT || 8000,function(){
console.log('Server started, listening on '+(process.env.PORT || 8000));
});

View file

@ -1,248 +1,312 @@
import { compile } from "ejs";
import { readFileSync } from "fs";
let template = compile(readFileSync("./lib/template.ejs", "utf8"));
import { format } from "timeago.js";
import got from "got";
const map = new Map();
const hour = 3600000;
// get JSON for an AP URL, by either fetching it or grabbing it from a cache.
// note: rejects on HTTP 4xx or 5xx
async function apGet(url) {
return new Promise(function(resolve,reject){
// fail early
if (!url){
reject(new Error("URL is invalid"));
}
got( {
url:url,
cache:map,
headers: {
"accept": "application/activity+json"
}
})
.then(response=>JSON.parse(response.body))
.then(resolve)
.catch(reject);
});
}
// like Promise.all except returns null on error instead of failing all the promises
async function promiseSome(proms){
function noRejectWrap(prom){
return new Promise(function(resolve){
prom // it's already been called
.then(resolve)
.catch( ()=>{
// console.warn(e);// for debugging
resolve(null);
});
});
}
return await Promise.all(proms.map(noRejectWrap));
}
export default async function (opts) {
// let opts = opts;
let feedUrl = opts.feedUrl;
let userUrl = opts.userUrl;
let isIndex = false;
if (!userUrl) {
throw new Error("need user URL");
}
let user, feed;
// get user and feed in parallel if I have both URLs.
// can cache feed aggressively since it is a specific start and end.
if (userUrl && feedUrl){
[user, feed] = await Promise.all([ apGet(userUrl), apGet(feedUrl) ]);
}else{
// get user, then outbox, then feed
user = await apGet(userUrl,24 * hour);
isIndex = true;
let outbox = await apGet(user.outbox, 1 * hour);
// outbox.first can be a string for a URL, or an object with stuffs in it
if (typeof outbox.first == "object"){
feed = outbox.first;
} else {
feed = await apGet(outbox.first, hour/6);// 10 mins. Because the base feed URL can get new toots quickly.
}
}
let templateData = {
opts: opts,// from the request
meta: metaForUser(user),
items: await itemsForFeed(opts,user,feed),
nextPageLink: getNextPage(opts,user,feed),
isIndex: isIndex
};
return template(templateData);
}
function metaForUser(user) {
return {
avatar: user.icon && user.icon.url?user.icon.url:null,
headerImage:user.image && user.image.url?user.image.url:null,
title: user.name||user.preferredUsername||null,
description: user.summary||null,
link:user.url||"#"
};
}
async function itemsForFeed(opts,user,feed) {
let items = feed.orderedItems;
if (opts.boosts){
// yes, I have to fetch all the fucking boosts for this whole feed apparently >:/
let boostData = [];
let boostUrls = feed.orderedItems.filter(i=>i.type=="Announce").map(i=>i.object);
// console.log(boostUrls);
boostData = await promiseSome(boostUrls.map(apGet));
// now get user data for each of those
let userData = await promiseSome(boostData.map(d=>d?d.attributedTo||"":null).map(apGet));
// put a ._userdata key on the item object if this is a boost
for (let i = 0; i < boostData.length; i ++){
if (userData[i] && boostData[i]){
boostData[i]._userdata = userData[i];
}
}
// some URLs may have failed but IDGAF
// console.log(boostData[0]);
boostData.forEach((boostToot)=>{
if (!boostToot){// failed request
return;
}
// inject in-place into items
let index = -1;
for (var i = 0; i < items.length; i ++){
if (items[i].object == boostToot.id){
index = i;
break;
}
}
if (index == -1){
console.warn("warning: couldn't match boost to item: ",boostToot);
return;
}
boostToot.object = boostToot;// this lets the later stage parser access object without errors :)
items[i] = boostToot;
});
}
return items.filter((item)=>{
return typeof item.object == "object";// handle weird cases
}).map((item)=>{
let enclosures = (item.object.attachment||[]).filter((a)=>{
return a.type == "Document";
}).map((a)=>{
return {
name:a.name,
type:a.mediaType,
url:a.url
};
});
let op = item._userdata?item._userdata:user;
return {
isBoost:!!item._userdata,
title:item._userdata?user.preferredUsername+" shared a status by "+op.preferredUsername:"",
isReply:!!(item.object && item.object.inReplyTo),
hasCw:item.object.sensitive||false,
cw:item.object.summary,
content: item.object&&item.object.content?item.object.content:"",//TODO sanitize then render without entity escapes
atomHref:item.published?item.published.replace(/\W+/g,""):Math.random().toString().replace(".",""),// used for IDs
enclosures:enclosures,
stringDate:item.published?getTimeDisplay(Date.parse(item.published)):"",
permalink:item.object.id?item.object.id:"#",
author:{
uri:op.url,// link to author page
avatar:op.icon&&op.icon.url?op.icon.url:"",
displayName:op.name || op.preferredUsername,
fullName:op.preferredUsername+"@"+(new URL(op.url).hostname),
}
};
});
}
function getNextPage(opts,user,feed){
//based on feed.next
if (!feed.next){return null;}
// take feed.next, uriencode it, then take user url, then take options.mastofeedUrl
//let base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf("?"));
let ret = "/api/v1/feed?userurl=" + encodeURIComponent(opts.userUrl) + "&feedurl=" +encodeURIComponent(feed.next) + "&instance_type=" + opts.instance_type;
// add other params to the end
(["theme","header","size","boosts","replies"]).forEach((k)=>{
if (typeof opts[k] != "undefined"){
ret+=`&${k}=${ opts[k].toString() }`;
}
});
return ret;
}
// utilities below
function getTimeDisplay(d) {
// let d = d;
if (typeof d !== "object") {
d = new Date(d);
}
// convert to number
let dt = d.getTime();
let now = Date.now();
let delta = now - dt;
// over 6 days ago
if (delta > 1000 * 60 * 60 * 24 * 6) {
return isoDateToEnglish(d.toISOString());
} else {
return format(dt);
}
}
function isoDateToEnglish(d) {
let dt = d.split(/[t-]/ig);
let months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
return months[Number(dt[1]) - 1] + " " + dt[2] + ", " + dt[0];
}
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;
}
if(item.summary){
item.hasCw = true;
item.cw = item.summary;
}
if(item['activity:object'] && item['activity:object'].summary && item['activity:object'].summary['#']){
item.hasCw = true;
item.cw = item['activity:object'].summary['#'];
}
// get a pagination ID for an entry
item.paginationId = false;
if (item['atom:link']){
var links = item['atom:link'];
if (!isArray(links) && typeof links == 'object'){
links = [links];
}else if (!isArray(links)){
links = [];
}
links.forEach((link)=>{
if (!link['@']){return}
if (link['@']['rel']=='self'){
// parse out the pagination id
// href looks like this in mastodon: https://octodon.social/users/fenwick67/updates/732275.atom
// href looks like this in pleroma (and we should ignore): https://social.fenwick.pizza/objects/1e2fa906-378c-43f8-98fa-271aae455758
var href = link['@']['href'];
if (!href){return}
item.atomHref = href;
var match = href.match(/\/\d+.atom/);
if(!match){return}
var id = match[0].replace(/\D/g,'');
if (id){
item.paginationId = id;
}else{
return;
}
}
})
}
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;
}
});
}
// get next page
jsonObj.feedUrl = opts.feedUrl;
jsonObj.isIndex = (opts.feedUrl.indexOf('?') == -1);
// prefer link(rel=next)
var nextPageFeedUrl = '';
var links = jsonObj.meta['atom:link'];
if (links){
if (!isArray(links) && typeof links == 'object'){
links = [links];
}else if (!isArray(links)){
links = [];
}
links.forEach(function(link){
if (link['@'] && link['@']['rel'] == 'next' && link['@']['href']){
nextPageFeedUrl = link['@']['href'];
}
})
}
if (!nextPageFeedUrl){
// alternative: try to get the oldest entry id on this page
var lowestId = Infinity;
jsonObj.items.forEach(function(item){
var id = Number(item.paginationId);
if ( id < lowestId ){
lowestId = id;
}
});
if (lowestId < Infinity && opts.feedUrl){
nextPageFeedUrl = opts.feedUrl.replace(/\?.+/g,'') + '?max_id='+lowestId;
}
}
if(nextPageFeedUrl){
jsonObj.nextPageLink = opts.mastofeedUrl.replace(encodeURIComponent(opts.feedUrl),encodeURIComponent(nextPageFeedUrl));
console.log(jsonObj.nextPageLink);
}
return template(jsonObj);
}
// utilities below
// 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];
}

247
lib/convertv2.js Normal file
View file

@ -0,0 +1,247 @@
var ejs = require('ejs');
var fs = require('fs');
var template = ejs.compile(fs.readFileSync('./lib/template.ejs', 'utf8'));
var timeAgo = require('timeago.js');
var request = require('request-promise-cache')
const hour = 3600000;
// get JSON for an AP URL, by either fetching it or grabbing it from a cache.
// Honestly request-promise-cache should be good enough. Redis would be a nice upgrade but for
// a single process install it will be fine.
// note: rejects on HTTP 4xx or 5xx
async function apGet(url,ttl) {
return new Promise(function(resolve,reject){
// fail early
if (!url){
reject(new Error('URL is invalid'));
}
request( {
uri:url,
cacheKey:url,
cacheTTL:ttl || 24 * hour,
headers: {
"accept": "application/activity+json"
}
})
.then(body=>JSON.parse(body))
.then(resolve)
.catch(reject)
})
}
// like Promise.all except returns null on error instead of failing all the promises
async function promiseSome(proms){
function noRejectWrap(prom){
return new Promise(function(resolve,reject){
prom // it's already been called
.then(resolve)
.catch(e=>{
// console.warn(e);// for debugging
resolve(null)
})
})
}
return await Promise.all(proms.map(noRejectWrap))
}
module.exports = async function (opts) {
var opts = opts;
var feedUrl = opts.feedUrl;
var userUrl = opts.userUrl;
var isIndex = false;
if (!userUrl) {
throw new Error('need user URL');
}
var user, feed;
// get user and feed in parallel if I have both URLs.
// can cache feed aggressively since it is a specific start and end.
if (userUrl && feedUrl){
[user, feed] = await Promise.all([ apGet(userUrl), apGet(feedUrl) ]);
}else{
// get user, then outbox, then feed
user = await apGet(userUrl,24 * hour);
isIndex = true;
var outbox = await apGet(user.outbox,24 * hour);
feedUrl = outbox.first;
feed = await apGet(feedUrl,hour/6);// 10 mins. Because the base feed URL can get new toots quickly.
}
var templateData = {
opts: opts,// from the request
meta: metaForUser(user),
items: await itemsForFeed(opts,user,feed),
nextPageLink: getNextPage(opts,user,feed),
isIndex: isIndex
};
return template(templateData);
}
function metaForUser(user) {
return {
avatar: user.icon && user.icon.url?user.icon.url:null,
headerImage:user.image && user.image.url?user.image.url:null,
title: user.name||user.preferredUsername||null,
description: user.summary||null,
link:user.url||"#"
}
}
// TODO make function
async function itemsForFeed(opts,user,feed) {
var items = feed.orderedItems;
if (opts.boosts){
// yes, I have to fetch all the fucking boosts for this whole feed apparently >:/
var boostData = [];
var boostUrls = feed.orderedItems.filter(i=>i.type=="Announce").map(i=>i.object);
// console.log(boostUrls);
boostData = await promiseSome(boostUrls.map(apGet));
// now get user data for each of those
let userData = await promiseSome(boostData.map(d=>d?d.attributedTo||'':null).map(apGet));
// put a ._userdata key on the item object if this is a boost
for (var i = 0; i < boostData.length; i ++){
if (userData[i] && boostData[i]){
boostData[i]._userdata = userData[i];
}
}
// some URLs may have failed but IDGAF
// console.log(boostData[0]);
boostData.forEach((boostToot)=>{
if (!boostToot){// failed request
return;
}
// inject in-place into items
var index = -1;
for (var i = 0; i < items.length; i ++){
if (items[i].object == boostToot.id){
index = i;
break;
}
}
if (index == -1){
console.warn("warning: couldn't match boost to item: ",boostToot)
return;
}
boostToot.object = boostToot;// this lets the later stage parser access object without errors :)
items[i] = boostToot;
})
}
return items.filter((item)=>{
return typeof item.object == 'object';// handle weird cases
}).map((item)=>{
var enclosures = (item.object.attachment||[]).filter((a)=>{
return a.type == "Document";
}).map((a)=>{
return {
name:a.name,
type:a.mediaType,
url:a.url
}
});
var op = item._userdata?item._userdata:user;
return {
isBoost:!!item._userdata,
title:item._userdata?user.preferredUsername+' shared a status by '+op.preferredUsername:'',
isReply:!!(item.object && item.object.inReplyTo),
hasCw:item.object.sensitive||false,
cw:item.object.summary,
content: item.object&&item.object.content?item.object.content:'',//TODO sanitize then render without entity escapes
atomHref:item.published?item.published.replace(/\W+/g,''):Math.random().toString().replace('.',''),
enclosures:enclosures,
stringDate:item.published?getTimeDisplay(Date.parse(item.published)):'',
author:{
uri:op.url,// link to author page
avatar:op.icon&&op.icon.url?op.icon.url:'',
displayName:op.name,
fullName:op.preferredUsername+'@'+(new URL(op.url).hostname),
}
}
})
}
// TODO
function getNextPage(opts,user,feed){
//based on feed.next
// take feed.next, uriencode it, then take user url, then take options.mastofeedUrl
var base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf('?'));
var ret = '/apiv2/feed?userurl=' + encodeURIComponent(opts.userUrl) + '&feedurl=' +encodeURIComponent(feed.next);
// add other params to the end
(['theme','header','size','boosts','replies']).forEach((k)=>{
if (typeof opts[k] != 'undefined'){
ret+=`&${k}=${ opts[k].toString() }`;
}
})
return ret;
}
// utilities below
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];
}

View file

@ -1,15 +0,0 @@
import axios from "axios";
import fs from "fs";
//Grab emote list so when rendering
export default function getEmojiJson(url) {
let emojiUrl = "https://" + url + "/api/v1/custom_emojis";
axios
.get(emojiUrl)
.then(res => {
console.log(`statusCode: ${res.status}`);
const emojiJson = res.data;
console.log(emojiJson);
});
}
getEmojiJson("freecumextremist.com");

View file

@ -1,20 +1,20 @@
import { compile } from "ejs";
import { readFileSync } from "fs";
let template = compile(readFileSync("./lib/template.ejs", "utf8"));
var ejs = require('ejs');
var fs = require('fs');
var template = ejs.compile(fs.readFileSync('./lib/template.ejs', 'utf8'));
export default function(code,message,displayOptions){
module.exports = function(code,message,displayOptions){
let msg;
// const displayOptions = displayOptions || {};
var msg;
var displayOptions = displayOptions || {};
if (code == 500 && !message){
msg = "<p>Sorry, we are having trouble fetching posts for this user. Please try again later.</p><br><p>If the issue persists, <a href=\"https://git.froth.zone/Sam/fedifeed/issues\">please open an issue on Gitea</a>, or message sam@froth.zone</p>";
msg = '<p>Sorry, we are having trouble fetching posts for this user. Please try again later.</p><br><p>If the issue persists, <a href="https://github.com/fenwick67/mastofeed/issues">please open an issue on GitHub</a>, or message fenwick67@octodon.social</p>'
}else{
msg = message||"";
msg = message||'';
}
let options = {
var options = {
opts:{
header:true,
theme:displayOptions.theme||null,
@ -23,14 +23,14 @@ export default function(code,message,displayOptions){
meta:{
title:code.toString(),
description:msg,
link:"#"
link:'#'
// avatar:'',
// headerImage:''
},
items:[],
nextPageLink:null,
isIndex:true
};
}
return template(options);
}

View file

@ -4,20 +4,10 @@
<style type="text/css"></style>
<base target="_top" /><!-- this element is amazing-->
<% if (opts.theme && opts.theme.toLowerCase() == 'masto-light'){ %>
<link rel="stylesheet" href="/css/masto-light.css"></link>
<% } else if (opts.theme && opts.theme.toLowerCase() == 'auto'){ %>
<link rel="stylesheet" href="/css/masto-auto.css"></link>
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-dark'){ %>
<link rel="stylesheet" href="/css/misskey-dark.css"></link>
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-light'){ %>
<link rel="stylesheet" href="/css/misskey-light.css"></link>
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-auto'){ %>
<link rel="stylesheet" href="/css/misskey-auto.css"></link>
<% } else if (opts.theme && opts.theme.toLowerCase() == 'pleroma'){ %>
<link rel="stylesheet" href="/css/pleroma.css"></link>
<% if (opts.theme && opts.theme.toLowerCase() == 'light'){ %>
<link rel="stylesheet" href="/light.css"></link>
<% } else { %>
<link rel="stylesheet" href="/css/masto-dark.css"></link>
<link rel="stylesheet" href="/dark.css"></link>
<% } %>
<% if (opts.size){ %>
@ -51,7 +41,7 @@
<div class="container">
<% let filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
<% var filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
<% filtered.forEach(function(item){ %>
<div class="item">
<% if (item.isBoost) { %>
@ -67,33 +57,21 @@
</div>
</div>
<% if (item.hasCw){ %>
<% let cwId = (item.cw+item.atomHref).replace(/\W+/g,'') %>
<% var cwId = (item.cw+item.atomHref).replace(/\W+/g,'') %>
<span class="cw"><%- item.cw %></span>
<input type="checkbox" class="showmore" id="<%- cwId %>">
<label class="button" for="<%- cwId %>">Show</label>
<% } %>
<!--
Ok so this is so fucked but basically, this matches a status with the regex expression (to capture text between two colons) and
needs to take the matched text and search the instances emoji pack for the value and replace it in item content.
I tested this and it _does not_ alter posts without emojis.
-->
<% let regexp = /s*(?<=:).+?(?=:)s*/;
let postWithEmoji = item.content;
let emoteName = postWithEmoji.match(regexp);
<!--If a post with matching regex for :whatever: is found replace with img tag for the instance's emoji packs-->
item.content = postWithEmoji.replace(regexp, `<img src=https://freecumextremist.com/emoji/emojos/${emoteName}.jpg width='30' height='30'/>`)
%>
<div class="item-content">
<%- item.content %>
</div>
<% if (item.enclosures.length > 0){ %>
<div class="enclosures">
<% for (let i = 0; i < item.enclosures.length; i ++){ %>
<% let e = item.enclosures[i] %>
<% if (e.type.indexOf('audio') > -1) {%>
<audio class="enclosure" controls loop src="<%= e.url %>"/>
<% }else if (e.type.indexOf('video') > -1){ %>
<% for (var i = 0; i < item.enclosures.length; i ++){ %>
<% var e = item.enclosures[i] %>
<% if (e.type.indexOf('video') > -1){ %>
<video class="enclosure" controls loop src="<%= e.url %>"/>
<% } else { %>
<a target="_top" class="enclosure" href="<%= e.url %>" >
@ -107,27 +85,27 @@
<% } %>
</div>
<% } %>
<a class="date" href="<%= item.permalink %>"><%= item.stringDate %></a>
<div class="date"><%= item.stringDate %></div>
</div>
<% }); %>
<% if (nextPageLink){ %>
<div class="item hidden">
<a class="hacky_link" href="<%- nextPageLink %>">More</a>
</div>
<% if (nextPageLink) {%>
<div class="item hidden">
<a class="hacky_link" href="<%- nextPageLink %>">More</a>
</div>
<% } %>
</div> <!-- end item container -->
<% if (nextPageLink){ %>
<div class="pagination">
<a class="button" href="<%- nextPageLink %>">Load More</a>
</div>
<% if (nextPageLink) {%>
<div class="pagination">
<a class="button" href="<%- nextPageLink %>">Load More</a>
</div>
<% } %>
<% if ( isIndex ){ %>
<script src="/infinite-scroll.js"></script>
<script type="text/javascript">
let lastPageLoaded = null;
let infScroll = new InfiniteScroll( '.container', {
var lastPageLoaded = null;
var infScroll = new InfiniteScroll( '.container', {
// options
hideNav:'.pagination',
append: '.item',
@ -135,14 +113,14 @@
prefill:true,
path: function(){
// need to query this DOM my damn self
let pageLinks = document.querySelectorAll('.hacky_link');
var pageLinks = document.querySelectorAll('.hacky_link');
if (!pageLinks || pageLinks.length == 0){
console.log ('next page link could not be found');
return false;
}else{
let finalLink = pageLinks[pageLinks.length-1].href;
var finalLink = pageLinks[pageLinks.length-1].href;
// make sure we don't load the same page twice
if (!finalLink || finalLink == window.location.href || finalLink == lastPageLoaded){
if (finalLink == lastPageLoaded){
console.log('this was the last page');
return false;
}else{

1949
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load diff

7617
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,22 @@
{
"dependencies": {
"cors": "^2.8.5",
"ejs": "^3.1.6",
"express": "^4.17.2",
"feedparser": "^2.2.10",
"got": "^12.0.1",
"megalodon": "^4.0.0",
"morgan": "^1.10.0",
"serve-static": "^1.14.2",
"timeago.js": "^4.0.2"
"ejs": "^2.5.8",
"express": "^4.16.4",
"feedparser": "^2.2.9",
"morgan": "^1.9.1",
"request": "^2.88.0",
"request-promise-cache": "^2.0.1",
"request-promise-native": "^1.0.7",
"serve-static": "^1.13.2",
"timeago.js": "^3.0.2"
},
"main": "index.js",
"type": "module",
"license": "MIT",
"scripts": {
"start": "yarn build-styles && node src/index.js",
"build-styles": "node src/build-styles.js",
"lint": "eslint --ext .js src lib",
"test": "echo \"Error: no test specified\" && exit 0"
"start": "node index.js",
"build-styles": "node build-styles.js"
},
"devDependencies": {
"eslint": "^8.8.0",
"node-sass": "^7.0.1"
"node-sass": "^4.12.0"
}
}

View file

@ -1,22 +0,0 @@
// build the styles
import { readFileSync, writeFileSync } from "fs";
import { renderSync } from "node-sass";
let staticDir = "./src/public/";
let srcDir = "./src/stylesrc/";
let themes = ["masto-light","masto-dark","masto-auto"];
themes.forEach(function(s){
let sassFile = srcDir+s+".scss";
let cssFile = staticDir+s+".css";
let result = renderSync({
data: readFileSync(sassFile,"utf8"),
includePaths:[srcDir]
});
writeFileSync(cssFile,result.css,"utf8");
});
console.log("ok");

View file

@ -1,130 +0,0 @@
import Express from "express";
// v2 api
import convertv2 from "../lib/convert.js";
import serveStatic from "serve-static";
import cors from "cors";
import errorPage from "../lib/errorPage.js";
import morgan from "morgan";
import { detector } from "megalodon";
const app = Express();
const logger = morgan(":method :url :status via :referrer - :response-time ms");
app.use(
serveStatic("src/public", {
maxAge: "1d",
}),
);
function doCache(res, durationSecs) {
res.set({
"Cache-Control": "max-age=" + durationSecs,
});
}
// this just redirects to the
app.options("/api/feed", cors());
app.get("/api/feed", cors(), logger, function (req, res) {
// get feed url
const feedUrl = req.query.url;
if (!feedUrl) {
res.status(400);
res.send(errorPage(400, "You need to specify a feed URL"));
return;
}
const userUrl = feedUrl.replace(/\.atom.*/i, "");
const redirectUrl = "/api/v1/feed?";
const qs = [ "userurl=" + encodeURIComponent(userUrl), "api=v1" ];
([ "size", "theme", "boosts", "replies" ]).forEach((key) => {
if (typeof req.query[ key ] != "undefined") {
qs.push(key + "=" + encodeURIComponent(req.query[ key ]));
}
});
res.redirect(redirectUrl + qs.join("&"));
});
app.options("/api/v1/feed", cors());
// http://localhost:8000/api/v1/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67
app.get("/api/v1/feed", cors(), logger, async function (req, res) {
// get feed url
// userUrl
let type = req.query.instance_type;
let userUrl = req.query.userurl;
if (userUrl === "" || userUrl === undefined) {
const user = req.query.user;
const instance = req.query.instance;
if (type === "" || type === undefined) {
type = await detector(instance).catch(() =>
"",
);
}
if (type === "mastodon" || type === "pleroma")
userUrl = instance + "/users/" + user;
else if (type === "misskey")
userUrl = instance + "/@" + user;
else {
res.status(400);
res.send(errorPage(400, "You need to specify a user URL"));
return;
}
}
const feedUrl = req.query.feedurl;
const 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;
}
}
opts.instance_type = type;
opts.boosts = true;
if (req.query.boosts) {
if (req.query.boosts.toLowerCase() == "no" || req.query.boosts.toLowerCase() == "false") {
opts.boosts = false;
} else {
opts.boosts = true;
}
}
opts.replies = true;
if (req.query.replies) {
if (req.query.replies.toLowerCase() == "no" || req.query.replies.toLowerCase() == "false") {
opts.replies = false;
} else {
opts.replies = true;
}
}
opts.userUrl = userUrl;
opts.feedUrl = feedUrl;
opts.mastofeedUrl = req.url;
convertv2(opts).then((data) => {
res.status(200);
doCache(res, 60 * 60);
res.send(data);
}).catch((er) => {
res.status(500);
res.send(errorPage(500, null, { theme: opts.theme, size: opts.size }));
// TODO log the error
console.error(er, er.stack);
});
});
app.listen(process.env.PORT || 8000, "127.0.0.1", function () {
console.log("Server started, listening on " + (process.env.PORT || 8000));
});

View file

@ -1,309 +0,0 @@
html,
body {
background-color: #ffffff;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #282c37;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #ecf0f4; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(255, 255, 255, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #8494ab; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #90a1ba;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #282c37;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #282c37;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #8494ab;
color: #ffffff;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }
.item-content,
.description,
.title,
html,
body {
font-weight: normal; }
@media (prefers-color-scheme: masto-dark) {
html,
body {
background-color: #282c37;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #ffffff;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #39404d; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(40, 44, 55, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #9baec8;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #ffffff;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #ffffff;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; } }

View file

@ -1,170 +0,0 @@
html,
body {
background-color: #ffffff;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #282c37;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #ecf0f4; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(255, 255, 255, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #8494ab; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #90a1ba;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #282c37;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #282c37;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #8494ab;
color: #ffffff;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }
.item-content,
.description,
.title,
html,
body {
font-weight: normal; }

View file

@ -1,351 +0,0 @@
html,
body {
background-color: rgb(249, 249, 249);
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #282c37;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: rgb(68, 164, 193); }
a[rel = "tag"] {
color: rgb(255, 145, 86); }
.meta {
background-color: #ecf0f4; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(255, 255, 255, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #8494ab; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #90a1ba;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #282c37;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.u-url,
.mention{
color: #86b300; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #282c37;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #8494ab;
color: #ffffff;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }
.item-content,
.description,
.title,
html,
body {
font-weight: normal; }
@media (prefers-color-scheme: misskey-dark) {
html,
body {
background-color: #232323;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #dadada;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: rgb(134, 179, 0); }
a[rel = "tag"] {
color: rgb(76, 184, 212); }
.meta {
background-color: #39404d; }
.header {
display: flex;
background-size: cover;
min-height: 8rem;
color: rgb(199, 209, 216); }
.header .header-left, .header .header-right {
margin: 0; }
.header .header-left {
min-width: 8rem;
position: relative;
text-align: center;
background: rgba(45, 45, 45, 0.3); }
.header .header-left .avatar {
width: 6rem;
height: 6rem;
position: relative;
top: calc(50% - 3rem); }
.header .header-right {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(45, 45, 45, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #9baec8;
font-size: 0.9rem;
opacity: .5; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #dadada;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.u-url,
.mention{
color: #86b300; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.1);
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; } }

View file

@ -1,171 +0,0 @@
html,
body {
background-color: #232323;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #dadada;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: rgb(134, 179, 0); }
a[rel = "tag"] {
color: rgb(76, 184, 212); }
.meta {
background-color: #39404d; }
.header {
display: flex;
background-size: cover;
min-height: 8rem;
color: rgb(199, 209, 216); }
.header .header-left, .header .header-right {
margin: 0; }
.header .header-left {
min-width: 8rem;
position: relative;
text-align: center;
background: rgba(45, 45, 45, 0.3); }
.header .header-left .avatar {
width: 6rem;
height: 6rem;
position: relative;
top: calc(50% - 3rem); }
.header .header-right {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(45, 45, 45, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #9baec8;
font-size: 0.9rem;
opacity: .5; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #dadada;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.u-url,
.mention{
color: #86b300; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.1);
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }

View file

@ -1,177 +0,0 @@
html,
body {
background-color: rgb(249, 249, 249);
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #282c37;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: rgb(68, 164, 193); }
a[rel = "tag"] {
color: rgb(255, 145, 86); }
.meta {
background-color: #ecf0f4; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(255, 255, 255, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #8494ab; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #90a1ba;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #282c37;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.u-url,
.mention{
color: #86b300; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #282c37;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #8494ab;
color: #ffffff;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }
.item-content,
.description,
.title,
html,
body {
font-weight: normal; }

View file

@ -1,163 +0,0 @@
html,
body {
background-color: rgba(15, 22, 30, 1);
font-family: 'Roboto', roboto, Arial, sans-serif;
color: rgba(185, 185, 186, 1);
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #e2b188; }
.meta {
background-color: #39404d; }
.header {
display: flex;
background-size: cover;
min-height: 8rem;
color: rgba(185, 185, 186, 1); }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(40, 44, 55, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: rgba(185, 185, 186, 0.5);
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: rgba(185, 185, 186, 1);
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: rgba(185, 185, 186, 1);
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }

View file

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
<title>Fedifeed - embeddable ActivityPub feeds</title>
<link rel="stylesheet" href="./css/stylesheet.css">
<script src="./js/script.js" defer></script>
</head>
<body>
<br>
<div>
<h1>Fedifeed</h1>
<h4>Embedded ActivityPub feeds for blogs, websites, etc.</h4>
<a href="https://github.com/SamTherapy/fedifeed" 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="mastodon.social"
oninvalid="this.setCustomValidity('Insert your instance URL. Example: mastodon.social')"
oninput="this.setCustomValidity('')"></label>
<br>
<label>Username:<input required type="text" id="usernamein" placeholder="gargron"
oninvalid="this.setCustomValidity('Insert your username. Example: gargron')"
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="masto-dark">masto-dark</option>
<option value="masto-light">masto-light</option>
<option value="masto-auto">masto-auto (based on css prefers-color-scheme)</option>
<option value="misskey-dark">misskey-dark</option>
<option value="misskey-light">misskey-light</option>
<option value="misskey-auto">misskey-auto (based on css prefers-color-scheme)</option>
<option value="pleroma">pleroma</option>
</select>
</label>
<br>
<label>Show Header?<input id="header" type="checkbox" checked="checked"></label>
<br>
<label>Hide replies?<input type="checkbox" id="hidereplies" checked="checked"></label>
<br>
<label>Hide boosts?<input type="checkbox" id="hideboosts" checked="checked"></label>
<br>
<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><br>
<h3>Live Preview:</h3>
<span class="iframe-contain">
<iframe id="frame" allowfullscreen sandbox="allow-top-navigation allow-scripts" width="400" height="800"
src="/api/v1/feed?userurl=https%3A%2F%2Ffreecumextremist.com%2Fusers%2Fgrumbulon&replies=false&boosts=true">></iframe>
</span>
<br>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,27 +0,0 @@
import getEmojiJson from "./emojis.js";
window.genUrl = function genUrl() {
function val(id) {
return document.getElementById(id).value;
}
//download emoji pack
getEmojiJson(val("urlin"));
let user = val("usernamein");
let instance = "https://" + val("urlin");
let showBoosts = (!document.getElementById("hideboosts").checked).toString();
let showReplies = (!document.getElementById("hidereplies").checked).toString();
let showHeader = document.getElementById("header").checked.toString();
let portStr = (window.location.port && window.location.port != 80) ? (":" + window.location.port) : "";
let iframeUrl = window.location.protocol + "//" + window.location.hostname + portStr
+ "/api/v1/feed?user=" + encodeURIComponent(user) + "&instance=" + encodeURIComponent(instance) + "&instance_type=" + "&theme=" + val("theme") + "&size=" + val("size")
+ "&header=" + showHeader + "&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>";
let iframe = document.getElementById("frame");
iframe.src = iframeUrl;
iframe.width = val("width");
iframe.height = val("height");
};

View file

@ -1,309 +0,0 @@
html,
body {
background-color: #ffffff;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #282c37;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #ecf0f4; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(255, 255, 255, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #8494ab; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #90a1ba;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #282c37;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #282c37;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #8494ab;
color: #ffffff;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }
.item-content,
.description,
.title,
html,
body {
font-weight: normal; }
@media (prefers-color-scheme: masto-dark) {
html,
body {
background-color: #282c37;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #ffffff;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #39404d; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(40, 44, 55, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #9baec8;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #ffffff;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #ffffff;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; } }

View file

@ -1,163 +0,0 @@
html,
body {
background-color: #282c37;
font-family: 'Roboto', roboto, Arial, sans-serif;
color: #ffffff;
font-weight: lighter;
overflow-x: hidden;
font-size: 100%;
word-break: break-word; }
* {
margin: 0;
padding: 0; }
a,
a * {
color: #2b90d9; }
.meta {
background-color: #39404d; }
.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 {
flex-grow: 1;
font-size: 0.9rem;
padding: 0.9rem;
background: rgba(40, 44, 55, 0.85); }
.header .header-title {
font-size: 1.3rem; }
.item {
padding: 1rem;
border-top: solid 1px #626d80; }
.item-content,
.cw,
.title {
font-size: 1.1rem;
font-weight: lighter; }
.item-content *, .cw {
margin: 1rem 0;
line-height: 1.4rem; }
.item-title,
.date,
.author-fullname {
color: #9baec8;
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
.author {
display: flex;
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
display: flex;
flex-direction: column;
justify-content: space-around; }
.author-info .author-displayname {
font-size: 1.2rem;
color: #ffffff;
text-decoration: none;
display: block;
font-weight: bolder; }
.avatar {
width: 3rem;
height: 3rem;
border: none;
border-radius: 10%; }
.avatar.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%;
border: none;
max-height: 12rem; }
a.enclosure {
cursor: zoom-in; }
.enclosure > * {
flex: 1 1 auto;
width: 100%;
height: 100%;
object-fit: cover; }
.meta .title {
font-weight: bold; }
.hidden {
display: none; }
.button {
padding: 0.5rem 1rem;
border: none;
margin: 1rem;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
background: #2b90d9;
color: #ffffff;
font-weight: 400;
cursor: pointer;
text-transform: uppercase;
user-select: none; }
label.button {
padding: 0.25rem 0.5rem;
margin: 0.4rem;
background: #626d80;
color: #282c37;
font-size: 0.8rem; }
input[type=checkbox] {
position: absolute;
left: -9999px; }
input[type=checkbox]:checked ~ label::after {
content: " Less"; }
input[type=checkbox]:not(:checked) ~ label::after {
content: " More"; }
input[type=checkbox]:not(:checked) ~ div {
display: none; }

View file

@ -1,5 +0,0 @@
@import 'masto-light.scss';
@media (prefers-color-scheme: masto-dark) {
@import 'masto-dark.scss';
}

View file

@ -65,19 +65,11 @@ a * {
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
margin: 1rem 0 0 0; }
.author {
display: flex;
margin-bottom: 1rem; }
margin: 1rem 0; }
.author-info {
margin: 0 1rem;
@ -110,6 +102,7 @@ a * {
display: flex;
flex: 1 1 auto;
width: 50%;
display: inline-block;
border: none;
max-height: 12rem; }

View file

Before

(image error) Size: 1.5 KiB

After

(image error) Size: 1.5 KiB

65
static/index.html Normal file
View file

@ -0,0 +1,65 @@
<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" checked="checked"></label><br>
<label>Hide boosts?<input type="checkbox" id="hideboosts" 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="/apiv2/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&replies=false&boosts=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');
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):'')
+"/apiv2/feed?userurl="+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');
}
</script>
</body>
</html>

12
static/infinite-scroll.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -65,19 +65,11 @@ a * {
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0;
text-decoration: none;
display: block; }
.date:hover {
text-decoration: underline; }
.item-title {
margin-bottom: 0.7rem; }
margin: 1rem 0 0 0; }
.author {
display: flex;
margin-bottom: 1rem; }
margin: 1rem 0; }
.author-info {
margin: 0 1rem;
@ -110,6 +102,7 @@ a * {
display: flex;
flex: 1 1 auto;
width: 50%;
display: inline-block;
border: none;
max-height: 12rem; }

View file

@ -3,6 +3,7 @@
*/
@import url(https://fonts.googleapis.com/css?family=Quando|Judson|Montserrat:500|Roboto:400,500);
@import url(https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css);
a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote,
body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl,
dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4,
@ -43,7 +44,7 @@ body {
text-size-adjust: none;
box-sizing: border-box;
font-family: Roboto, sans-serif;
background-color: #000000;
background-color: #1f232b;
color: #5f6b84;
font-size: 13px;
line-height: 18px;
@ -57,7 +58,22 @@ h1 {
margin-bottom: 20px
}
input[type="text"], input[type="number"], select {
button {
background: transparent;
border: 1px solid #9baec8;
padding: 3px 15px;
color: #9baec8;
margin-left: 4px;
font-weight: 500;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, .1)
}
button:hover {
background-color: #b5c3d6
}
input[type="text"], input[type="number"] {
outline: 0;
box-sizing: border-box;
border-radius: 4px;
@ -94,12 +110,11 @@ div {
margin: 0 auto
}
.iframe-contain {
display:block;
text-align:center;
iframe {
float: middle;
}
button, a {
a {
display: inline-block;
text-align: center;
font-size: 16px;
@ -111,8 +126,5 @@ button, a {
border-radius: 4px;
padding: 3px 15px;
color: #9baec8;
cursor:pointer;
margin-left: 4px
}
button:hover, a:hover {
background-color: #394150;
}

View file

@ -88,19 +88,11 @@ a * {
}
.date{
margin: 1rem 0 0 0;
text-decoration: none;
display:block;
}
.date:hover{
text-decoration: underline;
}
.item-title{
margin-bottom:0.7rem;
}
.author {
display: flex;
margin-bottom: 1rem;
margin: 1rem 0;
}
.author-info {
@ -139,6 +131,7 @@ a * {
display:flex;
flex: 1 1 auto;
width:50%;
display: inline-block;
border: none;
max-height: 12rem;
}

View file

@ -7,4 +7,4 @@ $dim: $lighter;
$dimmer: mix($darkest,$lighter,50%);
$link: $vibrant;
@import 'masto-base.scss';
@import 'base.scss';

View file

@ -7,7 +7,7 @@ $dim: mix($lighter,$darkest,90%);
$dimmer: mix($lighter,$darkest,80%);
$link: $vibrant;
@import 'masto-base.scss';
@import 'base.scss';
.item-content,
.description,

View file

@ -10,7 +10,7 @@
</a>boosted
</span>
</div>
<div class="detailed-status masto-light">
<div class="detailed-status light">
<a class="detailed-status__display-name p-author h-card" target="_blank" rel="noopener" href="https://social.tchncs.de/@muninnherself"><div>
<div class="avatar">
<img alt="" class="u-photo" src="https://assets.octodon.social/accounts/avatars/000/018/289/original/eb6b32e85f8606b4.jpg" width="48" height="48">

View file

@ -4,7 +4,7 @@
<style type="text/css"></style>
<link rel="stylesheet" href="/masto-dark.css"></link>
<link rel="stylesheet" href="/dark.css"></link>

View file

@ -1,14 +1,15 @@
// do a test
import { createReadStream, writeFileSync } from "fs";
import convert from "../lib/convert.js";
var fs = require('fs'),
request = require('request'),
convert = require('../lib/convert')
let r = createReadStream("./test/sample.atom");
var r = fs.createReadStream('./test/sample.atom');
convert(r,function(er,data){
if (er){return console.log("error: ",er);}
console.log("ok");
writeFileSync("./test/result.html",data,"utf8");
});
if (er){return console.log('error: ',er)}
console.log('ok');
fs.writeFileSync('./test/result.html',data,'utf8');
})

2958
yarn.lock

File diff suppressed because it is too large Load diff