forked from sam/fedifeed
Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
|
c03766b565 |
43 changed files with 2890 additions and 13246 deletions
21
.drone.yml
21
.drone.yml
|
@ -1,21 +0,0 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: dependencies
|
||||
image: node
|
||||
commands:
|
||||
- yarn
|
||||
|
||||
- name: lint
|
||||
image: node
|
||||
commands:
|
||||
- yarn lint
|
||||
depends: [dependencies]
|
||||
|
||||
- name: test
|
||||
image: node
|
||||
commands:
|
||||
- yarn test
|
||||
depends: [lint]
|
|
@ -1 +0,0 @@
|
|||
src/public/infinite-scroll.js
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
132
.gitignore
vendored
132
.gitignore
vendored
|
@ -1,130 +1,2 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
node_modules
|
||||
*.atom
|
||||
|
|
46
README.md
46
README.md
|
@ -1,11 +1,8 @@
|
|||
# Fedifeed
|
||||
# Mastofeed
|
||||
|
||||
[![Build Status](https://ci.git.froth.zone/api/badges/Sam/fedifeed/status.svg)](https://ci.git.froth.zone/Sam/fedifeed)
|
||||
Embed a mastodon feed in your blog et cetera.
|
||||
|
||||
Embed an activitypub feed in your blog et cetera. \
|
||||
This is a fork of [mastofeed](https://github.com/fenwick67/mastofeed) that adds support for more themes and Misskey.
|
||||
|
||||
https://www.fedifeed.com
|
||||
https://www.mastofeed.com
|
||||
|
||||
## User guide
|
||||
|
||||
|
@ -13,35 +10,46 @@ The homepage has a tool for generating iframe code for you, with a sensible `san
|
|||
|
||||
## API
|
||||
|
||||
### V1
|
||||
### V2
|
||||
|
||||
#### GET `/api/v1/feed`
|
||||
#### GET `/apiv2/feed`
|
||||
|
||||
> example: `/api/v1/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=masto-light`
|
||||
> example: `/api/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&scale=90&theme=light`
|
||||
|
||||
Returns a html page which displays a feed for a user URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67')` ).
|
||||
Returns a html page which displays a mastodon feed for a user URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67')` ).
|
||||
|
||||
Querystring options:
|
||||
|
||||
| option | required | description |
|
||||
| ------ | -------- | ----------- |
|
||||
| `userurl` | **\*** | Mastodon/Pleroma/Misskey account URL (usually `https://${instance}/users/${username}` for MastoAPI or `https://${instance}/@${username}` for Misskey) |
|
||||
| `instance` | **\*\***| Mastodon/Pleroma/Misskey instance URL (usually `https://${instance}`) |
|
||||
| `user` | **\*\*** | Mastodon/Pleroma/Misskey user ID (usually `${username}`) |
|
||||
| `userurl` | **yes** | Mastodon/ActivityPub account URL (usually `https://${instance}/users/${username}`) |
|
||||
| `feedurl` | no | a URL to a page of an ActivityPub post collection. Only used for pages beyond the first. |
|
||||
| `theme` | no | either `masto-dark`, `masto-light` or `masto-auto`, to select the UI theme (default is `masto-dark`). `auto` will appear masto-light unless the user sets up masto-dark mode on their device. |
|
||||
| `theme` | no | either `dark` or `light`, to select the UI theme (default is `dark`). |
|
||||
| `boosts` | no | whether to show boosts or not |
|
||||
| `replies` | no | whether to show replies or not |
|
||||
| `size` | no | the scale of the UI in percent. |
|
||||
|
||||
\* `userurl` is required if `instance` and `user` are not specified.\*\*\* \
|
||||
\*\* `instance` **and** `user` are required if `userurl` is not specified.\*\*\*
|
||||
### V1 (deprecated, will now just redirect you to the v2 API)
|
||||
|
||||
#### GET `/api/feed`
|
||||
|
||||
> example: `/api/feed?url=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67.atom&scale=90&theme=light`
|
||||
|
||||
Returns a html page which displays a mastodon feed for an atom feed URL. Note that URLs must be URI encoded (i.e. `encodeURIComponent('https://octodon.social/users/fenwick67.atom')` ).
|
||||
|
||||
Querystring options:
|
||||
|
||||
|
||||
| option | required | description |
|
||||
| ------ | -------- | ----------- |
|
||||
| `url` | **yes** | Mastodon Atom feed URL |
|
||||
| `theme` | no | either `dark` or `light`, to select the UI theme (default is `dark`). |
|
||||
| `size` | no | the scale of the UI in percent. |
|
||||
|
||||
\*\*\* **`userurl` and `instance`/`user` are mutually exclusive.**
|
||||
## Server Installation
|
||||
|
||||
This is a straightforward node project with zero databases or anything, you should just be able to run `yarn install` and then `yarn start` to get up and running. Set your `PORT` environment variable to change the port it listens on.
|
||||
This is a straightforward node project with zero databases or anything, you should just be able to run `npm install` and then `npm start` to get up and running. Set your `PORT` environment variable to change the port it listens on.
|
||||
|
||||
## Improve me
|
||||
|
||||
Feel free to add a caching layer, improve the styles and add more features.
|
||||
Feel free to add a chaching layer, improve the styles and add more features.
|
||||
|
|
22
build-styles.js
Normal file
22
build-styles.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// build the styles
|
||||
var fs = require('fs');
|
||||
var sass = require('node-sass');
|
||||
|
||||
var staticDir = './static/'
|
||||
var srcDir = './stylesrc/';
|
||||
var themes = ['light','dark'];
|
||||
|
||||
|
||||
themes.forEach(function(s){
|
||||
var sassFile = srcDir+s+'.scss'
|
||||
var cssFile = staticDir+s+'.css'
|
||||
var result = sass.renderSync({
|
||||
data: fs.readFileSync(sassFile,'utf8'),
|
||||
includePaths:[srcDir]
|
||||
});
|
||||
|
||||
fs.writeFileSync(cssFile,result.css,'utf8')
|
||||
|
||||
});
|
||||
|
||||
console.log('ok');
|
166
index.js
Normal file
166
index.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
var Express = require('express');
|
||||
// v1 api
|
||||
var convert = require('./lib/convert');
|
||||
// v2 api
|
||||
var convertv2 = require('./lib/convertv2');
|
||||
var serveStatic = require('serve-static');
|
||||
var request = require('request');
|
||||
var cors = require('cors');
|
||||
var errorPage = require('./lib/errorPage');
|
||||
var morgan = require('morgan');
|
||||
|
||||
var app = Express();
|
||||
|
||||
var logger = morgan(':method :url :status via :referrer - :response-time ms')
|
||||
|
||||
app.use(
|
||||
serveStatic('static',{
|
||||
maxAge:'1d'
|
||||
})
|
||||
);
|
||||
|
||||
function doCache(res,durationSecs){
|
||||
res.set({
|
||||
"Cache-Control":"max-age="+durationSecs
|
||||
})
|
||||
}
|
||||
|
||||
// web.site:another.one.here => [ /web\.site/i , /another\.one/i ]
|
||||
var blocklist = [];
|
||||
if (process.env["BLOCKLIST"]){
|
||||
blocklist = process.env["BLOCKLIST"].split(':').map((s)=>{
|
||||
var dotsFixed = s.replace(/\./gi,'\\.');
|
||||
return new RegExp(dotsFixed, 'i');
|
||||
});
|
||||
}
|
||||
|
||||
// this just redirects to the
|
||||
app.options('/api/feed',cors());
|
||||
app.get('/api/feed',cors(),logger,function(req,res){
|
||||
|
||||
// get feed url
|
||||
var feedUrl = req.query.url;
|
||||
if (!feedUrl){
|
||||
res.status(400);
|
||||
res.send(errorPage(400,'You need to specify a feed URL'));
|
||||
return;
|
||||
}
|
||||
|
||||
var userUrl = feedUrl.replace(/\.atom.*/i,'');
|
||||
|
||||
var redirectUrl = '/apiv2/feed?';
|
||||
var qs = ['userurl='+encodeURIComponent(userUrl),"api=v1"];
|
||||
|
||||
(['size','theme','boosts','replies']).forEach(key=>{
|
||||
if (typeof req.query[key] != 'undefined'){
|
||||
qs.push(key+'='+encodeURIComponent(req.query[key]));
|
||||
}
|
||||
})
|
||||
|
||||
res.redirect(redirectUrl + qs.join('&'));
|
||||
|
||||
});
|
||||
|
||||
app.options('/apiv2/feed',cors());
|
||||
// http://localhost:8000/apiv2/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67
|
||||
app.get('/apiv2/feed',cors(),logger,function(req,res){
|
||||
|
||||
// get feed url
|
||||
var userUrl = req.query.userurl;
|
||||
if (!userUrl){
|
||||
res.status(400);
|
||||
res.send(errorPage(400,'You need to specify a user URL'));
|
||||
return;
|
||||
}
|
||||
var feedUrl = req.query.feedurl;
|
||||
|
||||
var opts = {};
|
||||
if (req.query.size){
|
||||
opts.size = req.query.size;
|
||||
}
|
||||
if (req.query.theme){
|
||||
opts.theme = req.query.theme;
|
||||
}
|
||||
if (req.query.header){
|
||||
if (req.query.header.toLowerCase() == 'no' || req.query.header.toLowerCase() == 'false'){
|
||||
opts.header = false;
|
||||
}else{
|
||||
opts.header = true;
|
||||
}
|
||||
}
|
||||
|
||||
opts.boosts = true;
|
||||
if (req.query.boosts){
|
||||
if (req.query.boosts.toLowerCase() == 'no' || req.query.boosts.toLowerCase() == 'false'){
|
||||
opts.boosts = false;
|
||||
}else{
|
||||
opts.boosts = true;
|
||||
}
|
||||
}
|
||||
|
||||
opts.replies = true;
|
||||
if (req.query.replies){
|
||||
if (req.query.replies.toLowerCase() == 'no' || req.query.replies.toLowerCase() == 'false'){
|
||||
opts.replies = false;
|
||||
}else{
|
||||
opts.replies = true;
|
||||
}
|
||||
}
|
||||
opts.userUrl = userUrl;
|
||||
opts.feedUrl = feedUrl;
|
||||
opts.mastofeedUrl = req.url;
|
||||
|
||||
var blocked = false;
|
||||
|
||||
function fakeFail(){
|
||||
var t = 1000 + 1000 * Math.random() * Math.random();
|
||||
blocked = true;
|
||||
setTimeout(function(){
|
||||
res.status(500);
|
||||
res.send(errorPage(500,null,{theme:opts.theme,size:opts.size}));
|
||||
},t);
|
||||
}
|
||||
|
||||
// shall I block the user?
|
||||
var base = new URL(userUrl).hostname;
|
||||
for (var i = 0; i < blocklist.length; i++){
|
||||
var re = blocklist[i];
|
||||
if (re.test(base)){
|
||||
fakeFail();
|
||||
console.log("blocked domain: "+base+" (matches "+re.source+")");
|
||||
return; // need to exit this function so feed isn't actually fetched
|
||||
}
|
||||
}
|
||||
|
||||
// block by referer
|
||||
var ref = req.get("referer")
|
||||
if (ref){
|
||||
for (var i = 0; i < blocklist.length; i++){
|
||||
var re = blocklist[i];
|
||||
if (re.test(ref)){
|
||||
fakeFail();
|
||||
console.log("blocked domain via referer: "+base+" (matches "+re.source+")");
|
||||
return; // need to exit this function so feed isn't actually fetched
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!blocked){
|
||||
|
||||
convertv2(opts).then((data)=>{
|
||||
res.status(200);
|
||||
doCache(res,60*60);
|
||||
res.send(data);
|
||||
}).catch((er)=>{
|
||||
res.status(500);
|
||||
res.send(errorPage(500,null,{theme:opts.theme,size:opts.size}));
|
||||
// TODO log the error
|
||||
console.error(er,er.stack);
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(process.env.PORT || 8000,function(){
|
||||
console.log('Server started, listening on '+(process.env.PORT || 8000));
|
||||
});
|
560
lib/convert.js
560
lib/convert.js
|
@ -1,248 +1,312 @@
|
|||
import { compile } from "ejs";
|
||||
import { readFileSync } from "fs";
|
||||
let template = compile(readFileSync("./lib/template.ejs", "utf8"));
|
||||
import { format } from "timeago.js";
|
||||
|
||||
import got from "got";
|
||||
const map = new Map();
|
||||
|
||||
const hour = 3600000;
|
||||
|
||||
// get JSON for an AP URL, by either fetching it or grabbing it from a cache.
|
||||
|
||||
// note: rejects on HTTP 4xx or 5xx
|
||||
async function apGet(url) {
|
||||
return new Promise(function(resolve,reject){
|
||||
|
||||
// fail early
|
||||
if (!url){
|
||||
reject(new Error("URL is invalid"));
|
||||
}
|
||||
|
||||
got( {
|
||||
url:url,
|
||||
cache:map,
|
||||
headers: {
|
||||
"accept": "application/activity+json"
|
||||
}
|
||||
})
|
||||
.then(response=>JSON.parse(response.body))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// like Promise.all except returns null on error instead of failing all the promises
|
||||
async function promiseSome(proms){
|
||||
|
||||
function noRejectWrap(prom){
|
||||
return new Promise(function(resolve){
|
||||
|
||||
prom // it's already been called
|
||||
.then(resolve)
|
||||
.catch( ()=>{
|
||||
// console.warn(e);// for debugging
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return await Promise.all(proms.map(noRejectWrap));
|
||||
|
||||
}
|
||||
|
||||
export default async function (opts) {
|
||||
// let opts = opts;
|
||||
|
||||
let feedUrl = opts.feedUrl;
|
||||
let userUrl = opts.userUrl;
|
||||
let isIndex = false;
|
||||
|
||||
if (!userUrl) {
|
||||
throw new Error("need user URL");
|
||||
}
|
||||
|
||||
let user, feed;
|
||||
|
||||
// get user and feed in parallel if I have both URLs.
|
||||
// can cache feed aggressively since it is a specific start and end.
|
||||
if (userUrl && feedUrl){
|
||||
[user, feed] = await Promise.all([ apGet(userUrl), apGet(feedUrl) ]);
|
||||
}else{
|
||||
// get user, then outbox, then feed
|
||||
|
||||
user = await apGet(userUrl,24 * hour);
|
||||
isIndex = true;
|
||||
let outbox = await apGet(user.outbox, 1 * hour);
|
||||
|
||||
// outbox.first can be a string for a URL, or an object with stuffs in it
|
||||
if (typeof outbox.first == "object"){
|
||||
feed = outbox.first;
|
||||
} else {
|
||||
feed = await apGet(outbox.first, hour/6);// 10 mins. Because the base feed URL can get new toots quickly.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let templateData = {
|
||||
opts: opts,// from the request
|
||||
meta: metaForUser(user),
|
||||
items: await itemsForFeed(opts,user,feed),
|
||||
nextPageLink: getNextPage(opts,user,feed),
|
||||
isIndex: isIndex
|
||||
};
|
||||
|
||||
return template(templateData);
|
||||
|
||||
}
|
||||
|
||||
function metaForUser(user) {
|
||||
return {
|
||||
avatar: user.icon && user.icon.url?user.icon.url:null,
|
||||
headerImage:user.image && user.image.url?user.image.url:null,
|
||||
title: user.name||user.preferredUsername||null,
|
||||
description: user.summary||null,
|
||||
link:user.url||"#"
|
||||
};
|
||||
}
|
||||
|
||||
async function itemsForFeed(opts,user,feed) {
|
||||
|
||||
let items = feed.orderedItems;
|
||||
if (opts.boosts){
|
||||
// yes, I have to fetch all the fucking boosts for this whole feed apparently >:/
|
||||
let boostData = [];
|
||||
let boostUrls = feed.orderedItems.filter(i=>i.type=="Announce").map(i=>i.object);
|
||||
// console.log(boostUrls);
|
||||
boostData = await promiseSome(boostUrls.map(apGet));
|
||||
|
||||
// now get user data for each of those
|
||||
let userData = await promiseSome(boostData.map(d=>d?d.attributedTo||"":null).map(apGet));
|
||||
|
||||
// put a ._userdata key on the item object if this is a boost
|
||||
for (let i = 0; i < boostData.length; i ++){
|
||||
if (userData[i] && boostData[i]){
|
||||
boostData[i]._userdata = userData[i];
|
||||
}
|
||||
}
|
||||
|
||||
// some URLs may have failed but IDGAF
|
||||
|
||||
// console.log(boostData[0]);
|
||||
|
||||
boostData.forEach((boostToot)=>{
|
||||
|
||||
if (!boostToot){// failed request
|
||||
return;
|
||||
}
|
||||
|
||||
// inject in-place into items
|
||||
|
||||
let index = -1;
|
||||
for (var i = 0; i < items.length; i ++){
|
||||
if (items[i].object == boostToot.id){
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1){
|
||||
console.warn("warning: couldn't match boost to item: ",boostToot);
|
||||
return;
|
||||
}
|
||||
|
||||
boostToot.object = boostToot;// this lets the later stage parser access object without errors :)
|
||||
items[i] = boostToot;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return items.filter((item)=>{
|
||||
return typeof item.object == "object";// handle weird cases
|
||||
}).map((item)=>{
|
||||
|
||||
let enclosures = (item.object.attachment||[]).filter((a)=>{
|
||||
return a.type == "Document";
|
||||
}).map((a)=>{
|
||||
return {
|
||||
name:a.name,
|
||||
type:a.mediaType,
|
||||
url:a.url
|
||||
};
|
||||
});
|
||||
|
||||
let op = item._userdata?item._userdata:user;
|
||||
|
||||
|
||||
return {
|
||||
isBoost:!!item._userdata,
|
||||
title:item._userdata?user.preferredUsername+" shared a status by "+op.preferredUsername:"",
|
||||
isReply:!!(item.object && item.object.inReplyTo),
|
||||
hasCw:item.object.sensitive||false,
|
||||
cw:item.object.summary,
|
||||
content: item.object&&item.object.content?item.object.content:"",//TODO sanitize then render without entity escapes
|
||||
atomHref:item.published?item.published.replace(/\W+/g,""):Math.random().toString().replace(".",""),// used for IDs
|
||||
enclosures:enclosures,
|
||||
stringDate:item.published?getTimeDisplay(Date.parse(item.published)):"",
|
||||
permalink:item.object.id?item.object.id:"#",
|
||||
author:{
|
||||
uri:op.url,// link to author page
|
||||
avatar:op.icon&&op.icon.url?op.icon.url:"",
|
||||
displayName:op.name || op.preferredUsername,
|
||||
fullName:op.preferredUsername+"@"+(new URL(op.url).hostname),
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getNextPage(opts,user,feed){
|
||||
//based on feed.next
|
||||
if (!feed.next){return null;}
|
||||
// take feed.next, uriencode it, then take user url, then take options.mastofeedUrl
|
||||
//let base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf("?"));
|
||||
|
||||
let ret = "/api/v1/feed?userurl=" + encodeURIComponent(opts.userUrl) + "&feedurl=" +encodeURIComponent(feed.next) + "&instance_type=" + opts.instance_type;
|
||||
|
||||
// add other params to the end
|
||||
(["theme","header","size","boosts","replies"]).forEach((k)=>{
|
||||
if (typeof opts[k] != "undefined"){
|
||||
ret+=`&${k}=${ opts[k].toString() }`;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
// utilities below
|
||||
|
||||
function getTimeDisplay(d) {
|
||||
// let d = d;
|
||||
if (typeof d !== "object") {
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
let dt = d.getTime();
|
||||
let now = Date.now();
|
||||
|
||||
let delta = now - dt;
|
||||
|
||||
// over 6 days ago
|
||||
if (delta > 1000 * 60 * 60 * 24 * 6) {
|
||||
return isoDateToEnglish(d.toISOString());
|
||||
} else {
|
||||
return format(dt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isoDateToEnglish(d) {
|
||||
|
||||
let dt = d.split(/[t-]/ig);
|
||||
let months = ["January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"];
|
||||
|
||||
return months[Number(dt[1]) - 1] + " " + dt[2] + ", " + dt[0];
|
||||
}
|
||||
var FeedParser = require('feedparser');
|
||||
var ejs = require('ejs');
|
||||
var fs = require('fs');
|
||||
var template = ejs.compile(fs.readFileSync('./lib/template.ejs','utf8'));
|
||||
var timeAgo = require('timeago.js');
|
||||
|
||||
function isArray(a){
|
||||
return Array.isArray(a);
|
||||
}
|
||||
|
||||
// accumulate a stream of XML into a html file
|
||||
|
||||
module.exports = function(stream,opts,callback){
|
||||
var callback = callback;
|
||||
var opts = opts;
|
||||
if (typeof opts == 'function'){
|
||||
callback = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
// convert s from atom feed to a full html page for rendering
|
||||
breakDown(stream,function(er,data){
|
||||
if (er) {
|
||||
return callback(er);
|
||||
}
|
||||
// try and build up
|
||||
try{
|
||||
var result = buildUp(data,opts)
|
||||
}catch(e){
|
||||
return callback(e);
|
||||
}
|
||||
return callback(null,result);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// break the xml into json
|
||||
function breakDown(stream,callback){
|
||||
|
||||
var spent = false;
|
||||
function cbOnce(er,data){
|
||||
if (!spent){
|
||||
callback(er,data);
|
||||
spent = true;
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('error',cbOnce);
|
||||
var feedparser = new FeedParser();
|
||||
feedparser.on('error', cbOnce);
|
||||
stream.pipe(feedparser)
|
||||
|
||||
|
||||
feedparser.items = [];
|
||||
feedparser.on('readable', function () {
|
||||
// This is where the action is!
|
||||
var stream = this; // `this` is `feedparser`, which is a stream
|
||||
var items = [];
|
||||
var item;
|
||||
|
||||
while (item = stream.read()) {
|
||||
feedparser.items.push(item);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
feedparser.on('end',function(er){cbOnce(null,feedparser)});
|
||||
|
||||
}
|
||||
|
||||
// hydrate the json to html
|
||||
function buildUp(jsonObj,opts){
|
||||
|
||||
// assign opts to the obj
|
||||
jsonObj.opts = opts||{};
|
||||
|
||||
// iterate through the items
|
||||
jsonObj.items = jsonObj.items.filter(function(item){
|
||||
|
||||
// get date
|
||||
item.stringDate = getTimeDisplay(item.date);
|
||||
|
||||
item.content = getH(item,'atom:content');
|
||||
if (!item.content ){// item was deleted
|
||||
return false;
|
||||
}
|
||||
|
||||
// make anchor tags have the "_top" target
|
||||
item.content = item.content.replace(/\<\s*a\s*/ig,'<a target="_top"');
|
||||
|
||||
// get enclosures
|
||||
item.enclosures = [];
|
||||
|
||||
function findEnclosure(link){
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'enclosure'){
|
||||
item.enclosures.push({
|
||||
url:href,
|
||||
type:link['@'].type||''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// find them in the item's links
|
||||
if (item['atom:link']){
|
||||
var links = item['atom:link'];
|
||||
if (!isArray(links) && typeof links == 'object'){
|
||||
links = [links];
|
||||
}else if (!isArray(links)){
|
||||
links = [];
|
||||
}
|
||||
links.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
// find them in activity:object
|
||||
if (item["activity:object"] && item["activity:object"].link){
|
||||
var enclosureLinks = item["activity:object"].link;
|
||||
if (!isArray(enclosureLinks) && typeof enclosureLinks == 'object'){
|
||||
enclosureLinks = [enclosureLinks];
|
||||
}else if (!isArray(enclosureLinks)){
|
||||
enclosureLinks = [];// not an object or array.
|
||||
}
|
||||
enclosureLinks.forEach(findEnclosure);
|
||||
}
|
||||
|
||||
|
||||
// get author info
|
||||
|
||||
item.author = {};
|
||||
var _author = item.meta['atom:author'];
|
||||
if ( item['activity:object'] && item['activity:object'].author){
|
||||
_author = item['activity:object'].author;
|
||||
}
|
||||
|
||||
item.author.name = getH(_author,'name');
|
||||
item.author.uri = getH(_author,'uri');
|
||||
item.author.fullName = getH(_author,'email');
|
||||
item.author.displayName = getH(_author,'poco:displayname')||getH(_author,'poco:preferredUsername')||item.author.name;
|
||||
|
||||
var authorLinks = _author.link || [];
|
||||
|
||||
if (!isArray(authorLinks) && typeof authorLinks == 'object'){
|
||||
authorLinks = [authorLinks];
|
||||
}else if ( !isArray(authorLinks) ){
|
||||
authorLinks = [];// not an object or string.
|
||||
}
|
||||
|
||||
authorLinks.forEach(function(link){// todo: guard against authorLinks not being an array
|
||||
if (!link['@']){return;} // avoid keyerror
|
||||
var rel = link['@'].rel;
|
||||
var href = link['@'].href;
|
||||
if (rel == 'avatar'){
|
||||
item.author.avatar = href;
|
||||
}else if(rel == 'alternate'){
|
||||
item.author.alternate = href;
|
||||
}
|
||||
});
|
||||
|
||||
// now detect if item is a reply or boost
|
||||
item.isBoost = false;// <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
|
||||
item.isReply = false;
|
||||
|
||||
var v = getH(item,'activity:verb')
|
||||
if (v.indexOf('share') > -1){
|
||||
item.isBoost = true;
|
||||
}
|
||||
|
||||
if (item['thr:in-reply-to']){
|
||||
item.isReply = true;
|
||||
}
|
||||
|
||||
if(item.summary){
|
||||
item.hasCw = true;
|
||||
item.cw = item.summary;
|
||||
}
|
||||
|
||||
if(item['activity:object'] && item['activity:object'].summary && item['activity:object'].summary['#']){
|
||||
item.hasCw = true;
|
||||
item.cw = item['activity:object'].summary['#'];
|
||||
}
|
||||
|
||||
|
||||
// get a pagination ID for an entry
|
||||
item.paginationId = false;
|
||||
if (item['atom:link']){
|
||||
var links = item['atom:link'];
|
||||
if (!isArray(links) && typeof links == 'object'){
|
||||
links = [links];
|
||||
}else if (!isArray(links)){
|
||||
links = [];
|
||||
}
|
||||
links.forEach((link)=>{
|
||||
if (!link['@']){return}
|
||||
if (link['@']['rel']=='self'){
|
||||
// parse out the pagination id
|
||||
// href looks like this in mastodon: https://octodon.social/users/fenwick67/updates/732275.atom
|
||||
// href looks like this in pleroma (and we should ignore): https://social.fenwick.pizza/objects/1e2fa906-378c-43f8-98fa-271aae455758
|
||||
var href = link['@']['href'];
|
||||
if (!href){return}
|
||||
item.atomHref = href;
|
||||
var match = href.match(/\/\d+.atom/);
|
||||
if(!match){return}
|
||||
var id = match[0].replace(/\D/g,'');
|
||||
if (id){
|
||||
item.paginationId = id;
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
if (jsonObj.meta && jsonObj.meta['atom:author'] && jsonObj.meta['atom:author'].link && Array.isArray(jsonObj.meta['atom:author'].link) ){
|
||||
jsonObj.meta['atom:author'].link.forEach(link=>{
|
||||
var l = link['@'];
|
||||
if (l.rel=="header"){
|
||||
jsonObj.meta.headerImage = l.href;
|
||||
}
|
||||
else if(l.rel=="avatar"){
|
||||
jsonObj.meta.avatar = l.href;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// get next page
|
||||
jsonObj.feedUrl = opts.feedUrl;
|
||||
jsonObj.isIndex = (opts.feedUrl.indexOf('?') == -1);
|
||||
|
||||
// prefer link(rel=next)
|
||||
var nextPageFeedUrl = '';
|
||||
var links = jsonObj.meta['atom:link'];
|
||||
if (links){
|
||||
if (!isArray(links) && typeof links == 'object'){
|
||||
links = [links];
|
||||
}else if (!isArray(links)){
|
||||
links = [];
|
||||
}
|
||||
links.forEach(function(link){
|
||||
if (link['@'] && link['@']['rel'] == 'next' && link['@']['href']){
|
||||
nextPageFeedUrl = link['@']['href'];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!nextPageFeedUrl){
|
||||
// alternative: try to get the oldest entry id on this page
|
||||
var lowestId = Infinity;
|
||||
jsonObj.items.forEach(function(item){
|
||||
var id = Number(item.paginationId);
|
||||
if ( id < lowestId ){
|
||||
lowestId = id;
|
||||
}
|
||||
});
|
||||
|
||||
if (lowestId < Infinity && opts.feedUrl){
|
||||
nextPageFeedUrl = opts.feedUrl.replace(/\?.+/g,'') + '?max_id='+lowestId;
|
||||
}
|
||||
}
|
||||
|
||||
if(nextPageFeedUrl){
|
||||
jsonObj.nextPageLink = opts.mastofeedUrl.replace(encodeURIComponent(opts.feedUrl),encodeURIComponent(nextPageFeedUrl));
|
||||
console.log(jsonObj.nextPageLink);
|
||||
}
|
||||
return template(jsonObj);
|
||||
}
|
||||
|
||||
// utilities below
|
||||
|
||||
|
||||
// get obj[key]['#'] or ''
|
||||
function getH(obj,key){
|
||||
if (!obj[key]){return ''}
|
||||
return obj[key]['#']||'';
|
||||
}
|
||||
|
||||
function getTimeDisplay(d){
|
||||
var d = d;
|
||||
if (typeof d !== 'object'){
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
dt = d.getTime();
|
||||
var now = Date.now();
|
||||
|
||||
var delta = now - dt;
|
||||
|
||||
// over 6 days ago
|
||||
if (delta > 1000*60*60*24*6){
|
||||
return isoDateToEnglish(d.toISOString());
|
||||
}else{
|
||||
return timeAgo().format(dt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isoDateToEnglish(d){
|
||||
|
||||
var dt = d.split(/[t\-]/ig);
|
||||
var months = [ "January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December" ];
|
||||
|
||||
return months[Number(dt[1])-1] +' '+dt[2]+ ', '+dt[0];
|
||||
}
|
||||
|
|
247
lib/convertv2.js
Normal file
247
lib/convertv2.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
var ejs = require('ejs');
|
||||
var fs = require('fs');
|
||||
var template = ejs.compile(fs.readFileSync('./lib/template.ejs', 'utf8'));
|
||||
var timeAgo = require('timeago.js');
|
||||
|
||||
var request = require('request-promise-cache')
|
||||
|
||||
const hour = 3600000;
|
||||
|
||||
// get JSON for an AP URL, by either fetching it or grabbing it from a cache.
|
||||
|
||||
// Honestly request-promise-cache should be good enough. Redis would be a nice upgrade but for
|
||||
// a single process install it will be fine.
|
||||
|
||||
// note: rejects on HTTP 4xx or 5xx
|
||||
async function apGet(url,ttl) {
|
||||
|
||||
return new Promise(function(resolve,reject){
|
||||
|
||||
// fail early
|
||||
if (!url){
|
||||
reject(new Error('URL is invalid'));
|
||||
}
|
||||
|
||||
request( {
|
||||
uri:url,
|
||||
cacheKey:url,
|
||||
cacheTTL:ttl || 24 * hour,
|
||||
headers: {
|
||||
"accept": "application/activity+json"
|
||||
}
|
||||
})
|
||||
.then(body=>JSON.parse(body))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// like Promise.all except returns null on error instead of failing all the promises
|
||||
async function promiseSome(proms){
|
||||
|
||||
function noRejectWrap(prom){
|
||||
return new Promise(function(resolve,reject){
|
||||
|
||||
prom // it's already been called
|
||||
.then(resolve)
|
||||
.catch(e=>{
|
||||
// console.warn(e);// for debugging
|
||||
resolve(null)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
return await Promise.all(proms.map(noRejectWrap))
|
||||
|
||||
}
|
||||
|
||||
module.exports = async function (opts) {
|
||||
var opts = opts;
|
||||
|
||||
var feedUrl = opts.feedUrl;
|
||||
var userUrl = opts.userUrl;
|
||||
var isIndex = false;
|
||||
|
||||
if (!userUrl) {
|
||||
throw new Error('need user URL');
|
||||
}
|
||||
|
||||
var user, feed;
|
||||
|
||||
// get user and feed in parallel if I have both URLs.
|
||||
// can cache feed aggressively since it is a specific start and end.
|
||||
if (userUrl && feedUrl){
|
||||
[user, feed] = await Promise.all([ apGet(userUrl), apGet(feedUrl) ]);
|
||||
}else{
|
||||
// get user, then outbox, then feed
|
||||
|
||||
user = await apGet(userUrl,24 * hour);
|
||||
isIndex = true;
|
||||
var outbox = await apGet(user.outbox,24 * hour);
|
||||
feedUrl = outbox.first;
|
||||
feed = await apGet(feedUrl,hour/6);// 10 mins. Because the base feed URL can get new toots quickly.
|
||||
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
opts: opts,// from the request
|
||||
meta: metaForUser(user),
|
||||
items: await itemsForFeed(opts,user,feed),
|
||||
nextPageLink: getNextPage(opts,user,feed),
|
||||
isIndex: isIndex
|
||||
};
|
||||
|
||||
return template(templateData);
|
||||
|
||||
}
|
||||
|
||||
function metaForUser(user) {
|
||||
return {
|
||||
avatar: user.icon && user.icon.url?user.icon.url:null,
|
||||
headerImage:user.image && user.image.url?user.image.url:null,
|
||||
title: user.name||user.preferredUsername||null,
|
||||
description: user.summary||null,
|
||||
link:user.url||"#"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make function
|
||||
async function itemsForFeed(opts,user,feed) {
|
||||
|
||||
var items = feed.orderedItems;
|
||||
|
||||
if (opts.boosts){
|
||||
// yes, I have to fetch all the fucking boosts for this whole feed apparently >:/
|
||||
var boostData = [];
|
||||
var boostUrls = feed.orderedItems.filter(i=>i.type=="Announce").map(i=>i.object);
|
||||
// console.log(boostUrls);
|
||||
boostData = await promiseSome(boostUrls.map(apGet));
|
||||
|
||||
// now get user data for each of those
|
||||
let userData = await promiseSome(boostData.map(d=>d?d.attributedTo||'':null).map(apGet));
|
||||
|
||||
// put a ._userdata key on the item object if this is a boost
|
||||
for (var i = 0; i < boostData.length; i ++){
|
||||
if (userData[i] && boostData[i]){
|
||||
boostData[i]._userdata = userData[i];
|
||||
}
|
||||
}
|
||||
|
||||
// some URLs may have failed but IDGAF
|
||||
|
||||
// console.log(boostData[0]);
|
||||
|
||||
boostData.forEach((boostToot)=>{
|
||||
|
||||
if (!boostToot){// failed request
|
||||
return;
|
||||
}
|
||||
|
||||
// inject in-place into items
|
||||
|
||||
var index = -1;
|
||||
for (var i = 0; i < items.length; i ++){
|
||||
if (items[i].object == boostToot.id){
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1){
|
||||
console.warn("warning: couldn't match boost to item: ",boostToot)
|
||||
return;
|
||||
}
|
||||
|
||||
boostToot.object = boostToot;// this lets the later stage parser access object without errors :)
|
||||
items[i] = boostToot;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return items.filter((item)=>{
|
||||
return typeof item.object == 'object';// handle weird cases
|
||||
}).map((item)=>{
|
||||
|
||||
var enclosures = (item.object.attachment||[]).filter((a)=>{
|
||||
return a.type == "Document";
|
||||
}).map((a)=>{
|
||||
return {
|
||||
name:a.name,
|
||||
type:a.mediaType,
|
||||
url:a.url
|
||||
}
|
||||
});
|
||||
|
||||
var op = item._userdata?item._userdata:user;
|
||||
|
||||
return {
|
||||
isBoost:!!item._userdata,
|
||||
title:item._userdata?user.preferredUsername+' shared a status by '+op.preferredUsername:'',
|
||||
isReply:!!(item.object && item.object.inReplyTo),
|
||||
hasCw:item.object.sensitive||false,
|
||||
cw:item.object.summary,
|
||||
content: item.object&&item.object.content?item.object.content:'',//TODO sanitize then render without entity escapes
|
||||
atomHref:item.published?item.published.replace(/\W+/g,''):Math.random().toString().replace('.',''),
|
||||
enclosures:enclosures,
|
||||
stringDate:item.published?getTimeDisplay(Date.parse(item.published)):'',
|
||||
author:{
|
||||
uri:op.url,// link to author page
|
||||
avatar:op.icon&&op.icon.url?op.icon.url:'',
|
||||
displayName:op.name,
|
||||
fullName:op.preferredUsername+'@'+(new URL(op.url).hostname),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO
|
||||
function getNextPage(opts,user,feed){
|
||||
//based on feed.next
|
||||
// take feed.next, uriencode it, then take user url, then take options.mastofeedUrl
|
||||
var base = opts.mastofeedUrl.slice(0,opts.mastofeedUrl.indexOf('?'));
|
||||
|
||||
var ret = '/apiv2/feed?userurl=' + encodeURIComponent(opts.userUrl) + '&feedurl=' +encodeURIComponent(feed.next);
|
||||
|
||||
// add other params to the end
|
||||
(['theme','header','size','boosts','replies']).forEach((k)=>{
|
||||
if (typeof opts[k] != 'undefined'){
|
||||
ret+=`&${k}=${ opts[k].toString() }`;
|
||||
}
|
||||
})
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
// utilities below
|
||||
|
||||
function getTimeDisplay(d) {
|
||||
var d = d;
|
||||
if (typeof d !== 'object') {
|
||||
d = new Date(d);
|
||||
}
|
||||
// convert to number
|
||||
dt = d.getTime();
|
||||
var now = Date.now();
|
||||
|
||||
var delta = now - dt;
|
||||
|
||||
// over 6 days ago
|
||||
if (delta > 1000 * 60 * 60 * 24 * 6) {
|
||||
return isoDateToEnglish(d.toISOString());
|
||||
} else {
|
||||
return timeAgo().format(dt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isoDateToEnglish(d) {
|
||||
|
||||
var dt = d.split(/[t\-]/ig);
|
||||
var months = ["January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"];
|
||||
|
||||
return months[Number(dt[1]) - 1] + ' ' + dt[2] + ', ' + dt[0];
|
||||
}
|
15
lib/emoji.js
15
lib/emoji.js
|
@ -1,15 +0,0 @@
|
|||
import axios from "axios";
|
||||
import fs from "fs";
|
||||
|
||||
//Grab emote list so when rendering
|
||||
export default function getEmojiJson(url) {
|
||||
let emojiUrl = "https://" + url + "/api/v1/custom_emojis";
|
||||
axios
|
||||
.get(emojiUrl)
|
||||
.then(res => {
|
||||
console.log(`statusCode: ${res.status}`);
|
||||
const emojiJson = res.data;
|
||||
console.log(emojiJson);
|
||||
});
|
||||
}
|
||||
getEmojiJson("freecumextremist.com");
|
|
@ -1,20 +1,20 @@
|
|||
import { compile } from "ejs";
|
||||
import { readFileSync } from "fs";
|
||||
let template = compile(readFileSync("./lib/template.ejs", "utf8"));
|
||||
var ejs = require('ejs');
|
||||
var fs = require('fs');
|
||||
var template = ejs.compile(fs.readFileSync('./lib/template.ejs', 'utf8'));
|
||||
|
||||
export default function(code,message,displayOptions){
|
||||
module.exports = function(code,message,displayOptions){
|
||||
|
||||
let msg;
|
||||
// const displayOptions = displayOptions || {};
|
||||
var msg;
|
||||
var displayOptions = displayOptions || {};
|
||||
|
||||
if (code == 500 && !message){
|
||||
msg = "<p>Sorry, we are having trouble fetching posts for this user. Please try again later.</p><br><p>If the issue persists, <a href=\"https://git.froth.zone/Sam/fedifeed/issues\">please open an issue on Gitea</a>, or message sam@froth.zone</p>";
|
||||
msg = '<p>Sorry, we are having trouble fetching posts for this user. Please try again later.</p><br><p>If the issue persists, <a href="https://github.com/fenwick67/mastofeed/issues">please open an issue on GitHub</a>, or message fenwick67@octodon.social</p>'
|
||||
}else{
|
||||
msg = message||"";
|
||||
msg = message||'';
|
||||
}
|
||||
|
||||
|
||||
let options = {
|
||||
var options = {
|
||||
opts:{
|
||||
header:true,
|
||||
theme:displayOptions.theme||null,
|
||||
|
@ -23,14 +23,14 @@ export default function(code,message,displayOptions){
|
|||
meta:{
|
||||
title:code.toString(),
|
||||
description:msg,
|
||||
link:"#"
|
||||
link:'#'
|
||||
// avatar:'',
|
||||
// headerImage:''
|
||||
},
|
||||
items:[],
|
||||
nextPageLink:null,
|
||||
isIndex:true
|
||||
};
|
||||
}
|
||||
|
||||
return template(options);
|
||||
}
|
|
@ -4,20 +4,10 @@
|
|||
<style type="text/css"></style>
|
||||
<base target="_top" /><!-- this element is amazing-->
|
||||
|
||||
<% if (opts.theme && opts.theme.toLowerCase() == 'masto-light'){ %>
|
||||
<link rel="stylesheet" href="/css/masto-light.css"></link>
|
||||
<% } else if (opts.theme && opts.theme.toLowerCase() == 'auto'){ %>
|
||||
<link rel="stylesheet" href="/css/masto-auto.css"></link>
|
||||
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-dark'){ %>
|
||||
<link rel="stylesheet" href="/css/misskey-dark.css"></link>
|
||||
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-light'){ %>
|
||||
<link rel="stylesheet" href="/css/misskey-light.css"></link>
|
||||
<% } else if (opts.theme && opts.theme.toLowerCase() == 'misskey-auto'){ %>
|
||||
<link rel="stylesheet" href="/css/misskey-auto.css"></link>
|
||||
<% } else if (opts.theme && opts.theme.toLowerCase() == 'pleroma'){ %>
|
||||
<link rel="stylesheet" href="/css/pleroma.css"></link>
|
||||
<% if (opts.theme && opts.theme.toLowerCase() == 'light'){ %>
|
||||
<link rel="stylesheet" href="/light.css"></link>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" href="/css/masto-dark.css"></link>
|
||||
<link rel="stylesheet" href="/dark.css"></link>
|
||||
<% } %>
|
||||
|
||||
<% if (opts.size){ %>
|
||||
|
@ -51,7 +41,7 @@
|
|||
|
||||
<div class="container">
|
||||
|
||||
<% let filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
|
||||
<% var filtered = items.filter(function(item){return !((item.isBoost && !opts.boosts) || (item.isReply && !opts.replies)) })%>
|
||||
<% filtered.forEach(function(item){ %>
|
||||
<div class="item">
|
||||
<% if (item.isBoost) { %>
|
||||
|
@ -67,33 +57,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<% if (item.hasCw){ %>
|
||||
<% let cwId = (item.cw+item.atomHref).replace(/\W+/g,'') %>
|
||||
<% var cwId = (item.cw+item.atomHref).replace(/\W+/g,'') %>
|
||||
<span class="cw"><%- item.cw %></span>
|
||||
<input type="checkbox" class="showmore" id="<%- cwId %>">
|
||||
<label class="button" for="<%- cwId %>">Show</label>
|
||||
<% } %>
|
||||
<!--
|
||||
Ok so this is so fucked but basically, this matches a status with the regex expression (to capture text between two colons) and
|
||||
needs to take the matched text and search the instances emoji pack for the value and replace it in item content.
|
||||
I tested this and it _does not_ alter posts without emojis.
|
||||
-->
|
||||
<% let regexp = /s*(?<=:).+?(?=:)s*/;
|
||||
let postWithEmoji = item.content;
|
||||
let emoteName = postWithEmoji.match(regexp);
|
||||
<!--If a post with matching regex for :whatever: is found replace with img tag for the instance's emoji packs-->
|
||||
item.content = postWithEmoji.replace(regexp, `<img src=https://freecumextremist.com/emoji/emojos/${emoteName}.jpg width='30' height='30'/>`)
|
||||
%>
|
||||
|
||||
<div class="item-content">
|
||||
<%- item.content %>
|
||||
</div>
|
||||
|
||||
<% if (item.enclosures.length > 0){ %>
|
||||
<div class="enclosures">
|
||||
<% for (let i = 0; i < item.enclosures.length; i ++){ %>
|
||||
<% let e = item.enclosures[i] %>
|
||||
<% if (e.type.indexOf('audio') > -1) {%>
|
||||
<audio class="enclosure" controls loop src="<%= e.url %>"/>
|
||||
<% }else if (e.type.indexOf('video') > -1){ %>
|
||||
<% for (var i = 0; i < item.enclosures.length; i ++){ %>
|
||||
<% var e = item.enclosures[i] %>
|
||||
<% if (e.type.indexOf('video') > -1){ %>
|
||||
<video class="enclosure" controls loop src="<%= e.url %>"/>
|
||||
<% } else { %>
|
||||
<a target="_top" class="enclosure" href="<%= e.url %>" >
|
||||
|
@ -107,27 +85,27 @@
|
|||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<a class="date" href="<%= item.permalink %>"><%= item.stringDate %></a>
|
||||
<div class="date"><%= item.stringDate %></div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% if (nextPageLink){ %>
|
||||
<div class="item hidden">
|
||||
<a class="hacky_link" href="<%- nextPageLink %>">More</a>
|
||||
</div>
|
||||
<% if (nextPageLink) {%>
|
||||
<div class="item hidden">
|
||||
<a class="hacky_link" href="<%- nextPageLink %>">More</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div> <!-- end item container -->
|
||||
<% if (nextPageLink){ %>
|
||||
<div class="pagination">
|
||||
<a class="button" href="<%- nextPageLink %>">Load More</a>
|
||||
</div>
|
||||
<% if (nextPageLink) {%>
|
||||
<div class="pagination">
|
||||
<a class="button" href="<%- nextPageLink %>">Load More</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if ( isIndex ){ %>
|
||||
<script src="/infinite-scroll.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
let lastPageLoaded = null;
|
||||
let infScroll = new InfiniteScroll( '.container', {
|
||||
var lastPageLoaded = null;
|
||||
var infScroll = new InfiniteScroll( '.container', {
|
||||
// options
|
||||
hideNav:'.pagination',
|
||||
append: '.item',
|
||||
|
@ -135,14 +113,14 @@
|
|||
prefill:true,
|
||||
path: function(){
|
||||
// need to query this DOM my damn self
|
||||
let pageLinks = document.querySelectorAll('.hacky_link');
|
||||
var pageLinks = document.querySelectorAll('.hacky_link');
|
||||
if (!pageLinks || pageLinks.length == 0){
|
||||
console.log ('next page link could not be found');
|
||||
return false;
|
||||
}else{
|
||||
let finalLink = pageLinks[pageLinks.length-1].href;
|
||||
var finalLink = pageLinks[pageLinks.length-1].href;
|
||||
// make sure we don't load the same page twice
|
||||
if (!finalLink || finalLink == window.location.href || finalLink == lastPageLoaded){
|
||||
if (finalLink == lastPageLoaded){
|
||||
console.log('this was the last page');
|
||||
return false;
|
||||
}else{
|
||||
|
|
1949
npm-shrinkwrap.json
generated
Normal file
1949
npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load diff
7617
package-lock.json
generated
7617
package-lock.json
generated
File diff suppressed because it is too large
Load diff
28
package.json
28
package.json
|
@ -1,26 +1,22 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.2",
|
||||
"feedparser": "^2.2.10",
|
||||
"got": "^12.0.1",
|
||||
"megalodon": "^4.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"serve-static": "^1.14.2",
|
||||
"timeago.js": "^4.0.2"
|
||||
"ejs": "^2.5.8",
|
||||
"express": "^4.16.4",
|
||||
"feedparser": "^2.2.9",
|
||||
"morgan": "^1.9.1",
|
||||
"request": "^2.88.0",
|
||||
"request-promise-cache": "^2.0.1",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"serve-static": "^1.13.2",
|
||||
"timeago.js": "^3.0.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "yarn build-styles && node src/index.js",
|
||||
"build-styles": "node src/build-styles.js",
|
||||
"lint": "eslint --ext .js src lib",
|
||||
"test": "echo \"Error: no test specified\" && exit 0"
|
||||
"start": "node index.js",
|
||||
"build-styles": "node build-styles.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.8.0",
|
||||
"node-sass": "^7.0.1"
|
||||
"node-sass": "^4.12.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
// build the styles
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { renderSync } from "node-sass";
|
||||
|
||||
let staticDir = "./src/public/";
|
||||
let srcDir = "./src/stylesrc/";
|
||||
let themes = ["masto-light","masto-dark","masto-auto"];
|
||||
|
||||
|
||||
themes.forEach(function(s){
|
||||
let sassFile = srcDir+s+".scss";
|
||||
let cssFile = staticDir+s+".css";
|
||||
let result = renderSync({
|
||||
data: readFileSync(sassFile,"utf8"),
|
||||
includePaths:[srcDir]
|
||||
});
|
||||
|
||||
writeFileSync(cssFile,result.css,"utf8");
|
||||
|
||||
});
|
||||
|
||||
console.log("ok");
|
130
src/index.js
130
src/index.js
|
@ -1,130 +0,0 @@
|
|||
import Express from "express";
|
||||
// v2 api
|
||||
import convertv2 from "../lib/convert.js";
|
||||
import serveStatic from "serve-static";
|
||||
import cors from "cors";
|
||||
import errorPage from "../lib/errorPage.js";
|
||||
import morgan from "morgan";
|
||||
import { detector } from "megalodon";
|
||||
|
||||
const app = Express();
|
||||
|
||||
const logger = morgan(":method :url :status via :referrer - :response-time ms");
|
||||
|
||||
app.use(
|
||||
serveStatic("src/public", {
|
||||
maxAge: "1d",
|
||||
}),
|
||||
);
|
||||
|
||||
function doCache(res, durationSecs) {
|
||||
res.set({
|
||||
"Cache-Control": "max-age=" + durationSecs,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// this just redirects to the
|
||||
app.options("/api/feed", cors());
|
||||
app.get("/api/feed", cors(), logger, function (req, res) {
|
||||
// get feed url
|
||||
const feedUrl = req.query.url;
|
||||
if (!feedUrl) {
|
||||
res.status(400);
|
||||
res.send(errorPage(400, "You need to specify a feed URL"));
|
||||
return;
|
||||
}
|
||||
|
||||
const userUrl = feedUrl.replace(/\.atom.*/i, "");
|
||||
|
||||
const redirectUrl = "/api/v1/feed?";
|
||||
const qs = [ "userurl=" + encodeURIComponent(userUrl), "api=v1" ];
|
||||
|
||||
([ "size", "theme", "boosts", "replies" ]).forEach((key) => {
|
||||
if (typeof req.query[ key ] != "undefined") {
|
||||
qs.push(key + "=" + encodeURIComponent(req.query[ key ]));
|
||||
}
|
||||
});
|
||||
|
||||
res.redirect(redirectUrl + qs.join("&"));
|
||||
});
|
||||
|
||||
app.options("/api/v1/feed", cors());
|
||||
// http://localhost:8000/api/v1/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67
|
||||
app.get("/api/v1/feed", cors(), logger, async function (req, res) {
|
||||
// get feed url
|
||||
// userUrl
|
||||
let type = req.query.instance_type;
|
||||
let userUrl = req.query.userurl;
|
||||
if (userUrl === "" || userUrl === undefined) {
|
||||
const user = req.query.user;
|
||||
const instance = req.query.instance;
|
||||
if (type === "" || type === undefined) {
|
||||
type = await detector(instance).catch(() =>
|
||||
"",
|
||||
);
|
||||
}
|
||||
if (type === "mastodon" || type === "pleroma")
|
||||
userUrl = instance + "/users/" + user;
|
||||
else if (type === "misskey")
|
||||
userUrl = instance + "/@" + user;
|
||||
else {
|
||||
res.status(400);
|
||||
res.send(errorPage(400, "You need to specify a user URL"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const feedUrl = req.query.feedurl;
|
||||
|
||||
const opts = {};
|
||||
if (req.query.size) {
|
||||
opts.size = req.query.size;
|
||||
}
|
||||
if (req.query.theme) {
|
||||
opts.theme = req.query.theme;
|
||||
}
|
||||
if (req.query.header) {
|
||||
if (req.query.header.toLowerCase() == "no" || req.query.header.toLowerCase() == "false") {
|
||||
opts.header = false;
|
||||
} else {
|
||||
opts.header = true;
|
||||
}
|
||||
}
|
||||
opts.instance_type = type;
|
||||
opts.boosts = true;
|
||||
if (req.query.boosts) {
|
||||
if (req.query.boosts.toLowerCase() == "no" || req.query.boosts.toLowerCase() == "false") {
|
||||
opts.boosts = false;
|
||||
} else {
|
||||
opts.boosts = true;
|
||||
}
|
||||
}
|
||||
|
||||
opts.replies = true;
|
||||
if (req.query.replies) {
|
||||
if (req.query.replies.toLowerCase() == "no" || req.query.replies.toLowerCase() == "false") {
|
||||
opts.replies = false;
|
||||
} else {
|
||||
opts.replies = true;
|
||||
}
|
||||
}
|
||||
opts.userUrl = userUrl;
|
||||
opts.feedUrl = feedUrl;
|
||||
opts.mastofeedUrl = req.url;
|
||||
|
||||
convertv2(opts).then((data) => {
|
||||
res.status(200);
|
||||
doCache(res, 60 * 60);
|
||||
res.send(data);
|
||||
}).catch((er) => {
|
||||
res.status(500);
|
||||
res.send(errorPage(500, null, { theme: opts.theme, size: opts.size }));
|
||||
// TODO log the error
|
||||
console.error(er, er.stack);
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT || 8000, "127.0.0.1", function () {
|
||||
console.log("Server started, listening on " + (process.env.PORT || 8000));
|
||||
});
|
|
@ -1,309 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #282c37;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #282c37;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #282c37;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #8494ab;
|
||||
color: #ffffff;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title,
|
||||
html,
|
||||
body {
|
||||
font-weight: normal; }
|
||||
|
||||
@media (prefers-color-scheme: masto-dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #282c37;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #ffffff; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(40, 44, 55, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(40, 44, 55, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem; }
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
.hidden {
|
||||
display: none; }
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; } }
|
|
@ -1,170 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #282c37;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #282c37;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #282c37;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #8494ab;
|
||||
color: #ffffff;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title,
|
||||
html,
|
||||
body {
|
||||
font-weight: normal; }
|
|
@ -1,351 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: rgb(249, 249, 249);
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #282c37;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: rgb(68, 164, 193); }
|
||||
|
||||
a[rel = "tag"] {
|
||||
color: rgb(255, 145, 86); }
|
||||
|
||||
.meta {
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #282c37;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.u-url,
|
||||
.mention{
|
||||
color: #86b300; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #282c37;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #8494ab;
|
||||
color: #ffffff;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title,
|
||||
html,
|
||||
body {
|
||||
font-weight: normal; }
|
||||
|
||||
@media (prefers-color-scheme: misskey-dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #232323;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #dadada;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: rgb(134, 179, 0); }
|
||||
|
||||
a[rel = "tag"] {
|
||||
color: rgb(76, 184, 212); }
|
||||
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: rgb(199, 209, 216); }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(45, 45, 45, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(45, 45, 45, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem;
|
||||
opacity: .5; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #dadada;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.u-url,
|
||||
.mention{
|
||||
color: #86b300; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; } }
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: #232323;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #dadada;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: rgb(134, 179, 0); }
|
||||
|
||||
a[rel = "tag"] {
|
||||
color: rgb(76, 184, 212); }
|
||||
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: rgb(199, 209, 216); }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(45, 45, 45, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(45, 45, 45, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem;
|
||||
opacity: .5; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #dadada;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.u-url,
|
||||
.mention{
|
||||
color: #86b300; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
|
@ -1,177 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: rgb(249, 249, 249);
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #282c37;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: rgb(68, 164, 193); }
|
||||
|
||||
a[rel = "tag"] {
|
||||
color: rgb(255, 145, 86); }
|
||||
|
||||
.meta {
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #282c37;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.u-url,
|
||||
.mention{
|
||||
color: #86b300; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #282c37;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #8494ab;
|
||||
color: #ffffff;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title,
|
||||
html,
|
||||
body {
|
||||
font-weight: normal; }
|
|
@ -1,163 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: rgba(15, 22, 30, 1);
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: rgba(185, 185, 186, 1);
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: #e2b188; }
|
||||
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: rgba(185, 185, 186, 1); }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(40, 44, 55, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(40, 44, 55, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: rgba(185, 185, 186, 0.5);
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: rgba(185, 185, 186, 1);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: rgba(185, 185, 186, 1);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
|
@ -1,76 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<title>Fedifeed - embeddable ActivityPub feeds</title>
|
||||
<link rel="stylesheet" href="./css/stylesheet.css">
|
||||
<script src="./js/script.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<br>
|
||||
<div>
|
||||
<h1>Fedifeed</h1>
|
||||
<h4>Embedded ActivityPub feeds for blogs, websites, etc.</h4>
|
||||
<a href="https://github.com/SamTherapy/fedifeed" class="cta button alt">Fork on Github <img
|
||||
class="link-logo after" src="github-logo.svg" alt="Github Logo" data-reactid="19"></a><br>
|
||||
<br>
|
||||
<hr><br>
|
||||
<form action="javascript:genUrl()">
|
||||
<label>Instance URL:<input required type="text" id="urlin" placeholder="mastodon.social"
|
||||
oninvalid="this.setCustomValidity('Insert your instance URL. Example: mastodon.social')"
|
||||
oninput="this.setCustomValidity('')"></label>
|
||||
<br>
|
||||
<label>Username:<input required type="text" id="usernamein" placeholder="gargron"
|
||||
oninvalid="this.setCustomValidity('Insert your username. Example: gargron')"
|
||||
oninput="this.setCustomValidity('')"></label>
|
||||
<br>
|
||||
<label>Width (px):<input required type="number" id="width" value="400"
|
||||
oninvalid="this.setCustomValidity('Insert width of generated feed. Default: 400')"
|
||||
oninput="this.setCustomValidity('')"></label>
|
||||
<br>
|
||||
<label>Height (px):<input required type="number" id="height" value="800"
|
||||
oninvalid="this.setCustomValidity('Insert height of generated feed. Default: 800')"
|
||||
oninput="this.setCustomValidity('')"></label>
|
||||
<br>
|
||||
<label>UI Scale (percent):<input required type="number" id="size" value="100"
|
||||
oninvalid="this.setCustomValidity('Insert UI scale. Default: 100')"
|
||||
oninput="this.setCustomValidity('')"></label>
|
||||
<br>
|
||||
<label>Theme:
|
||||
<select id="theme">
|
||||
<option value="masto-dark">masto-dark</option>
|
||||
<option value="masto-light">masto-light</option>
|
||||
<option value="masto-auto">masto-auto (based on css prefers-color-scheme)</option>
|
||||
<option value="misskey-dark">misskey-dark</option>
|
||||
<option value="misskey-light">misskey-light</option>
|
||||
<option value="misskey-auto">misskey-auto (based on css prefers-color-scheme)</option>
|
||||
<option value="pleroma">pleroma</option>
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>Show Header?<input id="header" type="checkbox" checked="checked"></label>
|
||||
<br>
|
||||
<label>Hide replies?<input type="checkbox" id="hidereplies" checked="checked"></label>
|
||||
<br>
|
||||
<label>Hide boosts?<input type="checkbox" id="hideboosts" checked="checked"></label>
|
||||
<br>
|
||||
<br>
|
||||
<button value="generate">Generate</button>
|
||||
</form>
|
||||
<br><br>
|
||||
<label>Use this markup in your HTML: <br><textarea id="result"
|
||||
placeholder="result will go here"></textarea></label>
|
||||
<br><br>
|
||||
<h3>Live Preview:</h3>
|
||||
<span class="iframe-contain">
|
||||
<iframe id="frame" allowfullscreen sandbox="allow-top-navigation allow-scripts" width="400" height="800"
|
||||
src="/api/v1/feed?userurl=https%3A%2F%2Ffreecumextremist.com%2Fusers%2Fgrumbulon&replies=false&boosts=true">></iframe>
|
||||
</span>
|
||||
<br>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
|
@ -1,27 +0,0 @@
|
|||
import getEmojiJson from "./emojis.js";
|
||||
|
||||
window.genUrl = function genUrl() {
|
||||
function val(id) {
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
//download emoji pack
|
||||
getEmojiJson(val("urlin"));
|
||||
let user = val("usernamein");
|
||||
let instance = "https://" + val("urlin");
|
||||
|
||||
let showBoosts = (!document.getElementById("hideboosts").checked).toString();
|
||||
let showReplies = (!document.getElementById("hidereplies").checked).toString();
|
||||
let showHeader = document.getElementById("header").checked.toString();
|
||||
let portStr = (window.location.port && window.location.port != 80) ? (":" + window.location.port) : "";
|
||||
|
||||
let iframeUrl = window.location.protocol + "//" + window.location.hostname + portStr
|
||||
+ "/api/v1/feed?user=" + encodeURIComponent(user) + "&instance=" + encodeURIComponent(instance) + "&instance_type=" + "&theme=" + val("theme") + "&size=" + val("size")
|
||||
+ "&header=" + showHeader + "&replies=" + showReplies + "&boosts=" + showBoosts;
|
||||
|
||||
document.getElementById("result").value = "<iframe allowfullscreen sandbox=\"allow-top-navigation allow-scripts\" width=\"" + val("width") + "\" height=\"" + val("height") + "\" src=\"" + iframeUrl + "\"></iframe>";
|
||||
|
||||
let iframe = document.getElementById("frame");
|
||||
iframe.src = iframeUrl;
|
||||
iframe.width = val("width");
|
||||
iframe.height = val("height");
|
||||
};
|
|
@ -1,309 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #282c37;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
background-color: #ecf0f4; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #282c37; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #8494ab; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #90a1ba;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #282c37;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #282c37;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #8494ab;
|
||||
color: #ffffff;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
||||
|
||||
.item-content,
|
||||
.description,
|
||||
.title,
|
||||
html,
|
||||
body {
|
||||
font-weight: normal; }
|
||||
|
||||
@media (prefers-color-scheme: masto-dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #282c37;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #ffffff; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(40, 44, 55, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(40, 44, 55, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem; }
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
.hidden {
|
||||
display: none; }
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; } }
|
|
@ -1,163 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: #282c37;
|
||||
font-family: 'Roboto', roboto, Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
font-weight: lighter;
|
||||
overflow-x: hidden;
|
||||
font-size: 100%;
|
||||
word-break: break-word; }
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
a,
|
||||
a * {
|
||||
color: #2b90d9; }
|
||||
|
||||
.meta {
|
||||
background-color: #39404d; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background-size: cover;
|
||||
min-height: 8rem;
|
||||
color: #ffffff; }
|
||||
.header .header-left, .header .header-right {
|
||||
margin: 0; }
|
||||
.header .header-left {
|
||||
min-width: 8rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba(40, 44, 55, 0.3); }
|
||||
.header .header-left .avatar {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
position: relative;
|
||||
top: calc(50% - 3rem); }
|
||||
.header .header-right {
|
||||
flex-grow: 1;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.9rem;
|
||||
background: rgba(40, 44, 55, 0.85); }
|
||||
.header .header-title {
|
||||
font-size: 1.3rem; }
|
||||
|
||||
.item {
|
||||
padding: 1rem;
|
||||
border-top: solid 1px #626d80; }
|
||||
|
||||
.item-content,
|
||||
.cw,
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: lighter; }
|
||||
|
||||
.item-content *, .cw {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4rem; }
|
||||
|
||||
.item-title,
|
||||
.date,
|
||||
.author-fullname {
|
||||
color: #9baec8;
|
||||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around; }
|
||||
.author-info .author-displayname {
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: bolder; }
|
||||
|
||||
.avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 10%; }
|
||||
.avatar.circular {
|
||||
border-radius: 100%; }
|
||||
|
||||
.enclosures {
|
||||
padding: 0.5em 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
overflow: hidden; }
|
||||
|
||||
.enclosure {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
||||
a.enclosure {
|
||||
cursor: zoom-in; }
|
||||
|
||||
.enclosure > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
|
||||
.meta .title {
|
||||
font-weight: bold; }
|
||||
|
||||
.hidden {
|
||||
display: none; }
|
||||
|
||||
.button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background: #2b90d9;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
user-select: none; }
|
||||
|
||||
label.button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.4rem;
|
||||
background: #626d80;
|
||||
color: #282c37;
|
||||
font-size: 0.8rem; }
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
|
||||
input[type=checkbox]:checked ~ label::after {
|
||||
content: " Less"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ label::after {
|
||||
content: " More"; }
|
||||
|
||||
input[type=checkbox]:not(:checked) ~ div {
|
||||
display: none; }
|
|
@ -1,5 +0,0 @@
|
|||
@import 'masto-light.scss';
|
||||
|
||||
@media (prefers-color-scheme: masto-dark) {
|
||||
@import 'masto-dark.scss';
|
||||
}
|
|
@ -65,19 +65,11 @@ a * {
|
|||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
margin: 1rem 0 0 0; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
margin: 1rem 0; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
|
@ -110,6 +102,7 @@ a * {
|
|||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
Before (image error) Size: 1.5 KiB After (image error) Size: 1.5 KiB |
65
static/index.html
Normal file
65
static/index.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<title>Mastofeed - embeddable Mastodon feeds</title>
|
||||
<link rel="stylesheet" href="./stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<div>
|
||||
<h1>Mastofeed</h1>
|
||||
<h4>Embedded Mastodon feeds for blogs etc.</h4>
|
||||
<a href="https://github.com/fenwick67/mastofeed" class="cta button alt">Fork on Github <img class="link-logo after" src="github-logo.svg" alt="Github Logo" data-reactid="19"></a><br>
|
||||
<br><hr><br>
|
||||
<form action="javascript:genUrl()">
|
||||
<label>Instance URL:<input required type="text" id="urlin" placeholder="octodon.social" oninvalid="this.setCustomValidity('Insert your instance URL. Example: octodon.social')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Username:<input required type="text" id="usernamein" placeholder="fenwick67" oninvalid="this.setCustomValidity('Insert your username. Example: fenwick67')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Width (px):<input required type="number" id="width" value="400" oninvalid="this.setCustomValidity('Insert width of generated feed. Default: 400')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Height (px):<input required type="number" id="height" value="800" oninvalid="this.setCustomValidity('Insert height of generated feed. Default: 800')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>UI Scale (percent):<input required type="number" id="size" value="100" oninvalid="this.setCustomValidity('Insert UI scale. Default: 100')" oninput="this.setCustomValidity('')"></label><br>
|
||||
<label>Theme:
|
||||
<select id="theme">
|
||||
<option value="dark">dark</option>
|
||||
<option value="light">light</option>
|
||||
</select>
|
||||
</label><br>
|
||||
<label>Show Header?<input id="header" type="checkbox" checked="checked"></label><br>
|
||||
<label>Hide replies?<input type="checkbox" id="hidereplies" checked="checked"></label><br>
|
||||
<label>Hide boosts?<input type="checkbox" id="hideboosts" checked="checked"></label><br>
|
||||
|
||||
<button value="generate">Generate</button>
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
<label>Use this markup in your HTML: <br><textarea id="result" placeholder="result will go here"></textarea></label>
|
||||
<br>
|
||||
<h3>Live Preview:</h3>
|
||||
<iframe id="frame" allowfullscreen sandbox="allow-top-navigation allow-scripts" width="400" height="800" src="/apiv2/feed?userurl=https%3A%2F%2Foctodon.social%2Fusers%2Ffenwick67&replies=false&boosts=true"></iframe>
|
||||
<br>
|
||||
</div>
|
||||
<script>
|
||||
window.genUrl = function genUrl(){
|
||||
function val(id){
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
var inUrl = 'https://' + val('urlin') + '/users/'+val('usernamein');
|
||||
|
||||
var showBoosts = (!document.getElementById('hideboosts').checked).toString();
|
||||
var showReplies = (!document.getElementById('hidereplies').checked).toString();
|
||||
|
||||
var iframeUrl = window.location.protocol + '//'+ window.location.hostname +((window.location.port && window.location.port!=80)?(':'+window.location.port):'')
|
||||
+"/apiv2/feed?userurl="+encodeURIComponent(inUrl)+"&theme="+val('theme')+'&size='+val('size')
|
||||
+ "&header="+(document.getElementById('header').checked.toString())+'&replies='+showReplies+'&boosts='+showBoosts;
|
||||
|
||||
document.getElementById('result').value = '<iframe allowfullscreen sandbox="allow-top-navigation allow-scripts" width="'+val('width')+'" height="'+val('height')+'" src="'+iframeUrl+'"></iframe>';
|
||||
|
||||
var iframe = document.getElementById('frame');
|
||||
iframe.src = iframeUrl;
|
||||
iframe.width = val('width');
|
||||
iframe.height = val('height');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
12
static/infinite-scroll.js
Normal file
12
static/infinite-scroll.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -65,19 +65,11 @@ a * {
|
|||
font-size: 0.9rem; }
|
||||
|
||||
.date {
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display: block; }
|
||||
|
||||
.date:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
.item-title {
|
||||
margin-bottom: 0.7rem; }
|
||||
margin: 1rem 0 0 0; }
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem; }
|
||||
margin: 1rem 0; }
|
||||
|
||||
.author-info {
|
||||
margin: 0 1rem;
|
||||
|
@ -110,6 +102,7 @@ a * {
|
|||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
max-height: 12rem; }
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
@import url(https://fonts.googleapis.com/css?family=Quando|Judson|Montserrat:500|Roboto:400,500);
|
||||
@import url(https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css);
|
||||
a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote,
|
||||
body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl,
|
||||
dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4,
|
||||
|
@ -43,7 +44,7 @@ body {
|
|||
text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
font-family: Roboto, sans-serif;
|
||||
background-color: #000000;
|
||||
background-color: #1f232b;
|
||||
color: #5f6b84;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
|
@ -57,7 +58,22 @@ h1 {
|
|||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"], select {
|
||||
button {
|
||||
background: transparent;
|
||||
border: 1px solid #9baec8;
|
||||
padding: 3px 15px;
|
||||
color: #9baec8;
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #b5c3d6
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"] {
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
|
@ -94,12 +110,11 @@ div {
|
|||
margin: 0 auto
|
||||
}
|
||||
|
||||
.iframe-contain {
|
||||
display:block;
|
||||
text-align:center;
|
||||
iframe {
|
||||
float: middle;
|
||||
}
|
||||
|
||||
button, a {
|
||||
a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
|
@ -111,8 +126,5 @@ button, a {
|
|||
border-radius: 4px;
|
||||
padding: 3px 15px;
|
||||
color: #9baec8;
|
||||
cursor:pointer;
|
||||
margin-left: 4px
|
||||
}
|
||||
button:hover, a:hover {
|
||||
background-color: #394150;
|
||||
}
|
|
@ -88,19 +88,11 @@ a * {
|
|||
}
|
||||
.date{
|
||||
margin: 1rem 0 0 0;
|
||||
text-decoration: none;
|
||||
display:block;
|
||||
}
|
||||
.date:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
.item-title{
|
||||
margin-bottom:0.7rem;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
|
@ -139,6 +131,7 @@ a * {
|
|||
display:flex;
|
||||
flex: 1 1 auto;
|
||||
width:50%;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
max-height: 12rem;
|
||||
}
|
|
@ -7,4 +7,4 @@ $dim: $lighter;
|
|||
$dimmer: mix($darkest,$lighter,50%);
|
||||
$link: $vibrant;
|
||||
|
||||
@import 'masto-base.scss';
|
||||
@import 'base.scss';
|
|
@ -7,7 +7,7 @@ $dim: mix($lighter,$darkest,90%);
|
|||
$dimmer: mix($lighter,$darkest,80%);
|
||||
$link: $vibrant;
|
||||
|
||||
@import 'masto-base.scss';
|
||||
@import 'base.scss';
|
||||
|
||||
.item-content,
|
||||
.description,
|
|
@ -10,7 +10,7 @@
|
|||
</a>boosted
|
||||
</span>
|
||||
</div>
|
||||
<div class="detailed-status masto-light">
|
||||
<div class="detailed-status light">
|
||||
<a class="detailed-status__display-name p-author h-card" target="_blank" rel="noopener" href="https://social.tchncs.de/@muninnherself"><div>
|
||||
<div class="avatar">
|
||||
<img alt="" class="u-photo" src="https://assets.octodon.social/accounts/avatars/000/018/289/original/eb6b32e85f8606b4.jpg" width="48" height="48">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<style type="text/css"></style>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/masto-dark.css"></link>
|
||||
<link rel="stylesheet" href="/dark.css"></link>
|
||||
|
||||
|
||||
|
||||
|
|
15
test/test.js
15
test/test.js
|
@ -1,14 +1,15 @@
|
|||
// do a test
|
||||
|
||||
|
||||
import { createReadStream, writeFileSync } from "fs";
|
||||
import convert from "../lib/convert.js";
|
||||
var fs = require('fs'),
|
||||
request = require('request'),
|
||||
convert = require('../lib/convert')
|
||||
|
||||
|
||||
let r = createReadStream("./test/sample.atom");
|
||||
var r = fs.createReadStream('./test/sample.atom');
|
||||
|
||||
convert(r,function(er,data){
|
||||
if (er){return console.log("error: ",er);}
|
||||
console.log("ok");
|
||||
writeFileSync("./test/result.html",data,"utf8");
|
||||
});
|
||||
if (er){return console.log('error: ',er)}
|
||||
console.log('ok');
|
||||
fs.writeFileSync('./test/result.html',data,'utf8');
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue