aboutsummaryrefslogtreecommitdiff
path: root/lib/serve.js
diff options
context:
space:
mode:
authorcel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2017-05-28 21:01:29 -1000
committercel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2017-05-28 21:01:29 -1000
commit0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c (patch)
tree30bfa6e7ede6c125eec3a51cbef4892208b1f622 /lib/serve.js
parentd8f6c6a19274cf53d9967d3597dd582ee7233ca3 (diff)
parentb66bcecec258b0a2631ec338501afa9409882fe8 (diff)
downloadpatchfoo-0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c.tar.gz
patchfoo-0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c.zip
Merge branch 'master' into votes
Diffstat (limited to 'lib/serve.js')
-rw-r--r--lib/serve.js670
1 files changed, 630 insertions, 40 deletions
diff --git a/lib/serve.js b/lib/serve.js
index 2f6caab..b971572 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -26,16 +26,6 @@ var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
-function isMsgEncrypted(msg) {
- var c = msg && msg.value.content
- return typeof c === 'string'
-}
-
-function isMsgReadable(msg) {
- var c = msg && msg.value && msg.value.content
- return typeof c === 'object' && c !== null
-}
-
function ctype(name) {
switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
case 'html': return 'text/html'
@@ -129,6 +119,7 @@ Serve.prototype.go = function () {
else if (data.action === 'publish') self.publishJSON(next)
else if (data.action === 'vote') self.publishVote(next)
else if (data.action === 'contact') self.publishContact(next)
+ else if (data.action === 'want-blobs') self.wantBlobs(next)
else next()
}
@@ -182,6 +173,22 @@ Serve.prototype.publishContact = function (cb) {
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.sbot.blobs.want(id, done())
+ })
+ 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})
@@ -219,7 +226,8 @@ Serve.prototype.respond = function (status, message) {
Serve.prototype.respondSink = function (status, headers, cb) {
var self = this
- if (status && headers) self.res.writeHead(status, headers)
+ 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)
})
@@ -266,6 +274,8 @@ Serve.prototype.path = function (url) {
case '/static': return this.static(m[2])
case '/emoji': return this.emoji(m[2])
case '/contacts': return this.contacts(m[2])
+ case '/about': return this.about(m[2])
+ case '/git': return this.git(m[2])
}
return this.respond(404, 'Not found')
}
@@ -365,9 +375,9 @@ Serve.prototype.private = function (ext) {
pull(
this.app.createLogStream(opts),
- pull.filter(isMsgEncrypted),
+ pull.filter(u.isMsgEncrypted),
this.app.unboxMessages(),
- pull.filter(isMsgReadable),
+ pull.filter(u.isMsgReadable),
pull.take(limit),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
@@ -538,7 +548,7 @@ Serve.prototype.votes = function (path) {
cb(null, ph('tr', [
ph('td', [String(item.value)]),
ph('td', [
- self.pullIdLink(item.id),
+ self.phIdLink(item.id),
pull.once(' dug by '),
self.renderIdsList()(pull.values(item.feeds))
])
@@ -628,20 +638,48 @@ Serve.prototype.peers = function (ext) {
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(
- self.app.streamChannels(),
- paramap(function (channel, cb) {
- var subscribed = false
- cb(null, [
- h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
- ' '
+ cat([
+ ph('section', {}, [
+ ph('h3', {}, 'Channels:'),
+ renderMyChannels(),
+ renderNetworkChannels()
])
- }, 8),
- pull.map(u.toHTML),
- self.wrapChannels(),
- self.wrapPage('channels'),
- self.respondSink(200, {
+ ]),
+ this.wrapPage('channels'),
+ this.respondSink(200, {
'Content-Type': ctype(ext)
})
)
@@ -671,7 +709,7 @@ Serve.prototype.contacts = function (path) {
pull(
cat([
ph('section', {}, [
- ph('h3', {}, ['Contacts: ', self.pullIdLink(id)]),
+ ph('h3', {}, ['Contacts: ', self.phIdLink(id)]),
ph('h4', {}, 'Friends'),
renderFriendsList()(contacts.friends),
ph('h4', {}, 'Follows'),
@@ -687,9 +725,79 @@ Serve.prototype.contacts = function (path) {
)
}
+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 (!value) return
+ 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(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.type = function (path) {
var q = this.query
- var type = path.substr(1)
+ var type = decodeURIComponent(path.substr(1))
var opts = {
reverse: !q.forwards,
lt: Number(q.lt) || Date.now(),
@@ -801,7 +909,8 @@ Serve.prototype.id = function (id, ext) {
if (self.query.raw != null) return self.rawId(id)
this.app.getMsgDecrypted(id, function (err, rootMsg) {
- if (err && err.name === 'NotFoundError') err = null, rootMsg = {key: id}
+ if (err && err.name === 'NotFoundError') err = null, rootMsg = {
+ key: id, value: {content: false}}
if (err) return self.respond(500, err.stack || err)
var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
var recps = rootContent && rootContent.recps
@@ -895,26 +1004,31 @@ Serve.prototype.blob = function (id) {
var self = this
var blobs = self.app.sbot.blobs
if (self.req.headers['if-none-match'] === id) return self.respond(304)
+ var done = multicb({pluck: 1, spread: true})
blobs.want(id, function (err, has) {
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
else return self.respond(500, err.message || err)
}
if (!has) return self.respond(404, 'Not found')
+ blobs.size(id, done())
pull(
blobs.get(id),
pull.map(Buffer),
- ident(function (type) {
- type = type && mime.lookup(type)
- if (type) self.res.setHeader('Content-Type', type)
- if (self.query.name) self.res.setHeader('Content-Disposition',
- 'inline; filename='+encodeDispositionFilename(self.query.name))
- self.res.setHeader('Cache-Control', 'public, max-age=315360000')
- self.res.setHeader('etag', id)
- self.res.writeHead(200)
- }),
+ ident(done().bind(self, null)),
self.respondSink()
)
+ done(function (err, size, type) {
+ if (err) console.trace(err)
+ type = type && mime.lookup(type)
+ if (type) self.res.setHeader('Content-Type', type)
+ if (typeof size === 'number') self.res.setHeader('Content-Length', size)
+ if (self.query.name) self.res.setHeader('Content-Disposition',
+ 'inline; filename='+encodeDispositionFilename(self.query.name))
+ self.res.setHeader('Cache-Control', 'public, max-age=315360000')
+ self.res.setHeader('etag', id)
+ self.res.writeHead(200)
+ })
})
}
@@ -1044,6 +1158,20 @@ function catchHTMLError() {
}
}
+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')
}
@@ -1099,6 +1227,7 @@ Serve.prototype.wrapPage = function (title, searchQ) {
'published ',
self.app.render.msgLink(self.publishedMsg, done())
) : '',
+ // self.note,
content
)))
done(cb)
@@ -1106,7 +1235,7 @@ Serve.prototype.wrapPage = function (title, searchQ) {
)
}
-Serve.prototype.pullIdLink = function (id) {
+Serve.prototype.phIdLink = function (id) {
return pull(
pull.once(id),
this.renderIdsList()
@@ -1187,7 +1316,8 @@ Serve.prototype.wrapUserFeed = function (isScrolled, id) {
h('tr',
h('td'),
h('td',
- h('a', {href: render.toUrl('/contacts/' + id)}, 'contacts')
+ h('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ',
+ h('a', {href: render.toUrl('/about/' + id)}, 'about')
)
),
h('tr',
@@ -1222,6 +1352,422 @@ Serve.prototype.wrapUserFeed = function (isScrolled, id) {
})
}
+Serve.prototype.git = function (url) {
+ var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url)
+ switch (m[1]) {
+ 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])
+ 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 tree ' + 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.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)
+ )
+
+ 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 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')
+ )]) : '',
+ h('pre', self.app.render.linkify(commit.body)).outerHTML,
+ ]
+ ]),
+ 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)
+ )
+
+ 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 tag ' + rev),
+ self.respondSink(400)
+ )
+ var msgDate = new Date(obj.msg.value.timestamp)
+ self.app.git.getTag(obj, function (err, tag) {
+ 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)),
+ 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.readTree(obj),
+ paramap(function (file, cb) {
+ self.app.git.getObjectMsg({
+ obj: file.hash,
+ headMsgId: obj.msg.key,
+ }, function (err, msg) {
+ if (err && err.name === 'ObjectNotFoundError') return cb(null, file)
+ if (err) return cb(err)
+ file.msg = msg
+ cb(null, file)
+ })
+ }, 8),
+ pull.map(function (item) {
+ var type = item.mode === 0040000 ? 'tree' :
+ item.mode === 0160000 ? 'commit' : 'blob'
+ if (!item.msg) return ph('tr', [
+ ph('td',
+ u.escapeHTML(item.name) + (type === 'tree' ? '/' : '')),
+ ph('td', 'missing')
+ ])
+ var path = '/git/' + type + '/' + item.hash
+ + '?msg=' + encodeURIComponent(item.msg.key)
+ 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) + (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))
+ ),
+ ])
+ })
+ )
+ ]),
+ ]),
+ 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)
+ )
+
+ self.app.getMsgDecrypted(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({
+ rawUrl: self.app.render.toUrl('/git/raw/' + rev
+ + '?msg=' + encodeURIComponent(msg.key))
+ })
+ ),
+ ]),
+ self.wrapPage('git blob ' + rev),
+ self.respondSink(200)
+ )
+ })
+ })
+}
+
+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 === '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)
+}
+
+// wrap a binary source and render it or turn into an embed
+Serve.prototype.wrapBinary = function (opts) {
+ var self = this
+ return function (read) {
+ var readRendered, type
+ read = ident(function (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
+ })
+ }
+ return ph('pre', pull.map(function (buf) {
+ return h('div',
+ self.app.render.linkify(buf.toString('utf8'))
+ ).innerHTML
+ })(read))
+ }
+}
+
Serve.prototype.wrapPublic = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
@@ -1237,6 +1783,36 @@ Serve.prototype.wrapPublic = function (opts) {
})
}
+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).map(function (link) {
+ if (!u.isRef(link.link)) return
+ return ph('tr', [
+ ph('td', ph('code', link.link)),
+ 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) {
@@ -1368,7 +1944,21 @@ Serve.prototype.wrapChannels = function (opts) {
return u.hyperwrap(function (channels, cb) {
cb(null, [
h('section',
- h('h3', 'Channels')
+ 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