Compare commits

..

38 commits

Author SHA1 Message Date
grumbulon
8906b82751 Get emote list for rendering. Still figuring out how it should all work together. 2022-04-14 00:23:07 -04:00
grumbulon
c222b50cbd Commented this wacky shit 2022-04-13 23:41:40 -04:00
grumbulon
1b6346f1b3 Removed commented stuff from earlier testing 2022-04-13 23:38:25 -04:00
grumbulon
e31eebc844 yeah 2022-04-13 23:33:46 -04:00
grumbulon
8063fd6680 started very poorly implementing emojis in posts 2022-04-13 23:23:18 -04:00
c7fe02652b
Make system only listen on localhost
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-03-30 19:35:42 +02:00
2d3e4ede87
Fix API inconsistencies
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-03-03 19:24:47 +01:00
9e0433fc56
fix lint
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-07 08:47:54 -06:00
8a6a005502
change var to let
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 18:46:21 -06:00
dce7ececa7
update README to be clear this is a fork
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 17:26:34 -06:00
fbc6c69729
Add misskey and pleroma themes
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 16:48:09 -06:00
34c1302905
remove typescript
It works now why change it

Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 15:30:57 -06:00
22c69bc1b7
Add fedifeed to readme
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 15:25:51 -06:00
79727bde88
fix drone yml agaom
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 14:59:52 -06:00
062a9b2555
fix drone yml
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 14:59:25 -06:00
9ef773497b
Add Misskey support
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 14:46:27 -06:00
73a0ccf871
Redo app from the ground up
Make it an ES module, TODO: Typescript
Make the themes explicitly mastodon

Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 13:14:18 -06:00
2f34e2a7d9
Change API link to the usual v2 link
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-06 12:28:08 -06:00
Drew Harwell
10720ac118
Change example to @gargron@mastodon.social 2021-12-20 10:50:03 -06:00
Drew Harwell
6075837da2
Merge pull request #24 from fenwick67/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-09-18 14:23:16 -04:00
dependabot[bot]
db2dcfa6f4
Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-18 17:24:04 +00:00
fenwick67
ec10336ccb update deps 2020-04-04 13:28:33 -04:00
fenwick67
f91f7cada8 upgrade node-sass to get rid of warnings 2020-04-04 13:27:11 -04:00
fenwick67
118b92f6b6 fix some visual bugs
* dates were wrapping with CWs
* sometimes the author's name wasn't showing up
* CWs were spaced weird with the author block
2020-04-04 13:21:51 -04:00
Drew Harwell
c3b65d291e
Update template.ejs 2020-01-22 17:54:48 -05:00
Drew Harwell
c1f9ea7b3a
fix issue with empty anchors 2020-01-22 17:50:49 -05:00
Drew Harwell
d33129ff96
handle case where href="" 2020-01-22 17:44:58 -05:00
fenwick67
91700a596d fix #20 2019-12-19 19:19:32 -05:00
Drew Harwell
06ece16d62 guard against next page not being found in AP feed 2019-11-22 10:46:46 -05:00
Drew Harwell
f4b6ab38f3 Merge branch 'master' of https://github.com/fenwick67/mastofeed 2019-11-08 11:46:36 -05:00
Drew Harwell
5b6fc4a099 improve landing page 2019-11-08 11:46:17 -05:00
Drew Harwell
de53c677a6 improve stylesheet, make audio enclosures work, add toot permalink 2019-11-08 10:54:39 -05:00
Drew Harwell
aa2a5b91e5
Update README.md 2019-11-05 10:04:43 -05:00
Drew Harwell
9ec240eb8b add auto color scheme 2019-11-04 17:22:03 -05:00
Drew Harwell
268c6bc22c
Update base.scss 2019-09-19 14:37:41 -04:00
Drew Harwell
3c2c77f628
Update dark.css 2019-09-19 14:37:12 -04:00
Drew Harwell
0caa354dd9
Update light.css 2019-09-19 14:36:57 -04:00
fenwick67
c1c0e2f7d0 fix issue with non-masto AP instances which may set outbox.first to the actual first page of the outbox instead of a URL 2019-09-05 15:42:40 -04:00
43 changed files with 13246 additions and 2890 deletions

21
.drone.yml Normal file
View file

@ -0,0 +1,21 @@
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]

1
.eslintignore Normal file
View file

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

30
.eslintrc.json Normal file
View file

