Add Misskey support
Signed-off-by: Sam Therapy <sam@samtherapy.net>
This commit is contained in:
parent
73a0ccf871
commit
9ef773497b
28 changed files with 1273 additions and 585 deletions
20
.drone.yml
Normal file
20
.drone.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: dependencies
|
||||
image: node
|
||||
command: yarn
|
||||
|
||||
- name: lint
|
||||
image: node
|
||||
commands:
|
||||
- yarn lint
|
||||
depends: [dependencies]
|
||||
|
||||
- name: test
|
||||
image: node
|
||||
commands:
|
||||
- yarn test
|
||||
depends: [lint]
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/public/infinite-scroll.js
|
29
.eslintrc.json
Normal file
29
.eslintrc.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"env": {
|
||||
"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
132
.gitignore
vendored
|
@ -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.*
|
20
README.md
20
README.md
|
@ -1,8 +1,8 @@
|
|||
# Mastofeed
|
||||
# Fedifeed
|
||||
|
||||
Embed a mastodon feed in your blog et cetera.
|
||||
Embed an activitypub feed in your blog et cetera.
|
||||
|
||||
https://www.mastofeed.com
|
||||
https://www.fedifeed.com
|
||||
|
||||
## User guide
|
||||
|
||||
|
@ -14,24 +14,30 @@ The homepage has a tool for generating iframe code for you, with a sensible `san
|
|||
|
||||
#### GET `/api/v1/feed`
|
||||
|
||||
> example: `/api/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=masto-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 `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. |
|
||||
|
||||
\* `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
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
// build the styles
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { renderSync } from 'node-sass';
|
||||
|
||||
var staticDir = './static/'
|
||||
var srcDir = './stylesrc/';
|
||||
var themes = ['masto-light','masto-dark','masto-auto'];
|
||||
|
||||
|
||||
themes.forEach(function(s){
|
||||
var sassFile = srcDir+s+'.scss'
|
||||
var cssFile = staticDir+s+'.css'
|
||||
var result = renderSync({
|
||||
data: readFileSync(sassFile,'utf8'),
|
||||
includePaths:[srcDir]
|
||||
});
|
||||
|
||||
writeFileSync(cssFile,result.css,'utf8')
|
||||
|
||||
});
|
||||
|
||||
console.log('ok');
|
117
index.js
117
index.js
|
@ -1,117 +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';
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 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 = '/api/v1/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('/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,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;
|
||||
|
||||
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));
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
import { compile } from 'ejs';
|
||||
import { readFileSync } from 'fs';
|
||||
var template = compile(readFileSync('./lib/template.ejs', 'utf8'));
|
||||
import { format } from 'timeago.js';
|
||||
import { compile } from "ejs";
|
||||
import { readFileSync } from "fs";
|
||||
var template = compile(readFileSync("./lib/template.ejs", "utf8"));
|
||||
import { format } from "timeago.js";
|
||||
|
||||
import got from 'got';
|
||||
import got from "got";
|
||||
const map = new Map();
|
||||
|
||||
const hour = 3600000;
|
||||
|
@ -14,13 +14,13 @@ const hour = 3600000;
|
|||
// a single process install it will be fine.
|
||||
|
||||
// note: rejects on HTTP 4xx or 5xx
|
||||
async function apGet(url,ttl) {
|
||||
async function apGet(url) {
|
||||
|
||||
return new Promise(function(resolve,reject){
|
||||
|
||||
// fail early
|
||||
if (!url){
|
||||
reject(new Error('URL is invalid'));
|
||||
reject(new Error("URL is invalid"));
|
||||
}
|
||||
|
||||
got( {
|
||||
|
@ -30,11 +30,11 @@ async function apGet(url,ttl) {
|
|||
"accept": "application/activity+json"
|
||||
}
|
||||
})
|
||||
.then(response=>JSON.parse(response.body))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(response=>JSON.parse(response.body))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
@ -42,31 +42,31 @@ async function apGet(url,ttl) {
|
|||
async function promiseSome(proms){
|
||||
|
||||
function noRejectWrap(prom){
|
||||
return new Promise(function(resolve,reject){
|
||||
return new Promise(function(resolve){
|
||||
|
||||
prom // it's already been called
|
||||
.then(resolve)
|
||||
.catch(e=>{
|
||||
.then(resolve)
|
||||
.catch( ()=>{
|
||||
// console.warn(e);// for debugging
|
||||
resolve(null)
|
||||
})
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return await Promise.all(proms.map(noRejectWrap))
|
||||
return await Promise.all(proms.map(noRejectWrap));
|
||||
|
||||
}
|
||||
|
||||
export default async function (opts) {
|
||||
var opts = opts;
|
||||
// var opts = opts;
|
||||
|
||||
var feedUrl = opts.feedUrl;
|
||||
var userUrl = opts.userUrl;
|
||||
var isIndex = false;
|
||||
|
||||
if (!userUrl) {
|
||||
throw new Error('need user URL');
|
||||
throw new Error("need user URL");
|
||||
}
|
||||
|
||||
var user, feed;
|
||||
|
@ -83,7 +83,7 @@ export default async function (opts) {
|
|||
var 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'){
|
||||
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.
|
||||
|
@ -110,7 +110,7 @@ function metaForUser(user) {
|
|||
title: user.name||user.preferredUsername||null,
|
||||
description: user.summary||null,
|
||||
link:user.url||"#"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function itemsForFeed(opts,user,feed) {
|
||||
|
@ -125,7 +125,7 @@ async function itemsForFeed(opts,user,feed) {
|
|||
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));
|
||||
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 ++){
|
||||
|
@ -155,18 +155,18 @@ async function itemsForFeed(opts,user,feed) {
|
|||
}
|
||||
|
||||
if (index == -1){
|
||||
console.warn("warning: couldn't match boost to item: ",boostToot)
|
||||
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
|
||||
return typeof item.object == "object";// handle weird cases
|
||||
}).map((item)=>{
|
||||
|
||||
var enclosures = (item.object.attachment||[]).filter((a)=>{
|
||||
|
@ -176,46 +176,46 @@ async function itemsForFeed(opts,user,feed) {
|
|||
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:'',
|
||||
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
|
||||
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:'#',
|
||||
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:'',
|
||||
avatar:op.icon&&op.icon.url?op.icon.url:"",
|
||||
displayName:op.name || op.preferredUsername,
|
||||
fullName:op.preferredUsername+'@'+(new URL(op.url).hostname),
|
||||
fullName:op.preferredUsername+"@"+(new URL(op.url).hostname),
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getNextPage(opts,user,feed){
|
||||
//based on feed.next
|
||||
if (!feed.next){return null}
|
||||
if (!feed.next){return null;}
|
||||
// take feed.next, uriencode it, then take user url, then take options.mastofeedUrl
|
||||
var base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf('?'));
|
||||
//var base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf("?"));
|
||||
|
||||
var ret = '/api/v1/feed?userurl=' + encodeURIComponent(opts.userUrl) + '&feedurl=' +encodeURIComponent(feed.next);
|
||||
var 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'){
|
||||
(["theme","header","size","boosts","replies"]).forEach((k)=>{
|
||||
if (typeof opts[k] != "undefined"){
|
||||
ret+=`&${k}=${ opts[k].toString() }`;
|
||||
}
|
||||
})
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -223,8 +223,8 @@ function getNextPage(opts,user,feed){
|
|||
// utilities below
|
||||
|
||||
function getTimeDisplay(d) {
|
||||
var d = d;
|
||||
if (typeof d !== 'object') {
|
||||
// var d = d;
|
||||
if (typeof d !== "object") {
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
|
@ -244,9 +244,9 @@ function getTimeDisplay(d) {
|
|||
|
||||
function isoDateToEnglish(d) {
|
||||
|
||||
var dt = d.split(/[t\-]/ig);
|
||||
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];
|
||||
return months[Number(dt[1]) - 1] + " " + dt[2] + ", " + dt[0];
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { compile } from 'ejs';
|
||||
import { readFileSync } from 'fs';
|
||||
var template = compile(readFileSync('./lib/template.ejs', 'utf8'));
|
||||
import { compile } from "ejs";
|
||||
import { readFileSync } from "fs";
|
||||
var template = compile(readFileSync("./lib/template.ejs", "utf8"));
|
||||
|
||||
export default function(code,message,displayOptions){
|
||||
|
||||
var msg;
|
||||
var displayOptions = displayOptions || {};
|
||||
// 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://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://git.froth.zone/Sam/fedifeed/issues\">please open an issue on Gitea</a>, or message sam@froth.zone</p>";
|
||||
}else{
|
||||
msg = message||'';
|
||||
msg = message||"";
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
17
package.json
17
package.json
|
@ -4,21 +4,30 @@
|
|||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.2",
|
||||
"feedparser": "^2.2.10",
|
||||
"morgan": "^1.10.0",
|
||||
"got": "^12.0.1",
|
||||
"megalodon": "^4.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-cache": "^2.0.1",
|
||||
"serve-static": "^1.14.2",
|
||||
"timeago.js": "^4.0.2"
|
||||
"timeago.js": "^4.0.2",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"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": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/serve-static": "^1.13.10",
|
||||
"eslint": "^8.8.0",
|
||||
"node-sass": "^7.0.1"
|
||||
}
|
||||
}
|
||||
|
|
22
src/build-styles.js
Normal file
22
src/build-styles.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// build the styles
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { renderSync } from "node-sass";
|
||||
|
||||
var staticDir = "./src/public/";
|
||||
var srcDir = "./src/stylesrc/";
|
||||
var themes = ["masto-light","masto-dark","masto-auto"];
|
||||
|
||||
|
||||
themes.forEach(function(s){
|
||||
var sassFile = srcDir+s+".scss";
|
||||
var cssFile = staticDir+s+".css";
|
||||
var result = renderSync({
|
||||
data: readFileSync(sassFile,"utf8"),
|
||||
includePaths:[srcDir]
|
||||
});
|
||||
|
||||
writeFileSync(cssFile,result.css,"utf8");
|
||||
|
||||
});
|
||||
|
||||
console.log("ok");
|
130
src/index.js
Normal file
130
src/index.js
Normal 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 = "";
|
||||
if (type === "") {
|
||||
const user = req.query.user;
|
||||
const instance = req.query.instance;
|
||||
let instanceType = await detector(instance);
|
||||
if (instanceType === "mastodon" || instanceType === "pleroma") {
|
||||
userUrl = instance + "/users/" + user;
|
||||
type = instanceType;
|
||||
} else if (instanceType === "misskey") {
|
||||
userUrl = instance + "/@" + user;
|
||||
type = instanceType;
|
||||
} else {
|
||||
res.status(400);
|
||||
res.send(errorPage(400, "You need to specify a user URL"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
userUrl = req.query.userurl;
|
||||
}
|
||||
|
||||
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, function() {
|
||||
console.log("Server started, listening on "+(process.env.PORT || 8000));
|
||||
});
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<title>Mastofeed - embeddable Mastodon feeds</title>
|
||||
<title>Fedifeed - embeddable ActivityPub feeds</title>
|
||||
<link rel="stylesheet" href="./stylesheet.css">
|
||||
</head>
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
|||
<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
|
||||
<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>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<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%2Fmastodon.social%2Fusers%2Fgargron&replies=false&boosts=true"></iframe>
|
||||
src=""></iframe>
|
||||
</span>
|
||||
<br>
|
||||
</div>
|
||||
|
@ -71,7 +71,9 @@
|
|||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
var inUrl = 'https://' + val('urlin') + '/users/' + val('usernamein');
|
||||
// var inUrl = 'https://' + val('urlin') + '/users/' + val('usernamein');
|
||||
let user = val('usernamein');
|
||||
let instance = "https://" + val('urlin');
|
||||
|
||||
var showBoosts = (!document.getElementById('hideboosts').checked).toString();
|
||||
var showReplies = (!document.getElementById('hidereplies').checked).toString();
|
||||
|
@ -79,7 +81,7 @@
|
|||
var portStr = (window.location.port && window.location.port != 80) ? (':' + window.location.port) : ''
|
||||
|
||||
var iframeUrl = window.location.protocol + '//' + window.location.hostname + portStr
|
||||
+ "/api/v1/feed?userurl=" + encodeURIComponent(inUrl) + "&theme=" + val('theme') + '&size=' + val('size')
|
||||
+ "/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>';
|
12
src/public/infinite-scroll.js
Normal file
12
src/public/infinite-scroll.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -169,7 +169,7 @@ html,
|
|||
body {
|
||||
font-weight: normal; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@media (prefers-color-scheme: masto-dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #282c37;
|
File diff suppressed because one or more lines are too long
14
test/test.js
14
test/test.js
|
@ -1,14 +1,14 @@
|
|||
// do a test
|
||||
|
||||
|
||||
import { createReadStream, writeFileSync } from 'fs';
|
||||
import convert from '../lib/convert.js';
|
||||
import { createReadStream, writeFileSync } from "fs";
|
||||
import convert from "../lib/convert.js";
|
||||
|
||||
|
||||
var r = createReadStream('./test/sample.atom');
|
||||
var r = 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");
|
||||
writeFileSync("./test/result.html",data,"utf8");
|
||||
});
|
||||
|
|
Reference in a new issue