aboutsummaryrefslogtreecommitdiff
path: root/lib/serve.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/serve.js')
-rw-r--r--lib/serve.js369
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)
+ )
+}