diff options
Diffstat (limited to 'lib/serve.js')
-rw-r--r-- | lib/serve.js | 369 |
1 files changed, 283 insertions, 86 deletions
diff --git a/lib/serve.js b/lib/serve.js index b971572..4f2ed42 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -19,6 +19,7 @@ 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') module.exports = Serve @@ -74,6 +75,11 @@ Serve.prototype.go = function () { filesCb(function (err) { gotData(err, data) }) + function addField(name, value) { + 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 done = multicb({pluck: 1, spread: true}) var cb = filesCb() @@ -85,14 +91,13 @@ Serve.prototype.go = function () { done(function (err, size, id) { if (err) return cb(err) if (size === 0 && !filename) return cb() - data[fieldname] = {link: id, name: filename, type: mimetype, size: size} + addField(fieldname, + {link: id, name: filename, type: mimetype, size: size}) cb() }) }) busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { - if (!(fieldname in data)) data[fieldname] = val - else if (Array.isArray(data[fieldname])) data[fieldname].push(val) - else data[fieldname] = [data[fieldname], val] + addField(fieldname, val) }) this.req.pipe(busboy) } else { @@ -117,9 +122,10 @@ Serve.prototype.go = function () { self.data = data if (err) next(err) 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 if (data.action_vote) self.publishVote(next) + else if (data.action_attend) self.publishAttend(next) else next() } @@ -156,8 +162,8 @@ Serve.prototype.publishVote = function (cb) { channel: this.data.channel || undefined, vote: { link: this.data.link, - value: Number(this.data.value), - expression: this.data.expression, + value: Number(this.data.vote_value), + expression: this.data.vote_expression, } } if (this.data.recps) content.recps = this.data.recps.split(',') @@ -173,6 +179,19 @@ Serve.prototype.publishContact = function (cb) { 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() @@ -264,6 +283,7 @@ Serve.prototype.path = function (url) { 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]) } m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) @@ -1004,22 +1024,18 @@ 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) { + 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) } - if (!has) return self.respond(404, 'Not found') - blobs.size(id, done()) pull( blobs.get(id), pull.map(Buffer), - ident(done().bind(self, null)), + ident(gotType), self.respondSink() ) - done(function (err, size, type) { - if (err) console.trace(err) + function gotType(type) { type = type && mime.lookup(type) if (type) self.res.setHeader('Content-Type', type) if (typeof size === 'number') self.res.setHeader('Content-Length', size) @@ -1028,7 +1044,7 @@ Serve.prototype.blob = function (id) { self.res.setHeader('Cache-Control', 'public, max-age=315360000') self.res.setHeader('etag', id) self.res.writeHead(200) - }) + } }) } @@ -1136,8 +1152,13 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { } Serve.prototype.renderRawMsgPage = function (id) { + var showMarkdownSource = (this.query.raw === 'md') + var raw = !showMarkdownSource return pull( - this.app.render.renderFeeds(true), + this.app.render.renderFeeds({ + raw: raw, + markdownSource: showMarkdownSource + }), pull.map(u.toHTML), this.wrapMessages(), this.wrapPage(id) @@ -1216,6 +1237,7 @@ Serve.prototype.wrapPage = function (title, searchQ) { h('a', {href: render.toUrl('/live')}, 'live'), ' ', h('a', {href: render.toUrl('/compose')}, 'compose'), ' ', h('a', {href: render.toUrl('/votes')}, 'votes'), ' ', + h('a', {href: render.toUrl('/emojis')}, 'emojis'), ' ', render.idLink(self.app.sbot.id, done()), ' ', h('input.search-input', {name: 'q', value: searchQ, placeholder: 'search'}) @@ -1486,7 +1508,23 @@ Serve.prototype.gitCommit = function (rev) { pull.once(commit.tree), self.gitObjectLinks(obj.msg.key, 'tree') )]) : '', - h('pre', self.app.render.linkify(commit.body)).outerHTML, + h('blockquote', + self.app.render.gitCommitBody(commit.body)).outerHTML, + ph('h4', 'files'), + ph('table', pull( + self.app.git.readCommitChanges(commit), + pull.map(function (file) { + 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.mode ? 'mode changed' + : JSON.stringify(file)) + ]) + }) + )) ] ]), self.wrapPage('git commit ' + rev), @@ -1619,20 +1657,19 @@ Serve.prototype.gitTree = function (rev) { }) }, 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' ? '/' : '')), + u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')), + ph('td', u.escapeHTML(item.hash)), ph('td', 'missing') ]) - var path = '/git/' + type + '/' + item.hash + var path = '/git/' + item.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' ? '/' : ''))), + u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))), ph('td', self.phIdLink(item.msg.value.author)), ph('td', @@ -1719,6 +1756,8 @@ Serve.prototype.gitObjectLinks = function (headMsgId, type) { 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)']) @@ -1793,7 +1832,7 @@ Serve.prototype.askWantBlobsForm = function (links) { if (!u.isRef(link.link)) return return ph('tr', [ ph('td', ph('code', link.link)), - ph('td', self.app.render.formatSize(link.size)), + !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '', ]) })), ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), @@ -1975,6 +2014,7 @@ Serve.prototype.composer = function (opts, cb) { var self = this opts = opts || {} var data = self.data + var myId = self.app.sbot.id var blobs = u.tryDecodeJSON(data.blobs) || {} if (data.upload && typeof data.upload === 'object') { @@ -1995,6 +2035,19 @@ Serve.prototype.composer = function (opts, cb) { 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, + } + } + if (data.upload) { // TODO: be able to change the content-type var isImage = /^image\//.test(data.upload.type) @@ -2005,7 +2058,8 @@ Serve.prototype.composer = function (opts, cb) { // get bare feed names var unknownMentionNames = {} - var unknownMentions = ssbMentions(data.text, {bareFeedNames: true}) + var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) + var unknownMentions = mentions .filter(function (mention) { return mention.link === '@' }) @@ -2018,6 +2072,26 @@ Serve.prototype.composer = function (opts, cb) { 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} + }) + // strip content other than feed ids from the recps field if (data.recps) { data.recps = u.extractFeedIds(data.recps).filter(uniques()).join(', ') @@ -2056,7 +2130,19 @@ Serve.prototype.composer = function (opts, cb) { h('input', {name: 'mention_name', type: 'hidden', value: mention.name}), h('input.id-input', {name: 'mention_id', size: 60, - value: mention.id, placeholder: 'id'})) + 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})) })) ] : '', h('table.ssb-msgs', @@ -2076,75 +2162,132 @@ Serve.prototype.composer = function (opts, cb) { )) done(cb) + function prepareContent(cb) { + var done = multicb({pluck: 1}) + content = { + type: 'post', + text: String(data.text).replace(/\r\n/g, '\n'), + } + var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) + .filter(function (mention) { + 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 + } 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 + }) + 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.branch = u.fromArray(opts.postBranches) || undefined + } else { + content.root = opts.root || undefined + content.branch = u.fromArray(opts.branches) || undefined + } + if (channel) content.channel = data.channel + + done(function (err) { + cb(err, content) + }) + } + function preview(raw, cb) { - var myId = self.app.sbot.id + var msgContainer = h('table.ssb-msgs') + var contentInput = h('input', {type: 'hidden', name: 'content'}) + var warningsContainer = h('div') + var content - try { - content = JSON.parse(data.text) - } catch (err) { - data.text = String(data.text).replace(/\r\n/g, '\n') - content = { - type: 'post', - text: data.text, - } - var mentions = ssbMentions(data.text, {bareFeedNames: true}) - .filter(function (mention) { - 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 - } 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 - } - return true - }) - 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.branch = u.fromArray(opts.postBranches) || undefined - } else { - content.root = opts.root || undefined - content.branch = u.fromArray(opts.branches) || undefined + 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) + contentInput.value = JSON.stringify(content) + var msg = { + value: { + author: myId, + timestamp: Date.now(), + content: content + } } - if (channel) content.channel = data.channel - } - 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')) + } + }) + 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), + pull.drain(function (el) { + msgContainer.appendChild(h('tbody', el)) + }, cb) + ) } - if (content.recps) msg.value.private = true - var msgContainer = h('table.ssb-msgs') - pull( - pull.once(msg), - self.app.unboxMessages(), - self.app.render.renderFeeds(raw), - pull.drain(function (el) { - msgContainer.appendChild(h('tbody', el)) - }, cb) - ) + return [ - h('input', {type: 'hidden', name: 'content', - value: JSON.stringify(content)}), + contentInput, opts.redirectToPublishedMsg ? h('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}) : '', + warningsContainer, h('div', h('em', 'draft:')), msgContainer, h('div.composer-actions', @@ -2154,3 +2297,57 @@ Serve.prototype.composer = function (opts, cb) { } } + +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.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) + ) +} |