diff options
author | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2018-01-10 21:28:44 -1000 |
---|---|---|
committer | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2018-01-10 21:28:44 -1000 |
commit | 5c93e1267a7951f6aadae0ac269f73de9da7f423 (patch) | |
tree | 35e5af749ee1298a8a57120684af94cc5ce6df56 /lib/serve.js | |
parent | 23cd085f4cf52dfdd2d00b75c2370cf34508a98a (diff) | |
parent | 964cb82322e65c807382c6a84eba0a8e21c1c70f (diff) | |
download | patchfoo-5c93e1267a7951f6aadae0ac269f73de9da7f423.tar.gz patchfoo-5c93e1267a7951f6aadae0ac269f73de9da7f423.zip |
Merge branch 'master' into layout
Diffstat (limited to 'lib/serve.js')
-rw-r--r-- | lib/serve.js | 739 |
1 files changed, 630 insertions, 109 deletions
diff --git a/lib/serve.js b/lib/serve.js index 766b524..7e2fe3b 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -21,6 +21,10 @@ 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') module.exports = Serve @@ -76,7 +80,7 @@ Serve.prototype.go = function () { if (auth) { var a = auth.split(' ') if (a[0] == 'Basic') { - tok = Buffer.from(a[1],'base64').toString('ascii') + tok = Buffer.from(a[1],'base64').toString('ascii') } } if (tok != authtok) { @@ -196,7 +200,7 @@ Serve.prototype.publishVote = function (next) { } } -Serve.prototype.publishContact = function (cb) { +Serve.prototype.publishContact = function (next) { var content = { type: 'contact', contact: this.data.contact, @@ -205,7 +209,14 @@ Serve.prototype.publishContact = function (cb) { if (this.data.block) content.blocking = true if (this.data.unfollow) content.following = false if (this.data.unblock) content.blocking = false - this.publish(content, cb) + 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.publishAttend = function (cb) { @@ -335,6 +346,7 @@ Serve.prototype.path = function (url) { case '/npm-prebuilds': return this.npmPrebuilds(m[2]) case '/npm-readme': return this.npmReadme(m[2]) case '/markdown': return this.markdown(m[2]) + case '/zip': return this.zip(m[2]) } return this.respond(404, 'Not found') } @@ -529,7 +541,7 @@ Serve.prototype.advsearch = function (ext) { ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel', class: 'id-input', value: q.channel || ''}) - ]) + ]) ]), ph('tr', [ ph('td', {colspan: 2}, [ @@ -542,8 +554,8 @@ Serve.prototype.advsearch = function (ext) { hasQuery && pull( self.app.advancedSearch(q), self.renderThread({ - feed: q.source, - }), + feed: q.source, + }), self.wrapMessages() ) ]), @@ -569,7 +581,7 @@ Serve.prototype.live = function (ext) { self.app.sbot.createLogStream(opts), self.app.render.renderFeeds({ withGt: true, - filter: q.filter, + filter: q.filter, }), pull.map(u.toHTML) )), @@ -1005,57 +1017,71 @@ function threadHeads(msgs, rootId) { })) } -Serve.prototype.id = function (id, path) { +Serve.prototype.streamThreadWithComposer = function (opts) { var self = this - if (self.query.raw != null) return self.rawId(id) - - this.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) { - 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 - || (rootMsg.value.private - ? [rootMsg.value.author, self.app.sbot.id].filter(uniques()) - : undefined) - var threadRootId = rootContent && rootContent.root || id - var channel + var id = opts.root + 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)) + if (!rootMsg) { + console.log('id', id, 'opts', opts) + } + var rootContent = rootMsg && rootMsg.value && rootMsg.value.content + var recps = rootContent && rootContent.recps + || (rootMsg.value.private + ? [rootMsg.value.author, self.app.sbot.id].filter(uniques()) + : undefined) + var threadRootId = rootContent && rootContent.root || id + var channel = opts.channel - pull( - cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]), - pull.unique('key'), - self.app.unboxMessages(), - pull.through(function (msg) { - var c = msg && msg.value.content - if (!channel && 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 self.respond(500, err.stack || err) pull( - pull.values(sort(links)), - self.renderThread({ - msgId: id, - }), - self.wrapMessages(), - self.wrapThread({ - recps: recps, - root: threadRootId, - post: id, - branches: threadHeads(links, threadRootId), - postBranches: threadRootId !== id && threadHeads(links, id), - channel: channel, + cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]), + pull.unique('key'), + self.app.unboxMessages(), + pull.through(function (msg) { + var c = msg && msg.value.content + if (!channel && c.channel) channel = c.channel }), - self.wrapPage(id), - self.respondSink(200) + 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)) + cb(null, pull( + pull.values(sort(links)), + self.renderThread({ + msgId: id, + }), + self.wrapMessages(), + self.wrapThread({ + recps: recps, + root: threadRootId, + post: id, + branches: threadHeads(links, threadRootId), + postBranches: threadRootId !== id && threadHeads(links, id), + placeholder: opts.placeholder, + channel: channel, + }) + )) + } + }) + } +} + +Serve.prototype.id = function (id, path) { + var self = this + if (self.query.raw != null) return self.rawId(id) + pull( + self.streamThreadWithComposer({root: id}), + self.wrapPage(id), + self.respondSink(200) + ) } Serve.prototype.userFeed = function (id, path) { @@ -1210,15 +1236,20 @@ Serve.prototype.image = function (path) { if (err) return heresTheData(err) buffer = Buffer.concat(buffer) - jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) { - if (!err) buffer = rotatedBuffer + try { + jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) { + if (!err) buffer = rotatedBuffer - heresTheData(null, buffer) - pull( - pull.once(buffer), - self.respondSink() - ) - }) + 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) { @@ -1260,6 +1291,7 @@ Serve.prototype.renderThread = function (opts) { msgId: opts && opts.msgId, filter: this.query.filter, limit: Number(this.query.limit), + serve: this, }), pull.map(u.toHTML) ) @@ -1286,7 +1318,7 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { function onFirst(msg, cb) { var num = feedId ? msg.value.sequence : opts.sortByTimestamp ? msg.value.timestamp : - msg.timestamp || msg.ts + msg.timestamp || msg.ts if (q.forwards) { cb(null, links({ lt: num, @@ -1314,7 +1346,7 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { function onLast(msg, cb) { var num = feedId ? msg.value.sequence : opts.sortByTimestamp ? msg.value.timestamp : - msg.timestamp || msg.ts + msg.timestamp || msg.ts if (q.forwards) { cb(null, links({ lt: null, @@ -1557,8 +1589,8 @@ Serve.prototype.friendInfo = function (id, myId) { this.app.render.friendsList(), pull.map(function (html) { if (!first) { - first = true - return 'followed by your friends: ' + html + first = true + return 'followed by your friends: ' + html } return html }) @@ -1610,15 +1642,15 @@ Serve.prototype.wrapUserFeed = function (isScrolled, id) { ) ]), 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)) - ]) - ] + ph('tr', [ + ph('td'), + ph('td', {class: 'follow-info'}, self.followInfo(id, myId)) + ]), + ph('tr', [ + ph('td'), + ph('td', self.friendInfo(id, myId)) + ]) + ] ])), thread ]) @@ -1633,6 +1665,8 @@ Serve.prototype.git = function (url) { 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 'line-comment': return this.gitLineComment(m[2]) default: return this.respond(404, 'Not found') } } @@ -1765,15 +1799,34 @@ Serve.prototype.gitCommit = function (rev) { 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', ph('code', u.escapeHTML(JSON.stringify(file.msg)))), ph('td', file.deleted ? 'deleted' - : file.created ? 'created' - : file.hash ? 'changed' + : 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) + }, '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) + }, '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 }) )) ] @@ -1895,23 +1948,8 @@ Serve.prototype.gitTree = function (rev) { ]), 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 && err.name === 'BlobNotFoundError') return cb(null, {missingBlobs: err.links}) - if (err) return cb(err) - file.msg = msg - cb(null, file) - }) - }, 8), + self.app.git.readTreeFull(obj), pull.map(function (item) { - if (item.missingBlobs) { - return self.askWantBlobsForm(item.missingBlobs) - } if (!item.msg) return ph('tr', [ ph('td', u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')), @@ -1936,6 +1974,11 @@ Serve.prototype.gitTree = function (rev) { }, htime(fileDate)) ), ]) + }), + Catch(function (err) { + if (err && err.name === 'ObjectNotFoundError') return + if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links) + return false }) ) ]), @@ -1993,6 +2036,7 @@ Serve.prototype.gitBlob = function (rev) { 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 @@ -2006,6 +2050,296 @@ Serve.prototype.gitBlob = function (rev) { }) } +Serve.prototype.gitDiff = function (revs) { + var self = this + var parts = revs.split('..') + if (parts.length !== 2) return pull( + ph('div.error', 'revs should be <rev1>..<rev2>'), + 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) 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) + pull( + ph('section', [ + ph('h3', ph('a', {href: ''}, revsShort)), + ph('div', [ + ph('a', { + href: self.app.render.toUrl('/git/blob/' + rev1 + '?msg=' + encodeURIComponent(msg1.key)) + }, 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('/git/blob/' + rev2 + '?msg=' + encodeURIComponent(msg2.key)) + }, 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', u.escapeHTML(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) { @@ -2197,10 +2531,107 @@ Serve.prototype.markdown = function (url) { ) } +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) + } + }) +} + // 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) { @@ -2227,9 +2658,83 @@ Serve.prototype.wrapBinary = function (opts) { src: opts.rawUrl }) } - return ph('pre', pull.map(function (buf) { - return self.app.render.highlight(buf.toString('utf8'), ext) - })(read)) + 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 = 0 + 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, + }) + ) + ) : '' + ] + }) + ) + )) + } + }) } } @@ -2300,7 +2805,8 @@ Serve.prototype.wrapThread = function (opts) { self.app.render.prepareLinks(opts.recps, function (err, recps) { if (err) return cb(er) self.composer({ - placeholder: recps ? 'private reply' : 'reply', + placeholder: opts.placeholder + || (recps ? 'private reply' : 'reply'), id: 'reply', root: opts.root, post: opts.post, @@ -2439,6 +2945,11 @@ Serve.prototype.composer = function (opts, cb) { var data = self.data var myId = self.app.sbot.id + 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 @@ -2532,6 +3043,7 @@ Serve.prototype.composer = function (opts, cb) { enctype: 'multipart/form-data'}, h('input', {type: 'hidden', name: 'blobs', value: JSON.stringify(blobs)}), + h('input', {type: 'hidden', name: 'composer_id', value: opts.id}), opts.recps ? self.app.render.privateLine(opts.recps, done()) : opts.private ? h('div', h('input.recps-input', {name: 'recps', value: data.recps || '', placeholder: 'recipient ids'})) : '', @@ -2599,6 +3111,15 @@ Serve.prototype.composer = function (opts, cb) { type: 'post', text: String(data.text).replace(/\r\n/g, '\n'), } + 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 (mention.emoji) { @@ -2702,15 +3223,15 @@ Serve.prototype.composer = function (opts, cb) { }) var draftMsg = { - key: '%0000000000000000000000000000000000000000000=.sha256', - value: { - previous: '%0000000000000000000000000000000000000000000=.sha256', - author: '@0000000000000000000000000000000000000000000=.ed25519', - sequence: 1000, - timestamp: 1000000000000, - hash: 'sha256', - content: content - } + key: '%0000000000000000000000000000000000000000000=.sha256', + value: { + previous: '%0000000000000000000000000000000000000000000=.sha256', + author: '@0000000000000000000000000000000000000000000=.ed25519', + sequence: 1000, + timestamp: 1000000000000, + hash: 'sha256', + content: content + } } var estSize = JSON.stringify(draftMsg, null, 2).length sizeEl.innerHTML = self.app.render.formatSize(estSize) @@ -2725,9 +3246,9 @@ Serve.prototype.composer = function (opts, cb) { pull.once(msg), self.app.unboxMessages(), self.app.render.renderFeeds({ - raw: raw, - filter: self.query.filter, - }), + raw: raw, + filter: self.query.filter, + }), pull.drain(function (el) { msgContainer.appendChild(h('tbody', el)) }, cb) |