Experiment with common-good module
This commit is contained in:
parent
cb4a6ef971
commit
b34b04c2c2
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly"
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
},
|
||||
rules: {}
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
**What's the problem you want to solved?**
|
||||
|
||||
**Is there a solution you'd like to recommend?**
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
**What's the problem you solved?**
|
||||
|
||||
**What solution are you recommending?**
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard"
|
||||
"extends": "stylelint-config-recommended"
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
language: node_js
|
||||
|
||||
# Both latest Node.js and latest LTS.
|
||||
node_js:
|
||||
node_js:
|
||||
- lts/*
|
||||
- node
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ Options:
|
|||
[boolean] [default: false]
|
||||
--host Hostname for web app to listen on [string] [default: "localhost"]
|
||||
--port Port for web app to listen on [number] [default: 3000]
|
||||
--debug Use verbose output for debugging [boolean] [default: false]
|
||||
--debug Use verbose output for debugging [boolean] [default: false]
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mkdirp = require('mkdirp')
|
||||
const { execSync } = require('child_process')
|
||||
const open = require('open')
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const mkdirp = require("mkdirp");
|
||||
const { execSync } = require("child_process");
|
||||
const open = require("open");
|
||||
|
||||
let xdgConfigHome = process.env.XDG_CONFIG_HOME
|
||||
let systemdUserHome = process.env.SYSTEMD_USER_HOME
|
||||
let xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
||||
let systemdUserHome = process.env.SYSTEMD_USER_HOME;
|
||||
|
||||
if (xdgConfigHome == null) {
|
||||
// Note: path.join() throws when arguments are null-ish.
|
||||
xdgConfigHome = path.join(process.env.HOME, '.config')
|
||||
xdgConfigHome = path.join(process.env.HOME, ".config");
|
||||
}
|
||||
|
||||
if (systemdUserHome == null) {
|
||||
systemdUserHome = path.join(xdgConfigHome, 'systemd', 'user')
|
||||
systemdUserHome = path.join(xdgConfigHome, "systemd", "user");
|
||||
}
|
||||
|
||||
const targetPath = path.join(systemdUserHome, 'oasis.service')
|
||||
const targetPath = path.join(systemdUserHome, "oasis.service");
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
console.log('Cowardly refusing to overwrite file:', targetPath)
|
||||
console.log("Cowardly refusing to overwrite file:", targetPath);
|
||||
} else {
|
||||
mkdirp(systemdUserHome)
|
||||
mkdirp(systemdUserHome);
|
||||
|
||||
const sourcePath = path.join(__dirname, 'oasis.service')
|
||||
fs.copyFileSync(sourcePath, targetPath)
|
||||
const sourcePath = path.join(__dirname, "oasis.service");
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
|
||||
execSync('systemctl --user daemon-reload')
|
||||
console.log('Service configuration has been installed to:', targetPath)
|
||||
execSync("systemctl --user daemon-reload");
|
||||
console.log("Service configuration has been installed to:", targetPath);
|
||||
}
|
||||
|
||||
// Since this isn't in a post-install script we can enable, start, and open it.
|
||||
execSync('systemctl --user enable oasis')
|
||||
execSync('systemctl --user start oasis')
|
||||
open('http://localhost:4515')
|
||||
execSync("systemctl --user enable oasis");
|
||||
execSync("systemctl --user start oasis");
|
||||
open("http://localhost:4515");
|
||||
|
|
|
@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation.
|
|||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
@ -74,4 +74,3 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
|
|||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ Hey, welcome! This project is still very experimental so I won't make any
|
|||
promises about the future architecture, but today it's pretty simple:
|
||||
|
||||
```
|
||||
├── assets: static assets like CSS
|
||||
├── assets: static assets like CSS
|
||||
├── cli: command-line interface (yargs)
|
||||
├── http: HTTP interface (koa)
|
||||
├── index: mediator that ties everything together
|
||||
|
@ -32,27 +32,27 @@ I have a hunch that importing modules from parent directories makes it really
|
|||
easy to create spaghetti code that's difficult to learn and maintain. I don't
|
||||
know whether that's true, but I'm experimenting with a layer-based approach
|
||||
where the only file with relative imports is `index.js` and all imports are in
|
||||
the style `require('./foo')`. I don't know whether this *actually* has any
|
||||
the style `require('./foo')`. I don't know whether this _actually_ has any
|
||||
interesting properties, but I'm trying it out to see whether it results in
|
||||
simpler software architectures.
|
||||
|
||||
#### Pattern
|
||||
|
||||
```javascript
|
||||
require('./foo') // foo.js
|
||||
require('./bar') // bar/index.js
|
||||
require("./foo"); // foo.js
|
||||
require("./bar"); // bar/index.js
|
||||
```
|
||||
|
||||
#### Anti-pattern
|
||||
|
||||
```javascript
|
||||
require('../ancestor') // two-way import
|
||||
require('./some/descendant') // layer violation
|
||||
require('./foobar/index.js') // excessive specificity
|
||||
require("../ancestor"); // two-way import
|
||||
require("./some/descendant"); // layer violation
|
||||
require("./foobar/index.js"); // excessive specificity
|
||||
```
|
||||
|
||||
**Note:** I want to make *very* clear that this is an experiment, not a claim
|
||||
that this is Objectively Better.
|
||||
**Note:** I want to make _very_ clear that this is an experiment, not a claim
|
||||
that this is Objectively Better.
|
||||
|
||||
### Any my [hyper]axe
|
||||
|
||||
|
@ -76,7 +76,7 @@ so I'm counting this as another experiment. It looks great and my first day with
|
|||
it has been really enjoyable, but if something goes horribly wrong then we can
|
||||
switch to hyperscript-helpers or something.
|
||||
|
||||
**Note:** I wasn't aware of hyperscript-helpers until I *after* I refactored
|
||||
**Note:** I wasn't aware of hyperscript-helpers until I _after_ I refactored
|
||||
the templates to use hyperaxe. Oops. I think hyperaxe has a cooler name anyway.
|
||||
|
||||
[dep-graph]: https://en.wikipedia.org/wiki/Dependency_graph
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Install
|
||||
|
||||
This is a guide on how to download the source code for Oasis so that you can
|
||||
build and install it on your computer. If you'd like an easier installation
|
||||
build and install it on your computer. If you'd like an easier installation
|
||||
option, try one of the options in the readme.
|
||||
|
||||
If you want to run Oasis in the background, see [`with-systemd.md`](./with-systemd.md).
|
||||
|
@ -47,4 +47,3 @@ oasis --help
|
|||
If you have problems, read the documentation on [downloading and installing
|
||||
packages globally](https://docs.npmjs.com/downloading-and-installing-packages-globally)
|
||||
or [get some help](https://github.com/fraction/oasis/issues/new/choose).
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -11,9 +11,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect src/index.js --debug --no-open",
|
||||
"fix": "standard --fix && stylelint --fix src/assets/*.css",
|
||||
"fix": "common-good fix",
|
||||
"start": "node src/index.js",
|
||||
"test": "standard && dependency-check ./package.json --unused --no-dev --ignore-module highlight.js --ignore-module @fraction/base16-css && cspell --no-summary '**/*.{js,md}' && stylelint src/assets/*.css && tsc --allowJs --resolveJsonModule --lib dom --checkJs --noEmit --skipLibCheck src/index.js",
|
||||
"test": "dependency_check_suffix='-i husky -i changelog-version -i mkdirp -i nodemon -i stylelint-config-recommended' common-good",
|
||||
"preversion": "npm test",
|
||||
"version": "changelog-version && git add CHANGELOG.md"
|
||||
},
|
||||
|
@ -21,7 +21,7 @@
|
|||
"@fraction/base16-css": "^1.1.0",
|
||||
"@fraction/flotilla": "3.0.0",
|
||||
"debug": "^4.1.1",
|
||||
"highlight.js": "^9.16.2",
|
||||
"highlight.js": "^9.18.0",
|
||||
"hyperaxe": "^1.3.0",
|
||||
"koa": "^2.7.0",
|
||||
"koa-body": "^4.1.0",
|
||||
|
@ -46,15 +46,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"changelog-version": "^1.0.1",
|
||||
"cspell": "^4.0.43",
|
||||
"dependency-check": "^4.1.0",
|
||||
"common-good": "^1.1.0",
|
||||
"husky": "^3.0.5",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nodemon": "^2.0.2",
|
||||
"standard": "^14.3.0",
|
||||
"stylelint": "^12.0.1",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"typescript": "^3.7.4"
|
||||
"stylelint-config-recommended": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "^0.23.0"
|
||||
|
|
|
@ -59,19 +59,9 @@
|
|||
|
||||
html {
|
||||
display: flex;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Oxygen",
|
||||
"Ubuntu",
|
||||
"Cantarell",
|
||||
"Fira Sans",
|
||||
"Droid Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
justify-content: center;
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
|
@ -85,12 +75,24 @@ main {
|
|||
}
|
||||
|
||||
/* https://www.desmos.com/calculator/3elcf5cwhn */
|
||||
h1 { font-size: 133%; } /* 4 / 3 */
|
||||
h2 { font-size: 115%; } /* 8 / 7 */
|
||||
h3 { font-size: 105%; } /* 16 / 15 */
|
||||
h4 { font-size: 103%; } /* 32 / 31 */
|
||||
h5 { font-size: 101%; } /* 64 / 63 */
|
||||
h6 { font-size: 100%; } /* 128 / 127 */
|
||||
h1 {
|
||||
font-size: 133%;
|
||||
} /* 4 / 3 */
|
||||
h2 {
|
||||
font-size: 115%;
|
||||
} /* 8 / 7 */
|
||||
h3 {
|
||||
font-size: 105%;
|
||||
} /* 16 / 15 */
|
||||
h4 {
|
||||
font-size: 103%;
|
||||
} /* 32 / 31 */
|
||||
h5 {
|
||||
font-size: 101%;
|
||||
} /* 64 / 63 */
|
||||
h6 {
|
||||
font-size: 100%;
|
||||
} /* 128 / 127 */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
|
|
48
src/cli.js
48
src/cli.js
|
@ -1,37 +1,37 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const yargs = require('yargs')
|
||||
const yargs = require("yargs");
|
||||
|
||||
module.exports = () =>
|
||||
yargs
|
||||
.scriptName('oasis')
|
||||
.env('OASIS')
|
||||
.help('h')
|
||||
.alias('h', 'help')
|
||||
.usage('Usage: $0 [options]')
|
||||
.options('open', {
|
||||
describe: 'Automatically open app in web browser. Use --no-open to disable.',
|
||||
.scriptName("oasis")
|
||||
.env("OASIS")
|
||||
.help("h")
|
||||
.alias("h", "help")
|
||||
.usage("Usage: $0 [options]")
|
||||
.options("open", {
|
||||
describe:
|
||||
"Automatically open app in web browser. Use --no-open to disable.",
|
||||
default: true,
|
||||
type: 'boolean'
|
||||
type: "boolean"
|
||||
})
|
||||
.options('offline', {
|
||||
.options("offline", {
|
||||
describe: "Don't try to connect to scuttlebutt peers or pubs",
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
type: "boolean"
|
||||
})
|
||||
.options('host', {
|
||||
describe: 'Hostname for web app to listen on',
|
||||
default: 'localhost',
|
||||
type: 'string'
|
||||
.options("host", {
|
||||
describe: "Hostname for web app to listen on",
|
||||
default: "localhost",
|
||||
type: "string"
|
||||
})
|
||||
.options('port', {
|
||||
describe: 'Port for web app to listen on',
|
||||
.options("port", {
|
||||
describe: "Port for web app to listen on",
|
||||
default: 3000,
|
||||
type: 'number'
|
||||
type: "number"
|
||||
})
|
||||
.options('debug', {
|
||||
describe: 'Use verbose output for debugging',
|
||||
.options("debug", {
|
||||
describe: "Use verbose output for debugging",
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
})
|
||||
.argv
|
||||
type: "boolean"
|
||||
}).argv;
|
||||
|
|
76
src/http.js
76
src/http.js
|
@ -1,60 +1,62 @@
|
|||
|
||||
const Koa = require('koa')
|
||||
const koaStatic = require('koa-static')
|
||||
const path = require('path')
|
||||
const mount = require('koa-mount')
|
||||
const Koa = require("koa");
|
||||
const koaStatic = require("koa-static");
|
||||
const path = require("path");
|
||||
const mount = require("koa-mount");
|
||||
|
||||
module.exports = ({ host, port, routes }) => {
|
||||
const assets = new Koa()
|
||||
assets.use(koaStatic(path.join(__dirname, 'assets')))
|
||||
const assets = new Koa();
|
||||
assets.use(koaStatic(path.join(__dirname, "assets")));
|
||||
|
||||
const app = new Koa()
|
||||
module.exports = app
|
||||
const app = new Koa();
|
||||
module.exports = app;
|
||||
|
||||
app.on('error', (e) => {
|
||||
app.on("error", e => {
|
||||
// Output full error objects
|
||||
e.message = e.stack
|
||||
e.expose = true
|
||||
return null
|
||||
})
|
||||
e.message = e.stack;
|
||||
e.expose = true;
|
||||
return null;
|
||||
});
|
||||
|
||||
app.use(mount('/assets', assets))
|
||||
app.use(mount("/assets", assets));
|
||||
|
||||
// headers
|
||||
app.use(async (ctx, next) => {
|
||||
await next()
|
||||
await next();
|
||||
|
||||
const csp = [
|
||||
'default-src \'none\'',
|
||||
'img-src \'self\'',
|
||||
'form-action \'self\'',
|
||||
'media-src \'self\'',
|
||||
'style-src \'self\' \'unsafe-inline\''
|
||||
].join('; ')
|
||||
"default-src 'none'",
|
||||
"img-src 'self'",
|
||||
"form-action 'self'",
|
||||
"media-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'"
|
||||
].join("; ");
|
||||
|
||||
// Disallow scripts.
|
||||
ctx.set('Content-Security-Policy', csp)
|
||||
ctx.set("Content-Security-Policy", csp);
|
||||
|
||||
// Disallow <iframe> embeds from other domains.
|
||||
ctx.set('X-Frame-Options', 'SAMEORIGIN')
|
||||
ctx.set("X-Frame-Options", "SAMEORIGIN");
|
||||
|
||||
// Disallow browsers overwriting declared media types.
|
||||
ctx.set('X-Content-Type-Options', 'nosniff')
|
||||
ctx.set("X-Content-Type-Options", "nosniff");
|
||||
|
||||
// Disallow sharing referrer with other domains.
|
||||
ctx.set('Referrer-Policy', 'same-origin')
|
||||
ctx.set("Referrer-Policy", "same-origin");
|
||||
|
||||
// Disallow extra browser features except audio output.
|
||||
ctx.set('Feature-Policy', 'speaker \'self\'')
|
||||
ctx.set("Feature-Policy", "speaker 'self'");
|
||||
|
||||
if (ctx.method !== 'GET') {
|
||||
const referer = ctx.request.header.referer
|
||||
ctx.assert(referer != null, `HTTP ${ctx.method} must include referer`)
|
||||
const refererUrl = new URL(referer)
|
||||
const isBlobUrl = refererUrl.pathname.startsWith('/blob/')
|
||||
ctx.assert(isBlobUrl === false, `HTTP ${ctx.method} from blob URL not allowed`)
|
||||
if (ctx.method !== "GET") {
|
||||
const referer = ctx.request.header.referer;
|
||||
ctx.assert(referer != null, `HTTP ${ctx.method} must include referer`);
|
||||
const refererUrl = new URL(referer);
|
||||
const isBlobUrl = refererUrl.pathname.startsWith("/blob/");
|
||||
ctx.assert(
|
||||
isBlobUrl === false,
|
||||
`HTTP ${ctx.method} from blob URL not allowed`
|
||||
);
|
||||
}
|
||||
})
|
||||
app.use(routes)
|
||||
app.listen({ host, port })
|
||||
}
|
||||
});
|
||||
app.use(routes);
|
||||
app.listen({ host, port });
|
||||
};
|
||||
|
|
629
src/index.js
629
src/index.js
|
@ -1,14 +1,14 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
// Koa application to provide HTTP interface.
|
||||
|
||||
const cli = require('./cli')
|
||||
const config = cli()
|
||||
const cli = require("./cli");
|
||||
const config = cli();
|
||||
|
||||
if (config.debug) {
|
||||
process.env.DEBUG = 'oasis,oasis:*'
|
||||
process.env.DEBUG = "oasis,oasis:*";
|
||||
}
|
||||
|
||||
// HACK: We must get the CLI config and then delete environment variables.
|
||||
|
@ -17,37 +17,30 @@ if (config.debug) {
|
|||
// Unfortunately some modules think that our CLI options are meant for them,
|
||||
// and since there's no way to disable that behavior (!) we have to hide them
|
||||
// manually by setting the args property to an empty array.
|
||||
process.argv = []
|
||||
process.argv = [];
|
||||
|
||||
const http = require('./http')
|
||||
const http = require("./http");
|
||||
|
||||
const debug = require('debug')('oasis')
|
||||
const fs = require('fs').promises
|
||||
const koaBody = require('koa-body')
|
||||
const { nav, ul, li, a } = require('hyperaxe')
|
||||
const open = require('open')
|
||||
const path = require('path')
|
||||
const pull = require('pull-stream')
|
||||
const requireStyle = require('require-style')
|
||||
const router = require('koa-router')()
|
||||
const ssbMentions = require('ssb-mentions')
|
||||
const ssbRef = require('ssb-ref')
|
||||
const { themeNames } = require('@fraction/base16-css')
|
||||
const debug = require("debug")("oasis");
|
||||
const fs = require("fs").promises;
|
||||
const koaBody = require("koa-body");
|
||||
const { nav, ul, li, a } = require("hyperaxe");
|
||||
const open = require("open");
|
||||
const path = require("path");
|
||||
const pull = require("pull-stream");
|
||||
const requireStyle = require("require-style");
|
||||
const router = require("koa-router")();
|
||||
const ssbMentions = require("ssb-mentions");
|
||||
const ssbRef = require("ssb-ref");
|
||||
const { themeNames } = require("@fraction/base16-css");
|
||||
|
||||
const ssb = require('./ssb')
|
||||
const ssb = require("./ssb");
|
||||
|
||||
// Create "cooler"-style interface from SSB connection.
|
||||
// This handle is passed to the models for their convenience.
|
||||
const cooler = ssb({ offline: config.offline })
|
||||
const cooler = ssb({ offline: config.offline });
|
||||
|
||||
const {
|
||||
about,
|
||||
blob,
|
||||
friend,
|
||||
meta,
|
||||
post,
|
||||
vote
|
||||
} = require('./models')(cooler)
|
||||
const { about, blob, friend, meta, post, vote } = require("./models")(cooler);
|
||||
|
||||
const {
|
||||
authorView,
|
||||
|
@ -58,94 +51,89 @@ const {
|
|||
publicView,
|
||||
replyView,
|
||||
searchView
|
||||
} = require('./views')
|
||||
} = require("./views");
|
||||
|
||||
let sharp
|
||||
let sharp;
|
||||
|
||||
try {
|
||||
sharp = require('sharp')
|
||||
sharp = require("sharp");
|
||||
} catch (e) {
|
||||
// Optional dependency
|
||||
}
|
||||
|
||||
const defaultTheme = 'atelier-sulphurPool-light'.toLowerCase()
|
||||
const defaultTheme = "atelier-sulphurPool-light".toLowerCase();
|
||||
|
||||
// TODO: refactor
|
||||
const start = async () => {
|
||||
const filePath = path.join(__dirname, '..', 'README.md')
|
||||
config.readme = await fs.readFile(filePath, 'utf8')
|
||||
}
|
||||
start()
|
||||
const filePath = path.join(__dirname, "..", "README.md");
|
||||
config.readme = await fs.readFile(filePath, "utf8");
|
||||
};
|
||||
start();
|
||||
|
||||
router
|
||||
.param('imageSize', (imageSize, ctx, next) => {
|
||||
const size = Number(imageSize)
|
||||
const isInteger = size % 1 === 0
|
||||
const overMinSize = size > 2
|
||||
const underMaxSize = size <= 256
|
||||
ctx.assert(isInteger && overMinSize && underMaxSize, 'Invalid image size')
|
||||
return next()
|
||||
.param("imageSize", (imageSize, ctx, next) => {
|
||||
const size = Number(imageSize);
|
||||
const isInteger = size % 1 === 0;
|
||||
const overMinSize = size > 2;
|
||||
const underMaxSize = size <= 256;
|
||||
ctx.assert(isInteger && overMinSize && underMaxSize, "Invalid image size");
|
||||
return next();
|
||||
})
|
||||
.param('blobId', (blobId, ctx, next) => {
|
||||
ctx.assert(ssbRef.isBlob(blobId), 400, 'Invalid blob link')
|
||||
return next()
|
||||
.param("blobId", (blobId, ctx, next) => {
|
||||
ctx.assert(ssbRef.isBlob(blobId), 400, "Invalid blob link");
|
||||
return next();
|
||||
})
|
||||
.param('message', (message, ctx, next) => {
|
||||
ctx.assert(ssbRef.isMsg(message), 400, 'Invalid message link')
|
||||
return next()
|
||||
.param("message", (message, ctx, next) => {
|
||||
ctx.assert(ssbRef.isMsg(message), 400, "Invalid message link");
|
||||
return next();
|
||||
})
|
||||
.param('feed', (message, ctx, next) => {
|
||||
ctx.assert(ssbRef.isFeedId(message), 400, 'Invalid feed link')
|
||||
return next()
|
||||
.param("feed", (message, ctx, next) => {
|
||||
ctx.assert(ssbRef.isFeedId(message), 400, "Invalid feed link");
|
||||
return next();
|
||||
})
|
||||
.get('/', async (ctx) => {
|
||||
ctx.redirect('/public/popular/day')
|
||||
.get("/", async ctx => {
|
||||
ctx.redirect("/public/popular/day");
|
||||
})
|
||||
.get('/public/popular/:period', async (ctx) => {
|
||||
const { period } = ctx.params
|
||||
.get("/public/popular/:period", async ctx => {
|
||||
const { period } = ctx.params;
|
||||
const publicPopular = async ({ period }) => {
|
||||
const messages = await post.popular({ period })
|
||||
const messages = await post.popular({ period });
|
||||
|
||||
const option = (somePeriod) =>
|
||||
const option = somePeriod =>
|
||||
li(
|
||||
period === somePeriod
|
||||
? a({ class: 'current', href: `./${somePeriod}` }, somePeriod)
|
||||
? a({ class: "current", href: `./${somePeriod}` }, somePeriod)
|
||||
: a({ href: `./${somePeriod}` }, somePeriod)
|
||||
)
|
||||
);
|
||||
|
||||
const prefix = nav(
|
||||
ul(
|
||||
option('day'),
|
||||
option('week'),
|
||||
option('month'),
|
||||
option('year')
|
||||
)
|
||||
)
|
||||
ul(option("day"), option("week"), option("month"), option("year"))
|
||||
);
|
||||
|
||||
return publicView({
|
||||
messages,
|
||||
prefix
|
||||
})
|
||||
}
|
||||
ctx.body = await publicPopular({ period })
|
||||
});
|
||||
};
|
||||
ctx.body = await publicPopular({ period });
|
||||
})
|
||||
.get('/public/latest', async (ctx) => {
|
||||
.get("/public/latest", async ctx => {
|
||||
const publicLatest = async () => {
|
||||
const messages = await post.latest()
|
||||
return publicView({ messages })
|
||||
}
|
||||
ctx.body = await publicLatest()
|
||||
const messages = await post.latest();
|
||||
return publicView({ messages });
|
||||
};
|
||||
ctx.body = await publicLatest();
|
||||
})
|
||||
.get('/author/:feed', async (ctx) => {
|
||||
const { feed } = ctx.params
|
||||
const author = async (feedId) => {
|
||||
const description = await about.description(feedId)
|
||||
const name = await about.name(feedId)
|
||||
const image = await about.image(feedId)
|
||||
const messages = await post.fromFeed(feedId)
|
||||
const relationship = await friend.getRelationship(feedId)
|
||||
.get("/author/:feed", async ctx => {
|
||||
const { feed } = ctx.params;
|
||||
const author = async feedId => {
|
||||
const description = await about.description(feedId);
|
||||
const name = await about.name(feedId);
|
||||
const image = await about.image(feedId);
|
||||
const messages = await post.fromFeed(feedId);
|
||||
const relationship = await friend.getRelationship(feedId);
|
||||
|
||||
const avatarUrl = `/image/256/${encodeURIComponent(image)}`
|
||||
const avatarUrl = `/image/256/${encodeURIComponent(image)}`;
|
||||
|
||||
return authorView({
|
||||
feedId,
|
||||
|
@ -154,60 +142,60 @@ router
|
|||
description,
|
||||
avatarUrl,
|
||||
relationship
|
||||
})
|
||||
}
|
||||
ctx.body = await author(feed)
|
||||
});
|
||||
};
|
||||
ctx.body = await author(feed);
|
||||
})
|
||||
.get('/search/', async (ctx) => {
|
||||
const { query } = ctx.query
|
||||
.get("/search/", async ctx => {
|
||||
const { query } = ctx.query;
|
||||
const search = async ({ query }) => {
|
||||
if (typeof query === 'string') {
|
||||
if (typeof query === "string") {
|
||||
// https://github.com/ssbc/ssb-search/issues/7
|
||||
query = query.toLowerCase()
|
||||
query = query.toLowerCase();
|
||||
}
|
||||
|
||||
const messages = await post.search({ query })
|
||||
const messages = await post.search({ query });
|
||||
|
||||
return searchView({ messages, query })
|
||||
}
|
||||
ctx.body = await search({ query })
|
||||
return searchView({ messages, query });
|
||||
};
|
||||
ctx.body = await search({ query });
|
||||
})
|
||||
.get('/inbox', async (ctx) => {
|
||||
.get("/inbox", async ctx => {
|
||||
const inbox = async () => {
|
||||
const messages = await post.inbox()
|
||||
const messages = await post.inbox();
|
||||
|
||||
return listView({ messages })
|
||||
}
|
||||
ctx.body = await inbox()
|
||||
return listView({ messages });
|
||||
};
|
||||
ctx.body = await inbox();
|
||||
})
|
||||
.get('/hashtag/:channel', async (ctx) => {
|
||||
const { channel } = ctx.params
|
||||
const hashtag = async (channel) => {
|
||||
const messages = await post.fromHashtag(channel)
|
||||
.get("/hashtag/:channel", async ctx => {
|
||||
const { channel } = ctx.params;
|
||||
const hashtag = async channel => {
|
||||
const messages = await post.fromHashtag(channel);
|
||||
|
||||
return listView({ messages })
|
||||
}
|
||||
ctx.body = await hashtag(channel)
|
||||
return listView({ messages });
|
||||
};
|
||||
ctx.body = await hashtag(channel);
|
||||
})
|
||||
.get('/theme.css', (ctx) => {
|
||||
const theme = ctx.cookies.get('theme') || defaultTheme
|
||||
.get("/theme.css", ctx => {
|
||||
const theme = ctx.cookies.get("theme") || defaultTheme;
|
||||
|
||||
const packageName = '@fraction/base16-css'
|
||||
const filePath = `${packageName}/src/base16-${theme}.css`
|
||||
ctx.type = 'text/css'
|
||||
ctx.body = requireStyle(filePath)
|
||||
const packageName = "@fraction/base16-css";
|
||||
const filePath = `${packageName}/src/base16-${theme}.css`;
|
||||
ctx.type = "text/css";
|
||||
ctx.body = requireStyle(filePath);
|
||||
})
|
||||
.get('/profile/', async (ctx) => {
|
||||
.get("/profile/", async ctx => {
|
||||
const profile = async () => {
|
||||
const myFeedId = await meta.myFeedId()
|
||||
const myFeedId = await meta.myFeedId();
|
||||
|
||||
const description = await about.description(myFeedId)
|
||||
const name = await about.name(myFeedId)
|
||||
const image = await about.image(myFeedId)
|
||||
const description = await about.description(myFeedId);
|
||||
const name = await about.name(myFeedId);
|
||||
const image = await about.image(myFeedId);
|
||||
|
||||
const messages = await post.fromFeed(myFeedId)
|
||||
const messages = await post.fromFeed(myFeedId);
|
||||
|
||||
const avatarUrl = `/image/256/${encodeURIComponent(image)}`
|
||||
const avatarUrl = `/image/256/${encodeURIComponent(image)}`;
|
||||
|
||||
return authorView({
|
||||
feedId: myFeedId,
|
||||
|
@ -216,316 +204,319 @@ router
|
|||
description,
|
||||
avatarUrl,
|
||||
relationship: null
|
||||
})
|
||||
}
|
||||
ctx.body = await profile()
|
||||
});
|
||||
};
|
||||
ctx.body = await profile();
|
||||
})
|
||||
.get('/json/:message', async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
ctx.type = 'application/json'
|
||||
const json = async (message) => {
|
||||
const json = await meta.get(message)
|
||||
return JSON.stringify(json, null, 2)
|
||||
}
|
||||
ctx.body = await json(message)
|
||||
.get("/json/:message", async ctx => {
|
||||
const { message } = ctx.params;
|
||||
ctx.type = "application/json";
|
||||
const json = async message => {
|
||||
const json = await meta.get(message);
|
||||
return JSON.stringify(json, null, 2);
|
||||
};
|
||||
ctx.body = await json(message);
|
||||
})
|
||||
.get('/blob/:blobId', async (ctx) => {
|
||||
const { blobId } = ctx.params
|
||||
.get("/blob/:blobId", async ctx => {
|
||||
const { blobId } = ctx.params;
|
||||
const getBlob = async ({ blobId }) => {
|
||||
const bufferSource = await blob.get({ blobId })
|
||||
const bufferSource = await blob.get({ blobId });
|
||||
|
||||
debug('got buffer source')
|
||||
return new Promise((resolve) => {
|
||||
debug("got buffer source");
|
||||
return new Promise(resolve => {
|
||||
pull(
|
||||
bufferSource,
|
||||
pull.collect(async (err, bufferArray) => {
|
||||
if (err) {
|
||||
await blob.want({ blobId })
|
||||
resolve(Buffer.alloc(0))
|
||||
await blob.want({ blobId });
|
||||
resolve(Buffer.alloc(0));
|
||||
} else {
|
||||
const buffer = Buffer.concat(bufferArray)
|
||||
resolve(buffer)
|
||||
const buffer = Buffer.concat(bufferArray);
|
||||
resolve(buffer);
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
ctx.body = await getBlob({ blobId })
|
||||
);
|
||||
});
|
||||
};
|
||||
ctx.body = await getBlob({ blobId });
|
||||
|
||||
if (ctx.body.length === 0) {
|
||||
ctx.response.status = 404
|
||||
ctx.response.status = 404;
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public,max-age=31536000,immutable')
|
||||
ctx.set("Cache-Control", "public,max-age=31536000,immutable");
|
||||
}
|
||||
|
||||
// This prevents an auto-download when visiting the URL.
|
||||
ctx.attachment(blobId, { type: 'inline' })
|
||||
ctx.attachment(blobId, { type: "inline" });
|
||||
})
|
||||
.get('/image/:imageSize/:blobId', async (ctx) => {
|
||||
const { blobId, imageSize } = ctx.params
|
||||
ctx.type = 'image/png'
|
||||
const fakePixel =
|
||||
Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64'
|
||||
)
|
||||
const fakeImage = (imageSize) =>
|
||||
.get("/image/:imageSize/:blobId", async ctx => {
|
||||
const { blobId, imageSize } = ctx.params;
|
||||
ctx.type = "image/png";
|
||||
const fakePixel = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64"
|
||||
);
|
||||
const fakeImage = imageSize =>
|
||||
sharp
|
||||
? sharp({
|
||||
create: {
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
channels: 4,
|
||||
background: {
|
||||
r: 0, g: 0, b: 0, alpha: 0.5
|
||||
create: {
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
channels: 4,
|
||||
background: {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
alpha: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
: new Promise((resolve) => resolve(fakePixel))
|
||||
})
|
||||
: new Promise(resolve => resolve(fakePixel));
|
||||
const image = async ({ blobId, imageSize }) => {
|
||||
const bufferSource = await blob.get({ blobId })
|
||||
const fakeId = '&0000000000000000000000000000000000000000000=.sha256'
|
||||
const bufferSource = await blob.get({ blobId });
|
||||
const fakeId = "&0000000000000000000000000000000000000000000=.sha256";
|
||||
|
||||
debug('got buffer source')
|
||||
return new Promise((resolve) => {
|
||||
debug("got buffer source");
|
||||
return new Promise(resolve => {
|
||||
if (blobId === fakeId) {
|
||||
debug('fake image')
|
||||
fakeImage(imageSize).then(result => resolve(result))
|
||||
debug("fake image");
|
||||
fakeImage(imageSize).then(result => resolve(result));
|
||||
} else {
|
||||
debug('not fake image')
|
||||
debug("not fake image");
|
||||
pull(
|
||||
bufferSource,
|
||||
pull.collect(async (err, bufferArray) => {
|
||||
if (err) {
|
||||
await blob.want({ blobId })
|
||||
const result = fakeImage(imageSize)
|
||||
debug({ result })
|
||||
resolve(result)
|
||||
await blob.want({ blobId });
|
||||
const result = fakeImage(imageSize);
|
||||
debug({ result });
|
||||
resolve(result);
|
||||
} else {
|
||||
const buffer = Buffer.concat(bufferArray)
|
||||
const buffer = Buffer.concat(bufferArray);
|
||||
|
||||
if (sharp) {
|
||||
sharp(buffer)
|
||||
.resize(imageSize, imageSize)
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then((data) => {
|
||||
resolve(data)
|
||||
})
|
||||
.then(data => {
|
||||
resolve(data);
|
||||
});
|
||||
} else {
|
||||
resolve(buffer)
|
||||
resolve(buffer);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
ctx.body = await image({ blobId, imageSize: Number(imageSize) })
|
||||
});
|
||||
};
|
||||
ctx.body = await image({ blobId, imageSize: Number(imageSize) });
|
||||
})
|
||||
.get('/meta/', async (ctx) => {
|
||||
const theme = ctx.cookies.get('theme') || defaultTheme
|
||||
.get("/meta/", async ctx => {
|
||||
const theme = ctx.cookies.get("theme") || defaultTheme;
|
||||
const getMeta = async ({ theme }) => {
|
||||
const status = await meta.status()
|
||||
const peers = await meta.peers()
|
||||
const status = await meta.status();
|
||||
const peers = await meta.peers();
|
||||
|
||||
return metaView({ status, peers, theme, themeNames })
|
||||
}
|
||||
ctx.body = await getMeta({ theme })
|
||||
return metaView({ status, peers, theme, themeNames });
|
||||
};
|
||||
ctx.body = await getMeta({ theme });
|
||||
})
|
||||
.get('/likes/:feed', async (ctx) => {
|
||||
const { feed } = ctx.params
|
||||
.get("/likes/:feed", async ctx => {
|
||||
const { feed } = ctx.params;
|
||||
|
||||
const likes = async ({ feed }) => {
|
||||
const messages = await post.likes({ feed })
|
||||
return listView({ messages })
|
||||
}
|
||||
ctx.body = await likes({ feed })
|
||||
const messages = await post.likes({ feed });
|
||||
return listView({ messages });
|
||||
};
|
||||
ctx.body = await likes({ feed });
|
||||
})
|
||||
.get('/meta/readme/', async (ctx) => {
|
||||
const status = async (text) => {
|
||||
return markdownView({ text })
|
||||
}
|
||||
ctx.body = await status(config.readme)
|
||||
.get("/meta/readme/", async ctx => {
|
||||
const status = async text => {
|
||||
return markdownView({ text });
|
||||
};
|
||||
ctx.body = await status(config.readme);
|
||||
})
|
||||
.get('/mentions/', async (ctx) => {
|
||||
.get("/mentions/", async ctx => {
|
||||
const mentions = async () => {
|
||||
const messages = await post.mentionsMe()
|
||||
const messages = await post.mentionsMe();
|
||||
|
||||
return listView({ messages })
|
||||
}
|
||||
ctx.body = await mentions()
|
||||
return listView({ messages });
|
||||
};
|
||||
ctx.body = await mentions();
|
||||
})
|
||||
.get('/thread/:message', async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
const thread = async (message) => {
|
||||
const messages = await post.fromThread(message)
|
||||
debug('got %i messages', messages.length)
|
||||
.get("/thread/:message", async ctx => {
|
||||
const { message } = ctx.params;
|
||||
const thread = async message => {
|
||||
const messages = await post.fromThread(message);
|
||||
debug("got %i messages", messages.length);
|
||||
|
||||
return listView({ messages })
|
||||
}
|
||||
return listView({ messages });
|
||||
};
|
||||
|
||||
ctx.body = await thread(message)
|
||||
ctx.body = await thread(message);
|
||||
})
|
||||
.get('/reply/:message', async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
const reply = async (parentId) => {
|
||||
const rootMessage = await post.get(parentId)
|
||||
const myFeedId = await meta.myFeedId()
|
||||
.get("/reply/:message", async ctx => {
|
||||
const { message } = ctx.params;
|
||||
const reply = async parentId => {
|
||||
const rootMessage = await post.get(parentId);
|
||||
const myFeedId = await meta.myFeedId();
|
||||
|
||||
debug('%O', rootMessage)
|
||||
const messages = [rootMessage]
|
||||
debug("%O", rootMessage);
|
||||
const messages = [rootMessage];
|
||||
|
||||
return replyView({ messages, myFeedId })
|
||||
}
|
||||
ctx.body = await reply(message)
|
||||
return replyView({ messages, myFeedId });
|
||||
};
|
||||
ctx.body = await reply(message);
|
||||
})
|
||||
.get('/comment/:message', async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
const comment = async (parentId) => {
|
||||
const parentMessage = await post.get(parentId)
|
||||
const myFeedId = await meta.myFeedId()
|
||||
.get("/comment/:message", async ctx => {
|
||||
const { message } = ctx.params;
|
||||
const comment = async parentId => {
|
||||
const parentMessage = await post.get(parentId);
|
||||
const myFeedId = await meta.myFeedId();
|
||||
|
||||
const hasRoot = typeof parentMessage.value.content.root === 'string' && ssbRef.isMsg(parentMessage.value.content.root)
|
||||
const hasFork = typeof parentMessage.value.content.fork === 'string' && ssbRef.isMsg(parentMessage.value.content.fork)
|
||||
const hasRoot =
|
||||
typeof parentMessage.value.content.root === "string" &&
|
||||
ssbRef.isMsg(parentMessage.value.content.root);
|
||||
const hasFork =
|
||||
typeof parentMessage.value.content.fork === "string" &&
|
||||
ssbRef.isMsg(parentMessage.value.content.fork);
|
||||
|
||||
const rootMessage = hasRoot
|
||||
? hasFork
|
||||
? parentMessage
|
||||
: await post.get(parentMessage.value.content.root)
|
||||
: parentMessage
|
||||
: parentMessage;
|
||||
|
||||
const messages = await post.threadReplies(rootMessage.key)
|
||||
const messages = await post.threadReplies(rootMessage.key);
|
||||
|
||||
messages.push(rootMessage)
|
||||
messages.push(rootMessage);
|
||||
|
||||
return commentView({ messages, myFeedId, parentMessage })
|
||||
}
|
||||
ctx.body = await comment(message)
|
||||
return commentView({ messages, myFeedId, parentMessage });
|
||||
};
|
||||
ctx.body = await comment(message);
|
||||
})
|
||||
.post('/reply/:message', koaBody(), async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
const text = String(ctx.request.body.text)
|
||||
.post("/reply/:message", koaBody(), async ctx => {
|
||||
const { message } = ctx.params;
|
||||
const text = String(ctx.request.body.text);
|
||||
const publishReply = async ({ message, text }) => {
|
||||
// TODO: rename `message` to `parent` or `ancestor` or similar
|
||||
const mentions = ssbMentions(text).filter((mention) =>
|
||||
mention != null
|
||||
) || undefined
|
||||
const mentions =
|
||||
ssbMentions(text).filter(mention => mention != null) || undefined;
|
||||
|
||||
const parent = await post.get(message)
|
||||
const parent = await post.get(message);
|
||||
return post.reply({
|
||||
parent,
|
||||
message: { text, mentions }
|
||||
})
|
||||
}
|
||||
ctx.body = await publishReply({ message, text })
|
||||
ctx.redirect(`/thread/${encodeURIComponent(message)}`)
|
||||
});
|
||||
};
|
||||
ctx.body = await publishReply({ message, text });
|
||||
ctx.redirect(`/thread/${encodeURIComponent(message)}`);
|
||||
})
|
||||
.post('/comment/:message', koaBody(), async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
const text = String(ctx.request.body.text)
|
||||
.post("/comment/:message", koaBody(), async ctx => {
|
||||
const { message } = ctx.params;
|
||||
const text = String(ctx.request.body.text);
|
||||
const publishComment = async ({ message, text }) => {
|
||||
// TODO: rename `message` to `parent` or `ancestor` or similar
|
||||
const mentions = ssbMentions(text).filter((mention) =>
|
||||
mention != null
|
||||
) || undefined
|
||||
const parent = await meta.get(message)
|
||||
const mentions =
|
||||
ssbMentions(text).filter(mention => mention != null) || undefined;
|
||||
const parent = await meta.get(message);
|
||||
|
||||
return post.comment({
|
||||
parent,
|
||||
message: { text, mentions }
|
||||
})
|
||||
}
|
||||
ctx.body = await publishComment({ message, text })
|
||||
ctx.redirect(`/thread/${encodeURIComponent(message)}`)
|
||||
});
|
||||
};
|
||||
ctx.body = await publishComment({ message, text });
|
||||
ctx.redirect(`/thread/${encodeURIComponent(message)}`);
|
||||
})
|
||||
.post('/publish/', koaBody(), async (ctx) => {
|
||||
const text = String(ctx.request.body.text)
|
||||
.post("/publish/", koaBody(), async ctx => {
|
||||
const text = String(ctx.request.body.text);
|
||||
const publish = async ({ text }) => {
|
||||
const mentions = ssbMentions(text).filter((mention) =>
|
||||
mention != null
|
||||
) || undefined
|
||||
const mentions =
|
||||
ssbMentions(text).filter(mention => mention != null) || undefined;
|
||||
|
||||
return post.root({
|
||||
text,
|
||||
mentions
|
||||
})
|
||||
}
|
||||
ctx.body = await publish({ text })
|
||||
ctx.redirect('/')
|
||||
});
|
||||
};
|
||||
ctx.body = await publish({ text });
|
||||
ctx.redirect("/");
|
||||
})
|
||||
.post('/follow/:feed', koaBody(), async (ctx) => {
|
||||
const { feed } = ctx.params
|
||||
const referer = new URL(ctx.request.header.referer)
|
||||
ctx.body = await friend.follow(feed)
|
||||
ctx.redirect(referer)
|
||||
.post("/follow/:feed", koaBody(), async ctx => {
|
||||
const { feed } = ctx.params;
|
||||
const referer = new URL(ctx.request.header.referer);
|
||||
ctx.body = await friend.follow(feed);
|
||||
ctx.redirect(referer);
|
||||
})
|
||||
.post('/unfollow/:feed', koaBody(), async (ctx) => {
|
||||
const { feed } = ctx.params
|
||||
const referer = new URL(ctx.request.header.referer)
|
||||
ctx.body = await friend.unfollow(feed)
|
||||
ctx.redirect(referer)
|
||||
.post("/unfollow/:feed", koaBody(), async ctx => {
|
||||
const { feed } = ctx.params;
|
||||
const referer = new URL(ctx.request.header.referer);
|
||||
ctx.body = await friend.unfollow(feed);
|
||||
ctx.redirect(referer);
|
||||
})
|
||||
.post('/like/:message', koaBody(), async (ctx) => {
|
||||
const { message } = ctx.params
|
||||
.post("/like/:message", koaBody(), async ctx => {
|
||||
const { message } = ctx.params;
|
||||
// TODO: convert all so `message` is full message and `messageKey` is key
|
||||
const messageKey = message
|
||||
const messageKey = message;
|
||||
|
||||
const voteValue = Number(ctx.request.body.voteValue)
|
||||
const voteValue = Number(ctx.request.body.voteValue);
|
||||
|
||||
const encoded = {
|
||||
message: encodeURIComponent(message)
|
||||
}
|
||||
};
|
||||
|
||||
const referer = new URL(ctx.request.header.referer)
|
||||
referer.hash = `centered-footer-${encoded.message}`
|
||||
const referer = new URL(ctx.request.header.referer);
|
||||
referer.hash = `centered-footer-${encoded.message}`;
|
||||
|
||||
const like = async ({ messageKey, voteValue }) => {
|
||||
const value = Number(voteValue)
|
||||
const message = await post.get(messageKey)
|
||||
const value = Number(voteValue);
|
||||
const message = await post.get(messageKey);
|
||||
|
||||
const isPrivate = message.value.meta.private === true
|
||||
const messageRecipients = isPrivate ? message.value.content.recps : []
|
||||
const isPrivate = message.value.meta.private === true;
|
||||
const messageRecipients = isPrivate ? message.value.content.recps : [];
|
||||
|
||||
const normalized = messageRecipients.map((recipient) => {
|
||||
if (typeof recipient === 'string') {
|
||||
return recipient
|
||||
const normalized = messageRecipients.map(recipient => {
|
||||
if (typeof recipient === "string") {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
if (typeof recipient.link === 'string') {
|
||||
return recipient.link
|
||||
if (typeof recipient.link === "string") {
|
||||
return recipient.link;
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
return null;
|
||||
});
|
||||
|
||||
const recipients = normalized.length > 0 ? normalized : undefined
|
||||
const recipients = normalized.length > 0 ? normalized : undefined;
|
||||
|
||||
return vote.publish({ messageKey, value, recps: recipients })
|
||||
}
|
||||
ctx.body = await like({ messageKey, voteValue })
|
||||
ctx.redirect(referer)
|
||||
})
|
||||
.post('/theme.css', koaBody(), async (ctx) => {
|
||||
const theme = String(ctx.request.body.theme)
|
||||
ctx.cookies.set('theme', theme)
|
||||
const referer = new URL(ctx.request.header.referer)
|
||||
ctx.redirect(referer)
|
||||
return vote.publish({ messageKey, value, recps: recipients });
|
||||
};
|
||||
ctx.body = await like({ messageKey, voteValue });
|
||||
ctx.redirect(referer);
|
||||
})
|
||||
.post("/theme.css", koaBody(), async ctx => {
|
||||
const theme = String(ctx.request.body.theme);
|
||||
ctx.cookies.set("theme", theme);
|
||||
const referer = new URL(ctx.request.header.referer);
|
||||
ctx.redirect(referer);
|
||||
});
|
||||
|
||||
const { host } = config
|
||||
const { port } = config
|
||||
const routes = router.routes()
|
||||
const { host } = config;
|
||||
const { port } = config;
|
||||
const routes = router.routes();
|
||||
|
||||
http({ host, port, routes })
|
||||
http({ host, port, routes });
|
||||
|
||||
const uri = `http://${host}:${port}/`
|
||||
const uri = `http://${host}:${port}/`;
|
||||
|
||||
const isDebugEnabled = debug.enabled
|
||||
debug.enabled = true
|
||||
debug(`Listening on ${uri}`)
|
||||
debug.enabled = isDebugEnabled
|
||||
const isDebugEnabled = debug.enabled;
|
||||
debug.enabled = true;
|
||||
debug(`Listening on ${uri}`);
|
||||
debug.enabled = isDebugEnabled;
|
||||
|
||||
if (config.open === true) {
|
||||
open(uri)
|
||||
open(uri);
|
||||
}
|
||||
|
|
1377
src/models/index.js
1377
src/models/index.js
File diff suppressed because it is too large
Load Diff
|
@ -1,38 +1,42 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const md = require('ssb-markdown')
|
||||
const ssbMessages = require('ssb-msgs')
|
||||
const ssbRef = require('ssb-ref')
|
||||
const md = require("ssb-markdown");
|
||||
const ssbMessages = require("ssb-msgs");
|
||||
const ssbRef = require("ssb-ref");
|
||||
|
||||
const toUrl = (mentions = []) => {
|
||||
const mentionNames = {}
|
||||
const mentionNames = {};
|
||||
|
||||
ssbMessages.links(mentions, 'feed').forEach((link) => {
|
||||
if (link.name && typeof link.name === 'string') {
|
||||
const name = (link.name.charAt(0) === '@') ? link.name : `@${link.name}`
|
||||
mentionNames[name] = link.link
|
||||
ssbMessages.links(mentions, "feed").forEach(link => {
|
||||
if (link.name && typeof link.name === "string") {
|
||||
const name = link.name.charAt(0) === "@" ? link.name : `@${link.name}`;
|
||||
mentionNames[name] = link.link;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (ref) => {
|
||||
return ref => {
|
||||
// @mentions
|
||||
if (ref in mentionNames) {
|
||||
return `/author/${encodeURIComponent(mentionNames[ref])}`
|
||||
return `/author/${encodeURIComponent(mentionNames[ref])}`;
|
||||
}
|
||||
|
||||
if (ssbRef.isFeedId(ref)) {
|
||||
return `/author/${encodeURIComponent(ref)}`
|
||||
} if (ssbRef.isMsgId(ref)) {
|
||||
return `/thread/${encodeURIComponent(ref)}`
|
||||
} if (ssbRef.isBlobId(ref)) {
|
||||
return `/blob/${encodeURIComponent(ref)}`
|
||||
} if (ref && ref[0] === '#') {
|
||||
return `/hashtag/${encodeURIComponent(ref.substr(1))}`
|
||||
return `/author/${encodeURIComponent(ref)}`;
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
if (ssbRef.isMsgId(ref)) {
|
||||
return `/thread/${encodeURIComponent(ref)}`;
|
||||
}
|
||||
if (ssbRef.isBlobId(ref)) {
|
||||
return `/blob/${encodeURIComponent(ref)}`;
|
||||
}
|
||||
if (ref && ref[0] === "#") {
|
||||
return `/hashtag/${encodeURIComponent(ref.substr(1))}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (input, mentions) => md.block(input, {
|
||||
toUrl: toUrl(mentions)
|
||||
})
|
||||
module.exports = (input, mentions) =>
|
||||
md.block(input, {
|
||||
toUrl: toUrl(mentions)
|
||||
});
|
||||
|
|
200
src/ssb.js
200
src/ssb.js
|
@ -1,97 +1,107 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
// This module exports a function that connects to SSB and returns a "cooler"
|
||||
// interface. This interface is poorly defined and should be replaced with
|
||||
// native support for Promises in the MuxRPC module and auto-generated manifest
|
||||
// files in the SSB-Client module.
|
||||
|
||||
const ssbClient = require('ssb-client')
|
||||
const ssbConfig = require('ssb-config')
|
||||
const flotilla = require('@fraction/flotilla')
|
||||
const debug = require('debug')('oasis')
|
||||
const ssbClient = require("ssb-client");
|
||||
const ssbConfig = require("ssb-config");
|
||||
const flotilla = require("@fraction/flotilla");
|
||||
const debug = require("debug")("oasis");
|
||||
|
||||
const server = flotilla(ssbConfig)
|
||||
const server = flotilla(ssbConfig);
|
||||
|
||||
const log = (...args) => {
|
||||
const isDebugEnabled = debug.enabled
|
||||
debug.enabled = true
|
||||
debug(...args)
|
||||
debug.enabled = isDebugEnabled
|
||||
}
|
||||
const isDebugEnabled = debug.enabled;
|
||||
debug.enabled = true;
|
||||
debug(...args);
|
||||
debug.enabled = isDebugEnabled;
|
||||
};
|
||||
|
||||
const rawConnect = () => new Promise((resolve, reject) => {
|
||||
ssbClient({
|
||||
manifest: {
|
||||
about: {
|
||||
socialValue: 'async',
|
||||
read: 'source'
|
||||
const rawConnect = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ssbClient(
|
||||
{
|
||||
manifest: {
|
||||
about: {
|
||||
socialValue: "async",
|
||||
read: "source"
|
||||
},
|
||||
backlinks: { read: "source" },
|
||||
blobs: {
|
||||
get: "source",
|
||||
ls: "source",
|
||||
want: "async"
|
||||
},
|
||||
conn: {
|
||||
peers: "source"
|
||||
},
|
||||
createUserStream: "source",
|
||||
createHistoryStream: "source",
|
||||
get: "sync",
|
||||
messagesByType: "source",
|
||||
publish: "async",
|
||||
status: "async",
|
||||
tangle: { branch: "async" },
|
||||
query: { read: "source" },
|
||||
friends: {
|
||||
isFollowing: "async",
|
||||
isBlocking: "async"
|
||||
},
|
||||
search: {
|
||||
query: "source"
|
||||
}
|
||||
}
|
||||
},
|
||||
backlinks: { read: 'source' },
|
||||
blobs: {
|
||||
get: 'source',
|
||||
ls: 'source',
|
||||
want: 'async'
|
||||
},
|
||||
conn: {
|
||||
peers: 'source'
|
||||
},
|
||||
createUserStream: 'source',
|
||||
createHistoryStream: 'source',
|
||||
get: 'sync',
|
||||
messagesByType: 'source',
|
||||
publish: 'async',
|
||||
status: 'async',
|
||||
tangle: { branch: 'async' },
|
||||
query: { read: 'source' },
|
||||
friends: {
|
||||
isFollowing: 'async',
|
||||
isBlocking: 'async'
|
||||
},
|
||||
search: {
|
||||
query: 'source'
|
||||
(err, api) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (err, api) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(api)
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
let handle
|
||||
let handle;
|
||||
|
||||
const createConnection = (config) => {
|
||||
handle = new Promise((resolve) => {
|
||||
rawConnect().then((ssb) => {
|
||||
log('Using pre-existing Scuttlebutt server instead of starting one')
|
||||
resolve(ssb)
|
||||
}).catch(() => {
|
||||
log('Initial connection attempt failed')
|
||||
log('Starting Scuttlebutt server')
|
||||
server(config)
|
||||
const connectOrRetry = () => {
|
||||
rawConnect().then((ssb) => {
|
||||
log('Retrying connection to own server')
|
||||
resolve(ssb)
|
||||
}).catch((e) => {
|
||||
log(e)
|
||||
connectOrRetry()
|
||||
})
|
||||
}
|
||||
const createConnection = config => {
|
||||
handle = new Promise(resolve => {
|
||||
rawConnect()
|
||||
.then(ssb => {
|
||||
log("Using pre-existing Scuttlebutt server instead of starting one");
|
||||
resolve(ssb);
|
||||
})
|
||||
.catch(() => {
|
||||
log("Initial connection attempt failed");
|
||||
log("Starting Scuttlebutt server");
|
||||
server(config);
|
||||
const connectOrRetry = () => {
|
||||
rawConnect()
|
||||
.then(ssb => {
|
||||
log("Retrying connection to own server");
|
||||
resolve(ssb);
|
||||
})
|
||||
.catch(e => {
|
||||
log(e);
|
||||
connectOrRetry();
|
||||
});
|
||||
};
|
||||
|
||||
connectOrRetry()
|
||||
})
|
||||
})
|
||||
connectOrRetry();
|
||||
});
|
||||
});
|
||||
|
||||
return handle
|
||||
}
|
||||
return handle;
|
||||
};
|
||||
|
||||
module.exports = ({ offline }) => {
|
||||
if (offline) {
|
||||
log('Offline mode activated - not connecting to scuttlebutt peers or pubs')
|
||||
log('WARNING: offline mode cannot control the behavior of pre-existing servers')
|
||||
log("Offline mode activated - not connecting to scuttlebutt peers or pubs");
|
||||
log(
|
||||
"WARNING: offline mode cannot control the behavior of pre-existing servers"
|
||||
);
|
||||
}
|
||||
|
||||
const config = {
|
||||
|
@ -101,11 +111,11 @@ module.exports = ({ offline }) => {
|
|||
ws: {
|
||||
http: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
createConnection(config)
|
||||
createConnection(config);
|
||||
return {
|
||||
connect () {
|
||||
connect() {
|
||||
// This has interesting behavior that may be unexpected.
|
||||
//
|
||||
// If `handle` is already an active [non-closed] connection, return that.
|
||||
|
@ -113,33 +123,33 @@ module.exports = ({ offline }) => {
|
|||
// If the connection is closed, we need to restart it. It's important to
|
||||
// note that if we're depending on an external service (like Patchwork) and
|
||||
// that app is closed, then Oasis will seamlessly start its own SSB service.
|
||||
return new Promise((resolve, reject) => {
|
||||
handle.then((ssb) => {
|
||||
return new Promise(resolve => {
|
||||
handle.then(ssb => {
|
||||
if (ssb.closed) {
|
||||
createConnection()
|
||||
createConnection();
|
||||
}
|
||||
resolve(handle)
|
||||
})
|
||||
})
|
||||
resolve(handle);
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {function} method
|
||||
*/
|
||||
get (method, ...opts) {
|
||||
get(method, ...opts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
method(...opts, (err, val) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(val)
|
||||
resolve(val);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
},
|
||||
read (method, ...args) {
|
||||
return new Promise((resolve) => {
|
||||
resolve(method(...args))
|
||||
})
|
||||
read(method, ...args) {
|
||||
return new Promise(resolve => {
|
||||
resolve(method(...args));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const debug = require('debug')('oasis')
|
||||
const ssbMarkdown = require('ssb-markdown')
|
||||
const highlightJs = require('highlight.js')
|
||||
const debug = require("debug")("oasis");
|
||||
const ssbMarkdown = require("ssb-markdown");
|
||||
const highlightJs = require("highlight.js");
|
||||
|
||||
const {
|
||||
a,
|
||||
|
@ -30,10 +30,10 @@ const {
|
|||
strong,
|
||||
textarea,
|
||||
ul
|
||||
} = require('hyperaxe')
|
||||
} = require("hyperaxe");
|
||||
|
||||
const template = require('./template')
|
||||
const post = require('./post')
|
||||
const template = require("./template");
|
||||
const post = require("./post");
|
||||
|
||||
exports.authorView = ({
|
||||
avatarUrl,
|
||||
|
@ -43,273 +43,308 @@ exports.authorView = ({
|
|||
name,
|
||||
relationship
|
||||
}) => {
|
||||
const mention = `[@${name}](${feedId})`
|
||||
const markdownMention = highlightJs.highlight('markdown', mention).value
|
||||
const mention = `[@${name}](${feedId})`;
|
||||
const markdownMention = highlightJs.highlight("markdown", mention).value;
|
||||
|
||||
const areFollowing = relationship === 'you are following'
|
||||
const areFollowing = relationship === "you are following";
|
||||
|
||||
const contactFormType = areFollowing
|
||||
? 'unfollow'
|
||||
: 'follow'
|
||||
const contactFormType = areFollowing ? "unfollow" : "follow";
|
||||
|
||||
// We're on our own profile!
|
||||
const contactForm = relationship !== null
|
||||
? form({ action: `/${contactFormType}/${encodeURIComponent(feedId)}`, method: 'post' },
|
||||
button({
|
||||
type: 'submit'
|
||||
},
|
||||
contactFormType))
|
||||
: null
|
||||
const contactForm =
|
||||
relationship !== null
|
||||
? form(
|
||||
{
|
||||
action: `/${contactFormType}/${encodeURIComponent(feedId)}`,
|
||||
method: "post"
|
||||
},
|
||||
button(
|
||||
{
|
||||
type: "submit"
|
||||
},
|
||||
contactFormType
|
||||
)
|
||||
)
|
||||
: null;
|
||||
|
||||
const prefix = section({ class: 'message' },
|
||||
header({ class: 'profile' },
|
||||
img({ class: 'avatar', src: avatarUrl }),
|
||||
h1(name)),
|
||||
const prefix = section(
|
||||
{ class: "message" },
|
||||
header(
|
||||
{ class: "profile" },
|
||||
img({ class: "avatar", src: avatarUrl }),
|
||||
h1(name)
|
||||
),
|
||||
pre({
|
||||
class: 'md-mention',
|
||||
class: "md-mention",
|
||||
innerHTML: markdownMention
|
||||
}),
|
||||
description !== '<p>null</p>\n'
|
||||
description !== "<p>null</p>\n"
|
||||
? article({ innerHTML: description })
|
||||
: null,
|
||||
footer(
|
||||
a({ href: `/likes/${encodeURIComponent(feedId)}` }, 'view likes'),
|
||||
a({ href: `/likes/${encodeURIComponent(feedId)}` }, "view likes"),
|
||||
span(relationship),
|
||||
contactForm
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
prefix,
|
||||
messages.map((msg) => post({ msg }))
|
||||
)
|
||||
}
|
||||
messages.map(msg => post({ msg }))
|
||||
);
|
||||
};
|
||||
|
||||
exports.commentView = async ({ messages, myFeedId, parentMessage }) => {
|
||||
let markdownMention
|
||||
let markdownMention;
|
||||
|
||||
const messageElements = await Promise.all(
|
||||
messages.reverse().map((message) => {
|
||||
debug('%O', message)
|
||||
const authorName = message.value.meta.author.name
|
||||
const authorFeedId = message.value.author
|
||||
messages.reverse().map(message => {
|
||||
debug("%O", message);
|
||||
const authorName = message.value.meta.author.name;
|
||||
const authorFeedId = message.value.author;
|
||||
if (authorFeedId !== myFeedId) {
|
||||
if (message.key === parentMessage.key) {
|
||||
const x = `[@${authorName}](${authorFeedId})\n\n`
|
||||
markdownMention = x
|
||||
const x = `[@${authorName}](${authorFeedId})\n\n`;
|
||||
markdownMention = x;
|
||||
}
|
||||
}
|
||||
return post({ msg: message })
|
||||
return post({ msg: message });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const action = `/comment/${encodeURIComponent(messages[0].key)}`
|
||||
const method = 'post'
|
||||
const action = `/comment/${encodeURIComponent(messages[0].key)}`;
|
||||
const method = "post";
|
||||
|
||||
const isPrivate = parentMessage.value.meta.private
|
||||
const isPrivate = parentMessage.value.meta.private;
|
||||
|
||||
const publicOrPrivate = isPrivate ? 'private' : 'public'
|
||||
const maybeReplyText = isPrivate ? null : [
|
||||
' Messages cannot be edited or deleted. To respond to an individual message, select ',
|
||||
strong('reply'),
|
||||
' instead.'
|
||||
]
|
||||
const publicOrPrivate = isPrivate ? "private" : "public";
|
||||
const maybeReplyText = isPrivate
|
||||
? null
|
||||
: [
|
||||
" Messages cannot be edited or deleted. To respond to an individual message, select ",
|
||||
strong("reply"),
|
||||
" instead."
|
||||
];
|
||||
|
||||
return template(
|
||||
messageElements,
|
||||
p('Write a ',
|
||||
p(
|
||||
"Write a ",
|
||||
strong(`${publicOrPrivate} comment`),
|
||||
' on this thread with ',
|
||||
a({ href: 'https://commonmark.org/help/' }, 'Markdown'),
|
||||
'.',
|
||||
" on this thread with ",
|
||||
a({ href: "https://commonmark.org/help/" }, "Markdown"),
|
||||
".",
|
||||
maybeReplyText
|
||||
),
|
||||
form({ action, method },
|
||||
textarea({
|
||||
autofocus: true,
|
||||
required: true,
|
||||
name: 'text'
|
||||
}, markdownMention),
|
||||
button({
|
||||
type: 'submit'
|
||||
}, 'comment'))
|
||||
)
|
||||
}
|
||||
|
||||
exports.listView = ({ messages }) => template(
|
||||
messages.map((msg) => post({ msg }))
|
||||
)
|
||||
|
||||
exports.markdownView = ({ text }) => {
|
||||
const rawHtml = ssbMarkdown.block(text)
|
||||
|
||||
return template(
|
||||
section({ class: 'message' }, { innerHTML: rawHtml })
|
||||
)
|
||||
}
|
||||
|
||||
exports.metaView = ({ status, peers, theme, themeNames }) => {
|
||||
const max = status.sync.since
|
||||
|
||||
const progressElements = Object.entries(status.sync.plugins).map((e) => {
|
||||
const [key, val] = e
|
||||
const id = `progress-${key}`
|
||||
return div(
|
||||
label({ for: id }, key),
|
||||
progress({ id, value: val, max }, val)
|
||||
)
|
||||
})
|
||||
|
||||
const peerList = (peers || [])
|
||||
.map(([, data]) =>
|
||||
li(
|
||||
a(
|
||||
{ href: `/author/${encodeURIComponent(data.key)}` },
|
||||
code(data.key)
|
||||
)
|
||||
form(
|
||||
{ action, method },
|
||||
textarea(
|
||||
{
|
||||
autofocus: true,
|
||||
required: true,
|
||||
name: "text"
|
||||
},
|
||||
markdownMention
|
||||
),
|
||||
button(
|
||||
{
|
||||
type: "submit"
|
||||
},
|
||||
"comment"
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const themeElements = themeNames.map((cur) => {
|
||||
const isCurrentTheme = cur === theme
|
||||
exports.listView = ({ messages }) =>
|
||||
template(messages.map(msg => post({ msg })));
|
||||
|
||||
exports.markdownView = ({ text }) => {
|
||||
const rawHtml = ssbMarkdown.block(text);
|
||||
|
||||
return template(section({ class: "message" }, { innerHTML: rawHtml }));
|
||||
};
|
||||
|
||||
exports.metaView = ({ status, peers, theme, themeNames }) => {
|
||||
const max = status.sync.since;
|
||||
|
||||
const progressElements = Object.entries(status.sync.plugins).map(e => {
|
||||
const [key, val] = e;
|
||||
const id = `progress-${key}`;
|
||||
return div(label({ for: id }, key), progress({ id, value: val, max }, val));
|
||||
});
|
||||
|
||||
const peerList = (peers || []).map(([, data]) =>
|
||||
li(a({ href: `/author/${encodeURIComponent(data.key)}` }, code(data.key)))
|
||||
);
|
||||
|
||||
const themeElements = themeNames.map(cur => {
|
||||
const isCurrentTheme = cur === theme;
|
||||
if (isCurrentTheme) {
|
||||
return option({ value: cur, selected: true }, cur)
|
||||
return option({ value: cur, selected: true }, cur);
|
||||
} else {
|
||||
return option({ value: cur }, cur)
|
||||
return option({ value: cur }, cur);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const base16 = [
|
||||
// '00', removed because this is the background
|
||||
'01',
|
||||
'02',
|
||||
'03',
|
||||
'04',
|
||||
'05',
|
||||
'06',
|
||||
'07',
|
||||
'08',
|
||||
'09',
|
||||
'0A',
|
||||
'0B',
|
||||
'0C',
|
||||
'0D',
|
||||
'0E',
|
||||
'0F'
|
||||
]
|
||||
"01",
|
||||
"02",
|
||||
"03",
|
||||
"04",
|
||||
"05",
|
||||
"06",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"0A",
|
||||
"0B",
|
||||
"0C",
|
||||
"0D",
|
||||
"0E",
|
||||
"0F"
|
||||
];
|
||||
|
||||
const base16Elements = base16.map((base) =>
|
||||
const base16Elements = base16.map(base =>
|
||||
div({
|
||||
style: {
|
||||
'background-color': `var(--base${base})`,
|
||||
width: `${1 / base16.length * 100}%`,
|
||||
height: '1em',
|
||||
'margin-top': '1em',
|
||||
display: 'inline-block'
|
||||
"background-color": `var(--base${base})`,
|
||||
width: `${(1 / base16.length) * 100}%`,
|
||||
height: "1em",
|
||||
"margin-top": "1em",
|
||||
display: "inline-block"
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
section({ class: 'message' },
|
||||
h1('Meta'),
|
||||
section(
|
||||
{ class: "message" },
|
||||
h1("Meta"),
|
||||
p(
|
||||
'Check out ',
|
||||
a({ href: '/meta/readme' }, 'the readme'),
|
||||
', configure your theme, or view debugging information below.'
|
||||
"Check out ",
|
||||
a({ href: "/meta/readme" }, "the readme"),
|
||||
", configure your theme, or view debugging information below."
|
||||
),
|
||||
h2("Theme"),
|
||||
p(
|
||||
"Choose from any theme you'd like. The default theme is Atelier-SulphurPool-Light."
|
||||
),
|
||||
form(
|
||||
{ action: "/theme.css", method: "post" },
|
||||
select({ name: "theme" }, ...themeElements),
|
||||
button({ type: "submit" }, "set theme")
|
||||
),
|
||||
h2('Theme'),
|
||||
p('Choose from any theme you\'d like. The default theme is Atelier-SulphurPool-Light.'),
|
||||
form({ action: '/theme.css', method: 'post' },
|
||||
select({ name: 'theme' }, ...themeElements),
|
||||
button({ type: 'submit' }, 'set theme')),
|
||||
base16Elements,
|
||||
h2('Status'),
|
||||
h3('Indexes'),
|
||||
h2("Status"),
|
||||
h3("Indexes"),
|
||||
progressElements,
|
||||
peerList.length > 0
|
||||
? [h3('Peers'), ul(peerList)]
|
||||
: null
|
||||
peerList.length > 0 ? [h3("Peers"), ul(peerList)] : null
|
||||
)
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
exports.publicView = ({ messages, prefix = null }) => {
|
||||
const publishForm = '/publish/'
|
||||
const publishForm = "/publish/";
|
||||
|
||||
return template(
|
||||
prefix,
|
||||
section(
|
||||
header(strong('🌐 Publish')),
|
||||
form({ action: publishForm, method: 'post' },
|
||||
header(strong("🌐 Publish")),
|
||||
form(
|
||||
{ action: publishForm, method: "post" },
|
||||
label(
|
||||
{ for: 'text' },
|
||||
'Write a new message in ',
|
||||
a({
|
||||
href: 'https://commonmark.org/help/',
|
||||
target: '_blank'
|
||||
}, 'Markdown'),
|
||||
'. Messages cannot be edited or deleted.'
|
||||
{ for: "text" },
|
||||
"Write a new message in ",
|
||||
a(
|
||||
{
|
||||
href: "https://commonmark.org/help/",
|
||||
target: "_blank"
|
||||
},
|
||||
"Markdown"
|
||||
),
|
||||
". Messages cannot be edited or deleted."
|
||||
),
|
||||
textarea({ required: true, name: 'text' }),
|
||||
button({ type: 'submit' }, 'submit')
|
||||
textarea({ required: true, name: "text" }),
|
||||
button({ type: "submit" }, "submit")
|
||||
)
|
||||
),
|
||||
messages.map((msg) => post({ msg }))
|
||||
)
|
||||
}
|
||||
messages.map(msg => post({ msg }))
|
||||
);
|
||||
};
|
||||
|
||||
exports.replyView = async ({ messages, myFeedId }) => {
|
||||
const replyForm = `/reply/${encodeURIComponent(messages[messages.length - 1].key)}`
|
||||
const replyForm = `/reply/${encodeURIComponent(
|
||||
messages[messages.length - 1].key
|
||||
)}`;
|
||||
|
||||
let markdownMention
|
||||
let markdownMention;
|
||||
|
||||
const messageElements = await Promise.all(
|
||||
messages.reverse().map((message) => {
|
||||
debug('%O', message)
|
||||
const authorName = message.value.meta.author.name
|
||||
const authorFeedId = message.value.author
|
||||
messages.reverse().map(message => {
|
||||
debug("%O", message);
|
||||
const authorName = message.value.meta.author.name;
|
||||
const authorFeedId = message.value.author;
|
||||
if (authorFeedId !== myFeedId) {
|
||||
if (message.key === messages[0].key) {
|
||||
const x = `[@${authorName}](${authorFeedId})\n\n`
|
||||
markdownMention = x
|
||||
const x = `[@${authorName}](${authorFeedId})\n\n`;
|
||||
markdownMention = x;
|
||||
}
|
||||
}
|
||||
return post({ msg: message })
|
||||
return post({ msg: message });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
messageElements,
|
||||
p('Write a ',
|
||||
strong('public reply'),
|
||||
' to this message with ',
|
||||
a({ href: 'https://commonmark.org/help/' }, 'Markdown'),
|
||||
'. Messages cannot be edited or deleted. To respond to an entire thread, select ',
|
||||
strong('comment'),
|
||||
' instead.'
|
||||
p(
|
||||
"Write a ",
|
||||
strong("public reply"),
|
||||
" to this message with ",
|
||||
a({ href: "https://commonmark.org/help/" }, "Markdown"),
|
||||
". Messages cannot be edited or deleted. To respond to an entire thread, select ",
|
||||
strong("comment"),
|
||||
" instead."
|
||||
),
|
||||
form({ action: replyForm, method: 'post' },
|
||||
textarea({
|
||||
autofocus: true,
|
||||
required: true,
|
||||
name: 'text'
|
||||
}, markdownMention),
|
||||
button({
|
||||
type: 'submit'
|
||||
}, 'reply'))
|
||||
)
|
||||
}
|
||||
form(
|
||||
{ action: replyForm, method: "post" },
|
||||
textarea(
|
||||
{
|
||||
autofocus: true,
|
||||
required: true,
|
||||
name: "text"
|
||||
},
|
||||
markdownMention
|
||||
),
|
||||
button(
|
||||
{
|
||||
type: "submit"
|
||||
},
|
||||
"reply"
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.searchView = ({ messages, query }) => template(
|
||||
section(
|
||||
form({ action: '/search', method: 'get' },
|
||||
header(strong('Search')),
|
||||
label({ for: 'query' }, 'Add word(s) to look for in downloaded messages.'),
|
||||
input({ required: true, type: 'search', name: 'query', value: query }),
|
||||
button({
|
||||
type: 'submit'
|
||||
}, 'submit'))
|
||||
),
|
||||
messages.map((msg) => post({ msg }))
|
||||
)
|
||||
exports.searchView = ({ messages, query }) =>
|
||||
template(
|
||||
section(
|
||||
form(
|
||||
{ action: "/search", method: "get" },
|
||||
header(strong("Search")),
|
||||
label(
|
||||
{ for: "query" },
|
||||
"Add word(s) to look for in downloaded messages."
|
||||
),
|
||||
input({ required: true, type: "search", name: "query", value: query }),
|
||||
button(
|
||||
{
|
||||
type: "submit"
|
||||
},
|
||||
"submit"
|
||||
)
|
||||
)
|
||||
),
|
||||
messages.map(msg => post({ msg }))
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
a,
|
||||
|
@ -14,17 +14,17 @@ const {
|
|||
span,
|
||||
summary,
|
||||
pre
|
||||
} = require('hyperaxe')
|
||||
} = require("hyperaxe");
|
||||
|
||||
const highlightJs = require('highlight.js')
|
||||
const lodash = require('lodash')
|
||||
const highlightJs = require("highlight.js");
|
||||
const lodash = require("lodash");
|
||||
|
||||
module.exports = ({ msg }) => {
|
||||
const encoded = {
|
||||
key: encodeURIComponent(msg.key),
|
||||
author: encodeURIComponent(msg.value.author),
|
||||
parent: encodeURIComponent(msg.value.content.root)
|
||||
}
|
||||
};
|
||||
|
||||
const url = {
|
||||
author: `/author/${encoded.author}`,
|
||||
|
@ -35,101 +35,94 @@ module.exports = ({ msg }) => {
|
|||
json: `/json/${encoded.key}`,
|
||||
reply: `/reply/${encoded.key}`,
|
||||
comment: `/comment/${encoded.key}`
|
||||
}
|
||||
};
|
||||
|
||||
const isPrivate = Boolean(msg.value.meta.private)
|
||||
const isRoot = msg.value.content.root == null
|
||||
const isThreadTarget = Boolean(lodash.get(
|
||||
msg,
|
||||
'value.meta.thread.target',
|
||||
false
|
||||
))
|
||||
const isPrivate = Boolean(msg.value.meta.private);
|
||||
const isRoot = msg.value.content.root == null;
|
||||
const isThreadTarget = Boolean(
|
||||
lodash.get(msg, "value.meta.thread.target", false)
|
||||
);
|
||||
|
||||
// TODO: I think this is actually true for both replies and comments.
|
||||
const isReply = Boolean(lodash.get(
|
||||
msg,
|
||||
'value.meta.thread.reply',
|
||||
false
|
||||
))
|
||||
const isReply = Boolean(lodash.get(msg, "value.meta.thread.reply", false));
|
||||
|
||||
const { name } = msg.value.meta.author
|
||||
const timeAgo = msg.value.meta.timestamp.received.since.replace('~', '')
|
||||
const { name } = msg.value.meta.author;
|
||||
const timeAgo = msg.value.meta.timestamp.received.since.replace("~", "");
|
||||
|
||||
const depth = lodash.get(msg, 'value.meta.thread.depth', 0)
|
||||
const depth = lodash.get(msg, "value.meta.thread.depth", 0);
|
||||
|
||||
const markdownContent = msg.value.meta.md.block()
|
||||
const markdownContent = msg.value.meta.md.block();
|
||||
|
||||
const hasContentWarning = typeof msg.value.content.contentWarning === 'string'
|
||||
const hasContentWarning =
|
||||
typeof msg.value.content.contentWarning === "string";
|
||||
|
||||
const likeButton = msg.value.meta.voted
|
||||
? { value: 0, class: 'liked' }
|
||||
: { value: 1, class: null }
|
||||
? { value: 0, class: "liked" }
|
||||
: { value: 1, class: null };
|
||||
|
||||
const likeCount = msg.value.meta.votes.length
|
||||
const likeCount = msg.value.meta.votes.length;
|
||||
|
||||
const messageClasses = []
|
||||
const messageClasses = [];
|
||||
|
||||
if (isPrivate) {
|
||||
messageClasses.push('private')
|
||||
messageClasses.push("private");
|
||||
}
|
||||
|
||||
if (isThreadTarget) {
|
||||
messageClasses.push('thread-target')
|
||||
messageClasses.push("thread-target");
|
||||
}
|
||||
|
||||
if (isReply) {
|
||||
// True for comments too, I think
|
||||
messageClasses.push('reply')
|
||||
messageClasses.push("reply");
|
||||
}
|
||||
|
||||
const isFork = msg.value.meta.postType === 'reply'
|
||||
const isFork = msg.value.meta.postType === "reply";
|
||||
|
||||
const postOptions = {
|
||||
post: null,
|
||||
comment: [
|
||||
'commented on ',
|
||||
a({ href: url.parent }, ' thread')
|
||||
],
|
||||
reply: [
|
||||
'replied to ',
|
||||
a({ href: url.parent }, ' message')
|
||||
],
|
||||
mystery: 'posted a mysterious message'
|
||||
}
|
||||
comment: ["commented on ", a({ href: url.parent }, " thread")],
|
||||
reply: ["replied to ", a({ href: url.parent }, " message")],
|
||||
mystery: "posted a mysterious message"
|
||||
};
|
||||
|
||||
const emptyContent = '<p>undefined</p>\n'
|
||||
const articleElement = markdownContent === emptyContent
|
||||
? article({ class: 'content' }, pre({
|
||||
innerHTML: highlightJs.highlight(
|
||||
'json',
|
||||
JSON.stringify(msg, null, 2)
|
||||
).value
|
||||
}))
|
||||
: article({ class: 'content', innerHTML: markdownContent })
|
||||
const emptyContent = "<p>undefined</p>\n";
|
||||
const articleElement =
|
||||
markdownContent === emptyContent
|
||||
? article(
|
||||
{ class: "content" },
|
||||
pre({
|
||||
innerHTML: highlightJs.highlight(
|
||||
"json",
|
||||
JSON.stringify(msg, null, 2)
|
||||
).value
|
||||
})
|
||||
)
|
||||
: article({ class: "content", innerHTML: markdownContent });
|
||||
|
||||
const articleContent = hasContentWarning
|
||||
? details(
|
||||
summary(msg.value.content.contentWarning),
|
||||
articleElement
|
||||
)
|
||||
: articleElement
|
||||
? details(summary(msg.value.content.contentWarning), articleElement)
|
||||
: articleElement;
|
||||
|
||||
const fragment =
|
||||
section({
|
||||
const fragment = section(
|
||||
{
|
||||
id: msg.key,
|
||||
class: messageClasses.join(' '),
|
||||
class: messageClasses.join(" "),
|
||||
style: `margin-left: ${depth}rem;`
|
||||
},
|
||||
header(
|
||||
span({ class: 'author' },
|
||||
a({ href: url.author },
|
||||
img({ class: 'avatar', src: url.avatar, alt: '' }),
|
||||
span(
|
||||
{ class: "author" },
|
||||
a(
|
||||
{ href: url.author },
|
||||
img({ class: "avatar", src: url.avatar, alt: "" }),
|
||||
name
|
||||
),
|
||||
postOptions[msg.value.meta.postType]
|
||||
),
|
||||
span({ class: 'time' },
|
||||
isPrivate ? '🔒' : null,
|
||||
span(
|
||||
{ class: "time" },
|
||||
isPrivate ? "🔒" : null,
|
||||
a({ href: url.link }, timeAgo)
|
||||
)
|
||||
),
|
||||
|
@ -144,21 +137,26 @@ module.exports = ({ msg }) => {
|
|||
// This is used for redirecting users after they like a post, when we
|
||||
// want the like button that they just clicked to remain close-ish to
|
||||
// where it was before they clicked the button.
|
||||
div({ id: `centered-footer-${encoded.key}`, class: 'centered-footer' }),
|
||||
div({ id: `centered-footer-${encoded.key}`, class: "centered-footer" }),
|
||||
|
||||
footer(
|
||||
form({ action: url.likeForm, method: 'post' },
|
||||
button({
|
||||
name: 'voteValue',
|
||||
type: 'submit',
|
||||
value: likeButton.value,
|
||||
class: likeButton.class
|
||||
},
|
||||
`❤ ${likeCount}`)),
|
||||
a({ href: url.comment }, 'comment'),
|
||||
(isPrivate || isRoot || isFork) ? null : a({ href: url.reply }, 'reply'),
|
||||
a({ href: url.json }, 'json')
|
||||
))
|
||||
form(
|
||||
{ action: url.likeForm, method: "post" },
|
||||
button(
|
||||
{
|
||||
name: "voteValue",
|
||||
type: "submit",
|
||||
value: likeButton.value,
|
||||
class: likeButton.class
|
||||
},
|
||||
`❤ ${likeCount}`
|
||||
)
|
||||
),
|
||||
a({ href: url.comment }, "comment"),
|
||||
isPrivate || isRoot || isFork ? null : a({ href: url.reply }, "reply"),
|
||||
a({ href: url.json }, "json")
|
||||
)
|
||||
);
|
||||
|
||||
return fragment
|
||||
}
|
||||
return fragment;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
a,
|
||||
|
@ -12,47 +12,50 @@ const {
|
|||
nav,
|
||||
title,
|
||||
ul
|
||||
} = require('hyperaxe')
|
||||
} = require("hyperaxe");
|
||||
|
||||
const doctypeString = '<!DOCTYPE html>'
|
||||
const doctypeString = "<!DOCTYPE html>";
|
||||
|
||||
const toAttributes = (obj) =>
|
||||
Object.entries(obj).map(([key, val]) => `${key}=${val}`).join(', ')
|
||||
const toAttributes = obj =>
|
||||
Object.entries(obj)
|
||||
.map(([key, val]) => `${key}=${val}`)
|
||||
.join(", ");
|
||||
|
||||
module.exports = (...elements) => {
|
||||
const nodes =
|
||||
html({ lang: 'en' },
|
||||
head(
|
||||
title('🏝️ Oasis'),
|
||||
link({ rel: 'stylesheet', href: '/theme.css' }),
|
||||
link({ rel: 'stylesheet', href: '/assets/style.css' }),
|
||||
link({ rel: 'stylesheet', href: '/assets/highlight.css' }),
|
||||
meta({ charset: 'utf-8' }),
|
||||
meta({
|
||||
name: 'description',
|
||||
content: 'friendly neighborhood scuttlebutt interface'
|
||||
}),
|
||||
meta({
|
||||
name: 'viewport',
|
||||
content: toAttributes({ width: 'device-width', 'initial-scale': 1 })
|
||||
})
|
||||
const nodes = html(
|
||||
{ lang: "en" },
|
||||
head(
|
||||
title("🏝️ Oasis"),
|
||||
link({ rel: "stylesheet", href: "/theme.css" }),
|
||||
link({ rel: "stylesheet", href: "/assets/style.css" }),
|
||||
link({ rel: "stylesheet", href: "/assets/highlight.css" }),
|
||||
meta({ charset: "utf-8" }),
|
||||
meta({
|
||||
name: "description",
|
||||
content: "friendly neighborhood scuttlebutt interface"
|
||||
}),
|
||||
meta({
|
||||
name: "viewport",
|
||||
content: toAttributes({ width: "device-width", "initial-scale": 1 })
|
||||
})
|
||||
),
|
||||
body(
|
||||
nav(
|
||||
ul(
|
||||
li(a({ href: "/" }, "popular")),
|
||||
li(a({ href: "/public/latest" }, "latest")),
|
||||
li(a({ href: "/inbox" }, "inbox")),
|
||||
li(a({ href: "/mentions" }, "mentions")),
|
||||
li(a({ href: "/profile" }, "profile")),
|
||||
li(a({ href: "/search" }, "search")),
|
||||
li(a({ href: "/meta" }, "meta"))
|
||||
)
|
||||
),
|
||||
body(
|
||||
nav(
|
||||
ul(
|
||||
li(a({ href: '/' }, 'popular')),
|
||||
li(a({ href: '/public/latest' }, 'latest')),
|
||||
li(a({ href: '/inbox' }, 'inbox')),
|
||||
li(a({ href: '/mentions' }, 'mentions')),
|
||||
li(a({ href: '/profile' }, 'profile')),
|
||||
li(a({ href: '/search' }, 'search')),
|
||||
li(a({ href: '/meta' }, 'meta'))
|
||||
)
|
||||
),
|
||||
main({ id: 'content' }, elements)
|
||||
))
|
||||
main({ id: "content" }, elements)
|
||||
)
|
||||
);
|
||||
|
||||
const result = doctypeString + nodes.outerHTML
|
||||
const result = doctypeString + nodes.outerHTML;
|
||||
|
||||
return result
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue