Refactor dependency graph to create layers
I was playing around with Madge and noticed that the previous dependency graph looked almost *exactly* like a bowl of spaghetti I had last week. After a few hours on Wikipedia I got interested in refactoring the code so that each `require()` imported a module from one level deeper into the tree. I don't know if this is actually useful, but it's better than spaghetti. In the future I think I should probably refactor the database convenience functions out of the "models" since they really aren't models but it's the best name I could come up with for what they are and how they're used. This will probably go through some more evolution when I rip out EJS and replace it with something much smaller.
This commit is contained in:
parent
07e56b8879
commit
400477d5c1
53
index.js
53
index.js
|
@ -1,54 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const Koa = require('koa')
|
||||
const path = require('path')
|
||||
const router = require('koa-router')()
|
||||
const views = require('koa-views')
|
||||
const koaStatic = require('koa-static')
|
||||
const mount = require('koa-mount')
|
||||
const open = require('open')
|
||||
const koaBody = require('koa-body')
|
||||
|
||||
const author = require('./routes/author')
|
||||
const hashtag = require('./routes/hashtag')
|
||||
const home = require('./routes/home')
|
||||
const profile = require('./routes/profile')
|
||||
const raw = require('./routes/raw')
|
||||
const thread = require('./routes/thread')
|
||||
const like = require('./routes/like')
|
||||
|
||||
const assets = new Koa()
|
||||
assets.use(koaStatic(path.join(__dirname, 'public')))
|
||||
|
||||
const hljs = new Koa()
|
||||
hljs.use(koaStatic(path.join(__dirname, 'node_modules', 'highlight.js', 'styles')))
|
||||
|
||||
const app = module.exports = new Koa()
|
||||
|
||||
app.use(mount('/static/public', assets))
|
||||
app.use(mount('/static/hljs', hljs))
|
||||
|
||||
app.use(views(path.join(__dirname, 'views'), {
|
||||
map: { html: 'ejs' }
|
||||
}))
|
||||
|
||||
router
|
||||
.get('/', home)
|
||||
.get('/author/:id', author)
|
||||
.get('/hashtag/:id', hashtag)
|
||||
.get('/profile/', profile)
|
||||
.get('/thread/:id', thread)
|
||||
.get('/raw/:id', raw)
|
||||
.post('/like/:id', koaBody(), like)
|
||||
|
||||
app.use(router.routes())
|
||||
|
||||
const opts = {
|
||||
host: 'localhost',
|
||||
port: 3000
|
||||
}
|
||||
|
||||
const uri = `http://${opts.host}:${opts.port}/`
|
||||
app.listen(opts)
|
||||
console.log(`Listening on http://${uri}`)
|
||||
open(uri)
|
||||
require('./src/app')()
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
const ssbMd = require('ssb-markdown')
|
||||
const toUrl = require('./to-url')
|
||||
|
||||
module.exports = (msg) =>
|
||||
ssbMd.block(msg.value.content.text, { toUrl: toUrl(msg) })
|
|
@ -1,5 +0,0 @@
|
|||
const md = require('ssb-markdown')
|
||||
const toUrl = require('./to-url')
|
||||
|
||||
module.exports = (input) =>
|
||||
md.block(input, { toUrl: toUrl() })
|
|
@ -1,92 +0,0 @@
|
|||
const lodash = require('lodash')
|
||||
const md = require('ssb-markdown')
|
||||
const prettyMs = require('pretty-ms')
|
||||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('./cooler')
|
||||
const toUrl = require('./to-url')
|
||||
|
||||
module.exports = (ssb) => async function (msg) {
|
||||
lodash.set(msg, 'value.meta.md.block', () =>
|
||||
md.block(msg.value.content.text, { toUrl: toUrl(msg) })
|
||||
)
|
||||
|
||||
var filterQuery = {
|
||||
$filter: {
|
||||
dest: msg.key
|
||||
}
|
||||
}
|
||||
|
||||
const whoami = await cooler.get(ssb.whoami)
|
||||
|
||||
const backlinkStream = await cooler.read(ssb.backlinks.read, {
|
||||
query: [ filterQuery ],
|
||||
index: 'DTA', // use asserted timestamps
|
||||
private: true,
|
||||
meta: true
|
||||
})
|
||||
|
||||
const rawVotes = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
backlinkStream,
|
||||
pull.filter(ref =>
|
||||
typeof ref.value.content !== 'string' &&
|
||||
ref.value.content.type === 'vote' &&
|
||||
ref.value.content.vote &&
|
||||
typeof ref.value.content.vote.value === 'number' &&
|
||||
ref.value.content.vote.value >= 0 &&
|
||||
ref.value.content.vote.link === msg.key
|
||||
),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// { @key: 1, @key2: 0, @key3: 1 }
|
||||
//
|
||||
// only one vote per person!
|
||||
const reducedVotes = rawVotes.reduce((acc, vote) => {
|
||||
acc[vote.value.author] = vote.value.content.vote.value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// gets *only* the people who voted 1
|
||||
// [ @key, @key, @key ]
|
||||
const voters = Object.entries(reducedVotes).filter(e => e[1] === 1).map(e => e[0])
|
||||
|
||||
const name = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'name',
|
||||
dest: msg.value.author
|
||||
}
|
||||
)
|
||||
|
||||
const avatarMsg = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'image',
|
||||
dest: msg.value.author
|
||||
}
|
||||
)
|
||||
|
||||
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
|
||||
? avatarMsg.link
|
||||
: avatarMsg
|
||||
|
||||
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
|
||||
|
||||
const ts = new Date(msg.value.timestamp)
|
||||
lodash.set(msg, 'value.meta.timestamp.received.iso8601', ts.toISOString())
|
||||
|
||||
const ago = Date.now() - Number(ts)
|
||||
lodash.set(msg, 'value.meta.timestamp.received.since', prettyMs(ago, { compact: true }))
|
||||
lodash.set(msg, 'value.meta.author.name', name)
|
||||
lodash.set(msg, 'value.meta.author.avatar', {
|
||||
id: avatarId,
|
||||
url: avatarUrl
|
||||
})
|
||||
|
||||
lodash.set(msg, 'value.meta.votes', voters)
|
||||
lodash.set(msg, 'value.meta.voted', voters.includes(whoami.id))
|
||||
|
||||
return msg
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
const ssbRef = require('ssb-ref')
|
||||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('../lib/cooler')
|
||||
const renderMd = require('../lib/render-markdown')
|
||||
const renderMsg = require('../lib/render-message')
|
||||
|
||||
module.exports = async function (ctx) {
|
||||
if (ssbRef.isFeed(ctx.params.id) === false) {
|
||||
throw new Error(`not a feed: ${ctx.params.id}`)
|
||||
}
|
||||
var ssb = await cooler.connect()
|
||||
|
||||
var rawDescription = await cooler.get(
|
||||
ssb.about.socialValue,
|
||||
{ key: 'description', dest: ctx.params.id }
|
||||
)
|
||||
|
||||
const name = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'name',
|
||||
dest: ctx.params.id
|
||||
}
|
||||
)
|
||||
|
||||
const avatarMsg = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'image',
|
||||
dest: ctx.params.id
|
||||
}
|
||||
)
|
||||
|
||||
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
|
||||
? avatarMsg.link
|
||||
: avatarMsg
|
||||
|
||||
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
|
||||
|
||||
const description = renderMd(rawDescription)
|
||||
|
||||
var msgSource = await cooler.read(
|
||||
ssb.createUserStream, {
|
||||
id: ctx.params.id,
|
||||
private: true,
|
||||
reverse: true,
|
||||
meta: true
|
||||
}
|
||||
)
|
||||
|
||||
const rawMsgs = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
msgSource,
|
||||
pull.filter(msg =>
|
||||
typeof msg.value.content !== 'string' &&
|
||||
msg.value.content.type === 'post'
|
||||
),
|
||||
pull.take(32),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
|
||||
|
||||
await ctx.render('author', { msgs, name, description, avatarUrl })
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('../lib/cooler')
|
||||
const renderMsg = require('../lib/render-message')
|
||||
|
||||
module.exports = async function hashtag (ctx) {
|
||||
var ssb = await cooler.connect()
|
||||
|
||||
var filterQuery = {
|
||||
$filter: {
|
||||
dest: '#' + ctx.params.id
|
||||
}
|
||||
}
|
||||
|
||||
const msgSource = await cooler.read(
|
||||
ssb.backlinks.read,
|
||||
{
|
||||
query: [ filterQuery ],
|
||||
index: 'DTA', // use asserted timestamps
|
||||
reverse: true,
|
||||
private: true,
|
||||
meta: true
|
||||
}
|
||||
)
|
||||
|
||||
const rawMsgs = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
msgSource,
|
||||
pull.filter(msg =>
|
||||
typeof msg.value.content !== 'string' &&
|
||||
msg.value.content.type === 'post'
|
||||
),
|
||||
pull.take(32),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
|
||||
|
||||
await ctx.render('home', { msgs })
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('../lib/cooler')
|
||||
const renderMsg = require('../lib/render-message')
|
||||
|
||||
module.exports = async function home (ctx) {
|
||||
var ssb = await cooler.connect()
|
||||
|
||||
var msgSource = await cooler.read(
|
||||
ssb.messagesByType, {
|
||||
limit: 32,
|
||||
private: true,
|
||||
reverse: true,
|
||||
type: 'post'
|
||||
}
|
||||
)
|
||||
|
||||
const rawMsgs = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
msgSource,
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
|
||||
|
||||
await ctx.render('home', { msgs })
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
const cooler = require('../lib/cooler')
|
||||
|
||||
module.exports = async function like (ctx) {
|
||||
const ssb = await cooler.connect()
|
||||
|
||||
await cooler.get(ssb.publish, {
|
||||
type: 'vote',
|
||||
vote: {
|
||||
link: ctx.params.id,
|
||||
value: Number(ctx.request.body.voteValue)
|
||||
}
|
||||
})
|
||||
|
||||
const back = new URL(ctx.request.header.referer)
|
||||
back.hash = encodeURIComponent(ctx.params.id)
|
||||
ctx.redirect(back)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
const cooler = require('../lib/cooler')
|
||||
const author = require('./author')
|
||||
|
||||
module.exports = async function (ctx) {
|
||||
const ssb = await cooler.connect()
|
||||
const whoami = await cooler.get(ssb.whoami)
|
||||
ctx.params.id = whoami.id
|
||||
await author(ctx)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
const cooler = require('../lib/cooler')
|
||||
|
||||
module.exports = async function thread (ctx) {
|
||||
const ssb = await cooler.connect()
|
||||
const rawMsg = await cooler.get(ssb.get, {
|
||||
id: ctx.params.id,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
ctx.body = rawMsg
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
const lodash = require('lodash')
|
||||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('../lib/cooler')
|
||||
const renderMsg = require('../lib/render-message')
|
||||
|
||||
module.exports = async function thread (ctx) {
|
||||
const ssb = await cooler.connect()
|
||||
const rawMsg = await cooler.get(ssb.get, {
|
||||
id: ctx.params.id,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
const parents = []
|
||||
|
||||
const getParents = (msg) => new Promise(async (resolve, reject) => {
|
||||
if (typeof msg.value.content === 'string') {
|
||||
return resolve(parents)
|
||||
}
|
||||
|
||||
if (typeof msg.value.content.fork === 'string') {
|
||||
const fork = await cooler.get(ssb.get, {
|
||||
id: msg.value.content.fork,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
parents.push(fork)
|
||||
resolve(getParents(fork))
|
||||
} else if (typeof msg.value.content.root === 'string') {
|
||||
const root = await cooler.get(ssb.get, {
|
||||
id: msg.value.content.root,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
parents.push(root)
|
||||
resolve(getParents(root))
|
||||
} else {
|
||||
resolve(parents)
|
||||
}
|
||||
})
|
||||
|
||||
const ancestors = await getParents(rawMsg)
|
||||
|
||||
const root = rawMsg.key
|
||||
|
||||
var filterQuery = {
|
||||
$filter: {
|
||||
dest: root
|
||||
}
|
||||
}
|
||||
|
||||
const backlinkStream = await cooler.read(ssb.backlinks.read, {
|
||||
query: [filterQuery],
|
||||
index: 'DTA' // use asserted timestamps
|
||||
})
|
||||
|
||||
const replies = await new Promise((resolve, reject) =>
|
||||
pull(
|
||||
backlinkStream,
|
||||
pull.filter(msg => {
|
||||
const isPost = lodash.get(msg, 'value.content.type') === 'post'
|
||||
if (isPost === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = lodash.get(msg, 'value.content.root')
|
||||
const fork = lodash.get(msg, 'value.content.fork')
|
||||
|
||||
if (root !== rawMsg.key && fork !== rawMsg.key) {
|
||||
// mention
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const allMsgs = [...ancestors, rawMsg, ...replies]
|
||||
const msgs = await Promise.all(allMsgs.map(renderMsg(ssb)))
|
||||
|
||||
await ctx.render('home', { msgs })
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
const Koa = require('koa')
|
||||
const path = require('path')
|
||||
const router = require('koa-router')()
|
||||
const koaStatic = require('koa-static')
|
||||
const mount = require('koa-mount')
|
||||
const open = require('open')
|
||||
const koaBody = require('koa-body')
|
||||
|
||||
const author = require('./routes/author')
|
||||
const hashtag = require('./routes/hashtag')
|
||||
const home = require('./routes/home')
|
||||
const profile = require('./routes/profile')
|
||||
const raw = require('./routes/raw')
|
||||
const thread = require('./routes/thread')
|
||||
const like = require('./routes/like')
|
||||
|
||||
module.exports = (options) => {
|
||||
const assets = new Koa()
|
||||
assets.use(koaStatic(path.join(__dirname, 'assets')))
|
||||
|
||||
const hljs = new Koa()
|
||||
hljs.use(koaStatic(path.join(__dirname, '..', 'node_modules', 'highlight.js', 'styles')))
|
||||
|
||||
const app = module.exports = new Koa()
|
||||
|
||||
app.use(mount('/static/assets', assets))
|
||||
app.use(mount('/static/hljs', hljs))
|
||||
|
||||
router
|
||||
.get('/', home)
|
||||
.get('/author/:id', author)
|
||||
.get('/hashtag/:id', hashtag)
|
||||
.get('/profile/', profile)
|
||||
.get('/thread/:id', thread)
|
||||
.get('/raw/:id', raw)
|
||||
.post('/like/:id', koaBody(), like)
|
||||
|
||||
app.use(router.routes())
|
||||
|
||||
const defaultConfig = {
|
||||
host: 'localhost',
|
||||
port: 3000
|
||||
}
|
||||
|
||||
const config = Object.assign({}, defaultConfig, options)
|
||||
|
||||
const uri = `http://${config.host}:${config.port}/`
|
||||
app.listen(config)
|
||||
console.log(`Listening on http://${uri}`)
|
||||
open(uri)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
const ssbRef = require('ssb-ref')
|
||||
|
||||
const about = require('./models/about')
|
||||
const post = require('./models/post')
|
||||
const views = require('./views')
|
||||
|
||||
module.exports = async function (ctx) {
|
||||
const feedId = ctx.params.id
|
||||
|
||||
if (ssbRef.isFeed(feedId) === false) {
|
||||
throw new Error(`not a feed: ${ctx.params.id}`)
|
||||
}
|
||||
|
||||
const description = await about.description(feedId)
|
||||
const name = await about.name(feedId)
|
||||
const image = await about.image(feedId)
|
||||
|
||||
const msgs = await post.fromFeed(feedId)
|
||||
|
||||
const avatarUrl = `http://localhost:8989/blobs/get/${image}`
|
||||
|
||||
ctx.body = await views('author', {
|
||||
msgs,
|
||||
name,
|
||||
description,
|
||||
avatarUrl
|
||||
})
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
const post = require('./models/post')
|
||||
const views = require('./views')
|
||||
|
||||
module.exports = async function hashtag (ctx) {
|
||||
const hashtag = ctx.params.id
|
||||
|
||||
const msgs = await post.fromHashtag(hashtag)
|
||||
|
||||
ctx.body = await views('home', { msgs })
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const views = require('./views')
|
||||
const post = require('./models/post')
|
||||
|
||||
module.exports = async function home (ctx) {
|
||||
const msgs = await post.latest()
|
||||
|
||||
ctx.body = await views('home', { msgs })
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
const vote = require('./models/vote')
|
||||
|
||||
module.exports = async function like (ctx) {
|
||||
const msgId = ctx.params.id
|
||||
const value = Number(ctx.request.body.voteValue)
|
||||
const referer = new URL(ctx.request.header.referer)
|
||||
|
||||
await vote.publish(msgId, value)
|
||||
|
||||
referer.hash = encodeURIComponent(ctx.params.id)
|
||||
ctx.redirect(referer)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
const cooler = require('./lib/cooler')
|
||||
const markdown = require('./lib/markdown')
|
||||
|
||||
const nullImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
|
||||
|
||||
module.exports = {
|
||||
name: async (feedId) => {
|
||||
const ssb = await cooler.connect()
|
||||
return cooler.get(
|
||||
ssb.about.socialValue, {
|
||||
key: 'name',
|
||||
dest: feedId
|
||||
})
|
||||
},
|
||||
image: async (feedId) => {
|
||||
const ssb = await cooler.connect()
|
||||
const raw = await cooler.get(
|
||||
ssb.about.socialValue, {
|
||||
key: 'image',
|
||||
dest: feedId
|
||||
}
|
||||
)
|
||||
|
||||
if (raw == null || raw.link == null) {
|
||||
return nullImage
|
||||
} else if (typeof raw.link === 'string') {
|
||||
return raw.link
|
||||
} else {
|
||||
return raw
|
||||
}
|
||||
},
|
||||
description: async (feedId) => {
|
||||
const ssb = await cooler.connect()
|
||||
const raw = await cooler.get(
|
||||
ssb.about.socialValue, {
|
||||
key: 'description',
|
||||
dest: feedId
|
||||
})
|
||||
return markdown(raw)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const defaultOptions = {
|
||||
private: true,
|
||||
reverse: true,
|
||||
meta: true
|
||||
}
|
||||
|
||||
module.exports = (customOptions) =>
|
||||
Object.assign({}, customOptions, defaultOptions)
|
|
@ -1,12 +1,10 @@
|
|||
const ssbRef = require('ssb-ref')
|
||||
const md = require('ssb-markdown')
|
||||
const ssbMsgs = require('ssb-msgs')
|
||||
const lodash = require('lodash')
|
||||
const ssbRef = require('ssb-ref')
|
||||
|
||||
module.exports = (msg) => {
|
||||
const toUrl = (mentions = []) => {
|
||||
var mentionNames = {}
|
||||
|
||||
const mentions = lodash.get(msg, 'value.content.mentions', [])
|
||||
|
||||
ssbMsgs.links(mentions, 'feed').forEach(function (link) {
|
||||
if (link.name && typeof link.name === 'string') {
|
||||
var name = (link.name.charAt(0) === '@') ? link.name : '@' + link.name
|
||||
|
@ -32,3 +30,8 @@ module.exports = (msg) => {
|
|||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (input, mentions) =>
|
||||
md.block(input, {
|
||||
toUrl: toUrl(mentions)
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
const cooler = require('./lib/cooler')
|
||||
|
||||
module.exports = {
|
||||
whoami: async () => {
|
||||
const ssb = await cooler.connect()
|
||||
return cooler.get(ssb.whoami)
|
||||
},
|
||||
get: async (msgId) => {
|
||||
const ssb = await cooler.connect()
|
||||
return cooler.get(ssb.get, {
|
||||
id: msgId,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
const lodash = require('lodash')
|
||||
const pull = require('pull-stream')
|
||||
|
||||
const cooler = require('./lib/cooler')
|
||||
const configure = require('./lib/configure')
|
||||
const markdown = require('./lib/markdown')
|
||||
const prettyMs = require('pretty-ms')
|
||||
|
||||
const transform = (ssb, messages) => Promise.all(messages.map(async (msg) => {
|
||||
lodash.set(msg, 'value.meta.md.block', () =>
|
||||
markdown(msg.value.content.text, msg.value.content.mentions)
|
||||
)
|
||||
|
||||
var filterQuery = {
|
||||
$filter: {
|
||||
dest: msg.key
|
||||
}
|
||||
}
|
||||
|
||||
const whoami = await cooler.get(ssb.whoami)
|
||||
|
||||
const backlinkStream = await cooler.read(ssb.backlinks.read, {
|
||||
query: [ filterQuery ],
|
||||
index: 'DTA', // use asserted timestamps
|
||||
private: true,
|
||||
meta: true
|
||||
})
|
||||
|
||||
const rawVotes = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
backlinkStream,
|
||||
pull.filter(ref =>
|
||||
typeof ref.value.content !== 'string' &&
|
||||
ref.value.content.type === 'vote' &&
|
||||
ref.value.content.vote &&
|
||||
typeof ref.value.content.vote.value === 'number' &&
|
||||
ref.value.content.vote.value >= 0 &&
|
||||
ref.value.content.vote.link === msg.key
|
||||
),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// { @key: 1, @key2: 0, @key3: 1 }
|
||||
//
|
||||
// only one vote per person!
|
||||
const reducedVotes = rawVotes.reduce((acc, vote) => {
|
||||
acc[vote.value.author] = vote.value.content.vote.value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// gets *only* the people who voted 1
|
||||
// [ @key, @key, @key ]
|
||||
const voters = Object.entries(reducedVotes).filter(e => e[1] === 1).map(e => e[0])
|
||||
|
||||
const name = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'name',
|
||||
dest: msg.value.author
|
||||
}
|
||||
)
|
||||
|
||||
const avatarMsg = await cooler.get(
|
||||
ssb.about.socialValue, { key: 'image',
|
||||
dest: msg.value.author
|
||||
}
|
||||
)
|
||||
|
||||
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
|
||||
? avatarMsg.link
|
||||
: avatarMsg
|
||||
|
||||
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
|
||||
|
||||
const ts = new Date(msg.value.timestamp)
|
||||
lodash.set(msg, 'value.meta.timestamp.received.iso8601', ts.toISOString())
|
||||
|
||||
const ago = Date.now() - Number(ts)
|
||||
lodash.set(msg, 'value.meta.timestamp.received.since', prettyMs(ago, { compact: true }))
|
||||
lodash.set(msg, 'value.meta.author.name', name)
|
||||
lodash.set(msg, 'value.meta.author.avatar', {
|
||||
id: avatarId,
|
||||
url: avatarUrl
|
||||
})
|
||||
|
||||
lodash.set(msg, 'value.meta.votes', voters)
|
||||
lodash.set(msg, 'value.meta.voted', voters.includes(whoami.id))
|
||||
|
||||
return msg
|
||||
}))
|
||||
|
||||
module.exports = {
|
||||
fromFeed: async (feedId, customOptions = {}) => {
|
||||
const ssb = await cooler.connect()
|
||||
const options = configure({ id: feedId }, customOptions)
|
||||
|
||||
const source = await cooler.read(
|
||||
ssb.createUserStream,
|
||||
options
|
||||
)
|
||||
|
||||
const messages = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
source,
|
||||
pull.filter(msg =>
|
||||
typeof msg.value.content !== 'string' &&
|
||||
msg.value.content.type === 'post'
|
||||
),
|
||||
pull.take(32),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(transform(ssb, msgs))
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return messages
|
||||
},
|
||||
fromHashtag: async (hashtag, customOptions = {}) => {
|
||||
const ssb = await cooler.connect()
|
||||
const query = [ {
|
||||
$filter: {
|
||||
dest: '#' + hashtag
|
||||
}
|
||||
} ]
|
||||
|
||||
const options = configure({ query, index: 'DTA' }, customOptions)
|
||||
|
||||
const source = await cooler.read(
|
||||
ssb.backlinks.read, options
|
||||
)
|
||||
|
||||
const messages = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
source,
|
||||
pull.filter(msg =>
|
||||
typeof msg.value.content !== 'string' &&
|
||||
msg.value.content.type === 'post'
|
||||
),
|
||||
pull.take(32),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(transform(ssb, msgs))
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return messages
|
||||
},
|
||||
latest: async (customOptions = {}) => {
|
||||
const ssb = await cooler.connect()
|
||||
const options = configure({
|
||||
type: 'post',
|
||||
limit: 32
|
||||
}, customOptions)
|
||||
|
||||
const source = await cooler.read(
|
||||
ssb.messagesByType,
|
||||
options
|
||||
)
|
||||
|
||||
const messages = await new Promise((resolve, reject) => {
|
||||
pull(
|
||||
source,
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(transform(ssb, msgs))
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return messages
|
||||
},
|
||||
fromThread: async (msgId, customOptions) => {
|
||||
const ssb = await cooler.connect()
|
||||
const options = configure({ id: msgId }, customOptions)
|
||||
const rawMsg = await cooler.get(ssb.get, options)
|
||||
|
||||
const parents = []
|
||||
|
||||
const getParents = (msg) => new Promise(async (resolve, reject) => {
|
||||
if (typeof msg.value.content === 'string') {
|
||||
return resolve(parents)
|
||||
}
|
||||
|
||||
if (typeof msg.value.content.fork === 'string') {
|
||||
const fork = await cooler.get(ssb.get, {
|
||||
id: msg.value.content.fork,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
parents.push(fork)
|
||||
resolve(getParents(fork))
|
||||
} else if (typeof msg.value.content.root === 'string') {
|
||||
const root = await cooler.get(ssb.get, {
|
||||
id: msg.value.content.root,
|
||||
meta: true,
|
||||
private: true
|
||||
})
|
||||
|
||||
parents.push(root)
|
||||
resolve(getParents(root))
|
||||
} else {
|
||||
resolve(parents)
|
||||
}
|
||||
})
|
||||
|
||||
const ancestors = await getParents(rawMsg)
|
||||
|
||||
const root = rawMsg.key
|
||||
|
||||
var filterQuery = {
|
||||
$filter: {
|
||||
dest: root
|
||||
}
|
||||
}
|
||||
|
||||
const backlinkStream = await cooler.read(ssb.backlinks.read, {
|
||||
query: [filterQuery],
|
||||
index: 'DTA' // use asserted timestamps
|
||||
})
|
||||
|
||||
const replies = await new Promise((resolve, reject) =>
|
||||
pull(
|
||||
backlinkStream,
|
||||
pull.filter(msg => {
|
||||
const isPost = lodash.get(msg, 'value.content.type') === 'post'
|
||||
if (isPost === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = lodash.get(msg, 'value.content.root')
|
||||
const fork = lodash.get(msg, 'value.content.fork')
|
||||
|
||||
if (root !== rawMsg.key && fork !== rawMsg.key) {
|
||||
// mention
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
),
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err)
|
||||
resolve(msgs)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const allMessages = [...ancestors, rawMsg, ...replies]
|
||||
return transform(ssb, allMessages)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
const cooler = require('./lib/cooler')
|
||||
|
||||
module.exports = {
|
||||
publish: async (messageId, value) => {
|
||||
const ssb = await cooler.connect()
|
||||
await cooler.get(ssb.publish, {
|
||||
type: 'vote',
|
||||
vote: {
|
||||
link: messageId,
|
||||
value: Number(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
const ssbRef = require('ssb-ref')
|
||||
|
||||
const about = require('./models/about')
|
||||
const post = require('./models/post')
|
||||
const meta = require('./models/meta')
|
||||
const views = require('./views')
|
||||
|
||||
module.exports = async function (ctx) {
|
||||
const whoami = await meta.whoami()
|
||||
const feedId = whoami.id
|
||||
|
||||
if (ssbRef.isFeed(feedId) === false) {
|
||||
throw new Error(`not a feed: ${ctx.params.id}`)
|
||||
}
|
||||
|
||||
const description = await about.description(feedId)
|
||||
const name = await about.name(feedId)
|
||||
const image = await about.image(feedId)
|
||||
|
||||
const msgs = await post.fromFeed(feedId)
|
||||
|
||||
const avatarUrl = `http://localhost:8989/blobs/get/${image}`
|
||||
|
||||
ctx.body = await views('author', {
|
||||
msgs,
|
||||
name,
|
||||
description,
|
||||
avatarUrl
|
||||
})
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const meta = require('./models/meta')
|
||||
|
||||
module.exports = async function thread (ctx) {
|
||||
const msgId = ctx.params.id
|
||||
const message = await meta.get(msgId)
|
||||
|
||||
ctx.body = message
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
const views = require('./views')
|
||||
const post = require('./models/post')
|
||||
|
||||
module.exports = async function thread (ctx) {
|
||||
const msgId = ctx.params.id
|
||||
const msgs = await post.fromThread(msgId)
|
||||
|
||||
ctx.body = await views('home', { msgs })
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
const ejs = require('ejs')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = (filename, data) => new Promise((resolve, reject) => {
|
||||
const options = {}
|
||||
const target = path.join(__dirname, 'templates', filename + '.html')
|
||||
ejs.renderFile(target, data, options, (err, str) => {
|
||||
if (err) return reject(err)
|
||||
resolve(str)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
<%- include('partials/header.html'); %>
|
||||
<%- include('layout/header.html'); %>
|
||||
|
||||
<header class="profile">
|
||||
<img class="avatar" src="<%= avatarUrl %>">
|
||||
|
@ -14,4 +14,4 @@
|
|||
|
||||
<%- include('feed.html', { msgs }); %>
|
||||
|
||||
<%- include('partials/footer.html'); %>
|
||||
<%- include('layout/footer.html'); %>
|
|
@ -0,0 +1,5 @@
|
|||
<%- include('layout/header.html'); %>
|
||||
|
||||
<%- include('feed.html', { msgs }); %>
|
||||
|
||||
<%- include('layout/footer.html'); %>
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>🏝️ Oasis</title>
|
||||
<link rel="stylesheet" href="/static/public/style.css">
|
||||
<link rel="stylesheet" href="/static/assets/style.css">
|
||||
<link rel="stylesheet" href="/static/hljs/github.css">
|
||||
</head>
|
||||
<body>
|
|
@ -0,0 +1,5 @@
|
|||
<%- include('layout/header.html'); %>
|
||||
|
||||
<%- include('message.html', { msg }); %>
|
||||
|
||||
<%- include('layout/footer.html'); %>
|
|
@ -1,5 +0,0 @@
|
|||
<%- include('partials/header.html'); %>
|
||||
|
||||
<%- include('feed.html', { msgs }); %>
|
||||
|
||||
<%- include('partials/footer.html'); %>
|
|
@ -1,5 +0,0 @@
|
|||
<%- include('partials/header.html'); %>
|
||||
|
||||
<%- include('message.html', { msg }); %>
|
||||
|
||||
<%- include('partials/footer.html'); %>
|
Loading…
Reference in New Issue