aboutsummaryrefslogtreecommitdiff
path: root/lib/serve.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/serve.js')
-rw-r--r--lib/serve.js1254
1 files changed, 1106 insertions, 148 deletions
diff --git a/lib/serve.js b/lib/serve.js
index 1ce18c6..839725a 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -19,10 +19,13 @@ 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')
+var jpeg = require('jpeg-autorotate')
module.exports = Serve
var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
+var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles')
var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/
@@ -74,6 +77,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 cb = filesCb()
pull(
@@ -83,15 +91,13 @@ Serve.prototype.go = function () {
if (link.size === 0 && !filename) return cb()
link.name = filename
link.type = mimetype
- data[fieldname] = link
+ addField(fieldname, link)
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 {
@@ -116,8 +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()
}
@@ -154,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(',')
@@ -171,6 +179,36 @@ 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()
+ var ids = self.data.blob_ids.split(',')
+ if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(',')))
+ var done = multicb({pluck: 1})
+ ids.forEach(function (id) {
+ self.app.wantSizeBlob(id, done())
+ })
+ if (self.data.async_want) return cb()
+ done(function (err) {
+ if (err) return cb(err)
+ // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.')
+ cb()
+ })
+}
+
Serve.prototype.publish = function (content, cb) {
var self = this
var done = multicb({pluck: 1, spread: true})
@@ -238,14 +276,18 @@ Serve.prototype.path = function (url) {
case '/new': return this.new(m[2])
case '/public': return this.public(m[2])
case '/private': return this.private(m[2])
+ case '/mentions': return this.mentions(m[2])
case '/search': return this.search(m[2])
case '/advsearch': return this.advsearch(m[2])
case '/vote': return this.vote(m[2])
case '/peers': return this.peers(m[2])
+ case '/status': return this.status(m[2])
case '/channels': return this.channels(m[2])
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)
switch (m[1]) {
@@ -254,8 +296,13 @@ Serve.prototype.path = function (url) {
case '/links': return this.links(m[2])
case '/static': return this.static(m[2])
case '/emoji': return this.emoji(m[2])
+ case '/highlight': return this.highlight(m[2])
case '/contacts': return this.contacts(m[2])
case '/about': return this.about(m[2])
+ case '/git': return this.git(m[2])
+ case '/image': return this.image(m[2])
+ case '/npm': return this.npm(m[2])
+ case '/npm-readme': return this.npmReadme(m[2])
}
return this.respond(404, 'Not found')
}
@@ -347,18 +394,13 @@ Serve.prototype.private = function (ext) {
var q = this.query
var opts = {
reverse: !q.forwards,
- sortByTimestamp: q.sort === 'claimed',
lt: Number(q.lt) || Date.now(),
gt: Number(q.gt) || -Infinity,
+ limit: Number(q.limit) || 12,
}
- var limit = Number(q.limit) || 12
pull(
- this.app.createLogStream(opts),
- pull.filter(u.isMsgEncrypted),
- this.app.unboxMessages(),
- pull.filter(u.isMsgReadable),
- pull.take(limit),
+ this.app.streamPrivate(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapPrivate(opts),
@@ -369,6 +411,32 @@ Serve.prototype.private = function (ext) {
)
}
+Serve.prototype.mentions = function (ext) {
+ var self = this
+ var q = self.query
+ var opts = {
+ reverse: !q.forwards,
+ sortByTimestamp: q.sort === 'claimed',
+ lt: Number(q.lt) || Date.now(),
+ gt: Number(q.gt) || -Infinity,
+ limit: Number(q.limit) || 12,
+ }
+
+ return pull(
+ ph('section', {}, [
+ ph('h3', 'Mentions'),
+ pull(
+ self.app.streamMentions(opts),
+ self.app.unboxMessages(),
+ self.renderThreadPaginated(opts, null, q),
+ self.wrapMessages()
+ )
+ ]),
+ self.wrapPage('mentions'),
+ self.respondSink(200)
+ )
+}
+
Serve.prototype.search = function (ext) {
var searchQ = (this.query.q || '').trim()
var self = this
@@ -487,6 +555,90 @@ Serve.prototype.compose = function (ext) {
})
}
+Serve.prototype.votes = function (path) {
+ if (path) return pull(
+ pull.once(u.renderError(new Error('Not implemented')).outerHTML),
+ this.wrapPage('#' + channel),
+ this.respondSink(404, {'Content-Type': ctype('html')})
+ )
+
+ var self = this
+ var q = self.query
+ var opts = {
+ reverse: !q.forwards,
+ limit: Number(q.limit) || 50,
+ }
+ var gt = Number(q.gt)
+ if (gt) opts.gt = gt
+ var lt = Number(q.lt)
+ if (lt) opts.lt = lt
+
+ self.app.getVoted(opts, function (err, voted) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('#' + channel),
+ self.respondSink(500, {'Content-Type': ctype('html')})
+ )
+
+ pull(
+ ph('table', [
+ ph('thead', [
+ ph('tr', [
+ ph('td', {colspan: 2}, self.syncPager({
+ first: voted.firstTimestamp,
+ last: voted.lastTimestamp,
+ }))
+ ])
+ ]),
+ ph('tbody', pull(
+ pull.values(voted.items),
+ paramap(function (item, cb) {
+ cb(null, ph('tr', [
+ ph('td', [String(item.value)]),
+ ph('td', [
+ self.phIdLink(item.id),
+ pull.once(' dug by '),
+ self.renderIdsList()(pull.values(item.feeds))
+ ])
+ ]))
+ }, 8)
+ )),
+ ph('tfoot', {}, []),
+ ]),
+ self.wrapPage('votes'),
+ self.respondSink(200, {
+ 'Content-Type': ctype('html')
+ })
+ )
+ })
+}
+
+Serve.prototype.syncPager = function (opts) {
+ var q = this.query
+ var reverse = !q.forwards
+ var min = (reverse ? opts.last : opts.first) || Number(q.gt)
+ var max = (reverse ? opts.first : opts.last) || Number(q.lt)
+ var minDate = new Date(min)
+ var maxDate = new Date(max)
+ var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined})
+ var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1})
+ var atNewest = reverse ? !q.lt : !max
+ var atOldest = reverse ? !min : !q.gt
+ if (atNewest && !reverse) qOlder.lt++
+ if (atOldest && reverse) qNewer.gt--
+ return h('div',
+ atOldest ? 'oldest' : [
+ h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ',
+ h('span', {title: minDate.toString()}, htime(minDate)), ' ',
+ ],
+ ' - ',
+ atNewest ? 'now' : [
+ h('span', {title: maxDate.toString()}, htime(maxDate)), ' ',
+ h('a', {href: '?' + qs.stringify(qNewer)}, '>>')
+ ]
+ ).outerHTML
+}
+
Serve.prototype.peers = function (ext) {
var self = this
if (self.data.action === 'connect') {
@@ -532,6 +684,34 @@ Serve.prototype.peers = function (ext) {
)
}
+Serve.prototype.status = function (ext) {
+ var self = this
+
+ if (!self.app.sbot.status) return pull(
+ pull.once('missing sbot status method'),
+ this.wrapPage('status'),
+ self.respondSink(400)
+ )
+
+ pull(
+ ph('section', [
+ ph('h3', 'Status'),
+ pull(
+ u.readNext(function (cb) {
+ self.app.sbot.status(function (err, status) {
+ cb(err, status && pull.once(status))
+ })
+ }),
+ pull.map(function (status) {
+ return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML
+ })
+ )
+ ]),
+ this.wrapPage('status'),
+ this.respondSink(200)
+ )
+}
+
Serve.prototype.channels = function (ext) {
var self = this
var id = self.app.sbot.id
@@ -581,14 +761,6 @@ Serve.prototype.channels = function (ext) {
)
}
-Serve.prototype.phIdLink = function (id) {
- return pull(
- pull.once(id),
- pull.asyncMap(this.renderIdLink.bind(this)),
- pull.map(u.toHTML)
- )
-}
-
Serve.prototype.contacts = function (path) {
var self = this
var id = String(path).substr(1)
@@ -655,7 +827,7 @@ Serve.prototype.about = function (path) {
function renderAboutOpContent(op) {
if (op.prop === 'image')
- return renderAboutOpImage(op.value)
+ return renderAboutOpImage(u.toLink(op.value))
if (op.prop === 'description')
return h('div', {innerHTML: render.markdown(op.value)}).outerHTML
if (op.prop === 'title')
@@ -772,23 +944,11 @@ Serve.prototype.channel = function (path) {
lt: lt,
gt: gt,
limit: Number(q.limit) || 12,
- query: [{$filter: {
- value: {content: {channel: channel}},
- timestamp: {
- $gt: gt,
- $lt: lt,
- }
- }}]
+ channel: channel,
}
- if (!this.app.sbot.query) return pull(
- pull.once(u.renderError(new Error('Missing ssb-query plugin')).outerHTML),
- this.wrapPage('#' + channel),
- this.respondSink(400, {'Content-Type': ctype('html')})
- )
-
pull(
- this.app.sbot.query.read(opts),
+ this.app.streamChannel(opts),
this.renderThreadPaginated(opts, null, q),
this.wrapMessages(),
this.wrapChannel(channel),
@@ -900,9 +1060,12 @@ Serve.prototype.emoji = function (emoji) {
serveEmoji(this.req, this.res, emoji)
}
+Serve.prototype.highlight = function (dirs) {
+ this.file(path.join(hlCssDir, dirs))
+}
+
Serve.prototype.blob = function (id, path) {
var self = this
- var blobs = self.app.sbot.blobs
var etag = id + (path || '')
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
var key
@@ -921,22 +1084,18 @@ Serve.prototype.blob = function (id, path) {
return self.respond(400, 'Bad blob request')
}
}
- 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(
self.app.getBlob(id, key),
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)
// don't serve size for encrypted blob, because it refers to the size of
@@ -948,6 +1107,77 @@ Serve.prototype.blob = function (id, path) {
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
self.res.setHeader('etag', etag)
self.res.writeHead(200)
+ }
+ })
+}
+
+Serve.prototype.image = function (path) {
+ var self = this
+ var id, key
+ var m = urlIdRegex.exec(path)
+ if (m && m[2] === '&') id = m[1], path = m[3]
+ var etag = 'image-' + id + (path || '')
+ if (self.req.headers['if-none-match'] === etag) return self.respond(304)
+ if (path) {
+ path = decodeURIComponent(path)
+ if (path[0] === '#') {
+ try {
+ key = new Buffer(path.substr(1), 'base64')
+ } catch(err) {
+ return self.respond(400, err.message)
+ }
+ if (key.length !== 32) {
+ return self.respond(400, 'Bad blob key')
+ }
+ } else {
+ return self.respond(400, 'Bad blob request')
+ }
+ }
+ 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)
+ }
+
+ var done = multicb({pluck: 1, spread: true})
+ var heresTheData = done()
+ var heresTheType = done().bind(self, null)
+
+ pull(
+ self.app.getBlob(id, key),
+ pull.map(Buffer),
+ ident(heresTheType),
+ pull.collect(onFullBuffer)
+ )
+
+ function onFullBuffer (err, buffer) {
+ if (err) return heresTheData(err)
+ buffer = Buffer.concat(buffer)
+
+ jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
+ if (!err) buffer = rotatedBuffer
+
+ heresTheData(null, buffer)
+ pull(
+ pull.once(buffer),
+ self.respondSink()
+ )
+ })
+ }
+
+ done(function (err, data, type) {
+ if (err) {
+ console.trace(err)
+ self.respond(500, err.message || err)
+ }
+ type = type && mime.lookup(type)
+ if (type) self.res.setHeader('Content-Type', type)
+ self.res.setHeader('Content-Length', data.length)
+ if (self.query.name) self.res.setHeader('Content-Disposition',
+ 'inline; filename='+encodeDispositionFilename(self.query.name))
+ self.res.setHeader('Cache-Control', 'public, max-age=315360000')
+ self.res.setHeader('etag', etag)
+ self.res.writeHead(200)
})
})
}
@@ -972,38 +1202,28 @@ Serve.prototype.renderThread = function () {
)
}
-function mergeOpts(a, b) {
- var obj = {}, k
- for (k in a) {
- obj[k] = a[k]
- }
- for (k in b) {
- if (b[k] != null) obj[k] = b[k]
- else delete obj[k]
- }
- return obj
-}
-
Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
var self = this
function linkA(opts, name) {
- var q1 = mergeOpts(q, opts)
+ var q1 = u.mergeOpts(q, opts)
return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit)
}
function links(opts) {
var limit = opts.limit || q.limit || 10
return h('tr', h('td.paginate', {colspan: 3},
opts.forwards ? '↑ newer ' : '↓ older ',
- linkA(mergeOpts(opts, {limit: 1})), ' ',
- linkA(mergeOpts(opts, {limit: 10})), ' ',
- linkA(mergeOpts(opts, {limit: 100}))
+ linkA(u.mergeOpts(opts, {limit: 1})), ' ',
+ linkA(u.mergeOpts(opts, {limit: 10})), ' ',
+ linkA(u.mergeOpts(opts, {limit: 100}))
))
}
return pull(
paginate(
function onFirst(msg, cb) {
- var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts
+ var num = feedId ? msg.value.sequence :
+ opts.sortByTimestamp ? msg.value.timestamp :
+ msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: num,
@@ -1020,7 +1240,9 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
},
this.app.render.renderFeeds(),
function onLast(msg, cb) {
- var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts
+ var num = feedId ? msg.value.sequence :
+ opts.sortByTimestamp ? msg.value.timestamp :
+ msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: null,
@@ -1056,8 +1278,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)
@@ -1078,6 +1305,20 @@ function catchHTMLError() {
}
}
+function catchTextError() {
+ return function (read) {
+ var ended
+ return function (abort, cb) {
+ if (ended) return cb(ended)
+ read(abort, function (end, data) {
+ if (!end || end === true) return cb(end, data)
+ ended = true
+ cb(null, end.stack + '\n')
+ })
+ }
+ }
+}
+
function styles() {
return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8')
}
@@ -1108,19 +1349,25 @@ Serve.prototype.wrapPage = function (title, searchQ) {
h('title', title),
h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}),
h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}),
- h('style', styles())
+ h('style', styles()),
+ h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')})
),
h('body',
h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'},
h('a', {href: render.toUrl('/new')}, 'new') , ' ',
h('a', {href: render.toUrl('/public')}, 'public'), ' ',
h('a', {href: render.toUrl('/private')}, 'private') , ' ',
+ h('a', {href: render.toUrl('/mentions')}, 'mentions') , ' ',
h('a', {href: render.toUrl('/peers')}, 'peers') , ' ',
+ self.app.sbot.status ?
+ [h('a', {href: render.toUrl('/status')}, 'status'), ' '] : '',
h('a', {href: render.toUrl('/channels')}, 'channels') , ' ',
h('a', {href: render.toUrl('/friends')}, 'friends'), ' ',
h('a', {href: render.toUrl('/advsearch')}, 'search'), ' ',
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'})
@@ -1132,6 +1379,7 @@ Serve.prototype.wrapPage = function (title, searchQ) {
'published ',
self.app.render.msgLink(self.publishedMsg, done())
) : '',
+ // self.note,
content
)))
done(cb)
@@ -1139,25 +1387,18 @@ Serve.prototype.wrapPage = function (title, searchQ) {
)
}
-Serve.prototype.renderIdLink = function (id, cb) {
- var render = this.app.render
- var el = render.idLink(id, function (err) {
- if (err || !el) {
- el = h('a', {href: render.toUrl(id)}, id)
- }
- cb(null, el)
- })
+Serve.prototype.phIdLink = function (id) {
+ return pull(
+ pull.once(id),
+ this.renderIdsList()
+ )
}
Serve.prototype.friends = function (path) {
var self = this
pull(
self.app.sbot.friends.createFriendStream({hops: 1}),
- self.renderFriends(),
- pull.map(function (el) {
- return [el, ' ']
- }),
- pull.map(u.toHTML),
+ self.renderIdsList(),
u.hyperwrap(function (items, cb) {
cb(null, [
h('section',
@@ -1173,14 +1414,17 @@ Serve.prototype.friends = function (path) {
)
}
-Serve.prototype.renderFriends = function () {
+Serve.prototype.renderIdsList = function () {
var self = this
- return paramap(function (id, cb) {
- self.renderIdLink(id, function (err, el) {
- if (err) el = u.renderError(err, ext)
- cb(null, el)
- })
- }, 8)
+ return pull(
+ paramap(function (id, cb) {
+ self.app.render.getNameLink(id, cb)
+ }, 8),
+ pull.map(function (el) {
+ return [el, ' ']
+ }),
+ pull.map(u.toHTML)
+ )
}
var relationships = [
@@ -1260,6 +1504,532 @@ Serve.prototype.wrapUserFeed = function (isScrolled, id) {
})
}
+Serve.prototype.git = function (url) {
+ var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url)
+ switch (m[1]) {
+ case 'commit': return this.gitCommit(m[2])
+ case 'tag': return this.gitTag(m[2])
+ case 'tree': return this.gitTree(m[2])
+ case 'blob': return this.gitBlob(m[2])
+ case 'raw': return this.gitRaw(m[2])
+ default: return this.respond(404, 'Not found')
+ }
+}
+
+Serve.prototype.gitRaw = function (rev) {
+ var self = this
+ if (!/[0-9a-f]{24}/.test(rev)) {
+ return pull(
+ pull.once('\'' + rev + '\' is not a git object id'),
+ self.respondSink(400, {'Content-Type': 'text/plain'})
+ )
+ }
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git tree ' + rev),
+ self.respondSink(400)
+ )
+
+ self.app.git.openObject({
+ obj: rev,
+ msg: self.query.msg,
+ }, function (err, obj) {
+ if (err && err.name === 'BlobNotFoundError')
+ return self.askWantBlobs(err.links)
+ if (err) return pull(
+ pull.once(err.stack),
+ self.respondSink(400, {'Content-Type': 'text/plain'})
+ )
+ pull(
+ self.app.git.readObject(obj),
+ catchTextError(),
+ ident(function (type) {
+ type = type && mime.lookup(type)
+ if (type) self.res.setHeader('Content-Type', type)
+ self.res.setHeader('Cache-Control', 'public, max-age=315360000')
+ self.res.setHeader('etag', rev)
+ self.res.writeHead(200)
+ }),
+ self.respondSink()
+ )
+ })
+}
+
+Serve.prototype.gitAuthorLink = function (author) {
+ if (author.feed) {
+ var myName = this.app.getNameSync(author.feed)
+ var sigil = author.name === author.localpart ? '@' : ''
+ return ph('a', {
+ href: this.app.render.toUrl(author.feed),
+ title: author.localpart + (myName ? ' (' + myName + ')' : '')
+ }, u.escapeHTML(sigil + author.name))
+ } else {
+ return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)},
+ u.escapeHTML(author.name))
+ }
+}
+
+Serve.prototype.gitCommit = function (rev) {
+ var self = this
+ if (!/[0-9a-f]{24}/.test(rev)) {
+ return pull(
+ ph('div.error', 'rev is not a git object id'),
+ self.wrapPage('git'),
+ self.respondSink(400)
+ )
+ }
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git commit ' + rev),
+ self.respondSink(400)
+ )
+
+ self.app.git.openObject({
+ obj: rev,
+ msg: self.query.msg,
+ }, function (err, obj) {
+ if (err && err.name === 'BlobNotFoundError')
+ return self.askWantBlobs(err.links)
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git commit ' + rev),
+ self.respondSink(400)
+ )
+ var msgDate = new Date(obj.msg.value.timestamp)
+ self.app.git.getCommit(obj, function (err, commit) {
+ var missingBlobs
+ if (err && err.name === 'BlobNotFoundError')
+ missingBlobs = err.links, err = null
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git commit ' + rev),
+ self.respondSink(400)
+ )
+ pull(
+ ph('section', [
+ ph('h3', ph('a', {href: ''}, rev)),
+ ph('div', [
+ self.phIdLink(obj.msg.value.author), ' pushed ',
+ ph('a', {
+ href: self.app.render.toUrl(obj.msg.key),
+ title: msgDate.toLocaleString(),
+ }, htime(msgDate))
+ ]),
+ missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
+ ph('div', [
+ self.gitAuthorLink(commit.committer),
+ ' committed ',
+ ph('span', {title: commit.committer.date.toLocaleString()},
+ htime(commit.committer.date)),
+ ' in ', commit.committer.tz
+ ]),
+ commit.author ? ph('div', [
+ self.gitAuthorLink(commit.author),
+ ' authored ',
+ ph('span', {title: commit.author.date.toLocaleString()},
+ htime(commit.author.date)),
+ ' in ', commit.author.tz
+ ]) : '',
+ commit.parents.length ? ph('div', ['parents: ', pull(
+ pull.values(commit.parents),
+ self.gitObjectLinks(obj.msg.key, 'commit')
+ )]) : '',
+ commit.tree ? ph('div', ['tree: ', pull(
+ pull.once(commit.tree),
+ self.gitObjectLinks(obj.msg.key, 'tree')
+ )]) : '',
+ h('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),
+ self.respondSink(missingBlobs ? 409 : 200)
+ )
+ })
+ })
+}
+
+Serve.prototype.gitTag = function (rev) {
+ var self = this
+ if (!/[0-9a-f]{24}/.test(rev)) {
+ return pull(
+ ph('div.error', 'rev is not a git object id'),
+ self.wrapPage('git'),
+ self.respondSink(400)
+ )
+ }
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git tag ' + rev),
+ self.respondSink(400)
+ )
+
+ self.app.git.openObject({
+ obj: rev,
+ msg: self.query.msg,
+ }, function (err, obj) {
+ if (err && err.name === 'BlobNotFoundError')
+ return self.askWantBlobs(err.links)
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git tag ' + rev),
+ self.respondSink(400)
+ )
+ var msgDate = new Date(obj.msg.value.timestamp)
+ self.app.git.getTag(obj, function (err, tag) {
+ var missingBlobs
+ if (err && err.name === 'BlobNotFoundError')
+ missingBlobs = err.links, err = null
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git tag ' + rev),
+ self.respondSink(400)
+ )
+ pull(
+ ph('section', [
+ ph('h3', ph('a', {href: ''}, rev)),
+ ph('div', [
+ self.phIdLink(obj.msg.value.author), ' pushed ',
+ ph('a', {
+ href: self.app.render.toUrl(obj.msg.key),
+ title: msgDate.toLocaleString(),
+ }, htime(msgDate))
+ ]),
+ missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
+ ph('div', [
+ self.gitAuthorLink(tag.tagger),
+ ' tagged ',
+ ph('span', {title: tag.tagger.date.toLocaleString()},
+ htime(tag.tagger.date)),
+ ' in ', tag.tagger.tz
+ ]),
+ tag.type, ' ',
+ pull(
+ pull.once(tag.object),
+ self.gitObjectLinks(obj.msg.key, tag.type)
+ ), ' ',
+ ph('code', u.escapeHTML(tag.tag)),
+ h('pre', self.app.render.linkify(tag.body)).outerHTML,
+ ]
+ ]),
+ self.wrapPage('git tag ' + rev),
+ self.respondSink(missingBlobs ? 409 : 200)
+ )
+ })
+ })
+}
+
+Serve.prototype.gitTree = function (rev) {
+ var self = this
+ if (!/[0-9a-f]{24}/.test(rev)) {
+ return pull(
+ ph('div.error', 'rev is not a git object id'),
+ self.wrapPage('git'),
+ self.respondSink(400)
+ )
+ }
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git tree ' + rev),
+ self.respondSink(400)
+ )
+
+ self.app.git.openObject({
+ obj: rev,
+ msg: self.query.msg,
+ }, function (err, obj) {
+ var missingBlobs
+ if (err && err.name === 'BlobNotFoundError')
+ missingBlobs = err.links, err = null
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git tree ' + rev),
+ self.respondSink(400)
+ )
+ var msgDate = new Date(obj.msg.value.timestamp)
+ pull(
+ ph('section', [
+ ph('h3', ph('a', {href: ''}, rev)),
+ ph('div', [
+ self.phIdLink(obj.msg.value.author), ' ',
+ ph('a', {
+ href: self.app.render.toUrl(obj.msg.key),
+ title: msgDate.toLocaleString(),
+ }, htime(msgDate))
+ ]),
+ missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [
+ pull(
+ self.app.git.readTree(obj),
+ paramap(function (file, cb) {
+ self.app.git.getObjectMsg({
+ obj: file.hash,
+ headMsgId: obj.msg.key,
+ }, function (err, msg) {
+ if (err && err.name === 'ObjectNotFoundError') return cb(null, file)
+ if (err) return cb(err)
+ file.msg = msg
+ cb(null, file)
+ })
+ }, 8),
+ pull.map(function (item) {
+ if (!item.msg) return ph('tr', [
+ ph('td',
+ u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')),
+ ph('td', u.escapeHTML(item.hash)),
+ ph('td', 'missing')
+ ])
+ var ext = item.name.replace(/.*\./, '')
+ var path = '/git/' + item.type + '/' + item.hash
+ + '?msg=' + encodeURIComponent(item.msg.key)
+ + (ext ? '&ext=' + ext : '')
+ 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) + (item.type === 'tree' ? '/' : ''))),
+ ph('td',
+ self.phIdLink(item.msg.value.author)),
+ ph('td',
+ ph('a', {
+ href: self.app.render.toUrl(item.msg.key),
+ title: fileDate.toLocaleString(),
+ }, htime(fileDate))
+ ),
+ ])
+ })
+ )
+ ]),
+ ]),
+ self.wrapPage('git tree ' + rev),
+ self.respondSink(missingBlobs ? 409 : 200)
+ )
+ })
+}
+
+Serve.prototype.gitBlob = function (rev) {
+ var self = this
+ if (!/[0-9a-f]{24}/.test(rev)) {
+ return pull(
+ ph('div.error', 'rev is not a git object id'),
+ self.wrapPage('git'),
+ self.respondSink(400)
+ )
+ }
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git object ' + rev),
+ self.respondSink(400)
+ )
+
+ self.app.getMsgDecrypted(self.query.msg, function (err, msg) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git object ' + rev),
+ self.respondSink(400)
+ )
+ var msgDate = new Date(msg.value.timestamp)
+ self.app.git.openObject({
+ obj: rev,
+ msg: msg.key,
+ }, function (err, obj) {
+ var missingBlobs
+ if (err && err.name === 'BlobNotFoundError')
+ missingBlobs = err.links, err = null
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git object ' + rev),
+ self.respondSink(400)
+ )
+ pull(
+ ph('section', [
+ ph('h3', ph('a', {href: ''}, rev)),
+ ph('div', [
+ self.phIdLink(msg.value.author), ' ',
+ ph('a', {
+ href: self.app.render.toUrl(msg.key),
+ title: msgDate.toLocaleString(),
+ }, htime(msgDate))
+ ]),
+ missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull(
+ self.app.git.readObject(obj),
+ self.wrapBinary({
+ rawUrl: self.app.render.toUrl('/git/raw/' + rev
+ + '?msg=' + encodeURIComponent(msg.key)),
+ ext: self.query.ext
+ })
+ ),
+ ]),
+ self.wrapPage('git blob ' + rev),
+ self.respondSink(200)
+ )
+ })
+ })
+}
+
+Serve.prototype.gitObjectLinks = function (headMsgId, type) {
+ var self = this
+ return paramap(function (id, cb) {
+ self.app.git.getObjectMsg({
+ obj: id,
+ headMsgId: headMsgId,
+ type: type,
+ }, function (err, msg) {
+ if (err && err.name === '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)'])
+ if (err) return cb(err)
+ var path = '/git/' + type + '/' + id
+ + '?msg=' + encodeURIComponent(msg.key)
+ cb(null, [ph('code', ph('a', {
+ href: self.app.render.toUrl(path)
+ }, u.escapeHTML(id.substr(0, 8)))), ' '])
+ })
+ }, 8)
+}
+
+Serve.prototype.npm = function (url) {
+ var self = this
+ var parts = url.split('/')
+ var author = parts[1] && parts[1][0] === '@'
+ ? u.unescapeId(parts.splice(1, 1)[0]) : null
+ var name = parts[1]
+ var version = parts[2]
+ var distTag = parts[3]
+ var prefix = 'npm:' +
+ (name ? name + ':' +
+ (version ? version + ':' +
+ (distTag ? distTag + ':' : '') : '') : '')
+
+ var render = self.app.render
+ var base = '/npm/' + (author ? u.escapeId(author) + '/' : '')
+ var pathWithoutAuthor = '/npm' +
+ (name ? '/' + name +
+ (version ? '/' + version +
+ (distTag ? '/' + distTag : '') : '') : '')
+ return pull(
+ ph('section', {}, [
+ ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ',
+ author ? [
+ self.phIdLink(author), ' ',
+ ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '&times;')),
+ ' : '
+ ] : '',
+ name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
+ version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '',
+ distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : ''
+ ]),
+ ph('table', [
+ ph('thead', ph('tr', [
+ ph('td', 'publisher'),
+ ph('td', 'package'),
+ ph('td', 'version'),
+ ph('td', 'tag'),
+ ph('td', 'size'),
+ ph('td', 'tarball'),
+ ph('td', 'readme')
+ ])),
+ ph('tbody', pull(
+ self.app.blobMentions({
+ name: {$prefix: prefix},
+ author: author,
+ }),
+ distTag && !version && pull.filter(function (link) {
+ return link.name.split(':')[3] === distTag
+ }),
+ paramap(function (link, cb) {
+ self.app.render.npmPackageMention(link, {
+ withAuthor: true,
+ author: author,
+ name: name,
+ version: version,
+ distTag: distTag,
+ }, cb)
+ }, 4),
+ pull.map(u.toHTML)
+ ))
+ ])
+ ]),
+ self.wrapPage(prefix),
+ self.respondSink(200)
+ )
+}
+
+Serve.prototype.npmReadme = function (url) {
+ var self = this
+ var id = decodeURIComponent(url.substr(1))
+ return pull(
+ ph('section', {}, [
+ ph('h3', [
+ 'npm readme for ',
+ ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
+ ]),
+ ph('blockquote', u.readNext(function (cb) {
+ self.app.getNpmReadme(id, function (err, readme, isMarkdown) {
+ if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
+ cb(null, isMarkdown
+ ? ph('div', self.app.render.markdown(readme))
+ : ph('pre', readme))
+ })
+ }))
+ ]),
+ self.wrapPage('npm readme'),
+ self.respondSink(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
+ return function (read) {
+ var readRendered, type
+ read = ident(function (_ext) {
+ if (_ext) ext = _ext
+ type = ext && mime.lookup(ext) || 'text/plain'
+ })(read)
+ return function (abort, cb) {
+ if (readRendered) return readRendered(abort, cb)
+ if (abort) return read(abort, cb)
+ if (!type) read(null, function (end, buf) {
+ if (end) return cb(end)
+ if (!type) return cb(new Error('unable to get type'))
+ readRendered = pickSource(type, cat([pull.once(buf), read]))
+ readRendered(null, cb)
+ })
+ }
+ }
+ function pickSource(type, read) {
+ if (/^image\//.test(type)) {
+ read(true, function (err) {
+ if (err && err !== true) console.trace(err)
+ })
+ return ph('img', {
+ src: opts.rawUrl
+ })
+ }
+ return ph('pre', pull.map(function (buf) {
+ return self.app.render.highlight(buf.toString('utf8'), ext)
+ })(read))
+ }
+}
+
Serve.prototype.wrapPublic = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
@@ -1275,6 +2045,36 @@ Serve.prototype.wrapPublic = function (opts) {
})
}
+Serve.prototype.askWantBlobsForm = function (links) {
+ var self = this
+ return ph('form', {action: '', method: 'post'}, [
+ ph('section', [
+ ph('h3', 'Missing blobs'),
+ ph('p', 'The application needs these blobs to continue:'),
+ ph('table', links.map(u.toLink).map(function (link) {
+ if (!u.isRef(link.link)) return
+ return ph('tr', [
+ ph('td', ph('code', link.link)),
+ !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '',
+ ])
+ })),
+ ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
+ ph('input', {type: 'hidden', name: 'blob_ids',
+ value: links.map(u.linkDest).join(',')}),
+ ph('p', ph('input', {type: 'submit', value: 'Want Blobs'}))
+ ])
+ ])
+}
+
+Serve.prototype.askWantBlobs = function (links) {
+ var self = this
+ pull(
+ self.askWantBlobsForm(links),
+ self.wrapPage('missing blobs'),
+ self.respondSink(409)
+ )
+}
+
Serve.prototype.wrapPrivate = function (opts) {
var self = this
return u.hyperwrap(function (thread, cb) {
@@ -1438,6 +2238,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') {
@@ -1459,6 +2260,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) {
var href = data.upload.link
+ (data.upload.key ? '#' + data.upload.key : '')
@@ -1471,7 +2285,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 === '@'
})
@@ -1484,6 +2299,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(', ')
@@ -1522,7 +2357,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',
@@ -1544,75 +2391,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',
@@ -1622,3 +2526,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)
+ )
+}