Experiment with common-good module

This commit is contained in:
Christian Bundy 2020-01-21 16:22:19 -08:00
parent cb4a6ef971
commit b34b04c2c2
22 changed files with 2133 additions and 3641 deletions

16
.eslintrc.js Normal file
View File

@ -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: {}
};

View File

@ -1,4 +1,3 @@
**What's the problem you want to solved?**
**Is there a solution you'd like to recommend?**

View File

@ -1,4 +1,3 @@
**What's the problem you solved?**
**What solution are you recommending?**

View File

@ -1,3 +1,3 @@
{
"extends": "stylelint-config-standard"
"extends": "stylelint-config-recommended"
}

View File

@ -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");

View File

@ -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

View File

@ -32,26 +32,26 @@ 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
**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

View File

@ -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).

2570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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,

View File

@ -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;

View File

@ -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 });
};

View File

@ -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,60 +204,59 @@ 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: {
@ -277,255 +264,259 @@ router
height: imageSize,
channels: 4,
background: {
r: 0, g: 0, b: 0, alpha: 0.5
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;
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 recipients = normalized.length > 0 ? normalized : undefined
const { host } = config;
const { port } = config;
const routes = router.routes();
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)
})
http({ host, port, routes });
const { host } = config
const { port } = config
const routes = router.routes()
const uri = `http://${host}:${port}/`;
http({ host, port, routes })
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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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, {
module.exports = (input, mentions) =>
md.block(input, {
toUrl: toUrl(mentions)
})
});

View File

@ -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({
const rawConnect = () =>
new Promise((resolve, reject) => {
ssbClient(
{
manifest: {
about: {
socialValue: 'async',
read: 'source'
socialValue: "async",
read: "source"
},
backlinks: { read: 'source' },
backlinks: { read: "source" },
blobs: {
get: 'source',
ls: 'source',
want: 'async'
get: "source",
ls: "source",
want: "async"
},
conn: {
peers: 'source'
peers: "source"
},
createUserStream: 'source',
createHistoryStream: 'source',
get: 'sync',
messagesByType: 'source',
publish: 'async',
status: 'async',
tangle: { branch: 'async' },
query: { read: 'source' },
createUserStream: "source",
createHistoryStream: "source",
get: "sync",
messagesByType: "source",
publish: "async",
status: "async",
tangle: { branch: "async" },
query: { read: "source" },
friends: {
isFollowing: 'async',
isBlocking: 'async'
isFollowing: "async",
isBlocking: "async"
},
search: {
query: 'source'
query: "source"
}
}
}, (err, api) => {
},
(err, api) => {
if (err) {
reject(err)
reject(err);
} else {
resolve(api)
resolve(api);
}
}
);
});
let handle;
const createConnection = config => {
handle = new Promise(resolve => {
rawConnect()
.then(ssb => {
log("Using pre-existing Scuttlebutt server instead of starting one");
resolve(ssb);
})
})
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)
.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()
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));
});
}
}
}
};
};

View File

@ -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'
const contactForm =
relationship !== null
? form(
{
action: `/${contactFormType}/${encodeURIComponent(feedId)}`,
method: "post"
},
contactFormType))
: null
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({
form(
{ action, method },
textarea(
{
autofocus: true,
required: true,
name: 'text'
}, markdownMention),
button({
type: 'submit'
}, 'comment'))
name: "text"
},
markdownMention
),
button(
{
type: "submit"
},
"comment"
)
}
)
);
};
exports.listView = ({ messages }) => template(
messages.map((msg) => post({ msg }))
)
exports.listView = ({ messages }) =>
template(messages.map(msg => post({ msg })));
exports.markdownView = ({ text }) => {
const rawHtml = ssbMarkdown.block(text)
const rawHtml = ssbMarkdown.block(text);
return template(
section({ class: 'message' }, { innerHTML: rawHtml })
)
}
return template(section({ class: "message" }, { innerHTML: rawHtml }));
};
exports.metaView = ({ status, peers, theme, themeNames }) => {
const max = status.sync.since
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 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 peerList = (peers || []).map(([, data]) =>
li(a({ href: `/author/${encodeURIComponent(data.key)}` }, code(data.key)))
);
const themeElements = themeNames.map((cur) => {
const isCurrentTheme = cur === theme
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"
),
textarea({ required: true, name: 'text' }),
button({ type: 'submit' }, 'submit')
". Messages cannot be edited or deleted."
),
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({
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'))
name: "text"
},
markdownMention
),
messages.map((msg) => post({ msg }))
)
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 }))
);

View File

@ -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({
const emptyContent = "<p>undefined</p>\n";
const articleElement =
markdownContent === emptyContent
? article(
{ class: "content" },
pre({
innerHTML: highlightJs.highlight(
'json',
"json",
JSON.stringify(msg, null, 2)
).value
}))
: article({ class: 'content', innerHTML: markdownContent })
})
)
: 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',
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')
))
`${likeCount}`
)
),
a({ href: url.comment }, "comment"),
isPrivate || isRoot || isFork ? null : a({ href: url.reply }, "reply"),
a({ href: url.json }, "json")
)
);
return fragment
}
return fragment;
};

View File

@ -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' },
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' }),
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'
name: "description",
content: "friendly neighborhood scuttlebutt interface"
}),
meta({
name: 'viewport',
content: toAttributes({ width: 'device-width', 'initial-scale': 1 })
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'))
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;
};