var fs = require('fs')
var qs = require('querystring')
var pull = require('pull-stream')
var path = require('path')
var paramap = require('pull-paramap')
var sort = require('ssb-sort')
var crypto = require('crypto')
var toPull = require('stream-to-pull-stream')
var serveEmoji = require('emoji-server')()
var u = require('./util')
var cat = require('pull-cat')
var h = require('hyperscript')
var paginate = require('pull-paginate')
var ssbMentions = require('ssb-mentions')
var multicb = require('multicb')
var pkg = require('../package')
var Busboy = require('busboy')
var mime = require('mime-types')
var ident = require('pull-identify-filetype')
var htime = require('human-time')
var ph = require('pull-hyperscript')
var emojis = require('emoji-named-characters')
var jpeg = require('jpeg-autorotate')
var Catch = require('pull-catch')
var Diff = require('diff')
var split = require('pull-split')
var utf8 = require('pull-utf8-decoder')
var webresolve = require('ssb-web-resolver')
var Url = require('url')
module.exports = Serve
var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles')
var urlIdRegex = /^(?:\/+(([%&@]|%25|%26)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/
function ctype(name) {
switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
case 'html': return 'text/html'
case 'txt': return 'text/plain'
case 'js': return 'text/javascript'
case 'css': return 'text/css'
case 'png': return 'image/png'
case 'json': return 'application/json'
case 'ico': return 'image/x-icon'
}
}
function encodeDispositionFilename(fname) {
fname = String(fname).replace(/\/g/, '\\\\').replace(/"/, '\\\"')
return '"' + encodeURIComponent(fname) + '"'
}
function uniques() {
var set = {}
return function (item) {
if (set[item]) return false
return set[item] = true
}
}
function reduceLink(link) {
return link
&& typeof link.link === 'string'
&& typeof link.name === 'undefined'
&& Object.keys(link).length === 1
? link.link : link
}
function Serve(app, req, res) {
this.app = app
this.req = req
this.res = res
this.startDate = new Date()
var hostname = req.headers.host || app.hostname
this.baseUrl = 'http://' + hostname + (app.opts.base || '/')
}
Serve.prototype.go = function () {
console.log(this.req.method, this.req.url)
var self = this
this.res.setTimeout(0)
var conf = self.app.config.patchfoo || {}
this.conf = conf
var authtok = conf.auth || null
if (authtok) {
var auth = this.req.headers['authorization']
var tok = null
//console.log('Authorization: ',auth)
if (auth) {
var a = auth.split(' ')
if (a[0] == 'Basic') {
tok = Buffer.from(a[1],'base64').toString('ascii')
}
}
if (tok != authtok) {
self.res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Patchfoo"'})
self.res.end('Not authorized')
return
}
}
var allowAddresses = conf.allowAddresses
if (allowAddresses) {
var ip = this.req.socket.remoteAddress
if (allowAddresses.indexOf(ip) === -1) {
this.res.writeHead(401)
return this.res.end('Not authorized')
}
}
this.replyMentionFeeds = conf.replyMentionFeeds == null ? true :
Boolean(conf.replyMentionFeeds)
if (this.req.method === 'POST' || this.req.method === 'PUT') {
if (/^multipart\/form-data/.test(this.req.headers['content-type'])) {
var data = {}
var erred
var busboy = new Busboy({headers: this.req.headers})
var filesCb = multicb({pluck: 1})
busboy.on('finish', filesCb())
filesCb(function (err) {
gotData(err, data)
})
function addField(name, value) {
if (typeof value === 'string') value = value.replace(/\r\n/g, '\n')
if (!(name in data)) data[name] = value
else if (Array.isArray(data[name])) data[name].push(value)
else data[name] = [data[name], value]
}
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
var cb = filesCb()
var size = 0
pull(
toPull(file),
pull.map(function (data) {
size += data.length
return data
}),
self.app.addBlob(!!data.private, function (err, link) {
if (err) return cb(err)
if (size === 0 && !filename) return cb()
link.name = filename
link.type = mimetype
addField(fieldname, link)
cb()
})
)
})
busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
addField(fieldname, val)
})
this.req.pipe(busboy)
} else {
pull(
toPull(this.req),
pull.collect(function (err, bufs) {
var data
if (!err) try {
var str = Buffer.concat(bufs).toString('utf8')
str = str.replace(/%0D%0A/ig, '\n')
data = qs.parse(str)
} catch(e) {
err = e
}
gotData(err, data)
})
)
}
} else {
gotData(null, {})
}
function gotData(err, data) {
self.data = data
if (err) next(err)
else if (data.action === 'publish' && data.about_new_content) self.publishNewAbout(next)
else if (data.action === 'publish') self.publishJSON(next)
else if (data.action === 'contact') self.publishContact(next)
else if (data.action === 'want-blobs') self.wantBlobs(next)
else if (data.action === 'poll-position') self.publishPollPosition(next)
else if (data.action_vote) self.publishVote(next)
else if (data.action_attend) self.publishAttend(next)
else next()
}
function next(err, publishedMsg) {
if (err) {
if (err.redirectUrl) return self.redirect(err.redirectUrl)
self.res.writeHead(400, {'Content-Type': 'text/plain'})
self.res.end(err.stack)
} else if (publishedMsg) {
if (self.data.redirect_to_published_msg) {
self.redirect(self.app.render.toUrl(publishedMsg.key))
} else {
var u = Url.parse(self.req.url)
var q = u.query || (u.query = {})
q.published = publishedMsg.key
self.redirect(Url.format(u))
}
} else {
self.handle()
}
}
}
Serve.prototype.saveDraft = function (content, cb) {
var self = this
var data = self.data
var form = {}
for (var k in data) {
if (k === 'url' || k === 'draft_id' || k === 'content'
|| k === 'draft_id' || k === 'save_draft') continue
form[k] = data[k]
}
self.app.saveDraft(data.draft_id, data.url, form, content, function (err, id) {
if (err) return cb(err)
cb(null, id || data.draft_id)
})
}
Serve.prototype.publishJSON = function (cb) {
var content
try {
content = JSON.parse(this.data.content)
} catch(e) {
return cb(e)
}
this.publish(content, cb)
}
Serve.prototype.publishNewAbout = function (cb) {
var self = this
var aboutContent, aboutNewContent
try {
aboutContent = JSON.parse(this.data.content)
aboutNewContent = JSON.parse(this.data.about_new_content)
} catch(e) {
return cb(e)
}
if (typeof aboutContent !== 'object' || aboutContent === null) {
return cb(new TypeError('content must be object'))
}
aboutNewContent.recps = aboutContent.recps
self.app.publish(aboutNewContent, function (err, msg) {
if (err) return cb(err)
if (!msg) return cb(new Error('aborted'))
aboutContent.about = msg.key
self.publish(aboutContent, cb)
})
}
Serve.prototype.publishVote = function (next) {
var content = {
type: 'vote',
channel: this.data.channel || undefined,
root: this.data.root || undefined,
branch: this.data.branches ? this.data.branches.split(',') : undefined,
vote: {
link: this.data.link,
value: Number(this.data.vote_value),
expression: this.data.vote_expression || undefined,
}
}
if (this.data.recps) content.recps = this.data.recps.split(',')
if (this.app.previewVotes) {
var json = JSON.stringify(content, 0, 2)
var q = qs.stringify({text: json, action: 'preview'})
var url = this.app.render.toUrl('/compose?' + q)
this.redirect(url)
} else {
this.publish(content, next)
}
}
Serve.prototype.requestReplicate = function (id, replicateIt, next) {
var self = this
var replicate = self.app.sbot.replicate
var request = replicate && replicate.request
if (!request) return this.respond(500, 'Missing replicate.request method')
request(id, replicateIt, function (err) {
if (err) return pull(
pull.once(u.renderError(err, ext).outerHTML),
self.wrapPage('replicate request'),
self.respondSink(400)
)
self.requestedReplicate = replicateIt
next()
})
}
Serve.prototype.publishContact = function (next) {
if (this.data.replicate) return this.requestReplicate(this.data.contact, true, next)
if (this.data.unreplicate) return this.requestReplicate(this.data.contact, false, next)
if (this.data.block1) return this.redirect(this.app.render.toUrl('/block/' + this.data.contact))
var content = {
type: 'contact',
contact: this.data.contact,
}
if (this.data.follow) content.following = true
if (this.data.block) content.blocking = true
if (this.data.unfollow) content.following = false
if (this.data.unblock) content.blocking = false
if (this.data.mute) content.blocking = true
if (this.data.unmute) content.blocking = false
if (this.data.mute || this.data.unmute) content.recps = [this.app.sbot.id]
if (this.app.previewContacts) {
var json = JSON.stringify(content, 0, 2)
var q = qs.stringify({text: json, action: 'preview'})
var url = this.app.render.toUrl('/compose?' + q)
this.redirect(url)
} else {
this.publish(content, next)
}
}
Serve.prototype.publishPollPosition = function (cb) {
var content = {
type: 'position',
version: 'v1',
channel: this.data.channel || undefined,
root: this.data.poll_root,
branch: this.data.branches || [],
reason: this.data.poll_reason || undefined,
details: {
type: this.data.poll_type
}
}
if (this.data.poll_choice != null) {
content.details.choice = Number(this.data.poll_choice)
} else {
content.details.choices = u.toArray(this.data.poll_choices).map(Number)
}
if (this.data.recps) content.recps = this.data.recps.split(',')
var json = JSON.stringify(content, 0, 2)
var q = qs.stringify({text: json, action: 'preview'})
var url = this.app.render.toUrl('/compose?' + q)
this.redirect(url)
// this.publish(content, cb)
}
Serve.prototype.publishAttend = function (cb) {
var content = {
type: 'about',
channel: this.data.channel || undefined,
about: this.data.link,
attendee: {
link: this.app.sbot.id
}
}
if (this.data.recps) content.recps = this.data.recps.split(',')
this.publish(content, cb)
}
Serve.prototype.wantBlobs = function (cb) {
var self = this
if (!self.data.blob_ids) return cb()
var ids = self.data.blob_ids.split(',')
if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(',')))
var done = multicb({pluck: 1})
ids.forEach(function (id) {
self.app.wantSizeBlob(id, done())
})
if (self.data.async_want) return cb()
done(function (err) {
if (err) return cb(err)
// self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.')
cb()
})
}
Serve.prototype.publish = function (content, cb) {
var self = this
var done = multicb({pluck: 1, spread: true})
u.toArray(content && content.mentions).forEach(function (mention) {
if (mention.link && mention.link[0] === '&' && !isNaN(mention.size))
self.app.pushBlob(mention.link, done())
})
done(function (err) {
if (err) return cb(err)
self.publishMayRedirect(content, function (err, msg) {
if (err) return cb(err)
delete self.data.text
delete self.data.recps
return cb(null, msg)
})
})
}
Serve.prototype.publishMayRedirect = function (content, cb) {
var publishguard = this.app.sbot.publishguard
if (Array.isArray(content.recps)) {
var recps = content.recps.map(u.linkDest)
if (publishguard && publishguard.privatePublishGetUrl) {
return publishguard.privatePublishGetUrl({
content: content,
recps: recps,
redirectBase: this.baseUrl
}, onPublishGetUrl)
} else {
this.app.privatePublish(content, recps, cb)
}
} else {
if (publishguard && publishguard.publishGetUrl) {
publishguard.publishGetUrl({
content: content,
redirectBase: this.baseUrl
}, onPublishGetUrl)
} else {
this.app.sbot.publish(content, cb)
}
}
function onPublishGetUrl(err, url) {
if (err) return cb(err)
cb({redirectUrl: url})
}
}
Serve.prototype.handle = function () {
var m = urlIdRegex.exec(this.req.url)
this.query = m[5] ? qs.parse(m[5]) : {}
this.useOoo = this.query.ooo != null ?
Boolean(this.query.ooo) : this.app.useOoo
if (this.query.printView != null) {
this.printView = true
this.noNav = true
this.noFooter = true
this.noComposer = true
this.noActions = true
this.noAvatar = true
this.noMsgTime = true
this.msgDate = true
}
if (this.query.noThread != null) {
this.noThread = true
}
switch (m[2]) {
case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
case '%': return this.id(m[1], m[3])
case '@': return this.userFeed(m[1], m[3])
case '%26': m[2] = '&'; m[1] = decodeURIComponent(m[1])
case '&': return this.blob(m[1], m[3])
default: return this.path(m[4])
}
}
Serve.prototype.respond = function (status, message) {
this.res.writeHead(status)
this.res.end(message)
}
Serve.prototype.respondSink = function (status, headers, cb) {
var self = this
if (status || headers)
self.res.writeHead(status, headers || {'Content-Type': 'text/html'})
return toPull(self.res, cb || function (err) {
if (err) self.app.error(err)
})
}
Serve.prototype.redirect = function (dest) {
this.res.writeHead(302, {
Location: dest
})
this.res.end()
}
Serve.prototype.path = function (url) {
var m
url = url.replace(/^\/+/, '/')
switch (url) {
case '/': return this.home()
case '/robots.txt': return this.res.end('User-agent: *')
}
if (m = /^\/%23(.*)/.exec(url)) {
return this.redirect(this.app.render.toUrl('/channel/'
+ decodeURIComponent(m[1])))
}
m = /^([^.]*)(?:\.(.*))?$/.exec(url)
switch (m[1]) {
case '/new': return this.new(m[2])
case '/public': return this.public(m[2])
case '/threads': return this.threads(m[2])
case '/private': return this.private(m[2])
case '/mentions': return this.mentions(m[2])
case '/search': return this.search(m[2])
case '/advsearch': return this.advsearch(m[2])
case '/peers': return this.peers(m[2])
case '/status': return this.status(m[2])
case '/channels': return this.channels(m[2])
case '/tags': return this.tags(m[2])
case '/friends': return this.friends(m[2])
case '/live': return this.live(m[2])
case '/compose': return this.compose(m[2])
case '/emojis': return this.emojis(m[2])
case '/votes': return this.votes(m[2])
case '/about-self': return this.aboutSelf(m[2])
case '/new-gathering': return this.newGathering(m[2])
}
m = /^(\/?[^\/]*)(\/.*)?$/.exec(url)
switch (m[1]) {
case '/channel': return this.channel(m[2])
case '/type': return this.type(m[2])
case '/links': return this.links(m[2])
case '/static': return this.static(m[2])
case '/emoji': return this.emoji(m[2])
case '/highlight': return this.highlight(m[2])
case '/contacts': return this.contacts(m[2])
case '/about': return this.about(m[2])
case '/pub': return this.pub(m[2])
case '/git': return this.git(m[2])
case '/image': return this.image(m[2])
case '/npm': return this.npm(m[2])
case '/npm-prebuilds': return this.npmPrebuilds(m[2])
case '/npm-readme': return this.npmReadme(m[2])
case '/npm-registry': return this.npmRegistry(m[2])
case '/markdown': return this.markdown(m[2])
case '/edit-diff': return this.editDiff(m[2])
case '/about-diff': return this.aboutDiff(m[2])
case '/gathering': return this.gathering(m[2])
case '/edit-gathering': return this.editGathering(m[2])
case '/shard': return this.shard(m[2])
case '/zip': return this.zip(m[2])
case '/web': return this.web(m[2])
case '/block': return this.block(m[2])
case '/script': return this.script(m[2])
case '/drafts': return this.drafts(m[2])
}
return this.respond(404, 'Not found')
}
Serve.prototype.home = function () {
pull(
pull.empty(),
this.wrapPage('/'),
this.respondSink(200, {
'Content-Type': 'text/html'
})
)
}
Serve.prototype.public = function (ext) {
var q = this.query
var opts = {
reverse: !q.forwards,
sortByTimestamp: q.sort === 'claimed',
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
filter: q.filter,
}
pull(
this.app.createLogStream(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapPublic(),
this.wrapPage('public'),
this.respondSink(200, {
'Content-Type': ctype(ext)
})
)
}
Serve.prototype.threads = function (ext) {
var q = this.query
var opts = {
type: 'post',
reverse: !q.forwards,
sortByTimestamp: q.sort === 'claimed',
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
filter: q.filter,
}
pull(
this.app.sbotMessagesByType(opts),
pull.filter(msg => {
return !msg.value.content.root
}),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapPublic(),
this.wrapPage('threads'),
this.respondSink(200, {
'Content-Type': ctype(ext)
})
)
}
Serve.prototype.setCookie = function (key, value, options) {
var header = key + '=' + value
if (options) for (var k in options) {
header += '; ' + k + '=' + options[k]
}
this.res.setHeader('Set-Cookie', header)
}
Serve.prototype.new = function (ext) {
var self = this
var q = self.query
var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1]
var limit = Number(q.limit || self.conf.newLimit || 500)
var now = Date.now()
var opts = {
gt: Number(q.gt) || Number(latest) || now,
lte: now,
limit: limit
}
if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000})
var read = self.app.createLogStream(opts)
self.req.on('closed', function () {
console.error('closing')
read(true, function (err) {
console.log('closed')
if (err && err !== true) console.error(new Error(err.stack))
})
})
pull.collect(function (err, msgs) {
if (err) return pull(
pull.once(u.renderError(err, ext).outerHTML),
self.wrapPage('peers'),
self.respondSink(500, {'Content-Type': ctype(ext)})
)
sort(msgs)
var maxTS = msgs.reduce(function (max, msg) {
return Math.max(msg.timestamp, max)
}, -Infinity)
pull(
pull.values(msgs),
self.renderThread(),
self.wrapNew({
reachedLimit: msgs.length === limit && limit,
gt: isFinite(maxTS) ? maxTS : Date.now()
}),
self.wrapMessages(),
self.wrapPage('new'),
self.respondSink(200, {
'Content-Type': ctype(ext)
})
)
})(read)
}
Serve.prototype.private = function (ext) {
var q = this.query
var opts = {
sortByTimestamp: q.sort === 'claimed',
reverse: !q.forwards,
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
filter: q.filter,
}
pull(
this.app.streamPrivate(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapPrivate(opts),
this.wrapPage('private'),
this.respondSink(200, {
'Content-Type': ctype(ext)
})
)
}
Serve.prototype.mentions = function (ext) {
var self = this
var q = self.query
var opts = {
reverse: !q.forwards,
sortByTimestamp: q.sort === 'claimed',
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
filter: q.filter,
}
return pull(
ph('section', {}, [
ph('h3', 'Mentions'),
pull(
self.app.streamMentions(opts),
self.app.unboxMessages(),
self.renderThreadPaginated(opts, null, q),
self.wrapMessages()
)
]),
self.wrapPage('mentions'),
self.respondSink(200)
)
}
Serve.prototype.search = function (ext) {
var searchQ = (this.query.q || '').trim()
var self = this
if (/^ssb:\/\//.test(searchQ)) {
var maybeId = searchQ.substr(6)
if (u.isRef(maybeId)) searchQ = maybeId
}
if (u.isRef(searchQ) || searchQ[0] === '#') {
return self.redirect(self.app.render.toUrl(searchQ))
}
pull(
self.app.search(searchQ),
self.renderThread(),
self.wrapMessages(),
self.wrapPage('search · ' + searchQ, searchQ),
self.respondSink(200, {
'Content-Type': ctype(ext),
})
)
}
Serve.prototype.advsearch = function (ext) {
var self = this
var q = this.query || {}
if (q.source) q.source = u.extractFeedIds(q.source)[0]
if (q.dest) q.dest = u.extractFeedIds(q.dest)[0]
var hasQuery = q.text || q.source || q.dest || q.channel
pull(
cat([
ph('section', {}, [
ph('form', {action: '', method: 'get'}, [
ph('table', [
ph('tr', [
ph('td', 'text'),
ph('td', ph('input', {name: 'text', placeholder: 'regex',
class: 'id-input',
value: u.escapeHTML(q.text)}))
]),
ph('tr', [
ph('td', 'author'),
ph('td', ph('input', {name: 'source', placeholder: '@id',
class: 'id-input',
value: u.escapeHTML(q.source)}))
]),
ph('tr', [
ph('td', 'mentions'),
ph('td', ph('input', {name: 'dest', placeholder: 'id',
class: 'id-input',
value: u.escapeHTML(q.dest)}))
]),
ph('tr', [
ph('td', 'channel'),
ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel',
class: 'id-input',
value: u.escapeHTML(q.channel)})
])
]),
ph('tr', [
ph('td', {colspan: 2}, [
ph('input', {type: 'submit', value: 'search'})
])
]),
])
])
]),
hasQuery && pull(
self.app.advancedSearch(q),
self.renderThread({
feed: q.source,
}),
self.wrapMessages()
)
]),
self.wrapPage('advanced search'),
self.respondSink(200, {
'Content-Type': ctype(ext),
})
)
}
Serve.prototype.live = function (ext) {
var self = this
var q = self.query
var opts = {
live: true,
}
var gt = Number(q.gt)
if (gt) opts.gt = gt
else opts.old = false
pull(
ph('table', {class: 'ssb-msgs'}, pull(
self.app.sbot.createLogStream(opts),
self.app.render.renderFeeds({
serve: self,
withGt: true,
filter: q.filter,
}),
pull.map(u.toHTML)
)),
self.wrapPage('live'),
self.respondSink(200, {
'Content-Type': ctype(ext),
})
)
}
Serve.prototype.compose = function (ext) {
var self = this
self.composer({
channel: '',
redirectToPublishedMsg: true,
}, function (err, composer) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('compose'),
self.respondSink(500)
)
pull(
pull.once(u.toHTML(composer)),
self.wrapPage('compose'),
self.respondSink(200, {
'Content-Type': ctype(ext)
})
)
})
}
Serve.prototype.votes = function (path) {
if (path) return pull(
pull.once(u.renderError(new Error('Not implemented')).outerHTML),
this.wrapPage('#' + channel),
this.respondSink(404, {'Content-Type': ctype('html')})
)
var self = this
var q = self.query
var opts = {
reverse: !q.forwards,
limit: Number(q.limit) || 50,
}
var gt = Number(q.gt)
if (gt) opts.gt = gt
var lt = Number(q.lt)
if (lt) opts.lt = lt
self.app.getVoted(opts, function (err, voted) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('#' + channel),
self.respondSink(500, {'Content-Type': ctype('html')})
)
pull(
ph('table', [
ph('thead', [
ph('tr', [
ph('td', {colspan: 2}, self.syncPager({
first: voted.firstTimestamp,
last: voted.lastTimestamp,
}))
])
]),
ph('tbody', pull(
pull.values(voted.items),
paramap(function (item, cb) {
cb(null, ph('tr', [
ph('td', [String(item.value)]),
ph('td', [
self.phIdLink(item.id),
pull.once(' dug by '),
self.renderIdsList()(pull.values(item.feeds))
])
]))
}, 8)
)),
ph('tfoot', {}, []),
]),
self.wrapPage('votes'),
self.respondSink(200, {
'Content-Type': ctype('html')
})
)
})
}
Serve.prototype.syncPager = function (opts) {
var q = this.query
var reverse = !q.forwards
var min = (reverse ? opts.last : opts.first) || Number(q.gt)
var max = (reverse ? opts.first : opts.last) || Number(q.lt)
var minDate = new Date(min)
var maxDate = new Date(max)
var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined})
var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1})
var atNewest = reverse ? !q.lt : !max
var atOldest = reverse ? !min : !q.gt
if (atNewest && !reverse) qOlder.lt++
if (atOldest && reverse) qNewer.gt--
return h('div',
atOldest ? 'oldest' : [
h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ',
h('span', {title: minDate.toString()}, htime(minDate)), ' ',
],
' - ',
atNewest ? 'now' : [
h('span', {title: maxDate.toString()}, htime(maxDate)), ' ',
h('a', {href: '?' + qs.stringify(qNewer)}, '>>')
]
).outerHTML
}
Serve.prototype.peers = function (ext) {
var self = this
if (self.data.action === 'connect') {
return self.app.sbot.gossip.connect(self.data.address, function (err) {
if (err) return pull(
pull.once(u.renderError(err, ext).outerHTML),
self.wrapPage('peers'),
self.respondSink(400, {'Content-Type': ctype(ext)})
)
self.data = {}
return self.peers(ext)
})
}
pull(
self.app.streamPeers(),
paramap(function (peer, cb) {
var done = multicb({pluck: 1, spread: true})
var connectedTime = Date.now() - peer.stateChange
var addr = peer.host + ':' + peer.port + ':' + peer.key
done()(null, h('section',
h('form', {method: 'post', action: ''},
peer.client ? '→' : '←', ' ',
h('code', peer.host, ':', peer.port, ':'),
self.app.render.idLink(peer.key, done()), ' ',
peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '',
peer.state === 'connected' ? 'connected' : [
h('input', {name: 'action', type: 'submit', value: 'connect'}),
h('input', {name: 'address', type: 'hidden', value: addr})
]
)
// h('div', 'source: ', peer.source)
// JSON.stringify(peer, 0, 2)).outerHTML
))
done(cb)
}, 8),
pull.map(u.toHTML),
self.wrapPeers(),
self.wrapPage('peers'),
self.respondSink(200, {
'Content-Type': ctype(ext)
})
)
}
Serve.prototype.status = function (ext) {
var self = this
if (!self.app.sbot.status) return pull(
pull.once('missing sbot status method'),
this.wrapPage('status'),
self.respondSink(400)
)
pull(
ph('section', [
ph('h3', 'Status'),
pull(
u.readNext(function (cb) {
self.app.sbotStatus(function (err, status) {
cb(err, status && pull.once(status))
})
}),
pull.map(function (status) {
return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML
})
)
]),
this.wrapPage('status'),
this.respondSink(200)
)
}
Serve.prototype.channels = function (ext) {
var self = this
var id = self.app.sbot.id
function renderMyChannels() {
return pull(
self.app.streamMyChannels(id),
paramap(function (channel, cb) {
// var subscribed = false
cb(null, [
h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
' '
])
}, 8),
pull.map(u.toHTML),
self.wrapMyChannels()
)
}
function renderNetworkChannels() {
return pull(
self.app.streamChannels(),
paramap(function (channel, cb) {
// var subscribed = false
cb(null, [
h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
' '
])
}, 8),
pull.map(u.toHTML),
self.wrapChannels()
)
}
pull(
cat([
ph('section', {}, [
ph('h3', {}, 'Channels:'),
renderMyChannels(),
renderNetworkChannels()
])
]),
this.wrapPage('channels'),
this.respondSink(200, {
'Content-Type': ctype(ext)
})
)
}
Serve.prototype.tags = function (path) {
var self = this
var seen = {}
pull(
ph('section', [
ph('h3', 'Tags'),
pull(
self.app.streamTags(),
pull.map(function (msg) {
return [self.phIdLink(msg.key), ' ']
})
)
]),
this.wrapPage('tags'),
this.respondSink(200)
)
}
Serve.prototype.contacts = function (path) {
var self = this
var id = String(path).substr(1)
var contacts = self.app.contacts.createContactStreams({
id: id,
msgIds: true,
enemies: true
})
var render = self.app.render
pull(
cat([
ph('section', {}, [
ph('h3', {}, ['Contacts: ', self.phIdLink(id)]),
ph('h4', {}, 'Friends'),
render.friendsList('/contacts/')(contacts.friends),
ph('h4', {}, 'Follows'),
render.friendsList('/contacts/')(contacts.follows),
ph('h4', {}, 'Followers'),
render.friendsList('/contacts/')(contacts.followers),
ph('h4', {}, 'Blocks'),
render.friendsList('/contacts/')(contacts.blocks),
ph('h4', {}, 'Blocked by'),
render.friendsList('/contacts/')(contacts.blockers),
contacts.enemies ? [
ph('h4', {}, 'Enemies'),
render.friendsList('/contacts/')(contacts.enemies),
] : ''
])
]),
this.wrapPage('contacts: ' + id),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
Serve.prototype.about = function (path) {
var self = this
var id = decodeURIComponent(String(path).substr(1))
var abouts = self.app.createAboutStreams(id)
var render = self.app.render
function renderAboutOpImage(link) {
if (!link) return
if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link))
return ph('img', {
class: 'ssb-avatar-image',
src: render.imageUrl(link.link),
alt: link.link
+ (link.size ? ' (' + render.formatSize(link.size) + ')' : '')
})
}
function renderAboutOpValue(value) {
if (typeof value === 'object' && value !== null) {
if (u.isRef(value.link)) return self.phIdLink(value.link)
if (value.epoch) return new Date(value.epoch).toUTCString()
}
return ph('code', {}, JSON.stringify(value))
}
function renderAboutOpContent(op) {
if (op.prop === 'image')
return renderAboutOpImage(u.toLink(op.value))
if (op.prop === 'description')
return h('div', {innerHTML: render.markdown(op.value)}).outerHTML
if (op.prop === 'title')
return h('strong', op.value).outerHTML
if (op.prop === 'name')
return h('u', op.value).outerHTML
return renderAboutOpValue(op.value)
}
function renderAboutOp(op) {
return ph('tr', {}, [
ph('td', self.phIdLink(op.author)),
ph('td',
ph('a', {href: render.toUrl(op.id)},
htime(new Date(op.timestamp)))),
ph('td', op.prop),
ph('td', renderAboutOpContent(op))
])
}
pull(
cat([
ph('section', {}, [
ph('h3', {}, ['About: ', self.phIdLink(id)]),
ph('table', {},
pull(abouts.scalars, pull.map(renderAboutOp))
),
pull(
abouts.sets,
pull.map(function (op) {
return h('pre', JSON.stringify(op, 0, 2))
}),
pull.map(u.toHTML)
)
])
]),
this.wrapPage('about: ' + id),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
Serve.prototype.aboutSelf = function (ext) {
var self = this
var id = self.app.sbot.id
var render = self.app.render
self.app.getAbout(id, function gotAbout(err, about) {
if (err) return cb(err)
var data = self.data
var aboutName = about.name ? String(about.name).replace(/^@/, '') : ''
var aboutImageLink = about.imageLink || {}
var name = data.name != null ?
data.name === '' ? null : data.name :
aboutName || null
var image = data.image_upload != null ? {
link: data.image_upload.link,
type: data.image_upload.type,
size: data.image_upload.size
} : data.image_id && data.image_id !== aboutImageLink.link ? {
link: data.image_id,
type: data.image_type,
size: data.image_size
} : aboutImageLink
var imageId = image.link || '&Zq5m3UOWlFfyUoenOL75ukghOdjmv2yxHREkBNrorWM=.sha256'
var description = data.description != null ?
data.description === '' ? null : data.description :
about.description || null
var publicWebHosting = data.publicWebHosting != null ?
data.publicWebHosting === 'false' ? false :
data.publicWebHosting === 'null' ? null : !!data.publicWebHosting :
about.publicWebHosting
var content
if (data.preview || data.preview_raw) {
content = {
type: 'about',
about: id
}
if (name != aboutName) content.name = name
if (image.link != about.image) content.image = image
if (description != about.description) content.description = description
if (publicWebHosting != about.publicWebHosting) content.publicWebHosting = publicWebHosting
}
pull(
ph('section', {}, [
ph('h4', 'Your public profile'),
ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
ph('div', [
'@', ph('input', {id: 'name', name: 'name', placeholder: 'name', value: name})
]),
ph('table', ph('tr', [
ph('td', [
ph('a', {href: render.toUrl(imageId)}, [
ph('img', {
class: 'ssb-avatar-image',
src: render.imageUrl(imageId),
alt: image.link || 'fallback avatar',
title: image.link || 'fallback avatar'
})
])
]),
ph('td', [
image.link ? ph('div', [
ph('small', ph('code', u.escapeHTML(image.link))),
ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
]) : '',
image.size ? [
ph('code', render.formatSize(image.size)),
ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
] : '',
image.type ? [
ph('input', {type: 'hidden', name: 'image_type', value: image.type})
] : '',
ph('div', [
ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
])
])
])),
ph('textarea', {
id: 'description', name: 'description', placeholder: 'description',
rows: Math.max(4, u.rows(description))
}, u.escapeHTML(description)),
ph('div', {
title: 'Allow your messages to be hosted on public viewer websites'
}, [
ph('label', {for: 'publicWebHosting'}, 'Public web hosting: '),
ph('select', {name: 'publicWebHosting', id: 'publicWebHosting'}, [
ph('option', {value: 'true', selected: publicWebHosting}, 'yes'),
ph('option', {value: 'false', selected: publicWebHosting === false}, 'no'),
ph('option', {value: 'null', selected: publicWebHosting == null}, '…'),
])
]),
self.phMsgActions(content),
]),
content ? self.phPreview(content, {raw: data.preview_raw}) : ''
]),
self.wrapPage('about self: ' + id),
self.respondSink(200, {
'Content-Type': ctype('html')
})
)
})
}
Serve.prototype.gathering = function (url) {
var self = this
var id
try {
id = decodeURIComponent(url.substr(1))
} catch(err) {
return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('Gathering ' + id),
self.respondSink(400)
)
}
var q = self.query
var data = self.data || {}
var selfId = self.app.sbot.id
var render = self.app.render
function hashLink(id) {
if (!id) return ''
if (typeof id !== 'string') return u.escapeHTML(JSON.stringify(id))
return h('a', {style: 'font: monospace', href: render.toUrl(id)},
id.substr(0, 8) + '…').outerHTML + ' '
}
self.app.getAbout(id, function (err, about) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.respondSink(400)
)
var sources = about._sources || {}
var start = about.startDateTime
var img = about.imageLink
var startDate = start && new Date(start.epoch)
pull(
ph('section', [
ph('h2', ['Gathering ', hashLink(id)]),
ph('div', ph('a', {href: render.toUrl('/edit-gathering/' + encodeURIComponent(id))}, 'Edit')),
ph('table', [
img ? ph('tr', [
ph('th', 'Image'),
ph('td', sources.image.map(hashLink)),
ph('td', u.isRef(img.link) ? ph('img', {
class: 'ssb-avatar-image',
src: render.imageUrl(img.link),
alt: img.link
+ (img.size ? ' (' + render.formatSize(img.size) + ')' : '')
}) : ph('code', JSON.stringify(img))),
]) : '',
about.title ? ph('tr', [
ph('th', 'Title'),
ph('td', sources.title.map(hashLink)),
ph('td', ph('h1', {style: 'margin: 0'}, u.escapeHTML(about.title)))
]) : '',
about.location ? ph('tr', [
ph('th', 'Location'),
ph('td', sources.location.map(hashLink)),
ph('td', u.escapeHTML(about.location))
]) : '',
start ? ph('tr', [
ph('th', 'Start time'),
ph('td', sources.startDateTime.map(hashLink)),
ph('td', [
ph('code', u.escapeHTML(startDate.toISOString().replace(/ .*/, ''))),
start.tz ? [
'
',
ph('code', u.escapeHTML(start.tz))
] : '',
ph('div', u.escapeHTML(startDate.toString()))
])
]) : '',
about.description ? ph('tr', [
ph('th', 'Description'),
ph('td', sources.description.map(hashLink)),
ph('td', render.markdown(about.description))
]) : '',
/*
about.branch ? ph('tr', [
ph('th', 'Latest updates'),
ph('th'),
ph('td', about.branch.map(hashLink))
]) : '',
*/
about.attendee ? about.attendee.map(function (link, i, rows) {
return ph('tr', [
i === 0 ? ph('th', {rowspan: rows.length}, 'Attendees') : '',
ph('td', hashLink(link.source)),
ph('td', self.phIdLink(link.link))
])
}) : '',
])
]),
self.wrapPage('Gathering ' + id),
self.respondSink(200)
)
})
}
var ignoreDateTimeProps = {
silent: true, // internal to spacetime module
}
function isDateTimeEqual(a, b) {
if (a === b) return true
if ((a && !b) || (!b && a)) return false
for (var k in a) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false
for (var k in b) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false
return true
}
Serve.prototype.editGathering = function (url) {
var self = this
var id
try {
id = decodeURIComponent(url.substr(1))
} catch(err) {
return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('Edit Gathering ' + id),
self.respondSink(400)
)
}
var q = self.query
var data = self.data || {}
var selfId = self.app.sbot.id
var render = self.app.render
self.app.getAbout(id, function (err, about) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.respondSink(400)
)
var aboutImageLink = about.imageLink || {}
var aboutStartDateTime = about.startDateTime || {}
var title = data.title != null ?
data.title === '' ? null : data.title :
about.title || null
var location = data.location != null ?
data.location === '' ? null : data.location :
about.location || null
var image = data.image_upload != null ? {
link: data.image_upload.link,
type: data.image_upload.type,
size: data.image_upload.size
} : data.remove_image ? null :
data.image_id && data.image_id !== aboutImageLink.link ? {
link: data.image_id,
type: data.image_type,
size: data.image_size
} : aboutImageLink
var description = data.description != null ?
data.description === '' ? null : data.description :
about.description || null
// use undefined instead of null to reset these values,
// since they are set as a whole object
var startDateTimeEpoch = data.startDateTimeStr != null ?
data.startDateTimeStr === '' ? null :
new Date(data.startDateTimeStr).getTime() :
aboutStartDateTime.epoch
var startDateTime = startDateTimeEpoch === null ? null
: typeof startDateTimeEpoch === 'number' && !isNaN(startDateTimeEpoch) ? {
epoch: startDateTimeEpoch,
tz: data.startTZ != null ?
data.startTZ === '' ? undefined : data.startTZ :
aboutStartDateTime.tz,
_weekStart: data.startWeekStart != null ?
data.startWeekStart === '' ? undefined : Number(data.startWeekStart) :
aboutStartDateTime._weekStart
} : about.startDateTime || null
var attendeeIds = u.toLinkArray(about.attendee).map(u.linkDest)
var aboutSelfAttending = attendeeIds.indexOf(selfId) !== -1
var selfAttending = data.attending != null ? Boolean(data.attending) :
aboutSelfAttending
var mentionAttendees = data.mention_attendees === '' ? true : Boolean(data.mention_attendees)
var attendeeMentions = mentionAttendees ? attendeeIds.filter(function (id) {
return id !== selfId
}) : []
var additionalMentions = data.mentions ?
u.extractRefs(data.mentions).filter(uniques()) : []
var mentions = attendeeMentions.concat(additionalMentions)
var content
if (data.preview || data.preview_raw) {
content = {
type: 'about',
about: id
}
if (about.recps) content.recps = about.recps
if (about.branch) content.branch = about.branch
if (title != about.title) content.title = title
if (location != about.location) content.location = location
if (image === null) {
if (about.image) content.image = {link: about.image, remove: true}
} else if (image.link != about.image) content.image = image
if (description != about.description) {
content.description = description
var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false})
// don't mention ids already mentioned in the thread
textMentions.forEach(function (link) {
if (mentions.indexOf(link.link) === -1) {
mentions.push(reduceLink(link))
}
})
}
if (!isDateTimeEqual(startDateTime, about.startDateTime)) content.startDateTime = startDateTime
if (mentions.length) content.mentions = mentions
if (selfAttending != aboutSelfAttending) content.attendee = selfAttending ? {
link: selfId
} : {
link: selfId,
remove: true
}
}
var startDateTimeStr = ''
if (startDateTime) try {
startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '')
} catch(e) {}
var startTz = startDateTime && startDateTime.tz || null
var startWeekStart = startDateTime && startDateTime._weekStart || null
pull(
ph('section', [
ph('h2', ['Edit Gathering ', self.phIdLink(id)]),
ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
ph('div', [
ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)})
]),
ph('div', [
ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)})
]),
ph('div', [
ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr),
style: 'font: monospace', size: 30, placeholder: 'Start date time'}),
ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '',
style: 'font: monospace', size: 20, placeholder: 'TZ'}),
ph('input', {name: 'startWeekStart',
value: startWeekStart == null ? '' : u.toString(startWeekStart),
style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'})
]),
ph('table', ph('tr', [
ph('td', [
data.remove_image ? ph('input', {type: 'hidden', name: 'remove_image', value: u.escapeHTML(data.remove_image)}) : '',
image && image.link ? ph('a', {href: render.toUrl(image.link)}, [
ph('img', {
class: 'ssb-avatar-image',
src: render.imageUrl(image.link),
alt: image.link || 'fallback avatar',
title: image.link || 'fallback avatar'
})
]) : ''
]),
ph('td', [
image && image.link ? ph('div', [
ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ',
ph('small', ph('code', u.escapeHTML(image.link))),
ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
]) : '',
image && image.size ? [
ph('code', render.formatSize(image.size)),
ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
] : '',
image && image.type ? [
ph('input', {type: 'hidden', name: 'image_type', value: image.type})
] : '',
ph('div', [
ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
])
])
])),
ph('div', ph('textarea', {
name: 'description', placeholder: 'Description',
cols: 70,
rows: Math.max(5, u.rows(description))
}, u.escapeHTML(description))),
ph('div', ph('select', {name: 'attending'}, [
ph('option', {value: '1', selected: !selfAttending ? 'selected' : undefined}, 'You are not attending'),
ph('option', {value: '1', selected: selfAttending ? 'selected' : undefined}, 'You are attending'),
])),
ph('div', ph('label', {for: 'mention_attendees'}, [
ph('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: '1', checked: mentionAttendees ? 'checked' : undefined}),
' mention attendees'
])),
ph('div', additionalMentions.length > 0 || data.add_mentions ? ph('textarea', {
name: 'mentions', placeholder: 'Additional mentions',
cols: 70, style: 'font: monospace',
rows: Math.max(3, additionalMentions.length + 2),
}, u.escapeHTML(additionalMentions.join('\n'))
) : ph('label', {for: 'additional_mentions'}, [
ph('input', {id: 'additional_mentions', type: 'checkbox', name: 'add_mentions', value: '1'}),
' additional mentions'
])
),
self.phMsgActions(content)
]),
content ? self.phPreview(content, {raw: data.preview_raw}) : ''
]),
self.wrapPage('Edit Gathering ' + id),
self.respondSink(200)
)
})
}
Serve.prototype.newGathering = function () {
var self = this
var q = self.query
var data = self.data || {}
var selfId = self.app.sbot.id
var render = self.app.render
var title = data.title ? u.toString(data.title) : null
var description = data.description ? u.toString(data.description) : null
var location = data.location ? u.toString(data.location) : null
var image = data.image_upload != null ? {
link: data.image_upload.link,
type: data.image_upload.type,
size: data.image_upload.size
} : data.remove_image ? null : data.image_id ? {
link: data.image_id,
type: data.image_type,
size: data.image_size
} : null
var private = Boolean(data.private)
var recps = data.recps ? u.extractRefs(data.recps) : private ? [selfId] : null
// default recps to self id but allow removing it
var startTs = data.startDateTimeStr ?
new Date(data.startDateTimeStr).getTime() : null
var startDateTime = typeof startTs === 'number' && !isNaN(startTs) ? {
epoch: startTs,
tz: data.startTZ ? u.toString(data.startTZ) : undefined,
_weekStart: data.startWeekStart ? Number(data.startWeekStart) : undefined
} : null
var selfAttending = Boolean(data.attending)
var initialMentions = data.mentions ? u.extractRefs(data.mentions) : []
var content
if (data.preview || data.preview_raw) {
content = {
type: 'about'
}
if (private && recps) content.recps = recps
if (title) content.title = title
if (location) content.location = location
if (image) content.image = image
if (description) {
content.description = description
var mentions = ssbMentions(description, {bareFeedNames: false, emoji: false})
.map(reduceLink)
if (mentions.length) content.mentions = mentions
}
if (startDateTime) content.startDateTime = startDateTime
if (selfAttending) content.attendee = {
link: selfId
}
}
var aboutNewContent = {
type: 'gathering',
mentions: initialMentions.length ? initialMentions : undefined
}
var startDateTimeStr = ''
if (startDateTime) try {
startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '')
} catch(e) {}
var startTz = startDateTime && startDateTime.tz || null
var startWeekStart = startDateTime && startDateTime._weekStart || null
pull(
ph('section', [
ph('h2', ['New Gathering']),
ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
ph('div', [
ph('select', {name: 'private'}, [
ph('option', {value: '', selected: !private ? 'selected' : undefined}, 'Public'),
ph('option', {value: '1', selected: private ? 'selected' : undefined}, 'Private')
])
]),
private ? ph('div', ph('textarea', {
name: 'recps', placeholder: 'Recipients',
title: 'Recipient IDs for private gathering',
cols: 70, style: 'font: monospace',
rows: Math.max(3, recps.length + 2),
}, u.escapeHTML(recps.join('\n')) + '\n')) : recps ? ph('input', {
type: 'hidden', name: 'recps', value: recps.join('\n')
}) : null,
ph('div', [
ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)})
]),
ph('div', [
ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)})
]),
ph('div', [
ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr),
style: 'font: monospace', size: 30, placeholder: 'Start date time'}),
ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '',
style: 'font: monospace', size: 20, placeholder: 'TZ'}),
ph('input', {name: 'startWeekStart',
value: startWeekStart == null ? '' : u.toString(startWeekStart),
style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'})
]),
ph('table', ph('tr', [
ph('td', [
image && image.link ? ph('a', {href: render.toUrl(image.link)}, [
ph('img', {
class: 'ssb-avatar-image',
src: render.imageUrl(image.link),
alt: image.link || 'fallback avatar',
title: image.link || 'fallback avatar'
})
]) : ''
]),
ph('td', [
image && image.link ? ph('div', [
ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ',
ph('small', ph('code', u.escapeHTML(image.link))),
ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
]) : '',
image && image.size ? [
ph('code', render.formatSize(image.size)),
ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
] : '',
image && image.type ? [
ph('input', {type: 'hidden', name: 'image_type', value: image.type})
] : '',
ph('div', [
ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
])
])
])),
ph('div', ph('textarea', {
name: 'description', placeholder: 'Description',
cols: 70,
rows: Math.max(5, u.rows(description))
}, u.escapeHTML(description))),
ph('div', ph('label', {for: 'attending'}, [
ph('input', {id: 'attending', type: 'checkbox', name: 'attending',
value: '1', checked: selfAttending ? 'checked' : undefined}),
' attending'
])),
ph('div', ph('textarea', {
name: 'mentions', placeholder: 'Initial mentions',
title: 'SSB feed/blob/message IDs to mention in the gathering root message',
cols: 53, style: 'font: monospace',
rows: Math.max(3, initialMentions.length + 1),
}, u.escapeHTML(initialMentions.join('\n')+'\n'))),
self.phMsgActions(content)
]),
content ? self.phPreview(content, {
raw: data.preview_raw,
aboutNewContent: aboutNewContent
}) : ''
]),
self.wrapPage('New Gathering'),
self.respondSink(200)
)
}
Serve.prototype.block = function (path) {
var self = this
var data = self.data
var id = String(path).substr(1)
try { id = decodeURIComponent(id) }
catch(e) {}
var content
if (data.preview || data.preview_raw) {
content = {
type: 'contact',
contact: id,
blocking: true
}
var reason = typeof data.reason === 'string' ? data.reason : null
if (reason) content.reason = reason
}
function renderDraftLink(draftId) {
return pull.values([
ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)),
title: 'draft link'}, u.escapeHTML(draftId)),
ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ',
])
}
pull(
ph('section', [
ph('h2', ['Block ', self.phIdLink(id)]),
ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
'Reason: ', ph('input', {name: 'reason', value: reason || '',
className: 'wide',
placeholder: 'spam, abuse, etc.'}),
self.phMsgActions(content),
]),
content ? self.phPreview(content, {raw: data.preview_raw}) : ''
]),
self.wrapPage('Block ' + id),
self.respondSink(200)
)
}
Serve.prototype.type = function (path) {
var q = this.query
var type = decodeURIComponent(path.substr(1))
var opts = {
sortByTimestamp: q.sort === 'claimed',
reverse: !q.forwards,
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
type: type,
filter: q.filter,
}
pull(
this.app.sbotMessagesByType(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapType(type),
this.wrapPage('type: ' + type),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
Serve.prototype.links = function (path) {
var q = this.query
var dest = path.substr(1)
var opts = {
dest: dest,
reverse: true,
values: true,
}
if (q.rel) opts.rel = q.rel
pull(
this.app.sbot.links(opts),
this.renderThread(),
this.wrapMessages(),
this.wrapLinks(dest),
this.wrapPage('links: ' + dest),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
Serve.prototype.rawId = function (id) {
var self = this
self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.respondSink(400, {'Content-Type': ctype('html')})
)
return pull(
pull.once(msg),
self.renderRawMsgPage(id),
self.respondSink(200, {
'Content-Type': ctype('html'),
})
)
})
}
Serve.prototype.channel = function (path) {
var channel = decodeURIComponent(String(path).substr(1))
var q = this.query
var gt = Number(q.gt) || -Infinity
var lt = Number(q.lt) || Date.now()
var opts = {
sortByTimestamp: q.sort === 'claimed',
reverse: !q.forwards,
lt: lt,
gt: gt,
channel: channel,
filter: q.filter,
}
pull(
this.app.streamChannel(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapChannel(channel),
this.wrapPage('#' + channel),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
function threadHeads(msgs, rootId, opts) {
var includeVotes = opts && opts.includeVotes
return sort.heads(msgs.filter(function (msg) {
var c = msg.value && msg.value.content
return (c && (
c.type === 'web-root' ? c.site === rootId :
c.type === 'talenet-idea-comment_reply' ? c.ideaKey === rootId :
c.type === 'vote' ? includeVotes :
c.root === rootId))
|| msg.key === rootId
}))
}
Serve.prototype.streamThreadWithComposer = function (opts) {
var self = this
var id = opts.root
var threadHeadsOpts = {includeVotes: self.app.voteBranches}
return ph('table', {class: 'ssb-msgs'}, u.readNext(next))
function next(cb) {
self.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) {
if (err && err.name === 'NotFoundError') err = null, rootMsg = {
key: id, value: {content: false}}
if (err) return cb(new Error(err.stack || err))
if (!rootMsg) {
console.log('id', id, 'opts', opts)
}
var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
var recps = rootContent && rootContent.recps
|| ((rootMsg.value.private || typeof rootMsg.value.content === 'string')
? [rootMsg.value.author, self.app.sbot.id].filter(uniques())
: undefined)
var threadRootId = rootContent && (
rootContent.type === 'web-root' ? rootContent.site : rootContent.root
) || id
var channel = opts.channel
pull(
self.noThread ? pull.once(rootMsg) : self.app.getThread(rootMsg),
pull.unique('key'),
self.app.unboxMessages(),
pull.through(function (msg) {
var c = msg && msg.value.content
if (!channel && c && c.channel) channel = c.channel
}),
pull.collect(function (err, links) {
if (err) return gotLinks(err)
if (!self.useOoo) return gotLinks(null, links)
self.app.expandOoo({msgs: links, dest: id}, gotLinks)
})
)
function gotLinks(err, links) {
if (err) return cb(new Error(err.stack))
var branches = threadHeads(links, threadRootId, threadHeadsOpts)
cb(null, pull(
pull.values(sort(links)),
self.app.voteBranches && pull.map(function (link) {
var o = {}
for (var k in link) o[k] = link[k]
o.threadBranches = branches
o.threadRoot = threadRootId
return o
}),
self.renderThread({
msgId: id,
branches: branches,
links: links,
}),
self.wrapMessages(),
self.wrapThread({
recps: recps,
root: threadRootId,
post: id,
branches: branches,
links: links,
postBranches: threadRootId !== id && threadHeads(links, id, threadHeadsOpts),
placeholder: opts.placeholder,
channel: channel,
})
))
}
})
}
}
Serve.prototype.streamMsg = function (id) {
var self = this
return pull(
self.app.pullGetMsg(id),
self.renderThread({
msgId: id,
single: self.query.single != null
}),
self.wrapMessages()
)
}
Serve.prototype.id = function (id, path) {
var self = this
if (self.query.raw != null) return self.rawId(id)
pull(
self.query.single != null
? self.streamMsg(id)
: self.streamThreadWithComposer({root: id}),
self.wrapPage(id),
self.respondSink(200)
)
}
Serve.prototype.userFeed = function (id, path) {
var self = this
var q = self.query
var opts = {
id: id,
reverse: !q.forwards,
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
feed: id,
filter: q.filter,
}
var isScrolled = q.lt || q.gt
self.app.getAbout(id, function (err, about) {
if (err) self.app.error(err)
pull(
self.app.sbotCreateUserStream(opts),
self.renderThreadPaginated(opts, id, q),
self.wrapMessages(),
self.wrapUserFeed(isScrolled, id),
self.wrapPage(about && about.name || id),
self.respondSink(200)
)
})
}
Serve.prototype.file = function (file) {
var self = this
fs.stat(file, function (err, stat) {
if (err && err.code === 'ENOENT') return self.respond(404, 'Not found')
if (err) return self.respond(500, err.stack || err)
if (!stat.isFile()) return self.respond(403, 'May only load files')
if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified')
self.res.writeHead(200, {
'Content-Type': ctype(file),
'Content-Length': stat.size,
'Last-Modified': stat.mtime.toGMTString()
})
fs.createReadStream(file).pipe(self.res)
})
}
Serve.prototype.static = function (file) {
this.file(path.join(__dirname, '../static', file))
}
Serve.prototype.emoji = function (emoji) {
serveEmoji(this.req, this.res, emoji)
}
Serve.prototype.highlight = function (dirs) {
this.file(path.join(hlCssDir, dirs))
}
Serve.prototype.blob = function (id, path) {
var self = this
var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+')
var etag = '"' + id + (path || '') + (unbox || '') + '"'
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
var key
if (path) {
try { path = decodeURIComponent(path) } catch(e) {}
if (path[0] === '#') {
unbox = path.substr(1)
} else {
return self.respond(400, 'Bad blob request')
}
}
if (unbox) {
try {
key = new Buffer(unbox, 'base64')
} catch(err) {
return self.respond(400, err.message)
}
if (key.length !== 32) {
return self.respond(400, 'Bad blob key')
}
}
self.app.wantSizeBlob(id, function (err, size) {
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
else return self.respond(500, err.message || err)
}
self.res.setHeader('Accept-Ranges', 'bytes')
var range = self.req.headers.range
if (range) {
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
// TODO: support multiple ranges
var m = /^bytes=([0-9]*)-([0-9]*)$/.exec(range)
if (!m) return self.respond(416, 'Unable to parse range')
var start = m[1]
var last = m[2]
if (start === '') {
start = size - last
last = size - 1
} else if (last === '') {
start = Number(start)
last = size - 1
} else {
start = Number(start)
last = Number(last)
}
if (start > size || last >= size) return res.writeHead(416, 'Range not satisfiable')
var end = last + 1
var length = end - start
var wroteHeaders = false
pull(
// TODO: figure out how to use readBlobSlice for private blob range request
key ? pull(
self.app.getBlob(id, key),
u.pullSlice(start, end)
) : self.app.readBlobSlice({
link: id,
size: size
}, {
start: start,
end: end,
}),
pull.through(function (buf) {
if (wroteHeaders) return
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('ETag', etag)
self.res.setHeader('Content-Length', length)
self.res.setHeader('Content-Range', 'bytes ' + start + '-' + last + '/' + size)
self.res.writeHead(206)
wroteHeaders = true
}),
pull.map(Buffer),
self.respondSink()
)
} else {
pull(
self.app.getBlob(id, key),
pull.map(Buffer),
ident(gotType),
self.respondSink()
)
function gotType(type) {
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
// don't serve size for encrypted blob, because it refers to the size of
// the ciphertext
if (typeof size === 'number' && !key)
self.res.setHeader('Content-Length', size)
if (self.query.filename) self.res.setHeader('Content-Disposition',
'inline; filename='+encodeDispositionFilename(self.query.filename))
if (self.query.gzip)
self.res.setHeader('Content-Encoding', 'gzip')
if (self.query.contentType)
self.res.setHeader('Content-Type', self.query.contentType)
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('etag', etag)
self.res.writeHead(200)
}
}
})
}
Serve.prototype.image = function (path) {
var self = this
var id, key
var m = urlIdRegex.exec(path)
if (m && m[2] === '&') id = m[1], path = m[3]
var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+')
var etag = '"image-' + id + (path || '') + (unbox || '') + '"'
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
if (path) {
try { path = decodeURIComponent(path) } catch(e) {}
if (path[0] === '#') {
unbox = path.substr(1)
} else {
return self.respond(400, 'Bad blob request')
}
}
if (unbox) {
try {
key = new Buffer(unbox, 'base64')
} catch(err) {
return self.respond(400, err.message)
}
if (key.length !== 32) {
return self.respond(400, 'Bad blob key')
}
}
self.app.wantSizeBlob(id, function (err, size) {
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
else return self.respond(500, err.message || err)
}
var done = multicb({pluck: 1, spread: true})
var heresTheData = done()
var heresTheType = done().bind(self, null)
pull(
self.app.getBlob(id, key),
pull.map(Buffer),
ident(heresTheType),
pull.collect(onFullBuffer)
)
function onFullBuffer (err, buffer) {
if (err) return heresTheData(err)
buffer = Buffer.concat(buffer)
try {
jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
if (!err) buffer = rotatedBuffer
heresTheData(null, buffer)
pull(
pull.once(buffer),
self.respondSink()
)
})
} catch (err) {
console.trace(err)
self.respond(500, err.message || err)
}
}
done(function (err, data, type) {
if (err) {
console.trace(err)
self.respond(500, err.message || err)
return
}
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
self.res.setHeader('Content-Length', data.length)
if (self.query.filename) self.res.setHeader('Content-Disposition',
'inline; filename='+encodeDispositionFilename(self.query.filename))
if (self.query.gzip)
self.res.setHeader('Content-Encoding', 'gzip')
if (self.query.contentType)
self.res.setHeader('Content-Type', self.query.contentType)
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('ETag', etag)
self.res.writeHead(200)
})
})
}
Serve.prototype.ifModified = function (lastMod) {
var ifModSince = this.req.headers['if-modified-since']
if (!ifModSince) return false
var d = new Date(ifModSince)
return d && Math.floor(d/1000) >= Math.floor(lastMod/1000)
}
Serve.prototype.wrapMessages = function () {
var self = this
return u.hyperwrap(function (content, cb) {
cb(null, h('table.ssb-msgs', content))
})
}
Serve.prototype.renderThread = function (opts) {
return pull(
this.app.render.renderFeeds({
raw: false,
full: this.query.full != null,
feed: opts && opts.feed,
msgId: opts && opts.msgId,
filter: this.query.filter,
limit: Number(this.query.limit),
serve: this,
links: opts && opts.links,
single: opts && opts.single,
branches: opts && opts.branches,
}),
pull.map(u.toHTML)
)
}
Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
var self = this
function linkA(opts, name) {
var q1 = u.mergeOpts(q, opts)
return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit)
}
function links(opts) {
var limit = opts.limit || q.limit || 10
return h('tr', h('td.paginate', {colspan: 3},
opts.forwards ? '↑ newer ' : '↓ older ',
linkA(u.mergeOpts(opts, {limit: 1})), ' ',
linkA(u.mergeOpts(opts, {limit: 10})), ' ',
linkA(u.mergeOpts(opts, {limit: 100}))
))
}
return pull(
self.app.filterMessages({
feed: opts && opts.feed,
msgId: opts && opts.msgId,
filter: this.query.filter,
limit: Number(this.query.limit) || 12,
}),
paginate(
function onFirst(msg, cb) {
var num = feedId ? msg.value.sequence :
opts.sortByTimestamp ? msg.value.timestamp :
msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: num,
gt: null,
forwards: null,
filter: opts.filter,
}))
} else {
cb(null, links({
lt: null,
gt: num,
forwards: 1,
filter: opts.filter,
}))
}
},
this.app.render.renderFeeds({
raw: false,
full: this.query.full != null,
feed: opts && opts.feed,
msgId: opts && opts.msgId,
filter: this.query.filter,
serve: this,
limit: Number(this.query.limit) || 12,
}),
function onLast(msg, cb) {
var num = feedId ? msg.value.sequence :
opts.sortByTimestamp ? msg.value.timestamp :
msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: null,
gt: num,
forwards: 1,
filter: opts.filter,
}))
} else {
cb(null, links({
lt: num,
gt: null,
forwards: null,
filter: opts.filter,
}))
}
},
function onEmpty(cb) {
if (q.forwards) {
cb(null, links({
gt: null,
lt: opts.gt + 1,
forwards: null,
filter: opts.filter,
}))
} else {
cb(null, links({
gt: opts.lt - 1,
lt: null,
forwards: 1,
filter: opts.filter,
}))
}
}
),
pull.map(u.toHTML)
)
}
Serve.prototype.renderRawMsgPage = function (id) {
var showMarkdownSource = (this.query.raw === 'md')
var raw = !showMarkdownSource
return pull(
this.app.render.renderFeeds({
raw: raw,
msgId: id,
filter: this.query.filter,
serve: this,
markdownSource: showMarkdownSource
}),
pull.map(u.toHTML),
this.wrapMessages(),
this.wrapPage(id)
)
}
function catchHTMLError() {
return function (read) {
var ended
return function (abort, cb) {
if (ended) return cb(ended)
read(abort, function (end, data) {
if (!end || end === true) {
try { return cb(end, data) }
catch(e) { return console.trace(e) }
}
ended = true
cb(null, u.renderError(end).outerHTML)
})
}
}
}
function catchTextError() {
return function (read) {
var ended
return function (abort, cb) {
if (ended) return cb(ended)
read(abort, function (end, data) {
if (!end || end === true) return cb(end, data)
ended = true
cb(null, end.stack + '\n')
})
}
}
}
function styles() {
return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8')
}
Serve.prototype.appendFooter = function () {
var self = this
return function (read) {
if (self.noFooter) return read
return cat([read, u.readNext(function (cb) {
var ms = new Date() - self.startDate
cb(null, pull.once(h('footer',
h('a', {href: pkg.homepage}, pkg.name), ' · ',
ms/1000 + 's'
).outerHTML))
})])
}
}
Serve.prototype.wrapPage = function (title, searchQ) {
var self = this
var render = self.app.render
return pull(
catchHTMLError(),
self.appendFooter(),
u.hyperwrap(function (content, cb) {
var done = multicb({pluck: 1, spread: true})
done()(null, h('html', h('head',
h('meta', {charset: 'utf-8'}),
h('meta', {name: 'referrer', content: 'no-referrer'}),
h('title', title),
h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}),
h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}),
h('style', styles()),
h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')})
),
h('body' + (self.printView ? '.print-view' : ''),
self.noNav ? '' :
h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'},
self.app.navLinks.map(function (link, i) {
return [i == 0 ? '' : ' ',
link === 'self' ? render.idLink(self.app.sbot.id, done()) :
link === 'searchbox' ? h('input.search-input',
{name: 'q', value: searchQ, placeholder: 'search'}) :
link === 'search' ? h('a', {href: render.toUrl('/advsearch')}, 'search') :
typeof link === 'string' ? h('a', {href: render.toUrl('/' + link)}, link) :
link ? h('a', {href: render.toUrl(link.url)}, link.name) : ''
]
})
)),
self.query.published ? h('div',
'published ',
render.msgIdLink(self.query.published, done())
) : '',
// self.note,
content
)))
done(cb)
})
)
}
Serve.prototype.phIdLink = function (id, opts) {
return pull(
pull.once(id),
this.renderIdsList(opts)
)
}
Serve.prototype.phIdAvatar = function (id) {
var self = this
return u.readNext(function (cb) {
var el = self.app.render.avatarImage(id, function (err) {
if (err) return cb(err)
cb(null, pull.once(u.toHTML(el)))
})
})
}
Serve.prototype.friends = function (path) {
var self = this
var friends = self.app.sbot.friends
if (!friends) return pull(
pull.once('missing ssb-friends plugin'),
this.wrapPage('friends'),
self.respondSink(400)
)
if (!friends.createFriendStream) return pull(
pull.once('missing friends.createFriendStream method'),
this.wrapPage('friends'),
self.respondSink(400)
)
pull(
friends.createFriendStream({hops: 1}),
self.renderIdsList(),
u.hyperwrap(function (items, cb) {
cb(null, [
h('section',
h('h3', 'Friends')
),
h('section', items)
])
}),
this.wrapPage('friends'),
this.respondSink(200, {
'Content-Type': ctype('html')
})
)
}
Serve.prototype.renderIdsList = function (opts) {
var self = this
return pull(
paramap(function (id, cb) {
self.app.render.getNameLink(id, opts, cb)
}, 8),
pull.map(function (el) {
return [el, ' ']
}),
pull.map(u.toHTML)
)
}
Serve.prototype.aboutDescription = function (id) {
var self = this
return u.readNext(function (cb) {
self.app.getAbout(id, function (err, about) {
if (err) return cb(err)
if (!about.description) return cb(null, pull.empty())
cb(null, ph('div', self.app.render.markdown(about.description)))
})
})
}
Serve.prototype.followInfo = function (id, myId) {
var self = this
return u.readNext(function (cb) {
var done = multicb({pluck: 1, spread: true})
self.app.getContact(myId, id, done())
self.app.getContact(id, myId, done())
self.app.isMuted(id, done())
done(function (err, contactToThem, contactFromThem, isMuted) {
if (err) return cb(err)
cb(null, ph('form', {action: '', method: 'post'}, [
contactFromThem ? contactToThem ? 'friend ' : 'follows you ' :
contactFromThem === false ? 'blocks you ' : '',
ph('input', {type: 'hidden', name: 'action', value: 'contact'}),
ph('input', {type: 'hidden', name: 'contact', value: id}),
ph('input', {type: 'submit',
name: contactToThem ? 'unfollow' : 'follow',
value: contactToThem ? 'unfollow' : 'follow'}), ' ',
contactToThem === false
? ph('input', {type: 'submit', name: 'unblock', value: 'unblock'})
: ph('input', {type: 'submit', name: 'block1', value: 'block…'}), ' ',
ph('input', {type: 'submit',
name: isMuted ? 'unmute' : 'mute',
value: isMuted ? 'unmute' : 'mute',
title: isMuted ? 'unmute (private unblock)' : 'mute (private block)'}), ' ',
ph('input', {type: 'submit',
name: self.requestedReplicate ? 'unreplicate' : 'replicate',
value: self.requestedReplicate ? 'unreplicate' : 'replicate',
title: self.requestedReplicate
? 'Temporarily cancel replicating this feed'
: 'Temporarily replicate this feed'})
]))
})
})
}
Serve.prototype.friendInfo = function (id, myId) {
var first = false
return pull(
this.app.contacts.createFollowedFollowersStream(myId, id),
this.app.render.friendsList(),
pull.map(function (html) {
if (!first) {
first = true
return 'followed by your friends: ' + html
}
return html
})
)
}
Serve.prototype.wrapUserFeed = function (isScrolled, id) {
var self = this
var myId = self.app.sbot.id
var render = self.app.render
return function (thread) {
return cat([
ph('section', {class: 'ssb-feed'}, ph('table', [
isScrolled ? '' : ph('tr', [
ph('td', self.phIdAvatar(id)),
ph('td', {class: 'feed-about'}, [
ph('h3', {class: 'feed-name'},
ph('strong', self.phIdLink(id))),
ph('code', ph('small', id)),
self.aboutDescription(id)
])
]),
ph('tr', [
ph('td'),
ph('td', [
ph('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ',
ph('a', {href: render.toUrl('/about/' + id)}, 'about'),
id === myId ? [' ',
ph('a', {href: render.toUrl('/about-self')}, 'about-self')] : '',
!isScrolled ? u.readNext(function (cb) {
self.app.isPub(id, function (err, isPub) {
if (err) return cb(err)
if (!isPub) return cb(null, pull.empty())
cb(null, ph('span', [' ', ph('a', {href: render.toUrl('/pub/' + id)}, 'pub')]))
})
}) : ''
])
]),
ph('tr', [
ph('td'),
ph('td',
ph('form', {action: render.toUrl('/advsearch'), method: 'get'}, [
ph('input', {type: 'hidden', name: 'source', value: id}),
ph('input', {type: 'text', name: 'text', placeholder: 'text'}),
ph('input', {type: 'submit', value: 'search'})
])
)
]),
isScrolled || id === myId ? '' : [
ph('tr', [
ph('td'),
ph('td', {class: 'follow-info'}, self.followInfo(id, myId))
/*
]),
ph('tr', [
ph('td'),
ph('td', self.friendInfo(id, myId))
*/
])
]
])),
thread
])
}
}
Serve.prototype.git = function (url) {
var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url)
switch (m[1]) {
case 'object': return this.gitObject(m[2])
case 'commit': return this.gitCommit(m[2])
case 'tag': return this.gitTag(m[2])
case 'tree': return this.gitTree(m[2])
case 'blob': return this.gitBlob(m[2])
case 'raw': return this.gitRaw(m[2])
case 'diff': return this.gitDiff(m[2])
case 'signature': return this.gitSignature(m[2])
case 'line-comment': return this.gitLineComment(m[2])
default: return this.respond(404, 'Not found')
}
}
Serve.prototype.gitRaw = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
pull.once('\'' + rev + '\' is not a git object id'),
self.respondSink(400, {'Content-Type': 'text/plain'})
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
self.app.git.openObject({
obj: rev,
msg: self.query.msg,
}, function (err, obj) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(err.stack),
self.respondSink(400, {'Content-Type': 'text/plain'})
)
pull(
self.app.git.readObject(obj),
catchTextError(),
ident(function (type) {
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('ETag', rev)
self.res.writeHead(200)
}),
self.respondSink()
)
})
}
Serve.prototype.gitAuthorLink = function (author) {
if (author.feed) {
var myName = this.app.getNameSync(author.feed)
var sigil = author.name === author.localpart ? '@' : ''
return ph('a', {
href: this.app.render.toUrl(author.feed),
title: author.localpart + (myName ? ' (' + myName + ')' : '')
}, u.escapeHTML(sigil + author.name))
} else {
return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)},
u.escapeHTML(author.name))
}
}
Serve.prototype.gitObject = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
ph('div.error', 'rev is not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
if (self.query.search) {
return self.app.git.getObjectMsg({
obj: rev,
headMsgId: self.query.msg,
}, function (err, msg) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
var path = '/git/object/' + rev
+ '?msg=' + encodeURIComponent(msg.key)
return self.redirect(self.app.render.toUrl(path))
})
}
self.app.git.openObject({
obj: rev,
msg: self.query.msg,
}, function (err, obj) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
self.app.git.statObject(obj, function (err, stat) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
var path = '/git/' + stat.type + '/' + rev
+ '?msg=' + encodeURIComponent(self.query.msg)
return self.redirect(self.app.render.toUrl(path))
})
})
}
Serve.prototype.gitSignature = function (id) {
var self = this
if (!/[0-9a-f]{24}/.test(id)) {
return pull(
ph('div.error', 'not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git signature for ' + id),
self.respondSink(400)
)
self.app.git.openObject({
obj: id,
msg: self.query.msg,
type: self.query.type,
}, function (err, obj) {
if (err) return handleError(err)
var msgDate = new Date(obj.msg.value.timestamp)
self.app.verifyGitObjectSignature(obj, function (err, verification) {
if (err) return handleError(err)
var objPath = '/git/object/' + id + '?msg=' + encodeURIComponent(obj.msg.key)
pull(
ph('section', [
ph('h3', [
ph('a', {href: self.app.render.toUrl(objPath)}, id), ': ',
ph('a', {href: ''}, 'signature')
]),
ph('div', [
self.phIdLink(obj.msg.value.author), ' pushed ',
ph('a', {
href: self.app.render.toUrl(obj.msg.key),
title: msgDate.toLocaleString(),
}, htime(msgDate))
]),
ph('pre', u.escapeHTML(verification.output))
/*
verification.goodsig ? 'good' : 'bad',
ph('pre', u.escapeHTML(verification.status))
*/
]),
self.wrapPage('git signature for ' + id),
self.respondSink(200)
)
})
})
function handleError(err) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git signature for ' + id),
self.respondSink(400)
)
}
}
Serve.prototype.gitCommit = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
ph('div.error', 'rev is not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git commit ' + rev),
self.respondSink(400)
)
if (self.query.search) {
return self.app.git.getObjectMsg({
obj: rev,
headMsgId: self.query.msg,
}, function (err, msg) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git commit ' + rev),
self.respondSink(400)
)
var path = '/git/commit/' + rev
+ '?msg=' + encodeURIComponent(msg.key)
return self.redirect(self.app.render.toUrl(path))
})
}
self.app.git.openObject({
obj: rev,
msg: self.query.msg,
type: 'commit',
}, function (err, obj) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git commit ' + rev),
self.respondSink(400)
)
var msgDate = new Date(obj.msg.value.timestamp)
self.app.git.getCommit(obj, function (err, commit) {
var missingBlobs
if (err && err.name === 'BlobNotFoundError')
missingBlobs = err.links, err = null
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git commit ' + rev),
self.respondSink(400)
)
pull(
ph('section', [
ph('h3', ph('a', {href: ''}, rev)),
ph('div', [
self.phIdLink(obj.msg.value.author), ' pushed ',
ph('a', {
href: self.app.render.toUrl(obj.msg.key),
title: msgDate.toLocaleString(),
}, htime(msgDate))
]),
missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
ph('div', [
self.gitAuthorLink(commit.committer),
' committed ',
ph('span', {title: commit.committer.date.toLocaleString()},
htime(commit.committer.date)),
' in ', commit.committer.tz
]),
commit.author ? ph('div', [
self.gitAuthorLink(commit.author),
' authored ',
ph('span', {title: commit.author.date.toLocaleString()},
htime(commit.author.date)),
' in ', commit.author.tz
]) : '',
commit.parents.length ? ph('div', ['parents: ', pull(
pull.values(commit.parents),
self.gitObjectLinks(obj.msg.key, 'commit')
)]) : '',
commit.tree ? ph('div', ['tree: ', pull(
pull.once(commit.tree),
self.gitObjectLinks(obj.msg.key, 'tree')
)]) : '',
commit.gpgsig ? ph('div', [
ph('a', {href: self.app.render.toUrl(
'/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg)
)}, 'signature'),
commit.signatureVersion ? [' from ', ph('code', u.escapeHTML(commit.signatureVersion))] : ''
]) : '',
h('blockquote',
self.app.render.gitCommitBody(commit.body)).outerHTML,
ph('h4', 'files'),
ph('table', pull(
self.app.git.readCommitChanges(commit),
pull.map(function (file) {
var msg = file.msg || obj.msg
return ph('tr', [
ph('td', ph('code', u.escapeHTML(file.name))),
ph('td', file.deleted ? 'deleted'
: file.created ?
ph('a', {href:
self.app.render.toUrl('/git/blob/'
+ (file.hash[1] || file.hash[0])
+ '?msg=' + encodeURIComponent(msg.key))
+ '&commit=' + rev
+ '&path=' + encodeURIComponent(file.name)
+ '&search=1'
}, 'created')
: file.hash ?
ph('a', {href:
self.app.render.toUrl('/git/diff/'
+ file.hash[0] + '..' + file.hash[1]
+ '?msg=' + encodeURIComponent(msg.key))
+ '&commit=' + rev
+ '&path=' + encodeURIComponent(file.name)
+ '&search=1'
}, 'changed')
: file.mode ? 'mode changed'
: JSON.stringify(file))
])
}),
Catch(function (err) {
if (err && err.name === 'ObjectNotFoundError') return
if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
return false
})
))
]
]),
self.wrapPage('git commit ' + rev),
self.respondSink(missingBlobs ? 409 : 200)
)
})
})
}
Serve.prototype.gitTag = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
ph('div.error', 'rev is not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git tag ' + rev),
self.respondSink(400)
)
if (self.query.search) {
return self.app.git.getObjectMsg({
obj: rev,
headMsgId: self.query.msg,
}, function (err, msg) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git tag ' + rev),
self.respondSink(400)
)
var path = '/git/tag/' + rev
+ '?msg=' + encodeURIComponent(msg.key)
return self.redirect(self.app.render.toUrl(path))
})
}
self.app.git.openObject({
obj: rev,
msg: self.query.msg,
type: 'tag',
}, function (err, obj) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git tag ' + rev),
self.respondSink(400)
)
var msgDate = new Date(obj.msg.value.timestamp)
self.app.git.getTag(obj, function (err, tag) {
if (err && err.message === 'expected type \'tag\' but found \'commit\'') {
var path = '/git/commit/' + rev
+ '?msg=' + encodeURIComponent(self.query.msg)
return self.redirect(self.app.render.toUrl(path))
}
var missingBlobs
if (err && err.name === 'BlobNotFoundError')
missingBlobs = err.links, err = null
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git tag ' + rev),
self.respondSink(400)
)
pull(
ph('section', [
ph('h3', ph('a', {href: ''}, rev)),
ph('div', [
self.phIdLink(obj.msg.value.author), ' pushed ',
ph('a', {
href: self.app.render.toUrl(obj.msg.key),
title: msgDate.toLocaleString(),
}, htime(msgDate))
]),
missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
ph('div', [
self.gitAuthorLink(tag.tagger),
' tagged ',
ph('span', {title: tag.tagger.date.toLocaleString()},
htime(tag.tagger.date)),
' in ', tag.tagger.tz
]),
tag.type, ' ',
pull(
pull.once(tag.object),
self.gitObjectLinks(obj.msg.key, tag.type)
), ' ',
ph('code', u.escapeHTML(tag.tag)),
tag.gpgsig ? ph('div', [
ph('a', {href: self.app.render.toUrl(
'/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg)
)}, 'signature'),
tag.signatureVersion ? [' from ', ph('code', u.escapeHTML(tag.signatureVersion))] : ''
]) : '',
h('pre', self.app.render.linkify(tag.body)).outerHTML,
]
]),
self.wrapPage('git tag ' + rev),
self.respondSink(missingBlobs ? 409 : 200)
)
})
})
}
Serve.prototype.gitTree = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
ph('div.error', 'rev is not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git tree ' + rev),
self.respondSink(400)
)
self.app.git.openObject({
obj: rev,
msg: self.query.msg,
}, function (err, obj) {
var missingBlobs
if (err && err.name === 'BlobNotFoundError')
missingBlobs = err.links, err = null
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git tree ' + rev),
self.respondSink(400)
)
var msgDate = new Date(obj.msg.value.timestamp)
pull(
ph('section', [
ph('h3', ph('a', {href: ''}, rev)),
ph('div', [
self.phIdLink(obj.msg.value.author), ' ',
ph('a', {
href: self.app.render.toUrl(obj.msg.key),
title: msgDate.toLocaleString(),
}, htime(msgDate))
]),
missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [
pull(
self.app.git.readTreeFull(obj),
pull.map(function (item) {
if (!item.msg) return ph('tr', [
ph('td',
u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')),
ph('td', u.escapeHTML(item.hash)),
ph('td', 'missing')
])
var ext = item.name.replace(/.*\./, '')
var path = '/git/' + item.type + '/' + item.hash
+ '?msg=' + encodeURIComponent(item.msg.key)
+ (ext ? '&ext=' + ext : '')
var fileDate = new Date(item.msg.value.timestamp)
return ph('tr', [
ph('td',
ph('a', {href: self.app.render.toUrl(path)},
u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))),
ph('td',
self.phIdLink(item.msg.value.author)),
ph('td',
ph('a', {
href: self.app.render.toUrl(item.msg.key),
title: fileDate.toLocaleString(),
}, htime(fileDate))
),
])
}),
Catch(function (err) {
if (err && err.name === 'ObjectNotFoundError') return
if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
return false
})
)
]),
]),
self.wrapPage('git tree ' + rev),
self.respondSink(missingBlobs ? 409 : 200)
)
})
}
Serve.prototype.gitBlob = function (rev) {
var self = this
if (!/[0-9a-f]{24}/.test(rev)) {
return pull(
ph('div.error', 'rev is not a git object id'),
self.wrapPage('git'),
self.respondSink(400)
)
}
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
if (self.query.search) {
return self.app.git.getObjectMsg({
obj: rev,
headMsgId: self.query.msg,
}, function (err, msg) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git blob ' + rev),
self.respondSink(400)
)
var path = '/git/blob/' + rev
+ '?msg=' + encodeURIComponent(msg.key)
return self.redirect(self.app.render.toUrl(path))
})
}
self.getMsgDecryptedMaybeOoo(self.query.msg, function (err, msg) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
var msgDate = new Date(msg.value.timestamp)
self.app.git.openObject({
obj: rev,
msg: msg.key,
}, function (err, obj) {
var missingBlobs
if (err && err.name === 'BlobNotFoundError')
missingBlobs = err.links, err = null
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git object ' + rev),
self.respondSink(400)
)
pull(
ph('section', [
ph('h3', ph('a', {href: ''}, rev)),
ph('div', [
self.phIdLink(msg.value.author), ' ',
ph('a', {
href: self.app.render.toUrl(msg.key),
title: msgDate.toLocaleString(),
}, htime(msgDate))
]),
missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull(
self.app.git.readObject(obj),
self.wrapBinary({
obj: obj,
rawUrl: self.app.render.toUrl('/git/raw/' + rev
+ '?msg=' + encodeURIComponent(msg.key)),
ext: self.query.ext
})
),
]),
self.wrapPage('git blob ' + rev),
self.respondSink(200)
)
})
})
}
Serve.prototype.gitDiff = function (revs) {
var self = this
var parts = revs.split('..')
if (parts.length !== 2) return pull(
ph('div.error', 'revs should be ..'),
self.wrapPage('git diff'),
self.respondSink(400)
)
var rev1 = parts[0]
var rev2 = parts[1]
if (!/[0-9a-f]{24}/.test(rev1)) return pull(
ph('div.error', 'rev 1 is not a git object id'),
self.wrapPage('git diff'),
self.respondSink(400)
)
if (!/[0-9a-f]{24}/.test(rev2)) return pull(
ph('div.error', 'rev 2 is not a git object id'),
self.wrapPage('git diff'),
self.respondSink(400)
)
if (!u.isRef(self.query.msg)) return pull(
ph('div.error', 'missing message id'),
self.wrapPage('git diff'),
self.respondSink(400)
)
var done = multicb({pluck: 1, spread: true})
// the msg qs param should point to the message for rev2 object. the msg for
// rev1 object we will have to look up.
self.app.git.getObjectMsg({
obj: rev1,
headMsgId: self.query.msg,
type: 'blob',
}, done())
self.getMsgDecryptedMaybeOoo(self.query.msg, done())
done(function (err, msg1, msg2) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('git diff ' + revs),
self.respondSink(400)
)
var msg1Date = new Date(msg1.value.timestamp)
var msg2Date = new Date(msg2.value.timestamp)
var revsShort = rev1.substr(0, 8) + '..' + rev2.substr(0, 8)
var path = self.query.path && String(self.query.path)
var ext = path && path.replace(/^[^.\/]*/, '')
var blob1Url = '/git/blob/' + rev1 +
'?msg=' + encodeURIComponent(msg1.key) +
(ext ? '&ext=' + encodeURIComponent(ext) : '')
var blob2Url = '/git/blob/' + rev2 +
'?msg=' + encodeURIComponent(msg2.key) +
(ext ? '&ext=' + encodeURIComponent(ext) : '')
pull(
ph('section', [
ph('h3', ph('a', {href: ''}, revsShort)),
ph('div', [
ph('a', {
href: self.app.render.toUrl(blob1Url)
}, rev1), ' ',
self.phIdLink(msg1.value.author), ' ',
ph('a', {
href: self.app.render.toUrl(msg1.key),
title: msg1Date.toLocaleString(),
}, htime(msg1Date))
]),
ph('div', [
ph('a', {
href: self.app.render.toUrl(blob2Url)
}, rev2), ' ',
self.phIdLink(msg2.value.author), ' ',
ph('a', {
href: self.app.render.toUrl(msg2.key),
title: msg2Date.toLocaleString(),
}, htime(msg2Date))
]),
u.readNext(function (cb) {
var done = multicb({pluck: 1, spread: true})
self.app.git.openObject({
obj: rev1,
msg: msg1.key,
}, done())
self.app.git.openObject({
obj: rev2,
msg: msg2.key,
}, done())
/*
self.app.git.guessCommitAndPath({
obj: rev2,
msg: msg2.key,
}, done())
*/
done(function (err, obj1, obj2/*, info2*/) {
if (err && err.name === 'BlobNotFoundError')
return cb(null, self.askWantBlobsForm(err.links))
if (err) return cb(err)
var done = multicb({pluck: 1, spread: true})
pull.collect(done())(self.app.git.readObject(obj1))
pull.collect(done())(self.app.git.readObject(obj2))
self.app.getLineComments({obj: obj2, hash: rev2}, done())
done(function (err, bufs1, bufs2, lineComments) {
if (err) return cb(err)
var str1 = Buffer.concat(bufs1, obj1.length).toString('utf8')
var str2 = Buffer.concat(bufs2, obj2.length).toString('utf8')
var diff = Diff.structuredPatch('', '', str1, str2)
cb(null, self.gitDiffTable(diff, lineComments, {
obj: obj2,
hash: rev2,
commit: self.query.commit, // info2.commit,
path: self.query.path, // info2.path,
}))
})
})
})
]),
self.wrapPage('git diff'),
self.respondSink(200)
)
})
}
Serve.prototype.gitDiffTable = function (diff, lineComments, lineCommentInfo) {
var updateMsg = lineCommentInfo.obj.msg
var self = this
return pull(
ph('table', [
pull(
pull.values(diff.hunks),
pull.map(function (hunk) {
var oldLine = hunk.oldStart
var newLine = hunk.newStart
return [
ph('tr', [
ph('td', {colspan: 3}),
ph('td', ph('pre',
'@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
'+' + newLine + ',' + hunk.newLines + ' @@'))
]),
pull(
pull.values(hunk.lines),
pull.map(function (line) {
var s = line[0]
if (s == '\\') return
var html = self.app.render.highlight(line)
var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
var hash = lineCommentInfo.hash
var newLineNum = lineNums[lineNums.length-1]
var id = hash + '-' + (newLineNum || (lineNums[0] + '-'))
var idEnc = encodeURIComponent(id)
var allowComment = s !== '-'
&& self.query.commit && self.query.path
return [
ph('tr', {
class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
}, [
lineNums.map(function (num, i) {
return ph('td', [
ph('a', {
name: i === 0 ? idEnc : undefined,
href: '#' + idEnc
}, String(num))
])
}),
ph('td',
allowComment ? ph('a', {
href: '?msg=' +
encodeURIComponent(self.query.msg)
+ '&comment=' + idEnc
+ '&commit=' + encodeURIComponent(self.query.commit)
+ '&path=' + encodeURIComponent(self.query.path)
+ '#' + idEnc
}, '…') : ''
),
ph('td', ph('pre', html))
]),
(lineComments[newLineNum] ?
ph('tr',
ph('td', {colspan: 4},
self.renderLineCommentThread(lineComments[newLineNum], id)
)
)
: newLineNum && lineCommentInfo && self.query.comment === id ?
ph('tr',
ph('td', {colspan: 4},
self.renderLineCommentForm({
id: id,
line: newLineNum,
updateId: updateMsg.key,
blobId: hash,
repoId: updateMsg.value.content.repo,
commitId: lineCommentInfo.commit,
filePath: lineCommentInfo.path,
})
)
)
: '')
]
})
)
]
})
)
])
)
}
Serve.prototype.renderLineCommentThread = function (lineComment, id) {
return this.streamThreadWithComposer({
root: lineComment.msg.key,
id: id,
placeholder: 'reply to line comment thread'
})
}
Serve.prototype.renderLineCommentForm = function (opts) {
return [
this.phComposer({
placeholder: 'comment on this line',
id: opts.id,
lineComment: opts
})
]
}
// return a composer, pull-hyperscript style
Serve.prototype.phComposer = function (opts) {
var self = this
return u.readNext(function (cb) {
self.composer(opts, function (err, composer) {
if (err) return cb(err)
cb(null, pull.once(composer.outerHTML))
})
})
}
Serve.prototype.gitLineComment = function (path) {
var self = this
var id
try {
id = decodeURIComponent(String(path))
if (id[0] === '%') {
return self.getMsgDecryptedMaybeOoo(id, gotMsg)
} else {
msg = JSON.parse(id)
}
} catch(e) {
return gotMsg(e)
}
gotMsg(null, msg)
function gotMsg(err, msg) {
if (err) return pull(
pull.once(u.renderError(err).outerHTML),
self.respondSink(400, {'Content-Type': ctype('html')})
)
var c = msg && msg.value && msg.value.content
if (!c) return pull(
pull.once('Missing message ' + id),
self.respondSink(500, {'Content-Type': ctype('html')})
)
self.app.git.diffFile({
msg: c.updateId,
commit: c.commitId,
path: c.filePath,
}, function (err, file) {
if (err && err.name === 'BlobNotFoundError')
return self.askWantBlobs(err.links)
if (err) return pull(
pull.once(err.stack),
self.respondSink(400, {'Content-Type': 'text/plain'})
)
var path
if (file.created) {
path = '/git/blob/' + file.hash[1]
+ '?msg=' + encodeURIComponent(c.updateId)
+ '&commit=' + c.commitId
+ '&path=' + encodeURIComponent(c.filePath)
+ '#' + file.hash[1] + '-' + c.line
} else {
path = '/git/diff/' + file.hash[0] + '..' + file.hash[1]
+ '?msg=' + encodeURIComponent(c.updateId)
+ '&commit=' + c.commitId
+ '&path=' + encodeURIComponent(c.filePath)
+ '#' + file.hash[1] + '-' + c.line
}
var url = self.app.render.toUrl(path)
/*
return pull(
ph('a', {href: url}, path),
self.wrapPage(id),
self.respondSink(200)
)
*/
self.redirect(url)
})
}
}
Serve.prototype.gitObjectLinks = function (headMsgId, type) {
var self = this
return paramap(function (id, cb) {
self.app.git.getObjectMsg({
obj: id,
headMsgId: headMsgId,
type: type,
}, function (err, msg) {
if (err && err.name === 'BlobNotFoundError')
return cb(null, self.askWantBlobsForm(err.links))
if (err && err.name === 'ObjectNotFoundError')
return cb(null, [
ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)'])
if (err) return cb(err)
var path = '/git/' + type + '/' + id
+ '?msg=' + encodeURIComponent(msg.key)
cb(null, [ph('code', ph('a', {
href: self.app.render.toUrl(path)
}, u.escapeHTML(id.substr(0, 8)))), ' '])
})
}, 8)
}
Serve.prototype.npm = function (url) {
var self = this
var parts = url.split('/')
var author = parts[1] && parts[1][0] === '@'
? u.unescapeId(parts.splice(1, 1)[0]) : null
var name = parts[1]
var version = parts[2]
var distTag = parts[3]
var prefix = 'npm:' +
(name ? name + ':' +
(version ? version + ':' +
(distTag ? distTag + ':' : '') : '') : '')
var render = self.app.render
var base = '/npm/' + (author ? u.escapeId(author) + '/' : '')
var pathWithoutAuthor = '/npm' +
(name ? '/' + name +
(version ? '/' + version +
(distTag ? '/' + distTag : '') : '') : '')
return pull(
ph('section', {}, [
ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ',
author ? [
self.phIdLink(author), ' ',
ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '×')),
' : '
] : '',
name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '',
distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : ''
]),
ph('table', [
ph('thead', ph('tr', [
ph('th', 'publisher'),
ph('th', 'package'),
ph('th', 'version'),
ph('th', 'tag'),
ph('th', 'size'),
ph('th', 'tarball'),
ph('th', 'readme')
])),
ph('tbody', pull(
self.app.blobMentions({
name: {$prefix: prefix},
author: author,
}),
distTag && !version && pull.filter(function (link) {
return link.name.split(':')[3] === distTag
}),
paramap(function (link, cb) {
self.app.render.npmPackageMention(link, {
withAuthor: true,
author: author,
name: name,
version: version,
distTag: distTag,
}, cb)
}, 4),
pull.map(u.toHTML)
))
])
]),
self.wrapPage(prefix),
self.respondSink(200)
)
}
Serve.prototype.npmPrebuilds = function (url) {
var self = this
var parts = url.split('/')
var author = parts[1] && parts[1][0] === '@'
? u.unescapeId(parts.splice(1, 1)[0]) : null
var name = parts[1]
var version = parts[2]
var prefix = 'prebuild:' +
(name ? name + '-' +
(version ? version + '-' : '') : '')
var render = self.app.render
var base = '/npm-prebuilds/' + (author ? u.escapeId(author) + '/' : '')
return pull(
ph('section', {}, [
ph('h3', [ph('a', {href: render.toUrl('/npm-prebuilds/')}, 'npm prebuilds'), ' : ',
name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version)] : '',
]),
ph('table', [
ph('thead', ph('tr', [
ph('th', 'publisher'),
ph('th', 'name'),
ph('th', 'version'),
ph('th', 'runtime'),
ph('th', 'abi'),
ph('th', 'platform+libc'),
ph('th', 'arch'),
ph('th', 'size'),
ph('th', 'tarball')
])),
ph('tbody', pull(
self.app.blobMentions({
name: {$prefix: prefix},
author: author,
}),
paramap(function (link, cb) {
self.app.render.npmPrebuildMention(link, {
withAuthor: true,
author: author,
name: name,
version: version,
}, cb)
}, 4),
pull.map(u.toHTML)
))
])
]),
self.wrapPage(prefix),
self.respondSink(200)
)
}
Serve.prototype.npmReadme = function (url) {
var self = this
var id = decodeURIComponent(url.substr(1))
return pull(
ph('section', {}, [
ph('h3', [
'npm readme for ',
ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
]),
ph('blockquote', u.readNext(function (cb) {
self.app.getNpmReadme(id, function (err, readme, isMarkdown) {
if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
cb(null, isMarkdown
? ph('div', self.app.render.markdown(readme))
: ph('pre', readme))
})
}))
]),
self.wrapPage('npm readme'),
self.respondSink(200)
)
}
Serve.prototype.npmRegistry = function (url) {
var self = this
self.req.url = url
self.app.serveSsbNpmRegistry(self.req, self.res)
}
Serve.prototype.markdown = function (url) {
var self = this
var id = decodeURIComponent(url.substr(1))
if (typeof self.query.unbox === 'string') id += '?unbox=' + self.query.unbox.replace(/\s/g, '+')
return pull(
ph('section', {}, [
ph('h3', [
ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
]),
u.readNext(function (cb) {
self.app.getHasBlob(id, function (err, has) {
if (err) return cb(err)
if (!has) return cb(null, self.askWantBlobsForm([id]))
pull(self.app.getBlob(id), pull.collect(function (err, chunks) {
if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
var text = Buffer.concat(chunks).toString()
cb(null, ph('blockquote', self.app.render.markdown(text)))
}))
})
})
]),
self.wrapPage('markdown'),
self.respondSink(200)
)
}
Serve.prototype.zip = function (url) {
var self = this
var parts = url.split('/').slice(1)
var id = decodeURIComponent(parts.shift())
var filename = parts.join('/')
var blobs = self.app.sbot.blobs
var etag = '"' + id + filename + '"'
var index = filename === '' || /\/$/.test(filename)
var indexFilename = index && (filename + 'index.html')
if (filename === '/' || /\/\/$/.test(filename)) {
// force directory listing if path ends in //
filename = filename.replace(/\/$/, '')
indexFilename = false
}
var files = index && []
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
blobs.size(id, function (err, size) {
if (size == null) return askWantBlobsForm([id])
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
else return self.respond(500, err.message || err)
}
var unzip = require('unzip')
var parseUnzip = unzip.Parse()
var gotEntry = false
parseUnzip.on('entry', function (entry) {
if (index) {
if (!gotEntry) {
if (entry.path === indexFilename) {
gotEntry = true
return serveFile(entry)
} else if (entry.path.substr(0, filename.length) === filename) {
files.push({path: entry.path, type: entry.type, props: entry.props})
}
}
} else {
if (!gotEntry && entry.path === filename) {
gotEntry = true
// if (false && entry.type === 'Directory') return serveDirectory(entry)
return serveFile(entry)
}
}
entry.autodrain()
})
parseUnzip.on('close', function () {
if (gotEntry) return
if (!index) return self.respond(404, 'Entry not found')
pull(
ph('section', {}, [
ph('h3', [
ph('a', {href: self.app.render.toUrl('/links/' + id)}, id.substr(0, 8) + '…'),
' ',
ph('a', {href: self.app.render.toUrl('/zip/' + encodeURIComponent(id) + '/' + filename)}, filename || '/'),
]),
pull(
pull.values(files),
pull.map(function (file) {
var path = '/zip/' + encodeURIComponent(id) + '/' + file.path
return ph('li', [
ph('a', {href: self.app.render.toUrl(path)}, file.path)
])
})
)
]),
self.wrapPage(id + filename),
self.respondSink(200)
)
gotEntry = true // so that the handler on error event does not run
})
parseUnzip.on('error', function (err) {
if (!gotEntry) return self.respond(400, err.message)
})
var size
function serveFile(entry) {
size = entry.size
pull(
toPull.source(entry),
ident(gotType),
self.respondSink()
)
}
pull(
self.app.getBlob(id),
toPull(parseUnzip)
)
function gotType(type) {
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
if (size) self.res.setHeader('Content-Length', size)
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('ETag', etag)
self.res.writeHead(200)
}
})
}
Serve.prototype.web = function (url) {
var self = this
var id = url.substr(1)
try { id = decodeURIComponent(id) }
catch(e) {}
var components = url.split('/')
if (components[0] === '') components.shift()
components[0] = decodeURIComponent(components[0])
var type = mime.lookup(components[components.length - 1])
var headers = {}
if (type) headers['Content-Type'] = type
webresolve(this.app.sbot, components, function (err, res) {
if (err) {
return pull(
pull.once(err.toString()),
self.respondSink(404)
)
}
headers['Content-Length'] = res.length
return pull(
pull.once(res),
self.respondSink(200, headers)
)
})
}
Serve.prototype.script = function (url) {
var self = this
var filepath = url.split('?')[0]
this.app.getScript(filepath, function (err, fn) {
try {
if (err) throw err
fn(self)
} catch(e) {
return pull(
pull.once(u.renderError(e).outerHTML),
self.wrapPage('local: ' + path),
self.respondSink(400)
)
}
})
}
// wrap a binary source and render it or turn into an embed
Serve.prototype.wrapBinary = function (opts) {
var self = this
var ext = opts.ext
var hash = opts.obj.hash
return function (read) {
var readRendered, type
read = ident(function (_ext) {
if (_ext) ext = _ext
type = ext && mime.lookup(ext) || 'text/plain'
})(read)
return function (abort, cb) {
if (readRendered) return readRendered(abort, cb)
if (abort) return read(abort, cb)
if (!type) read(null, function (end, buf) {
if (end) return cb(end)
if (!type) return cb(new Error('unable to get type'))
readRendered = pickSource(type, cat([pull.once(buf), read]))
readRendered(null, cb)
})
}
}
function pickSource(type, read) {
if (/^image\//.test(type)) {
read(true, function (err) {
if (err && err !== true) console.trace(err)
})
return ph('img', {
src: opts.rawUrl
})
}
if (type === 'text/markdown') {
// TODO: rewrite links to files/images to be correct
return ph('blockquote', u.readNext(function (cb) {
pull.collect(function (err, bufs) {
if (err) return cb(pull.error(err))
var text = Buffer.concat(bufs).toString('utf8')
return cb(null, pull.once(self.app.render.markdown(text)))
})(read)
}))
}
var i = 1
var updateMsg = opts.obj.msg
var commitId = self.query.commit
var filePath = self.query.path
var lineComments = opts.lineComments || {}
return u.readNext(function (cb) {
if (commitId && filePath) {
self.app.getLineComments({
obj: opts.obj,
hash: hash,
}, gotLineComments)
} else {
gotLineComments(null, {})
}
function gotLineComments(err, lineComments) {
if (err) return cb(err)
cb(null, ph('table',
pull(
read,
utf8(),
split(),
pull.map(function (line) {
var lineNum = i++
var id = hash + '-' + lineNum
var idEnc = encodeURIComponent(id)
var allowComment = self.query.commit && self.query.path
return [
ph('tr', [
ph('td',
allowComment ? ph('a', {
href: '?msg=' + encodeURIComponent(self.query.msg)
+ '&commit=' + encodeURIComponent(self.query.commit)
+ '&path=' + encodeURIComponent(self.query.path)
+ '&comment=' + idEnc
+ '#' + idEnc
}, '…') : ''
),
ph('td', ph('a', {
name: id,
href: '#' + idEnc
}, String(lineNum))),
ph('td', ph('pre', self.app.render.highlight(line, ext)))
]),
lineComments[lineNum] ? ph('tr',
ph('td', {colspan: 4},
self.renderLineCommentThread(lineComments[lineNum], id)
)
) : '',
self.query.comment === id ? ph('tr',
ph('td', {colspan: 4},
self.renderLineCommentForm({
id: id,
line: lineNum,
updateId: updateMsg.key,
repoId: updateMsg.value.content.repo,
commitId: commitId,
blobId: hash,
filePath: filePath,
})
)
) : ''
]
})
)
))
}
})
}
}
Serve.prototype.wrapPublic = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
self.composer({
channel: '',
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
composer,
thread
])
})
})
}
function uniqueLink() {
var seen = {}
return function (link) {
if (seen[link.link]) return false
return seen[link.link] = true
}
}
Serve.prototype.askWantBlobsForm = function (links) {
var self = this
return ph('form', {action: '', method: 'post'}, [
ph('section', [
ph('h3', 'Missing blobs'),
ph('p', 'The application needs these blobs to continue:'),
ph('table', links.map(u.toLink).filter(uniqueLink()).map(function (link) {
if (!u.isRef(link.link)) return
return ph('tr', [
ph('td', ph('code', link.link)),
!isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '',
])
})),
ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
ph('input', {type: 'hidden', name: 'blob_ids',
value: links.map(u.linkDest).join(',')}),
ph('p', ph('input', {type: 'submit', value: 'Want Blobs'}))
])
])
}
Serve.prototype.askWantBlobs = function (links) {
var self = this
pull(
self.askWantBlobsForm(links),
self.wrapPage('missing blobs'),
self.respondSink(409)
)
}
Serve.prototype.wrapPrivate = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
self.composer({
placeholder: 'private message',
private: true,
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
composer,
thread
])
})
})
}
Serve.prototype.wrapThread = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
self.app.render.prepareLinks(opts.recps, function (err, recps) {
if (err) return cb(er)
if (self.noComposer) return cb(null, thread)
self.composer({
placeholder: opts.placeholder
|| (recps ? 'private reply' : 'reply'),
id: 'reply',
root: opts.root,
post: opts.post,
channel: opts.channel || '',
branches: opts.branches,
postBranches: opts.postBranches,
recps: recps,
links: opts.links,
private: opts.recps != null,
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
thread,
composer
])
})
})
})
}
Serve.prototype.wrapNew = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
self.composer({
channel: '',
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
composer,
h('table.ssb-msgs',
opts.reachedLimit ? h('tr', h('td.paginate.msg-left', {colspan: 3},
'Reached limit of ' + opts.reachedLimit + ' messages'
)) : '',
thread,
h('tr', h('td.paginate.msg-left', {colspan: 3},
h('form', {method: 'get', action: ''},
h('input', {type: 'hidden', name: 'gt', value: opts.gt}),
self.query.limit ? h('input', {type: 'hidden', name: 'limit', value: self.query.limit}) : '',
h('input', {type: 'hidden', name: 'catchup', value: '1'}),
h('input', {type: 'submit', value: 'catchup'})
)
))
)
])
})
})
}
Serve.prototype.wrapChannel = function (channel) {
var self = this
return u.hyperwrap(function (thread, cb) {
self.composer({
placeholder: 'public message in #' + channel,
channel: channel,
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
h('section',
h('h3.feed-name',
h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel)
)
),
composer,
thread
])
})
})
}
Serve.prototype.wrapType = function (type) {
var self = this
return u.hyperwrap(function (thread, cb) {
cb(null, [
h('section',
h('h3.feed-name',
h('a', {href: self.app.render.toUrl('/type/' + type)},
h('code', type), 's'))
),
thread
])
})
}
Serve.prototype.wrapLinks = function (dest) {
var self = this
return u.hyperwrap(function (thread, cb) {
cb(null, [
h('section',
h('h3.feed-name', 'links: ',
h('a', {href: self.app.render.toUrl('/links/' + dest)},
h('code', dest)))
),
thread
])
})
}
Serve.prototype.wrapPeers = function (opts) {
var self = this
return u.hyperwrap(function (peers, cb) {
cb(null, [
h('section',
h('h3', 'Peers')
),
peers
])
})
}
Serve.prototype.wrapChannels = function (opts) {
var self = this
return u.hyperwrap(function (channels, cb) {
cb(null, [
h('section',
h('h4', 'Network')
),
h('section',
channels
)
])
})
}
Serve.prototype.wrapMyChannels = function (opts) {
var self = this
return u.hyperwrap(function (channels, cb) {
cb(null, [
h('section',
h('h4', 'Subscribed')
),
h('section',
channels
)
])
})
}
var blobPrefixesByType = {
audio: 'audio:',
video: 'video:',
}
var blobPrefixesByExt = {
mp3: 'audio:',
mp4: 'video:',
}
Serve.prototype.composer = function (opts, cb) {
var self = this
opts = opts || {}
var data = self.data
var myId = self.app.sbot.id
var links = opts.links || []
if (opts.id && data.composer_id && opts.id !== data.composer_id) {
// don't share data between multiple composers
data = {}
}
if (!data.text && self.query.text) data.text = self.query.text
if (!data.action && self.query.action) data.action = self.query.action
var blobs = u.tryDecodeJSON(data.blobs) || {}
var upload = data.upload
if (upload && typeof upload === 'object') {
data.upload = null
var href = upload.link + (upload.key ? '?unbox=' + upload.key + '.boxs': '')
var blobType = String(upload.type).split('/')[0]
var blobExt = String(upload.name).split('.').pop()
var blobPrefix = blobPrefixesByType[blobType] || blobPrefixesByExt[blobExt] || ''
var isMedia = blobPrefix || blobType === 'image'
var blobName = blobPrefix + upload.name
blobs[upload.link] = {
type: upload.type,
size: upload.size,
name: blobName,
key: upload.key,
}
data.text = (data.text ? data.text + '\n' : '')
+ (isMedia ? '!' : '')
+ '[' + blobName + '](' + href + ')'
}
var channel = data.channel != null ? data.channel : opts.channel
var formNames = {}
var mentionIds = u.toArray(data.mention_id)
var mentionNames = u.toArray(data.mention_name)
for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) {
formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0]
}
var formEmojiNames = {}
var emojiIds = u.toArray(data.emoji_id)
var emojiNames = u.toArray(data.emoji_name)
for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) {
var upload = data['emoji_upload_' + i]
formEmojiNames[emojiNames[i]] =
(upload && upload.link) || u.extractBlobIds(emojiIds[i])[0]
if (upload) blobs[upload.link] = {
type: upload.type,
size: upload.size,
name: upload.name,
}
}
var blobIds = u.toArray(data.blob_id)
var blobTypes = u.toArray(data.blob_type)
var blobNames = u.toArray(data.blob_name)
for (var i = 0; i < blobIds.length && i < blobTypes.length; i++) {
var id = blobIds[i]
var blob = blobs[id] || (blobs[id] = {})
blob.type = blobTypes[i]
blob.nameOverride = data['blob_name_override_' + i]
blob.name = blobNames[i]
}
// get bare feed names
var unknownMentionNames = {}
var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
var unknownMentions = mentions
.filter(function (mention) {
return mention.link === '@'
})
.map(function (mention) {
return mention.name
})
.filter(uniques())
.map(function (name) {
var id = formNames[name] || self.app.getReverseNameSync('@' + name)
return {name: name, id: id}
})
var emoji = mentions
.filter(function (mention) { return mention.emoji })
.map(function (mention) { return mention.name })
.filter(uniques())
.map(function (name) {
// 1. check emoji-image mapping for this message
var id = formEmojiNames[name]
if (id) return {name: name, id: id}
// 2. TODO: check user's preferred emoji-image mapping
// 3. check builtin emoji
var link = self.getBuiltinEmojiLink(name)
if (link) {
return {name: name, id: link.link}
blobs[id] = {type: link.type, size: link.size}
}
// 4. check recently seen emoji
id = self.app.getReverseEmojiNameSync(name)
return {name: name, id: id}
})
var blobMentions = mentions
.filter(function (mention) {
return mention
&& typeof mention.link === 'string'
&& mention.link[0] === '&'
})
blobMentions.forEach(function (mention) {
var blob = blobs[mention.link]
if (blob) {
mention.type = blob.type
if (blob.nameOverride) mention.name = blob.name
}
})
// strip content other than names and feed ids from the recps field
if (data.recps) {
data.recps = recpsToFeedIds(data.recps)
}
var draftLinkContainer
function renderDraftLink(draftId) {
if (!draftId) return []
var draftHref = self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId))
return [
h('a', {href: draftHref, title: 'draft link'}, u.escapeHTML(draftId)),
h('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)})
]
}
var done = multicb({pluck: 1, spread: true})
done()(null, h('section.composer',
h('form', {method: 'post', action: opts.id ? '#' + opts.id : '',
enctype: 'multipart/form-data'},
h('input', {type: 'hidden', name: 'blobs',
value: JSON.stringify(blobs)}),
h('input', {type: 'hidden', name: 'url', value: self.req.url}),
h('input', {type: 'hidden', name: 'composer_id', value: opts.id}),
opts.recps ? self.app.render.privateLine({recps: opts.recps,
isAuthorRecp: true}, done()) :
opts.private ? h('div', h('input.wide', {name: 'recps', size: 70,
value: data.recps || '', placeholder: 'recipient ids'})) : '',
channel != null ?
h('div', '#', h('input', {name: 'channel', placeholder: 'channel',
value: channel})) : '',
opts.root !== opts.post ? h('div',
h('label', {for: 'fork_thread'},
h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}),
' fork thread'
)
) : '',
closeIssueCheckbox(done()),
mentionAttendeesCheckbox(done()),
h('div', h('input.wide', {name: 'content_warning', size: 70,
value: data.content_warning || '',
placeholder: 'Content Warning'})),
h('textarea', {
id: opts.id,
name: 'text',
rows: Math.max(4, u.rows(data.text)),
cols: 70,
placeholder: opts.placeholder || 'public message',
}, data.text || ''),
unknownMentions.length > 0 ? [
h('div', h('em', 'names:')),
h('ul.mentions', unknownMentions.map(function (mention) {
return h('li',
h('code', '@' + mention.name), ': ',
h('input', {name: 'mention_name', type: 'hidden',
value: mention.name}),
h('input.id-input', {name: 'mention_id', size: 60,
value: mention.id, placeholder: '@id'}))
}))
] : '',
emoji.length > 0 ? [
h('div', h('em', 'emoji:')),
h('ul.mentions', emoji.map(function (link, i) {
return h('li',
h('code', link.name), ': ',
h('input', {name: 'emoji_name', type: 'hidden',
value: link.name}),
h('input.id-input', {name: 'emoji_id', size: 60,
value: link.id, placeholder: '&id'}), ' ',
h('input', {type: 'file', name: 'emoji_upload_' + i}))
}))
] : '',
blobMentions.length > 0 ? [
h('div', h('em', 'blobs:')),
h('ul.mentions', blobMentions.map(function (link, i) {
var link1 = blobs[link.link] || {}
var linkHref = self.app.render.toUrl(link.link)
return h('li',
h('a', {href: linkHref}, h('code', String(link.link).substr(0, 8) + '…')), ' ',
h('input', {name: 'blob_id', type: 'hidden',
value: link.link}),
h('input', {name: 'blob_type', size: 24,
value: link.type, placeholder: 'application/octet-stream',
title: 'blob mime-type'}),
h('input', {name: 'blob_name', size: 24,
value: link.name || '', placeholder: 'name',
title: 'blob name'}), ' ',
h('input', {name: 'blob_name_override_' + i, type: 'checkbox',
value: 1, checked: link1.nameOverride ? '1' : undefined,
title: 'override name in markdown'}))
}))
] : '',
h('table.ssb-msgs',
h('tr.msg-row',
h('td.msg-left', {colspan: 2},
opts.private ?
h('input', {type: 'hidden', name: 'private', value: '1'}) : '',
h('input', {type: 'file', name: 'upload'})
),
h('td.msg-right',
draftLinkContainer = h('span', renderDraftLink(data.draft_id)), ' ',
h('label', {for: 'save_draft'},
h('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1',
checked: data.save_draft || data.restored_draft ? 'checked' : undefined}),
' save draft '),
h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ',
h('input', {type: 'submit', name: 'action', value: 'preview'})
)
)
),
data.action === 'preview' ? preview(false, done()) :
data.action === 'raw' ? preview(true, done()) : ''
)
))
done(cb)
function recpsToFeedIds (recps) {
var res = data.recps.split(',')
.map(function (str) {
str = str.trim()
var ids = u.extractFeedIds(str).filter(uniques())
if (ids.length >= 1) {
return ids[0]
} else {
ids = u.extractFeedIds(self.app.getReverseNameSync(str))
if (ids.length >= 1) {
return ids[0]
} else {
return null
}
}
})
.filter(Boolean)
return res.join(', ')
}
function prepareContent(cb) {
var done = multicb({pluck: 1})
content = {
type: 'post',
text: String(data.text),
}
if (opts.lineComment) {
content.type = 'line-comment'
content.updateId = opts.lineComment.updateId
content.repo = opts.lineComment.repoId
content.commitId = opts.lineComment.commitId
content.filePath = opts.lineComment.filePath
content.blobId = opts.lineComment.blobId
content.line = opts.lineComment.line
}
var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
.filter(function (mention) {
if (typeof mention.name === 'string') {
mention.name = mention.name
.replace(/'/g, "'")
}
if (mention.emoji) {
mention.link = formEmojiNames[mention.name]
if (!mention.link) {
var link = self.getBuiltinEmojiLink(mention.name)
if (link) {
mention.link = link.link
mention.size = link.size
mention.type = link.type
} else {
mention.link = self.app.getReverseEmojiNameSync(mention.name)
if (!mention.link) return false
}
}
}
var blob = blobs[mention.link]
if (blob) {
if (!isNaN(blob.size))
mention.size = blob.size
if (blob.type && blob.type !== 'application/octet-stream')
mention.type = blob.type
if (blob.nameOverride)
mention.name = blob.name
} else if (mention.link === '@') {
// bare feed name
var name = mention.name
var id = formNames[name] || self.app.getReverseNameSync('@' + name)
if (id) mention.link = id
else return false
}
if (mention.link && mention.link[0] === '&' && mention.size == null) {
var linkCb = done()
self.app.sbot.blobs.size(mention.link, function (err, size) {
if (!err && size != null) mention.size = size
linkCb()
})
}
return true
})
.map(reduceLink)
if (mentions.length) content.mentions = mentions
if (data.recps != null) {
if (opts.recps) return cb(new Error('got recps in opts and data'))
content.recps = [myId]
u.extractFeedIds(data.recps).forEach(function (recp) {
if (content.recps.indexOf(recp) === -1) content.recps.push(recp)
})
} else {
if (opts.recps) content.recps = opts.recps
}
if (data.fork_thread) {
content.root = opts.post || undefined
content.fork = opts.root || undefined
content.branch = u.fromArray(opts.postBranches) || undefined
} else {
content.root = opts.root || undefined
content.branch = u.fromArray(opts.branches) || undefined
}
if (data.close_issue) {
content.issue = opts.root || undefined
content.project = data.project || undefined
content.repo = data.repo || undefined
content.issues = [
{
link: opts.root,
open: false
}
]
}
if (self.replyMentionFeeds && links && content.branch) {
var reply = {}
var ids = {}
u.toArray(content.branch).forEach(function (branch) {
ids[branch] = true
})
links.forEach(function (link) {
if (ids[link.key]) {
var author = link.value.author
if (author !== myId) reply[link.key] = author
}
})
if (Object.keys(reply).length > 0) content.reply = reply
}
if (data.mention_attendees) {
var attendeeLinks = u.toArray(String(data.attendees || '').split(','))
.filter(function (id) {
return id !== myId
})
if (!content.mentions) content.mentions = attendeeLinks
else {
var alreadyMentioned = {}
content.mentions.map(u.linkDest).forEach(function (id) {
alreadyMentioned[id] = true
})
attendeeLinks.forEach(function (link) {
if (!alreadyMentioned[link]) content.mentions.push(link)
})
}
}
if (data.content_warning) content.contentWarning = String(data.content_warning)
if (channel) content.channel = data.channel
done(function (err) {
cb(err, content)
})
}
function closeIssueCheckbox(cb) {
var container = h('div')
if (opts.root) self.getMsgDecryptedMaybeOoo(opts.root, function (err, rootMsg) {
if (err) return console.trace(err), cb(null)
var rootC = rootMsg && rootMsg.value.content && rootMsg.value.content
if (!rootC) return cb(null)
var canCloseIssue = rootC.type === 'issue'
var canClosePR = rootC.type === 'pull-request'
if (canCloseIssue || canClosePR) container.appendChild(h('label', {for: 'close_issue'},
h('input', {id: 'close_issue', type: 'checkbox', name: 'close_issue', value: 'post', checked: data.close_issue || undefined}),
' close ' + (canClosePR ? 'pull-request' : 'issue'),
rootC.project ? h('input', {type: 'hidden', name: 'project', value: rootC.project}) : '',
rootC.repo ? h('input', {type: 'hidden', name: 'repo', value: rootC.repo}) : ''
))
cb(null)
})
else cb(null)
return container
}
function mentionAttendeesCheckbox(cb) {
var container = h('div')
if (opts.root) self.getMsgDecryptedMaybeOoo(opts.root, function (err, rootMsg) {
if (err) return console.trace(err), cb(null)
var rootC = rootMsg && rootMsg.value.content && rootMsg.value.content
if (!rootC) return cb(null)
var canMentionAttendees = rootC.type === 'gathering'
if (!canMentionAttendees) return cb(null)
if (opts.id === opts.root) gotLinks(null, links)
else pull(
self.app.getLinksBy(opts.root, 'about'),
pull.unique('key'),
self.app.unboxMessages(),
pull.collect(gotLinks)
)
function gotLinks(err, links2) {
if (err) console.trace(err), links2 = links
var attendees = {}
links2.forEach(function (link) {
var c = link && link.value && link.value.content
var attendee = c && c.type === 'about' && c.about === opts.root
&& u.toLink(c.attendee)
if (!attendee) return
var author = link.value.author
if (attendee.link !== author) return
if (attendee.remove) delete attendees[author]
else attendees[author] = true
})
var attendeeIds = Object.keys(attendees)
container.appendChild(h('label', {for: 'mention_attendees'},
h('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: 'post', checked: data.mention_attendees || undefined}),
' mention attendees (' + attendeeIds.length + ')',
h('input', {type: 'hidden', name: 'attendees', value: attendeeIds.join(',')})
))
cb(null)
}
})
else cb(null)
return container
}
function preview(raw, cb) {
var msgContainer = h('table.ssb-msgs')
var contentInput = h('input', {type: 'hidden', name: 'content'})
var warningsContainer = h('div')
var sizeEl = h('span')
var content
try { content = JSON.parse(data.text) }
catch (err) {}
if (content) gotContent(null, content)
else prepareContent(gotContent)
function gotContent(err, _content) {
if (err) return cb(err)
content = _content
if (data.save_draft) self.saveDraft(content, saved)
else saved()
}
function saved(err, draftId) {
if (err) return cb(err)
if (draftId) {
draftLinkContainer.childNodes = renderDraftLink(draftId)
}
contentInput.value = JSON.stringify(content)
var msg = {
value: {
author: myId,
timestamp: Date.now(),
content: content
}
}
if (content.recps) msg.value.private = true
var warnings = []
u.toLinkArray(content.mentions).forEach(function (link) {
if (link.emoji && link.size >= 10e3) {
warnings.push(h('li',
'emoji ', h('q', link.name),
' (', h('code', String(link.link).substr(0, 8) + '…'), ')'
+ ' is >10KB'))
} else if (link.link[0] === '&' && link.size >= 10e6 && link.type) {
// if link.type is set, we probably just uploaded this blob
warnings.push(h('li',
'attachment ',
h('code', String(link.link).substr(0, 8) + '…'),
' is >10MB'))
}
})
var estSize = u.estimateMessageSize(content)
sizeEl.innerHTML = self.app.render.formatSize(estSize)
if (estSize > 8192) warnings.push(h('li', 'message is too long'))
if (warnings.length) {
warningsContainer.appendChild(h('div', h('em', 'warning:')))
warningsContainer.appendChild(h('ul.mentions', warnings))
}
pull(
pull.once(msg),
self.app.unboxMessages(),
self.app.render.renderFeeds({
raw: raw,
dualMarkdown: self.query.dualMd != null ? self.query.dualMd :
self.conf.dualMarkdownPreview,
serve: self,
filter: self.query.filter,
}),
pull.drain(function (el) {
msgContainer.appendChild(h('tbody', el))
}, cb)
)
}
return [
contentInput,
opts.redirectToPublishedMsg ? h('input', {type: 'hidden',
name: 'redirect_to_published_msg', value: '1'}) : '',
warningsContainer,
h('div', h('em', 'draft:'), ' ', sizeEl),
msgContainer,
h('div.composer-actions',
h('input', {type: 'submit', name: 'action', value: 'publish'})
)
]
}
}
Serve.prototype.phPreview = function (content, opts) {
var self = this
var msg = {
value: {
author: this.app.sbot.id,
timestamp: Date.now(),
content: content
}
}
opts = opts || {}
if (content.recps) msg.value.private = true
var warnings = []
var estSize = u.estimateMessageSize(content)
if (estSize > 8192) warnings.push(ph('li', 'message is too long'))
var aboutNewContent = opts.aboutNewContent, aboutNewMsg
if (aboutNewContent) {
aboutNewMsg = {
value: {
author: this.app.sbot.id,
timestamp: Date.now(),
content: aboutNewContent
}
}
// this duplicates functionality in publishNewAbout, for display purposes
if (content.recps) {
aboutNewContent.recps = content.recps
aboutNewMsg.value.private = true
}
}
return ph('form', {action: '', method: 'post'}, [
ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}),
warnings.length ? [
ph('div', ph('em', 'warning:')),
ph('ul', {class: 'mentions'}, warnings)
] : '',
ph('div', [
ph('em', 'draft:'), ' ',
u.escapeHTML(this.app.render.formatSize(estSize))
]),
ph('table', {class: 'ssb-msgs'}, pull(
aboutNewMsg
? pull.values([aboutNewMsg, msg])
: pull.once(msg),
this.app.unboxMessages(),
this.app.render.renderFeeds({
serve: self,
raw: opts.raw,
filter: this.query.filter,
}),
pull.map(u.toHTML)
)),
ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}),
aboutNewContent ? ph('input', {type: 'hidden', name: 'about_new_content', value: u.escapeHTML(JSON.stringify(aboutNewContent))}) : null,
ph('div', {class: 'composer-actions'}, [
ph('input', {type: 'submit', name: 'action', value: 'publish'})
])
])
}
Serve.prototype.phMsgActions = function (content) {
var self = this
var data = self.data
function renderDraftLink(draftId) {
return pull.values([
ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)),
title: 'draft link'}, u.escapeHTML(draftId)),
ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ',
])
}
return [
ph('input', {type: 'hidden', name: 'url', value: self.req.url}),
ph('p', {class: 'msg-right'}, [
data.save_draft && content ? u.readNext(function (cb) {
self.saveDraft(content, function (err, draftId) {
if (err) return cb(err)
cb(null, renderDraftLink(draftId))
})
}) : data.draft_id ? renderDraftLink(data.draft_id) : '',
ph('label', {for: 'save_draft'}, [
ph('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1',
checked: data.save_draft || data.restored_draft ? 'checked' : undefined}),
' save draft '
]),
ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ',
ph('input', {type: 'submit', name: 'preview', value: 'Preview'}),
])
]
}
function hashBuf(buf) {
var hash = crypto.createHash('sha256')
hash.update(buf)
return '&' + hash.digest('base64') + '.sha256'
}
Serve.prototype.getBuiltinEmojiLink = function (name) {
if (!(name in emojis)) return
var file = path.join(emojiDir, name + '.png')
var fileBuf = fs.readFileSync(file)
var id = hashBuf(fileBuf)
// seed the builtin emoji
pull(pull.once(fileBuf), this.app.sbot.blobs.add(id, function (err) {
if (err) console.error('error adding builtin emoji as blob', err)
}))
return {
link: id,
type: 'image/png',
size: fileBuf.length,
}
}
Serve.prototype.getMsgDecryptedMaybeOoo = function (key, cb) {
var self = this
if (this.useOoo) this.app.getMsgDecryptedOoo(key, next)
else this.app.getMsgDecrypted(key, next)
function next(err, msg) {
if (err) return cb(err)
var c = msg && msg.value && msg.value.content
if (typeof c === 'string' && self.query.unbox)
self.app.unboxMsgWithKey(msg, String(self.query.unbox).replace(/ /g, '+'), cb)
else cb(null, msg)
}
}
Serve.prototype.emojis = function (path) {
var self = this
var seen = {}
pull(
ph('section', [
ph('h3', 'Emojis'),
ph('ul', {class: 'mentions'}, pull(
self.app.streamEmojis(),
pull.map(function (emoji) {
if (!seen[emoji.name]) {
// cache the first use, so that our uses take precedence over other feeds'
self.app.reverseEmojiNameCache.set(emoji.name, emoji.link)
seen[emoji.name] = true
}
return ph('li', [
ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)},
ph('img', {
class: 'ssb-emoji',
src: self.app.render.imageUrl(emoji.link),
size: 32,
})
), ' ',
u.escapeHTML(emoji.name)
])
})
))
]),
this.wrapPage('emojis'),
this.respondSink(200)
)
}
Serve.prototype.editDiff = function (url) {
var self = this
var id
try {
id = decodeURIComponent(url.substr(1))
} catch(err) {
return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('diff: ' + id),
self.respondSink(400)
)
}
return pull(
ph('section', {}, [
'diff: ',
ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')),
u.readNext(function (cb) {
self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
var c = msg.value.content || {}
self.getMsgDecryptedMaybeOoo(c.updated, function (err, oldMsg) {
if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
cb(null, self.textEditDiffTable(oldMsg, msg))
})
})
})
]),
self.wrapPage('diff: ' + id),
self.respondSink(200)
)
}
function findMsg(msgs, id) {
for (var i = 0; i < msgs.length; i++) {
if (msgs[i].key === id) return i
}
return -1
}
Serve.prototype.aboutDiff = function (url) {
var self = this
var id
try {
id = decodeURIComponent(url.substr(1))
} catch(err) {
return pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('diff: ' + id),
self.respondSink(400)
)
}
return pull(
ph('section', {}, [
'diff: ',
ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')),
u.readNext(function (cb) {
// About messages don't always include branch links. So get the whole thread
// and use ssb-sort to find what to consider the previous message(s).
self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
var c = msg.value.content || {}
var rootId = c.about
if (!rootId) return gotLinks(new Error('Missing about root'))
var msgDate = new Date(msg.value.timestamp)
cb(null, ph('div', [
self.phIdLink(msg.value.author), ' ',
ph('span', {title: msgDate.toLocaleString()}, htime(msgDate)),
ph('div', u.readNext(next.bind(this, rootId, msg)))
]))
})
})
]),
self.wrapPage('diff: ' + id),
self.respondSink(200)
)
function next(rootId, msg, cb) {
pull(
rootId === msg.value.author && !self.query.fromAny ?
self.app.getLinks3(rootId, msg.value.author, 'about') :
self.app.getLinksBy(rootId, 'about'),
pull.unique('key'),
self.app.unboxMessages(),
pull.collect(function (err, links) {
if (err) return gotLinks(err)
if (!self.useOoo) return gotLinks(null, links)
self.app.expandOoo({msgs: links, dest: id}, gotLinks)
})
)
function gotLinks(err, links) {
if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
sort(links)
links = links.filter(function (msg) {
var c = msg && msg.value && msg.value.content
return c && c.type === 'about' && c.about === rootId
&& typeof c.description === 'string'
})
var i = findMsg(links, id)
if (i < 0) return cb(null, ph('div', 'Unable to find previous message'))
var prevMsg = links[i-1]
var nextMsg = links[i+1]
var prevHref = prevMsg ?
self.app.render.toUrl('/about-diff/' + encodeURIComponent(prevMsg.key)) : null
var nextHref = nextMsg ?
self.app.render.toUrl('/about-diff/' + encodeURIComponent(nextMsg.key)) : null
cb(null, cat([
prevMsg
? pull.values(['prev: ', ph('a', {href: prevHref}, ph('code',
prevMsg.key.substr(0, 8) + '…')), ', '])
: pull.empty(),
nextMsg
? pull.values(['next: ', ph('a', {href: nextHref}, ph('code',
nextMsg.key.substr(0, 8) + '…'))])
: pull.empty(),
prevMsg || msg ? self.textEditDiffTable(prevMsg, msg) : pull.empty()
]))
}
}
}
Serve.prototype.textEditDiffTable = function (oldMsg, newMsg) {
var oldC = oldMsg && oldMsg.value.content || {}
var newC = newMsg && newMsg.value.content || {}
var oldText = String(oldC.text || oldC.description || '')
var newText = String(newC.text || newC.description || '')
var diff = Diff.structuredPatch('', '', oldText, newText)
var self = this
// note: this structure is duplicated in lib/serve.js
return pull(
ph('table', {class: 'diff-table'}, [
pull(
pull.values(diff.hunks),
pull.map(function (hunk) {
var oldLine = hunk.oldStart
var newLine = hunk.newStart
return [
ph('tr', [
ph('td', {colspan: 2}),
ph('td', {colspan: 2}, ph('pre',
'@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
'+' + newLine + ',' + hunk.newLines + ' @@'))
]),
pull(
pull.values(hunk.lines),
pull.map(function (line) {
var s = line[0]
if (s == '\\') return
var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
return [
ph('tr', {
class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
}, [
lineNums.map(function (num, i) {
return ph('td', String(num))
}),
ph('td', {class: 'diff-sigil'}, ph('code', s)),
ph('td', {class: 'diff-line'}, [
u.unwrapP(self.app.render.markdown(line.substr(1),
s == '-' ? oldC.mentions : newC.mentions))
])
])
]
})
)
]
})
)
])
)
}
Serve.prototype.shard = function (url) {
var self = this
var id
try {
id = decodeURIComponent(url.substr(1))
} catch(err) {
return onError(err)
}
function onError(err) {
pull(
pull.once(u.renderError(err).outerHTML),
self.wrapPage('shard: ' + id),
self.respondSink(400)
)
}
self.app.getShard(id, function (err, shard) {
if (err) return onError(err)
pull(
pull.once(shard),
self.respondSink(200, {'Content-Type': 'text/plain'})
)
})
}
Serve.prototype.pub = function (path) {
var self = this
var id = String(path).substr(1)
try { id = decodeURIComponent(id) }
catch(e) {}
pull(
ph('section', [
ph('h3', ['Pub addresses: ', self.phIdLink(id)]),
pull(
self.app.getAddresses(id),
pull.map(function (address) {
return ph('div', [
ph('code', self.app.removeDefaultPort(address))
])
})
)
]),
self.wrapPage('Addresses: ' + id),
self.respondSink(200)
)
}
function hiddenInput(key, value) {
return Array.isArray(value) ? value.map(function (value) {
return ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)})
}) : ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)})
}
Serve.prototype.drafts = function (path) {
var self = this
var id = path && String(path).substr(1)
if (id) try { id = decodeURIComponent(id) }
catch(e) {}
if (id) {
return pull(
ph('section', [
ph('h3', [
ph('a', {href: self.app.render.toUrl('/drafts')}, 'Drafts'), ': ',
ph('a', {href: ''}, u.escapeHTML(id))
]),
u.readNext(function (cb) {
if (self.data.draft_discard) {
return self.app.discardDraft(id, function (err) {
if (err) return cb(err)
cb(null, ph('div', 'Discarded'))
})
}
self.app.getDraft(id, function (err, draft) {
if (err) return cb(err)
var form = draft.form || {}
var content = draft.content || {type: 'post', text: ''}
var composerUrl = self.app.render.toUrl(draft.url)
+ (form.composer_id ? '#' + encodeURIComponent(form.composer_id) : '')
cb(null, ph('div', [
ph('table', ph('tr', [
ph('td', ph('form', {method: 'post', action: u.escapeHTML(composerUrl)}, [
hiddenInput('draft_id', id),
hiddenInput('restored_draft', '1'),
Object.keys(form).map(function (key) {
if (key === 'draft_id' || key === 'save_draft') return ''
return hiddenInput(key, draft.form[key])
}),
ph('input', {type: 'submit', name: 'draft_edit', value: 'Edit'})
])),
ph('td', ph('form', {method: 'post', action: ''}, [
ph('input', {type: 'submit', name: 'draft_discard', value: 'Discard',
title: 'Discard draft'})
]))
])),
self.phPreview(content, {draftId: id})
]))
})
})
]),
self.wrapPage('draft: ' + id),
self.respondSink(200)
)
}
return pull(
ph('section', [
ph('h3', 'Drafts'),
ph('ul', pull(
self.app.listDrafts(),
pull.asyncMap(function (draft, cb) {
var form = draft.form || {}
var msg = {
key: '/drafts/' + draft.id,
value: {
author: self.app.sbot.id,
timestamp: Date.now(),
content: draft.content || {type: 'post'}
}
}
cb(null, ph('li', self.app.render.phMsgLink(msg)))
})
))
]),
self.wrapPage('drafts'),
self.respondSink(200)
)
}