@ -0,0 +1,30 @@
{
"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,2 +1,130 @@
node_modules
*.atom
# 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.*

View file

@ -1,8 +1,11 @@
# Mastofeed
# Fedifeed
Embed a mastodon feed in your blog et cetera.
[![Build Status](https://ci.git.froth.zone/api/badges/Sam/fedifeed/status.svg)](https://ci.git.froth.zone/Sam/fedifeed)
https://www.mastofeed.com
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
## User guide
@ -10,46 +13,35 @@ The homepage has a tool for generating iframe code for you, with a sensible `san
## API
### V2
### V1
#### GET `/apiv2/feed`
#### GET `/api/v1/feed`
> example: `/api/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=light`
> example: `/api/v1/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=masto-light`
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')` ).
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')` ).
Querystring options:
| option | required | description |
| ------ | -------- | ----------- |
| `userurl` | **yes** | Mastodon/ActivityPub account URL (usually `https://${instance}/users/${username}`) |
| `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}`) |
| `feedurl` | no | a URL to a page of an ActivityPub post collection. Only used for pages beyond the first. |
| `theme` | no | either `dark` or `light`, to select the UI theme (default is `dark`). |
| `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. |
| `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. |
### 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` is required if `instance` and `user` are not specified.\*\*\* \
\*\* `instance` **and** `user` are required if `userurl` is not specified.\*\*\*
\*\*\* **`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 `npm install` and then `npm 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 `yarn install` and then `yarn 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 chaching layer, improve the styles and add more features.
Feel free to add a caching layer, improve the styles and add more features.

View file

@ -1,22 +0,0 @@
// 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
View file

@ -1,166 +0,0 @@
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,312 +1,248 @@
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];
}
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];
}

View file

@ -1,247 +0,0 @@
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];
}

15
lib/emoji.js Normal file
View file

@ -0,0 +1,15 @@
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 @@
var ejs = require('ejs');
var fs = require('fs');
var template = ejs.compile(fs.readFileSync('./lib/template.ejs', 'utf8'));
import { compile } from "ejs";
import { readFileSync } from "fs";
let template = compile(readFileSync("./lib/template.ejs", "utf8"));
module.exports = function(code,message,displayOptions){
export default function(code,message,displayOptions){
var msg;
var displayOptions = displayOptions || {};
let msg;
// const 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://github.com/fenwick67/mastofeed/issues">please open an issue on GitHub</a>, or message fenwick67@octodon.social</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://git.froth.zone/Sam/fedifeed/issues\">please open an issue on Gitea</a>, or message sam@froth.zone</p>";
}else{
msg = message||'';
msg = message||"";
}
var options = {
let options = {
opts:{
header:true,
theme:displayOptions.theme||null,
@ -23,14 +23,14 @@ module.exports = 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,10 +4,20 @@
<style type="text/css"></style>
<base target="_top" /><!-- this element is amazing-->
<% if (opts.theme && opts.theme.toLowerCase() == 'light'){ %>
<link rel="stylesheet" href="/light.css"></link>
<% 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>
<% } else { %>
<link rel="stylesheet" href="/dark.css"></link>
<link rel="stylesheet" href="/css/masto-dark.css"></link>
<% } %>
<% if (opts.size){ %>
@ -41,7 +51,7 @@
<div class="container">
<% var filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
<% let filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
<% filtered.forEach(function(item){ %>
<div class="item">
<% if (item.isBoost) { %>
@ -57,21 +67,33 @@
</div>
</div>
<% if (item.hasCw){ %>
<% var cwId = (item.cw+item.atomHref).replace(/\W+/g,'') %>
<% let 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 (var i = 0; i < item.enclosures.length; i ++){ %>
<% var e = item.enclosures[i] %>
<% if (e.type.indexOf('video') > -1){ %>
<% 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){ %>
<video class="enclosure" controls loop src="<%= e.url %>"/>
<% } else { %>
<a target="_top" class="enclosure" href="<%= e.url %>" >
@ -85,27 +107,27 @@
<% } %>
</div>
<% } %>
<div class="date"><%= item.stringDate %></div>
<a class="date" href="<%= item.permalink %>"><%= item.stringDate %></a>
</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">
var lastPageLoaded = null;
var infScroll = new InfiniteScroll( '.container', {
let lastPageLoaded = null;
let infScroll = new InfiniteScroll( '.container', {
// options
hideNav:'.pagination',
append: '.item',
@ -113,14 +135,14 @@
prefill:true,
path: function(){
// need to query this DOM my damn self
var pageLinks = document.querySelectorAll('.hacky_link');
let pageLinks = document.querySelectorAll('.hacky_link');
if (!pageLinks || pageLinks.length == 0){
console.log ('next page link could not be found');
return false;
}else{
var finalLink = pageLinks[pageLinks.length-1].href;
let finalLink = pageLinks[pageLinks.length-1].href;
// make sure we don't load the same page twice
if (finalLink == lastPageLoaded){
if (!finalLink || finalLink == window.location.href || finalLink == lastPageLoaded){
console.log('this was the last page');
return false;
}else{

1949
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load diff

7617
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,26 @@
{
"dependencies": {
"cors": "^2.8.5",
"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"
"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"
},
"main": "index.js",
"type": "module",
"license": "MIT",
"scripts": {
"start": "node index.js",
"build-styles": "node build-styles.js"
"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"
},
"devDependencies": {
"node-sass": "^4.12.0"
"eslint": "^8.8.0",
"node-sass": "^7.0.1"
}
}

22
src/build-styles.js Normal file
View file

@ -0,0 +1,22 @@
// 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");

130
src/index.js Normal file
View file

@ -0,0 +1,130 @@
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

@ -0,0 +1,309 @@
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

@ -65,11 +65,19 @@ a * {
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0; }
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: 1rem 0; }
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
@ -102,7 +110,6 @@ a * {
display: flex;
flex: 1 1 auto;
width: 50%;
display: inline-block;
border: none;
max-height: 12rem; }

View file

@ -65,11 +65,19 @@ a * {
font-size: 0.9rem; }
.date {
margin: 1rem 0 0 0; }
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: 1rem 0; }
margin-bottom: 1rem; }
.author-info {
margin: 0 1rem;
@ -102,7 +110,6 @@ a * {
display: flex;
flex: 1 1 auto;
width: 50%;
display: inline-block;
border: none;
max-height: 12rem; }

View file

@ -0,0 +1,351 @@
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

@ -0,0 +1,171 @@
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

@ -0,0 +1,177 @@
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; }

163
src/public/css/pleroma.css Normal file
View file

@ -0,0 +1,163 @@
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

@ -3,7 +3,6 @@
*/
@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,
@ -44,7 +43,7 @@ body {
text-size-adjust: none;
box-sizing: border-box;
font-family: Roboto, sans-serif;
background-color: #1f232b;
background-color: #000000;
color: #5f6b84;
font-size: 13px;
line-height: 18px;
@ -58,22 +57,7 @@ h1 {
margin-bottom: 20px
}
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"] {
input[type="text"], input[type="number"], select {
outline: 0;
box-sizing: border-box;
border-radius: 4px;
@ -110,11 +94,12 @@ div {
margin: 0 auto
}
iframe {
float: middle;
.iframe-contain {
display:block;
text-align:center;
}
a {
button, a {
display: inline-block;
text-align: center;
font-size: 16px;
@ -126,5 +111,8 @@ a {
border-radius: 4px;
padding: 3px 15px;
color: #9baec8;
margin-left: 4px
cursor:pointer;
}
button:hover, a:hover {
background-color: #394150;
}

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

76
src/public/index.html Normal file
View file

@ -0,0 +1,76 @@
<!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

27
src/public/js/script.js Normal file
View file

@ -0,0 +1,27 @@
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");
};

309
src/public/masto-auto.css Normal file
View file

@ -0,0 +1,309 @@
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; } }

163
src/public/masto-dark.css Normal file
View file

@ -0,0 +1,163 @@
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; }

170
src/public/masto-light.css Normal file
View file

@ -0,0 +1,170 @@
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

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

View file

@ -88,11 +88,19 @@ 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: 1rem 0;
margin-bottom: 1rem;
}
.author-info {
@ -131,7 +139,6 @@ 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 'base.scss';
@import 'masto-base.scss';

View file

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

View file

@ -1,65 +0,0 @@
<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>

File diff suppressed because one or more lines are too long

View file

@ -10,7 +10,7 @@
</a>boosted
</span>
</div>
<div class="detailed-status light">
<div class="detailed-status masto-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="/dark.css"></link>
<link rel="stylesheet" href="/masto-dark.css"></link>

View file

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

2958
yarn.lock Normal file

File diff suppressed because it is too large Load diff