aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/about.js1
-rw-r--r--lib/app.js41
-rw-r--r--lib/git.js143
-rw-r--r--lib/render-msg.js78
-rw-r--r--lib/render.js30
-rw-r--r--lib/serve.js369
-rw-r--r--lib/util.js21
-rw-r--r--package.json4
-rw-r--r--static/styles.css2
9 files changed, 571 insertions, 118 deletions
diff --git a/lib/about.js b/lib/about.js
index e94b7fc..4ad153b 100644
--- a/lib/about.js
+++ b/lib/about.js
@@ -96,6 +96,7 @@ About.prototype.get = function (dest, cb) {
if (!c) return
var about = aboutByFeed[author] || (aboutByFeed[author] = {})
if (c.name) about.name = c.name
+ if (c.title) about.title = c.title
if (c.image) about.image = u.linkDest(c.image)
if (c.description) about.description = c.description
}, function (err) {
diff --git a/lib/app.js b/lib/app.js
index 03df277..d1f76f3 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -12,6 +12,7 @@ var About = require('./about')
var Serve = require('./serve')
var Render = require('./render')
var Git = require('./git')
+var cat = require('pull-cat')
module.exports = App
@@ -38,6 +39,7 @@ function App(sbot, config) {
this._getAbout.bind(this))
this.unboxContent = memo({cache: lru(100)}, sbot.private.unbox)
this.reverseNameCache = lru(500)
+ this.reverseEmojiNameCache = lru(500)
this.unboxMsg = this.unboxMsg.bind(this)
@@ -170,6 +172,17 @@ App.prototype.publish = function (content, cb) {
tryPublish(2)
}
+App.prototype.wantSizeBlob = function (id, cb) {
+ var blobs = this.sbot.blobs
+ blobs.size(id, function (err, size) {
+ if (size != null) return cb(null, size)
+ blobs.want(id, function (err) {
+ if (err) return cb(err)
+ blobs.size(id, cb)
+ })
+ })
+}
+
App.prototype.addBlob = function (cb) {
var done = multicb({pluck: 1, spread: true})
var hashCb = done()
@@ -177,7 +190,10 @@ App.prototype.addBlob = function (cb) {
done(function (err, hash, add) {
cb(err, hash)
})
- return sink
+ return pull(
+ hasher(hashCb),
+ this.sbot.blobs.add(addCb)
+ )
}
App.prototype.pushBlob = function (id, cb) {
@@ -230,6 +246,10 @@ App.prototype.getReverseNameSync = function (name) {
return id
}
+App.prototype.getReverseEmojiNameSync = function (name) {
+ return this.reverseEmojiNameCache.get(name)
+}
+
App.prototype.getNameSync = function (name) {
var about = this.aboutCache.get(name)
return about && about.name
@@ -432,3 +452,22 @@ App.prototype.getVoted = function (_opts, cb) {
App.prototype.createAboutStreams = function (id) {
return this.about.createAboutStreams(id)
}
+
+App.prototype.streamEmojis = function () {
+ return pull(
+ cat([
+ this.sbot.links({
+ rel: 'mentions',
+ source: this.sbot.id,
+ dest: '&',
+ values: true
+ }),
+ this.sbot.links({rel: 'mentions', dest: '&', values: true})
+ ]),
+ this.unboxMessages(),
+ pull.map(function (msg) { return msg.value.content.mentions }),
+ pull.flatten(),
+ pull.filter('emoji'),
+ pull.unique('link')
+ )
+}
diff --git a/lib/git.js b/lib/git.js
index 1360a6f..fa889ef 100644
--- a/lib/git.js
+++ b/lib/git.js
@@ -7,6 +7,9 @@ var packidx = require('pull-git-packidx-parser')
var Reader = require('pull-reader')
var toPull = require('stream-to-pull-stream')
var zlib = require('zlib')
+var looper = require('looper')
+var multicb = require('multicb')
+var kvdiff = require('pull-kvdiff')
var ObjectNotFoundError = u.customError('ObjectNotFoundError')
@@ -15,6 +18,7 @@ var types = {
commit: true,
tree: true,
}
+var emptyBlobHash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
module.exports = Git
@@ -81,6 +85,7 @@ Git.prototype.openObject = function (opts, cb) {
}
Git.prototype.readObject = function (obj) {
+ if (obj.offset === obj.next) return pull.empty()
return pull(
this.app.readBlobSlice(obj.packLink, {start: obj.offset, end: obj.next}),
this.decodeObject({
@@ -99,6 +104,19 @@ Git.prototype._findObject = function (opts, cb) {
if (!opts.obj) return cb(new TypeError('missing object id'))
var self = this
var objId = opts.obj
+ if (objId === emptyBlobHash) {
+ // special case: the empty blob may be found anywhere
+ self.app.getMsgDecrypted(opts.headMsgId, function (err, msg) {
+ if (err) return cb(err)
+ return cb(null, {
+ offset: 0,
+ next: 0,
+ packLink: null,
+ idx: null,
+ msg: msg,
+ })
+ })
+ }
self.findObjectMsgs(opts, function (err, msgs) {
if (err) return cb(err)
if (msgs.length === 0)
@@ -501,15 +519,20 @@ Git.prototype.getTag = function (obj, cb) {
function readCString(reader, cb) {
var chars = []
- reader.read(1, function next(err, ch) {
+ var loop = looper(function () {
+ reader.read(1, next)
+ })
+ function next(err, ch) {
if (err) return cb(err)
if (ch[0] === 0) return cb(null, Buffer.concat(chars).toString('utf8'))
chars.push(ch)
- reader.read(1, next)
- })
+ loop()
+ }
+ loop()
}
Git.prototype.readTree = function (obj) {
+ var self = this
var reader = Reader()
reader(this.readObject(obj))
return function (abort, cb) {
@@ -524,9 +547,121 @@ Git.prototype.readTree = function (obj) {
cb(null, {
name: name,
mode: mode,
- hash: hash.toString('hex')
+ hash: hash.toString('hex'),
+ type: mode === 0040000 ? 'tree' :
+ mode === 0160000 ? 'commit' : 'blob',
})
})
})
}
}
+
+Git.prototype.readCommitChanges = function (commit) {
+ var self = this
+ return u.readNext(function (cb) {
+ var done = multicb({pluck: 1})
+ commit.parents.forEach(function (rev) {
+ var cb = done()
+ self.getObjectMsg({
+ obj: rev,
+ headMsgId: commit.msg.key,
+ type: 'commit',
+ }, function (err, msg) {
+ if (err) return cb(err)
+ self.openObject({
+ obj: rev,
+ msg: msg.key,
+ }, function (err, obj) {
+ if (err) return cb(err)
+ self.getCommit(obj, cb)
+ })
+ })
+ })
+ done()(null, commit)
+ done(function (err, commits) {
+ if (err) return cb(err)
+ var done = multicb({pluck: 1})
+ commits.forEach(function (commit) {
+ var cb = done()
+ if (!commit.tree) return cb(null, pull.empty())
+ self.getObjectMsg({
+ obj: commit.tree,
+ headMsgId: commit.msg.key,
+ type: 'tree',
+ }, function (err, msg) {
+ if (err) return cb(err)
+ self.openObject({
+ obj: commit.tree,
+ msg: commit.msg.key,
+ }, cb)
+ })
+ })
+ done(function (err, trees) {
+ if (err) return cb(err)
+ cb(null, self.diffTreesRecursive(trees))
+ })
+ })
+ })
+}
+
+Git.prototype.diffTrees = function (objs) {
+ var self = this
+ return pull(
+ kvdiff(objs.map(function (obj) {
+ return self.readTree(obj)
+ }), 'name'),
+ pull.map(function (item) {
+ var diff = item.diff || {}
+ var head = item.values[item.values.length-1]
+ var created = true
+ for (var k = 0; k < item.values.length-1; k++)
+ if (item.values[k]) created = false
+ return {
+ name: item.key,
+ hash: diff.hash,
+ mode: diff.mode,
+ type: item.values.map(function (val) { return val.type }),
+ deleted: !head,
+ created: created
+ }
+ })
+ )
+}
+
+Git.prototype.diffTreesRecursive = function (objs) {
+ var self = this
+ return pull(
+ self.diffTrees(objs),
+ paramap(function (item, cb) {
+ if (!item.type.some(function (t) { return t === 'tree' }))
+ return cb(null, [item])
+ var done = multicb({pluck: 1})
+ item.type.forEach(function (type, i) {
+ var cb = done()
+ if (type !== 'tree') return cb(null, pull.once(item))
+ var hash = item.hash[i]
+ self.getObjectMsg({
+ obj: hash,
+ headMsgId: objs[i].msg.key,
+ }, function (err, msg) {
+ if (err) return cb(err)
+ self.openObject({
+ obj: hash,
+ msg: msg.key,
+ }, cb)
+ })
+ })
+ done(function (err, objs) {
+ if (err) return cb(err)
+ cb(null, pull(
+ self.diffTreesRecursive(objs),
+ pull.map(function (f) {
+ f.name = item.name + '/' + f.name
+ return f
+ })
+ ))
+ })
+ }, 4),
+ pull.flatten()
+ )
+}
diff --git a/lib/render-msg.js b/lib/render-msg.js
index 72b8015..8346baf 100644
--- a/lib/render-msg.js
+++ b/lib/render-msg.js
@@ -147,8 +147,14 @@ RenderMsg.prototype.actions = function () {
h('a', {href: '?gt=' + this.msg.timestamp}, '↓'), ' '] : '',
this.c.type === 'gathering' ? [
h('a', {href: this.render.toUrl('/about/' + encodeURIComponent(this.msg.key))}, 'about'), ' '] : '',
- h('a', {href: this.toUrl(this.msg.key) + '?raw'}, 'raw'), ' ',
- this.voteFormInner('dig')
+ typeof this.c.text === 'string' ? [
+ h('a', {href: this.toUrl(this.msg.key) + '?raw=md',
+ title: 'view markdown source'}, 'md'), ' '] : '',
+ h('a', {href: this.toUrl(this.msg.key) + '?raw',
+ title: 'view raw message'}, 'raw'), ' ',
+ this.buttonsCommon(),
+ this.c.type === 'gathering' ? [this.attendButton(), ' '] : '',
+ this.voteButton('dig')
) : [
this.msg.rel ? [this.msg.rel, ' '] : ''
]
@@ -175,16 +181,29 @@ RenderMsg.prototype.recpsIds = function () {
: []
}
-RenderMsg.prototype.voteFormInner = function (expression) {
+RenderMsg.prototype.buttonsCommon = function () {
var chan = this.msg.value.content.channel
+ var recps = this.recpsIds()
return [
- h('input', {type: 'hidden', name: 'action', value: 'vote'}),
- h('input', {type: 'hidden', name: 'recps',
- value: this.recpsIds().join(',')}),
chan ? h('input', {type: 'hidden', name: 'channel', value: chan}) : '',
h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
- h('input', {type: 'hidden', name: 'value', value: 1}),
- h('input', {type: 'submit', name: 'expression', value: expression})]
+ h('input', {type: 'hidden', name: 'recps', value: recps.join(',')})
+ ]
+}
+
+RenderMsg.prototype.voteButton = function (expression) {
+ var chan = this.msg.value.content.channel
+ return [
+ h('input', {type: 'hidden', name: 'vote_value', value: 1}),
+ h('input', {type: 'hidden', name: 'vote_expression', value: expression}),
+ h('input', {type: 'submit', name: 'action_vote', value: expression})]
+}
+
+RenderMsg.prototype.attendButton = function () {
+ var chan = this.msg.value.content.channel
+ return [
+ h('input', {type: 'submit', name: 'action_attend', value: 'attend'})
+ ]
}
RenderMsg.prototype.message = function (cb) {
@@ -231,9 +250,21 @@ RenderMsg.prototype.encrypted = function (cb) {
}
RenderMsg.prototype.markdown = function (cb) {
+ if (this.opts.markdownSource)
+ return this.markdownSource(this.c.text, this.c.mentions)
return this.render.markdown(this.c.text, this.c.mentions)
}
+RenderMsg.prototype.markdownSource = function (text, mentions) {
+ return h('div',
+ h('pre', String(text)),
+ mentions ? [
+ h('div', h('em', 'mentions:')),
+ this.valueTable(mentions, function () {})
+ ] : ''
+ ).innerHTML
+}
+
RenderMsg.prototype.post = function (cb) {
var self = this
var done = multicb({pluck: 1, spread: true})
@@ -360,15 +391,27 @@ RenderMsg.prototype.link1 = function (link, cb) {
function dateTime(d) {
var date = new Date(d.epoch)
- return date.toUTCString()
+ return date.toString()
// d.bias
// d.epoch
}
RenderMsg.prototype.about = function (cb) {
- var img = u.linkDest(this.c.image)
var done = multicb({pluck: 1, spread: true})
var elCb = done()
+
+ var isAttendingMsg = u.linkDest(this.c.attendee) === this.msg.value.author
+ && Object.keys(this.c).sort().join() === 'about,attendee,type'
+ if (isAttendingMsg) {
+ var attending = !this.c.attendee.remove
+ this.wrapMini([
+ attending ? ' is attending' : ' is not attending', ' ',
+ this.link1(this.c.about, done())
+ ], elCb)
+ return done(cb)
+ }
+
+ var img = u.linkDest(this.c.image)
// if there is a description, it is likely to be multi-line
var hasDescription = this.c.description != null
var wrap = hasDescription ? this.wrap : this.wrapMini
@@ -493,13 +536,16 @@ RenderMsg.prototype.gitUpdate = function (cb) {
return h('li', h('a', {href: self.render.toUrl(path)},
h('code', String(commit.sha1).substr(0, 8))), ' ',
self.linkify(String(commit.title)),
- self.gitCommitBody(commit.body)
+ self.render.gitCommitBody(commit.body)
)
})) : '',
Array.isArray(self.c.tags) ?
h('ul', self.c.tags.map(function (tag) {
+ var path = '/git/tag/' + encodeURIComponent(tag.sha1)
+ + '?msg=' + encodeURIComponent(self.msg.key)
return h('li',
- h('code', String(tag.sha1).substr(0, 8)), ' ',
+ h('a', {href: self.render.toUrl(path)},
+ h('code', String(tag.sha1).substr(0, 8))), ' ',
'tagged ', String(tag.type), ' ',
h('code', String(tag.object).substr(0, 8)), ' ',
String(tag.tag)
@@ -513,14 +559,6 @@ RenderMsg.prototype.gitUpdate = function (cb) {
})
}
-RenderMsg.prototype.gitCommitBody = function (body) {
- if (!body) return ''
- var isMarkdown = !/^# Conflicts:$/m.test(body)
- return isMarkdown
- ? h('div', {innerHTML: this.render.markdown('\n' + body)})
- : h('pre', this.linkify('\n' + body))
-}
-
RenderMsg.prototype.gitPullRequest = function (cb) {
var self = this
var done = multicb({pluck: 1, spread: true})
diff --git a/lib/render.js b/lib/render.js
index 102035b..c1c7edf 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -88,13 +88,27 @@ function Render(app, opts) {
Render.prototype.emoji = function (emoji) {
var name = ':' + emoji + ':'
- return emoji in emojis ?
- h('img.ssb-emoji', {
+ var link = this._mentions && this._mentions[name]
+ if (link && link.link) {
+ this.app.reverseEmojiNameCache.set(emoji, link.link)
+ return h('img.ssb-emoji', {
+ src: this.opts.img_base + link.link,
+ alt: name
+ + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : ''),
+ height: 17,
+ title: name,
+ })
+ }
+ if (emoji in emojis) {
+ return h('img.ssb-emoji', {
src: this.opts.emoji_base + emoji + '.png',
alt: name,
height: 17,
+ align: 'absmiddle',
title: name,
- }) : name
+ })
+ }
+ return name
}
/* disabled until it can be done safely without breaking html
@@ -113,6 +127,8 @@ Render.prototype.markdown = function (text, mentions) {
var mentionsByLink = this._mentionsByLink = {}
if (Array.isArray(mentions)) mentions.forEach(function (link) {
if (!link) return
+ else if (link.emoji)
+ mentionsObj[':' + link.name + ':'] = link
else if (link.name)
mentionsObj['@' + link.name] = link.link
else if (link.host === 'http://localhost:7777')
@@ -290,6 +306,14 @@ Render.prototype.renderFeeds = function (opts) {
}, 4)
}
+Render.prototype.gitCommitBody = function (body) {
+ if (!body) return ''
+ var isMarkdown = !/^# Conflicts:$/m.test(body)
+ return isMarkdown
+ ? h('div', {innerHTML: this.markdown('\n' + body)})
+ : h('pre', this.linkify('\n' + body))
+}
+
Render.prototype.getName = function (id, cb) {
// TODO: consolidate the get name/link functions
var self = this
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)
+ )
+}
diff --git a/lib/util.js b/lib/util.js
index d25ecaf..e4cf415 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -72,13 +72,17 @@ u.linkDest = function (link) {
}
u.toArray = function (x) {
- return !x ? [] : Array.isArray(x) ? x : [x]
+ return x == null ? [] : Array.isArray(x) ? x : [x]
}
u.fromArray = function (arr) {
return Array.isArray(arr) && arr.length === 1 ? arr[0] : arr
}
+u.toLinkArray = function (x) {
+ return u.toArray(x).map(u.toLink).filter(u.linkDest)
+}
+
u.renderError = function(err) {
return h('div.error',
h('h3', err.name),
@@ -102,7 +106,7 @@ u.tryDecodeJSON = function (json) {
}
}
-u.extractFeedIds = function (str) {
+u.extractRefs = function (str) {
var ids = []
String(str).replace(u.ssbRefRegex, function (id) {
ids.push(id)
@@ -110,6 +114,18 @@ u.extractFeedIds = function (str) {
return ids
}
+u.extractFeedIds = function (str) {
+ return u.extractRefs(str).filter(function (ref) {
+ return ref[0] === '@'
+ })
+}
+
+u.extractBlobIds = function (str) {
+ return u.extractRefs(str).filter(function (ref) {
+ return ref[0] === '&'
+ })
+}
+
u.isMsgReadable = function (msg) {
var c = msg && msg.value && msg.value.content
return typeof c === 'object' && c !== null
@@ -137,6 +153,7 @@ u.customError = function (name) {
}
u.escapeHTML = function (html) {
+ if (!html) return ''
return html.toString('utf8')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
diff --git a/package.json b/package.json
index 2a78964..816c115 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"human-time": "^0.0.1",
"hyperscript": "^2.0.2",
"hashlru": "^2.1.0",
+ "looper": "^4.0.0",
"mime-types": "^2.1.12",
"multicb": "^1.2.1",
"pull-cat": "^1.1.11",
@@ -17,13 +18,14 @@
"pull-hash": "^1.0.0",
"pull-hyperscript": "^0.2.2",
"pull-identify-filetype": "^1.1.0",
+ "pull-kvdiff": "^0.0.1",
"pull-paginate": "^1.0.0",
"pull-paramap": "^1.2.1",
"pull-reader": "^1.2.9",
"pull-stream": "^3.5.0",
"ssb-contact": "^1.0.0",
"ssb-marked": "^0.7.1",
- "ssb-mentions": "^0.2.0",
+ "ssb-mentions": "github:ssbc/ssb-mentions#emoji",
"ssb-party": "^0.3.0",
"ssb-sort": "^1.0.0",
"stream-to-pull-stream": "^1.7.2"
diff --git a/static/styles.css b/static/styles.css
index 3d513f2..4497f90 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -16,12 +16,12 @@ section {
.ssb-post img {
max-width: 100%;
- background-color: #eee;
}
img.ssb-emoji {
height: 1em;
width: auto;
+ border: none;
}
.nav-bar {