diff options
author | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-05-28 21:01:29 -1000 |
---|---|---|
committer | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-05-28 21:01:29 -1000 |
commit | 0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c (patch) | |
tree | 30bfa6e7ede6c125eec3a51cbef4892208b1f622 /lib/serve.js | |
parent | d8f6c6a19274cf53d9967d3597dd582ee7233ca3 (diff) | |
parent | b66bcecec258b0a2631ec338501afa9409882fe8 (diff) | |
download | patchfoo-0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c.tar.gz patchfoo-0e9903642e2e2c9cfb6f89f80a0256fa6f53ef5c.zip |
Merge branch 'master' into votes
Diffstat (limited to 'lib/serve.js')
-rw-r--r-- | lib/serve.js | 670 |
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 |