git ssb

16+

cel / patchfoo



Tree: 2eb748d24d3ee0d13f990d2b7eab167a1b41b930

Files: 2eb748d24d3ee0d13f990d2b7eab167a1b41b930 / lib / serve.js

73712 bytesRaw
1var fs = require('fs')
2var qs = require('querystring')
3var pull = require('pull-stream')
4var path = require('path')
5var paramap = require('pull-paramap')
6var sort = require('ssb-sort')
7var crypto = require('crypto')
8var toPull = require('stream-to-pull-stream')
9var serveEmoji = require('emoji-server')()
10var u = require('./util')
11var cat = require('pull-cat')
12var h = require('hyperscript')
13var paginate = require('pull-paginate')
14var ssbMentions = require('ssb-mentions')
15var multicb = require('multicb')
16var pkg = require('../package')
17var Busboy = require('busboy')
18var mime = require('mime-types')
19var ident = require('pull-identify-filetype')
20var htime = require('human-time')
21var ph = require('pull-hyperscript')
22var emojis = require('emoji-named-characters')
23var jpeg = require('jpeg-autorotate')
24
25module.exports = Serve
26
27var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
28var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles')
29
30var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
31
32function ctype(name) {
33 switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
34 case 'html': return 'text/html'
35 case 'txt': return 'text/plain'
36 case 'js': return 'text/javascript'
37 case 'css': return 'text/css'
38 case 'png': return 'image/png'
39 case 'json': return 'application/json'
40 case 'ico': return 'image/x-icon'
41 }
42}
43
44function encodeDispositionFilename(fname) {
45 fname = fname.replace(/\/g/, '\\\\').replace(/"/, '\\\"')
46 return '"' + encodeURIComponent(fname) + '"'
47}
48
49function uniques() {
50 var set = {}
51 return function (item) {
52 if (set[item]) return false
53 return set[item] = true
54 }
55}
56
57function Serve(app, req, res) {
58 this.app = app
59 this.req = req
60 this.res = res
61 this.startDate = new Date()
62}
63
64Serve.prototype.go = function () {
65 console.log(this.req.method, this.req.url)
66 var self = this
67
68 this.res.setTimeout(0)
69
70 if (this.req.method === 'POST' || this.req.method === 'PUT') {
71 if (/^multipart\/form-data/.test(this.req.headers['content-type'])) {
72 var data = {}
73 var erred
74 var busboy = new Busboy({headers: this.req.headers})
75 var filesCb = multicb({pluck: 1})
76 busboy.on('finish', filesCb())
77 filesCb(function (err) {
78 gotData(err, data)
79 })
80 function addField(name, value) {
81 if (!(name in data)) data[name] = value
82 else if (Array.isArray(data[name])) data[name].push(value)
83 else data[name] = [data[name], value]
84 }
85 busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
86 var done = multicb({pluck: 1, spread: true})
87 var cb = filesCb()
88 pull(
89 toPull(file),
90 u.pullLength(done()),
91 self.app.addBlob(done())
92 )
93 done(function (err, size, id) {
94 if (err) return cb(err)
95 if (size === 0 && !filename) return cb()
96 addField(fieldname,
97 {link: id, name: filename, type: mimetype, size: size})
98 cb()
99 })
100 })
101 busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
102 addField(fieldname, val)
103 })
104 this.req.pipe(busboy)
105 } else {
106 pull(
107 toPull(this.req),
108 pull.collect(function (err, bufs) {
109 var data
110 if (!err) try {
111 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
112 } catch(e) {
113 err = e
114 }
115 gotData(err, data)
116 })
117 )
118 }
119 } else {
120 gotData(null, {})
121 }
122
123 function gotData(err, data) {
124 self.data = data
125 if (err) next(err)
126 else if (data.action === 'publish') self.publishJSON(next)
127 else if (data.action === 'contact') self.publishContact(next)
128 else if (data.action === 'want-blobs') self.wantBlobs(next)
129 else if (data.action_vote) self.publishVote(next)
130 else if (data.action_attend) self.publishAttend(next)
131 else next()
132 }
133
134 function next(err, publishedMsg) {
135 if (err) {
136 self.res.writeHead(400, {'Content-Type': 'text/plain'})
137 self.res.end(err.stack)
138 } else if (publishedMsg) {
139 if (self.data.redirect_to_published_msg) {
140 self.redirect(self.app.render.toUrl(publishedMsg.key))
141 } else {
142 self.publishedMsg = publishedMsg
143 self.handle()
144 }
145 } else {
146 self.handle()
147 }
148 }
149}
150
151Serve.prototype.publishJSON = function (cb) {
152 var content
153 try {
154 content = JSON.parse(this.data.content)
155 } catch(e) {
156 return cb(e)
157 }
158 this.publish(content, cb)
159}
160
161Serve.prototype.publishVote = function (cb) {
162 var content = {
163 type: 'vote',
164 channel: this.data.channel || undefined,
165 vote: {
166 link: this.data.link,
167 value: Number(this.data.vote_value),
168 expression: this.data.vote_expression,
169 }
170 }
171 if (this.data.recps) content.recps = this.data.recps.split(',')
172 this.publish(content, cb)
173}
174
175Serve.prototype.publishContact = function (cb) {
176 var content = {
177 type: 'contact',
178 contact: this.data.contact,
179 following: !!this.data.following
180 }
181 this.publish(content, cb)
182}
183
184Serve.prototype.publishAttend = function (cb) {
185 var content = {
186 type: 'about',
187 channel: this.data.channel || undefined,
188 about: this.data.link,
189 attendee: {
190 link: this.app.sbot.id
191 }
192 }
193 if (this.data.recps) content.recps = this.data.recps.split(',')
194 this.publish(content, cb)
195}
196
197Serve.prototype.wantBlobs = function (cb) {
198 var self = this
199 if (!self.data.blob_ids) return cb()
200 var ids = self.data.blob_ids.split(',')
201 if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(',')))
202 var done = multicb({pluck: 1})
203 ids.forEach(function (id) {
204 self.app.wantSizeBlob(id, done())
205 })
206 if (self.data.async_want) return cb()
207 done(function (err) {
208 if (err) return cb(err)
209 // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.')
210 cb()
211 })
212}
213
214Serve.prototype.publish = function (content, cb) {
215 var self = this
216 var done = multicb({pluck: 1, spread: true})
217 u.toArray(content && content.mentions).forEach(function (mention) {
218 if (mention.link && mention.link[0] === '&' && !isNaN(mention.size))
219 self.app.pushBlob(mention.link, done())
220 })
221 done(function (err) {
222 if (err) return cb(err)
223 self.app.publish(content, function (err, msg) {
224 if (err) return cb(err)
225 delete self.data.text
226 delete self.data.recps
227 return cb(null, msg)
228 })
229 })
230}
231
232Serve.prototype.handle = function () {
233 var m = urlIdRegex.exec(this.req.url)
234 this.query = m[5] ? qs.parse(m[5]) : {}
235 switch (m[2]) {
236 case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
237 case '%': return this.id(m[1], m[3])
238 case '@': return this.userFeed(m[1], m[3])
239 case '&': return this.blob(m[1])
240 default: return this.path(m[4])
241 }
242}
243
244Serve.prototype.respond = function (status, message) {
245 this.res.writeHead(status)
246 this.res.end(message)
247}
248
249Serve.prototype.respondSink = function (status, headers, cb) {
250 var self = this
251 if (status || headers)
252 self.res.writeHead(status, headers || {'Content-Type': 'text/html'})
253 return toPull(self.res, cb || function (err) {
254 if (err) self.app.error(err)
255 })
256}
257
258Serve.prototype.redirect = function (dest) {
259 this.res.writeHead(302, {
260 Location: dest
261 })
262 this.res.end()
263}
264
265Serve.prototype.path = function (url) {
266 var m
267 url = url.replace(/^\/+/, '/')
268 switch (url) {
269 case '/': return this.home()
270 case '/robots.txt': return this.res.end('User-agent: *')
271 }
272 if (m = /^\/%23(.*)/.exec(url)) {
273 return this.redirect(this.app.render.toUrl('/channel/'
274 + decodeURIComponent(m[1])))
275 }
276 m = /^([^.]*)(?:\.(.*))?$/.exec(url)
277 switch (m[1]) {
278 case '/new': return this.new(m[2])
279 case '/public': return this.public(m[2])
280 case '/private': return this.private(m[2])
281 case '/mentions': return this.mentions(m[2])
282 case '/search': return this.search(m[2])
283 case '/advsearch': return this.advsearch(m[2])
284 case '/vote': return this.vote(m[2])
285 case '/peers': return this.peers(m[2])
286 case '/status': return this.status(m[2])
287 case '/channels': return this.channels(m[2])
288 case '/friends': return this.friends(m[2])
289 case '/live': return this.live(m[2])
290 case '/compose': return this.compose(m[2])
291 case '/emojis': return this.emojis(m[2])
292 case '/votes': return this.votes(m[2])
293 }
294 m = /^(\/?[^\/]*)(\/.*)?$/.exec(url)
295 switch (m[1]) {
296 case '/channel': return this.channel(m[2])
297 case '/type': return this.type(m[2])
298 case '/links': return this.links(m[2])
299 case '/static': return this.static(m[2])
300 case '/emoji': return this.emoji(m[2])
301 case '/highlight': return this.highlight(m[2])
302 case '/contacts': return this.contacts(m[2])
303 case '/about': return this.about(m[2])
304 case '/git': return this.git(m[2])
305 case '/image': return this.image(m[2])
306 case '/npm': return this.npm(m[2])
307 case '/npm-readme': return this.npmReadme(m[2])
308 }
309 return this.respond(404, 'Not found')
310}
311
312Serve.prototype.home = function () {
313 pull(
314 pull.empty(),
315 this.wrapPage('/'),
316 this.respondSink(200, {
317 'Content-Type': 'text/html'
318 })
319 )
320}
321
322Serve.prototype.public = function (ext) {
323 var q = this.query
324 var opts = {
325 reverse: !q.forwards,
326 sortByTimestamp: q.sort === 'claimed',
327 lt: Number(q.lt) || Date.now(),
328 gt: Number(q.gt) || -Infinity,
329 limit: Number(q.limit) || 12
330 }
331
332 pull(
333 this.app.createLogStream(opts),
334 this.renderThreadPaginated(opts, null, q),
335 this.wrapMessages(),
336 this.wrapPublic(),
337 this.wrapPage('public'),
338 this.respondSink(200, {
339 'Content-Type': ctype(ext)
340 })
341 )
342}
343
344Serve.prototype.setCookie = function (key, value, options) {
345 var header = key + '=' + value
346 if (options) for (var k in options) {
347 header += '; ' + k + '=' + options[k]
348 }
349 this.res.setHeader('Set-Cookie', header)
350}
351
352Serve.prototype.new = function (ext) {
353 var self = this
354 var q = self.query
355 var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1]
356 var opts = {
357 gt: Number(q.gt) || Number(latest) || Date.now(),
358 }
359
360 if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000})
361
362 var read = self.app.createLogStream(opts)
363 self.req.on('closed', function () {
364 console.error('closing')
365 read(true, function (err) {
366 console.log('closed')
367 if (err && err !== true) console.error(new Error(err.stack))
368 })
369 })
370 pull.collect(function (err, msgs) {
371 if (err) return pull(
372 pull.once(u.renderError(err, ext).outerHTML),
373 self.wrapPage('peers'),
374 self.respondSink(500, {'Content-Type': ctype(ext)})
375 )
376 sort(msgs)
377 var maxTS = msgs.reduce(function (max, msg) {
378 return Math.max(msg.timestamp, max)
379 }, -Infinity)
380 pull(
381 pull.values(msgs),
382 self.renderThread(opts, null, q),
383 self.wrapNew({
384 gt: isFinite(maxTS) ? maxTS : Date.now()
385 }),
386 self.wrapMessages(),
387 self.wrapPage('new'),
388 self.respondSink(200, {
389 'Content-Type': ctype(ext)
390 })
391 )
392 })(read)
393}
394
395Serve.prototype.private = function (ext) {
396 var q = this.query
397 var opts = {
398 reverse: !q.forwards,
399 lt: Number(q.lt) || Date.now(),
400 gt: Number(q.gt) || -Infinity,
401 limit: Number(q.limit) || 12,
402 }
403
404 pull(
405 this.app.streamPrivate(opts),
406 this.renderThreadPaginated(opts, null, q),
407 this.wrapMessages(),
408 this.wrapPrivate(opts),
409 this.wrapPage('private'),
410 this.respondSink(200, {
411 'Content-Type': ctype(ext)
412 })
413 )
414}
415
416Serve.prototype.mentions = function (ext) {
417 var self = this
418 var q = self.query
419 var opts = {
420 reverse: !q.forwards,
421 sortByTimestamp: q.sort === 'claimed',
422 lt: Number(q.lt) || Date.now(),
423 gt: Number(q.gt) || -Infinity,
424 limit: Number(q.limit) || 12,
425 }
426
427 return pull(
428 ph('section', {}, [
429 ph('h3', 'Mentions'),
430 pull(
431 self.app.streamMentions(opts),
432 self.app.unboxMessages(),
433 self.renderThreadPaginated(opts, null, q),
434 self.wrapMessages()
435 )
436 ]),
437 self.wrapPage('mentions'),
438 self.respondSink(200)
439 )
440}
441
442Serve.prototype.search = function (ext) {
443 var searchQ = (this.query.q || '').trim()
444 var self = this
445
446 if (/^ssb:\/\//.test(searchQ)) {
447 var maybeId = searchQ.substr(6)
448 if (u.isRef(maybeId)) searchQ = maybeId
449 }
450
451 if (u.isRef(searchQ) || searchQ[0] === '#') {
452 return self.redirect(self.app.render.toUrl(searchQ))
453 }
454
455 pull(
456 self.app.search(searchQ),
457 self.renderThread(),
458 self.wrapMessages(),
459 self.wrapPage('search · ' + searchQ, searchQ),
460 self.respondSink(200, {
461 'Content-Type': ctype(ext),
462 })
463 )
464}
465
466Serve.prototype.advsearch = function (ext) {
467 var self = this
468 var q = this.query || {}
469
470 if (q.source) q.source = u.extractFeedIds(q.source)[0]
471 if (q.dest) q.dest = u.extractFeedIds(q.dest)[0]
472 var hasQuery = q.text || q.source || q.dest
473
474 pull(
475 cat([
476 ph('section', {}, [
477 ph('form', {action: '', method: 'get'}, [
478 ph('table', [
479 ph('tr', [
480 ph('td', 'text'),
481 ph('td', ph('input', {name: 'text', placeholder: 'regex',
482 class: 'id-input',
483 value: q.text || ''}))
484 ]),
485 ph('tr', [
486 ph('td', 'author'),
487 ph('td', ph('input', {name: 'source', placeholder: '@id',
488 class: 'id-input',
489 value: q.source || ''}))
490 ]),
491 ph('tr', [
492 ph('td', 'mentions'),
493 ph('td', ph('input', {name: 'dest', placeholder: 'id',
494 class: 'id-input',
495 value: q.dest || ''}))
496 ]),
497 ph('tr', [
498 ph('td', {colspan: 2}, [
499 ph('input', {type: 'submit', value: 'search'})
500 ])
501 ]),
502 ])
503 ])
504 ]),
505 hasQuery && pull(
506 self.app.advancedSearch(q),
507 self.renderThread(),
508 self.wrapMessages()
509 )
510 ]),
511 self.wrapPage('advanced search'),
512 self.respondSink(200, {
513 'Content-Type': ctype(ext),
514 })
515 )
516}
517
518Serve.prototype.live = function (ext) {
519 var self = this
520 var q = self.query
521 var opts = {
522 live: true,
523 }
524 var gt = Number(q.gt)
525 if (gt) opts.gt = gt
526 else opts.old = false
527
528 pull(
529 ph('table', {class: 'ssb-msgs'}, pull(
530 self.app.sbot.createLogStream(opts),
531 self.app.render.renderFeeds({
532 withGt: true,
533 }),
534 pull.map(u.toHTML)
535 )),
536 self.wrapPage('live'),
537 self.respondSink(200, {
538 'Content-Type': ctype(ext),
539 })
540 )
541}
542
543Serve.prototype.compose = function (ext) {
544 var self = this
545 self.composer({
546 channel: '',
547 redirectToPublishedMsg: true,
548 }, function (err, composer) {
549 if (err) return cb(err)
550 pull(
551 pull.once(u.toHTML(composer)),
552 self.wrapPage('compose'),
553 self.respondSink(200, {
554 'Content-Type': ctype(ext)
555 })
556 )
557 })
558}
559
560Serve.prototype.votes = function (path) {
561 if (path) return pull(
562 pull.once(u.renderError(new Error('Not implemented')).outerHTML),
563 this.wrapPage('#' + channel),
564 this.respondSink(404, {'Content-Type': ctype('html')})
565 )
566
567 var self = this
568 var q = self.query
569 var opts = {
570 reverse: !q.forwards,
571 limit: Number(q.limit) || 50,
572 }
573 var gt = Number(q.gt)
574 if (gt) opts.gt = gt
575 var lt = Number(q.lt)
576 if (lt) opts.lt = lt
577
578 self.app.getVoted(opts, function (err, voted) {
579 if (err) return pull(
580 pull.once(u.renderError(err).outerHTML),
581 self.wrapPage('#' + channel),
582 self.respondSink(500, {'Content-Type': ctype('html')})
583 )
584
585 pull(
586 ph('table', [
587 ph('thead', [
588 ph('tr', [
589 ph('td', {colspan: 2}, self.syncPager({
590 first: voted.firstTimestamp,
591 last: voted.lastTimestamp,
592 }))
593 ])
594 ]),
595 ph('tbody', pull(
596 pull.values(voted.items),
597 paramap(function (item, cb) {
598 cb(null, ph('tr', [
599 ph('td', [String(item.value)]),
600 ph('td', [
601 self.phIdLink(item.id),
602 pull.once(' dug by '),
603 self.renderIdsList()(pull.values(item.feeds))
604 ])
605 ]))
606 }, 8)
607 )),
608 ph('tfoot', {}, []),
609 ]),
610 self.wrapPage('votes'),
611 self.respondSink(200, {
612 'Content-Type': ctype('html')
613 })
614 )
615 })
616}
617
618Serve.prototype.syncPager = function (opts) {
619 var q = this.query
620 var reverse = !q.forwards
621 var min = (reverse ? opts.last : opts.first) || Number(q.gt)
622 var max = (reverse ? opts.first : opts.last) || Number(q.lt)
623 var minDate = new Date(min)
624 var maxDate = new Date(max)
625 var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined})
626 var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1})
627 var atNewest = reverse ? !q.lt : !max
628 var atOldest = reverse ? !min : !q.gt
629 if (atNewest && !reverse) qOlder.lt++
630 if (atOldest && reverse) qNewer.gt--
631 return h('div',
632 atOldest ? 'oldest' : [
633 h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ',
634 h('span', {title: minDate.toString()}, htime(minDate)), ' ',
635 ],
636 ' - ',
637 atNewest ? 'now' : [
638 h('span', {title: maxDate.toString()}, htime(maxDate)), ' ',
639 h('a', {href: '?' + qs.stringify(qNewer)}, '>>')
640 ]
641 ).outerHTML
642}
643
644Serve.prototype.peers = function (ext) {
645 var self = this
646 if (self.data.action === 'connect') {
647 return self.app.sbot.gossip.connect(self.data.address, function (err) {
648 if (err) return pull(
649 pull.once(u.renderError(err, ext).outerHTML),
650 self.wrapPage('peers'),
651 self.respondSink(400, {'Content-Type': ctype(ext)})
652 )
653 self.data = {}
654 return self.peers(ext)
655 })
656 }
657
658 pull(
659 self.app.streamPeers(),
660 paramap(function (peer, cb) {
661 var done = multicb({pluck: 1, spread: true})
662 var connectedTime = Date.now() - peer.stateChange
663 var addr = peer.host + ':' + peer.port + ':' + peer.key
664 done()(null, h('section',
665 h('form', {method: 'post', action: ''},
666 peer.client ? '→' : '←', ' ',
667 h('code', peer.host, ':', peer.port, ':'),
668 self.app.render.idLink(peer.key, done()), ' ',
669 peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '',
670 peer.state === 'connected' ? 'connected' : [
671 h('input', {name: 'action', type: 'submit', value: 'connect'}),
672 h('input', {name: 'address', type: 'hidden', value: addr})
673 ]
674 )
675 // h('div', 'source: ', peer.source)
676 // JSON.stringify(peer, 0, 2)).outerHTML
677 ))
678 done(cb)
679 }, 8),
680 pull.map(u.toHTML),
681 self.wrapPeers(),
682 self.wrapPage('peers'),
683 self.respondSink(200, {
684 'Content-Type': ctype(ext)
685 })
686 )
687}
688
689Serve.prototype.status = function (ext) {
690 var self = this
691
692 if (!self.app.sbot.status) return pull(
693 pull.once('missing sbot status method'),
694 this.wrapPage('status'),
695 self.respondSink(400)
696 )
697
698 pull(
699 ph('section', [
700 ph('h3', 'Status'),
701 pull(
702 u.readNext(function (cb) {
703 self.app.sbot.status(function (err, status) {
704 cb(err, status && pull.once(status))
705 })
706 }),
707 pull.map(function (status) {
708 return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML
709 })
710 )
711 ]),
712 this.wrapPage('status'),
713 this.respondSink(200)
714 )
715}
716
717Serve.prototype.channels = function (ext) {
718 var self = this
719 var id = self.app.sbot.id
720
721 function renderMyChannels() {
722 return pull(
723 self.app.streamMyChannels(id),
724 paramap(function (channel, cb) {
725 // var subscribed = false
726 cb(null, [
727 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
728 ' '
729 ])
730 }, 8),
731 pull.map(u.toHTML),
732 self.wrapMyChannels()
733 )
734 }
735
736 function renderNetworkChannels() {
737 return pull(
738 self.app.streamChannels(),
739 paramap(function (channel, cb) {
740 // var subscribed = false
741 cb(null, [
742 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
743 ' '
744 ])
745 }, 8),
746 pull.map(u.toHTML),
747 self.wrapChannels()
748 )
749 }
750
751 pull(
752 cat([
753 ph('section', {}, [
754 ph('h3', {}, 'Channels:'),
755 renderMyChannels(),
756 renderNetworkChannels()
757 ])
758 ]),
759 this.wrapPage('channels'),
760 this.respondSink(200, {
761 'Content-Type': ctype(ext)
762 })
763 )
764}
765
766Serve.prototype.contacts = function (path) {
767 var self = this
768 var id = String(path).substr(1)
769 var contacts = self.app.createContactStreams(id)
770
771 function renderFriendsList() {
772 return pull(
773 paramap(function (id, cb) {
774 self.app.getAbout(id, function (err, about) {
775 var name = about && about.name || id.substr(0, 8) + '…'
776 cb(null, h('a', {href: self.app.render.toUrl('/contacts/' + id)}, name))
777 })
778 }, 8),
779 pull.map(function (el) {
780 return [el, ' ']
781 }),
782 pull.flatten(),
783 pull.map(u.toHTML)
784 )
785 }
786
787 pull(
788 cat([
789 ph('section', {}, [
790 ph('h3', {}, ['Contacts: ', self.phIdLink(id)]),
791 ph('h4', {}, 'Friends'),
792 renderFriendsList()(contacts.friends),
793 ph('h4', {}, 'Follows'),
794 renderFriendsList()(contacts.follows),
795 ph('h4', {}, 'Followers'),
796 renderFriendsList()(contacts.followers)
797 ])
798 ]),
799 this.wrapPage('contacts: ' + id),
800 this.respondSink(200, {
801 'Content-Type': ctype('html')
802 })
803 )
804}
805
806Serve.prototype.about = function (path) {
807 var self = this
808 var id = decodeURIComponent(String(path).substr(1))
809 var abouts = self.app.createAboutStreams(id)
810 var render = self.app.render
811
812 function renderAboutOpImage(link) {
813 if (!link) return
814 if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link))
815 return ph('img', {
816 class: 'ssb-avatar-image',
817 src: render.imageUrl(link.link),
818 alt: link.link
819 + (link.size ? ' (' + render.formatSize(link.size) + ')' : '')
820 })
821 }
822
823 function renderAboutOpValue(value) {
824 if (!value) return
825 if (u.isRef(value.link)) return self.phIdLink(value.link)
826 if (value.epoch) return new Date(value.epoch).toUTCString()
827 return ph('code', {}, JSON.stringify(value))
828 }
829
830 function renderAboutOpContent(op) {
831 if (op.prop === 'image')
832 return renderAboutOpImage(u.toLink(op.value))
833 if (op.prop === 'description')
834 return h('div', {innerHTML: render.markdown(op.value)}).outerHTML
835 if (op.prop === 'title')
836 return h('strong', op.value).outerHTML
837 if (op.prop === 'name')
838 return h('u', op.value).outerHTML
839 return renderAboutOpValue(op.value)
840 }
841
842 function renderAboutOp(op) {
843 return ph('tr', {}, [
844 ph('td', self.phIdLink(op.author)),
845 ph('td',
846 ph('a', {href: render.toUrl(op.id)},
847 htime(new Date(op.timestamp)))),
848 ph('td', op.prop),
849 ph('td', renderAboutOpContent(op))
850 ])
851 }
852
853 pull(
854 cat([
855 ph('section', {}, [
856 ph('h3', {}, ['About: ', self.phIdLink(id)]),
857 ph('table', {},
858 pull(abouts.scalars, pull.map(renderAboutOp))
859 ),
860 pull(
861 abouts.sets,
862 pull.map(function (op) {
863 return h('pre', JSON.stringify(op, 0, 2))
864 }),
865 pull.map(u.toHTML)
866 )
867 ])
868 ]),
869 this.wrapPage('about: ' + id),
870 this.respondSink(200, {
871 'Content-Type': ctype('html')
872 })
873 )
874}
875
876Serve.prototype.type = function (path) {
877 var q = this.query
878 var type = decodeURIComponent(path.substr(1))
879 var opts = {
880 reverse: !q.forwards,
881 lt: Number(q.lt) || Date.now(),
882 gt: Number(q.gt) || -Infinity,
883 limit: Number(q.limit) || 12,
884 type: type,
885 }
886
887 pull(
888 this.app.sbot.messagesByType(opts),
889 this.renderThreadPaginated(opts, null, q),
890 this.wrapMessages(),
891 this.wrapType(type),
892 this.wrapPage('type: ' + type),
893 this.respondSink(200, {
894 'Content-Type': ctype('html')
895 })
896 )
897}
898
899Serve.prototype.links = function (path) {
900 var q = this.query
901 var dest = path.substr(1)
902 var opts = {
903 dest: dest,
904 reverse: true,
905 values: true,
906 }
907 if (q.rel) opts.rel = q.rel
908
909 pull(
910 this.app.sbot.links(opts),
911 this.renderThread(opts, null, q),
912 this.wrapMessages(),
913 this.wrapLinks(dest),
914 this.wrapPage('links: ' + dest),
915 this.respondSink(200, {
916 'Content-Type': ctype('html')
917 })
918 )
919}
920
921Serve.prototype.rawId = function (id) {
922 var self = this
923
924 self.app.getMsgDecrypted(id, function (err, msg) {
925 if (err) return pull(
926 pull.once(u.renderError(err).outerHTML),
927 self.respondSink(400, {'Content-Type': ctype('html')})
928 )
929 return pull(
930 pull.once(msg),
931 self.renderRawMsgPage(id),
932 self.respondSink(200, {
933 'Content-Type': ctype('html'),
934 })
935 )
936 })
937}
938
939Serve.prototype.channel = function (path) {
940 var channel = decodeURIComponent(String(path).substr(1))
941 var q = this.query
942 var gt = Number(q.gt) || -Infinity
943 var lt = Number(q.lt) || Date.now()
944 var opts = {
945 reverse: !q.forwards,
946 lt: lt,
947 gt: gt,
948 limit: Number(q.limit) || 12,
949 channel: channel,
950 }
951
952 pull(
953 this.app.streamChannel(opts),
954 this.renderThreadPaginated(opts, null, q),
955 this.wrapMessages(),
956 this.wrapChannel(channel),
957 this.wrapPage('#' + channel),
958 this.respondSink(200, {
959 'Content-Type': ctype('html')
960 })
961 )
962}
963
964function threadHeads(msgs, rootId) {
965 return sort.heads(msgs.filter(function (msg) {
966 var c = msg.value && msg.value.content
967 return (c && c.root === rootId)
968 || msg.key === rootId
969 }))
970}
971
972
973Serve.prototype.id = function (id, ext) {
974 var self = this
975 if (self.query.raw != null) return self.rawId(id)
976
977 this.app.getMsgDecrypted(id, function (err, rootMsg) {
978 if (err && err.name === 'NotFoundError') err = null, rootMsg = {
979 key: id, value: {content: false}}
980 if (err) return self.respond(500, err.stack || err)
981 var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
982 var recps = rootContent && rootContent.recps
983 var threadRootId = rootContent && rootContent.root || id
984 var channel
985
986 pull(
987 cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]),
988 pull.unique('key'),
989 self.app.unboxMessages(),
990 pull.through(function (msg) {
991 var c = msg && msg.value.content
992 if (!channel && c.channel) channel = c.channel
993 }),
994 pull.collect(function (err, links) {
995 if (err) return self.respond(500, err.stack || err)
996 pull(
997 pull.values(sort(links)),
998 self.renderThread(),
999 self.wrapMessages(),
1000 self.wrapThread({
1001 recps: recps,
1002 root: threadRootId,
1003 post: id,
1004 branches: threadHeads(links, threadRootId),
1005 postBranches: threadRootId !== id && threadHeads(links, id),
1006 channel: channel,
1007 }),
1008 self.wrapPage(id),
1009 self.respondSink(200, {
1010 'Content-Type': ctype(ext),
1011 })
1012 )
1013 })
1014 )
1015 })
1016}
1017
1018Serve.prototype.userFeed = function (id, ext) {
1019 var self = this
1020 var q = self.query
1021 var opts = {
1022 id: id,
1023 reverse: !q.forwards,
1024 lt: Number(q.lt) || Date.now(),
1025 gt: Number(q.gt) || -Infinity,
1026 limit: Number(q.limit) || 20
1027 }
1028 var isScrolled = q.lt || q.gt
1029
1030 self.app.getAbout(id, function (err, about) {
1031 if (err) self.app.error(err)
1032 pull(
1033 self.app.sbot.createUserStream(opts),
1034 self.renderThreadPaginated(opts, id, q),
1035 self.wrapMessages(),
1036 self.wrapUserFeed(isScrolled, id),
1037 self.wrapPage(about.name || id),
1038 self.respondSink(200, {
1039 'Content-Type': ctype(ext)
1040 })
1041 )
1042 })
1043}
1044
1045Serve.prototype.file = function (file) {
1046 var self = this
1047 fs.stat(file, function (err, stat) {
1048 if (err && err.code === 'ENOENT') return self.respond(404, 'Not found')
1049 if (err) return self.respond(500, err.stack || err)
1050 if (!stat.isFile()) return self.respond(403, 'May only load files')
1051 if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified')
1052 self.res.writeHead(200, {
1053 'Content-Type': ctype(file),
1054 'Content-Length': stat.size,
1055 'Last-Modified': stat.mtime.toGMTString()
1056 })
1057 fs.createReadStream(file).pipe(self.res)
1058 })
1059}
1060
1061Serve.prototype.static = function (file) {
1062 this.file(path.join(__dirname, '../static', file))
1063}
1064
1065Serve.prototype.emoji = function (emoji) {
1066 serveEmoji(this.req, this.res, emoji)
1067}
1068
1069Serve.prototype.highlight = function (dirs) {
1070 this.file(path.join(hlCssDir, dirs))
1071}
1072
1073Serve.prototype.blob = function (id) {
1074 var self = this
1075 var blobs = self.app.sbot.blobs
1076 if (self.req.headers['if-none-match'] === id) return self.respond(304)
1077 self.app.wantSizeBlob(id, function (err, size) {
1078 if (err) {
1079 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
1080 else return self.respond(500, err.message || err)
1081 }
1082 pull(
1083 blobs.get(id),
1084 pull.map(Buffer),
1085 ident(gotType),
1086 self.respondSink()
1087 )
1088 function gotType(type) {
1089 type = type && mime.lookup(type)
1090 if (type) self.res.setHeader('Content-Type', type)
1091 if (typeof size === 'number') self.res.setHeader('Content-Length', size)
1092 if (self.query.name) self.res.setHeader('Content-Disposition',
1093 'inline; filename='+encodeDispositionFilename(self.query.name))
1094 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
1095 self.res.setHeader('etag', id)
1096 self.res.writeHead(200)
1097 }
1098 })
1099}
1100
1101Serve.prototype.image = function (path) {
1102 var id = path.substr(1)
1103 var etag = 'image-' + id
1104 var self = this
1105 var blobs = self.app.sbot.blobs
1106 if (self.req.headers['if-none-match'] === etag) return self.respond(304)
1107 self.app.wantSizeBlob(id, function (err, size) {
1108 if (err) {
1109 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
1110 else return self.respond(500, err.message || err)
1111 }
1112
1113 var done = multicb({pluck: 1, spread: true})
1114 var heresTheData = done()
1115 var heresTheType = done().bind(self, null)
1116
1117 pull(
1118 blobs.get(id),
1119 pull.map(Buffer),
1120 ident(heresTheType),
1121 pull.collect(onFullBuffer)
1122 )
1123
1124 function onFullBuffer (err, buffer) {
1125 if (err) return heresTheData(err)
1126 buffer = Buffer.concat(buffer)
1127
1128 jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
1129 if (!err) buffer = rotatedBuffer
1130
1131 heresTheData(null, buffer)
1132 pull(
1133 pull.once(buffer),
1134 self.respondSink()
1135 )
1136 })
1137 }
1138
1139 done(function (err, data, type) {
1140 if (err) {
1141 console.trace(err)
1142 self.respond(500, err.message || err)
1143 }
1144 type = type && mime.lookup(type)
1145 if (type) self.res.setHeader('Content-Type', type)
1146 self.res.setHeader('Content-Length', data.length)
1147 if (self.query.name) self.res.setHeader('Content-Disposition',
1148 'inline; filename='+encodeDispositionFilename(self.query.name))
1149 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
1150 self.res.setHeader('etag', etag)
1151 self.res.writeHead(200)
1152 })
1153 })
1154}
1155
1156Serve.prototype.ifModified = function (lastMod) {
1157 var ifModSince = this.req.headers['if-modified-since']
1158 if (!ifModSince) return false
1159 var d = new Date(ifModSince)
1160 return d && Math.floor(d/1000) >= Math.floor(lastMod/1000)
1161}
1162
1163Serve.prototype.wrapMessages = function () {
1164 return u.hyperwrap(function (content, cb) {
1165 cb(null, h('table.ssb-msgs', content))
1166 })
1167}
1168
1169Serve.prototype.renderThread = function () {
1170 return pull(
1171 this.app.render.renderFeeds(false),
1172 pull.map(u.toHTML)
1173 )
1174}
1175
1176Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
1177 var self = this
1178 function linkA(opts, name) {
1179 var q1 = u.mergeOpts(q, opts)
1180 return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit)
1181 }
1182 function links(opts) {
1183 var limit = opts.limit || q.limit || 10
1184 return h('tr', h('td.paginate', {colspan: 3},
1185 opts.forwards ? '↑ newer ' : '↓ older ',
1186 linkA(u.mergeOpts(opts, {limit: 1})), ' ',
1187 linkA(u.mergeOpts(opts, {limit: 10})), ' ',
1188 linkA(u.mergeOpts(opts, {limit: 100}))
1189 ))
1190 }
1191
1192 return pull(
1193 paginate(
1194 function onFirst(msg, cb) {
1195 var num = feedId ? msg.value.sequence :
1196 opts.sortByTimestamp ? msg.value.timestamp :
1197 msg.timestamp || msg.ts
1198 if (q.forwards) {
1199 cb(null, links({
1200 lt: num,
1201 gt: null,
1202 forwards: null,
1203 }))
1204 } else {
1205 cb(null, links({
1206 lt: null,
1207 gt: num,
1208 forwards: 1,
1209 }))
1210 }
1211 },
1212 this.app.render.renderFeeds(),
1213 function onLast(msg, cb) {
1214 var num = feedId ? msg.value.sequence :
1215 opts.sortByTimestamp ? msg.value.timestamp :
1216 msg.timestamp || msg.ts
1217 if (q.forwards) {
1218 cb(null, links({
1219 lt: null,
1220 gt: num,
1221 forwards: 1,
1222 }))
1223 } else {
1224 cb(null, links({
1225 lt: num,
1226 gt: null,
1227 forwards: null,
1228 }))
1229 }
1230 },
1231 function onEmpty(cb) {
1232 if (q.forwards) {
1233 cb(null, links({
1234 gt: null,
1235 lt: opts.gt + 1,
1236 forwards: null,
1237 }))
1238 } else {
1239 cb(null, links({
1240 gt: opts.lt - 1,
1241 lt: null,
1242 forwards: 1,
1243 }))
1244 }
1245 }
1246 ),
1247 pull.map(u.toHTML)
1248 )
1249}
1250
1251Serve.prototype.renderRawMsgPage = function (id) {
1252 var showMarkdownSource = (this.query.raw === 'md')
1253 var raw = !showMarkdownSource
1254 return pull(
1255 this.app.render.renderFeeds({
1256 raw: raw,
1257 markdownSource: showMarkdownSource
1258 }),
1259 pull.map(u.toHTML),
1260 this.wrapMessages(),
1261 this.wrapPage(id)
1262 )
1263}
1264
1265function catchHTMLError() {
1266 return function (read) {
1267 var ended
1268 return function (abort, cb) {
1269 if (ended) return cb(ended)
1270 read(abort, function (end, data) {
1271 if (!end || end === true) return cb(end, data)
1272 ended = true
1273 cb(null, u.renderError(end).outerHTML)
1274 })
1275 }
1276 }
1277}
1278
1279function catchTextError() {
1280 return function (read) {
1281 var ended
1282 return function (abort, cb) {
1283 if (ended) return cb(ended)
1284 read(abort, function (end, data) {
1285 if (!end || end === true) return cb(end, data)
1286 ended = true
1287 cb(null, end.stack + '\n')
1288 })
1289 }
1290 }
1291}
1292
1293function styles() {
1294 return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8')
1295}
1296
1297Serve.prototype.appendFooter = function () {
1298 var self = this
1299 return function (read) {
1300 return cat([read, u.readNext(function (cb) {
1301 var ms = new Date() - self.startDate
1302 cb(null, pull.once(h('footer',
1303 h('a', {href: pkg.homepage}, pkg.name), ' · ',
1304 ms/1000 + 's'
1305 ).outerHTML))
1306 })])
1307 }
1308}
1309
1310Serve.prototype.wrapPage = function (title, searchQ) {
1311 var self = this
1312 var render = self.app.render
1313 return pull(
1314 catchHTMLError(),
1315 self.appendFooter(),
1316 u.hyperwrap(function (content, cb) {
1317 var done = multicb({pluck: 1, spread: true})
1318 done()(null, h('html', h('head',
1319 h('meta', {charset: 'utf-8'}),
1320 h('title', title),
1321 h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}),
1322 h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}),
1323 h('style', styles()),
1324 h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')})
1325 ),
1326 h('body',
1327 h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'},
1328 h('a', {href: render.toUrl('/new')}, 'new') , ' ',
1329 h('a', {href: render.toUrl('/public')}, 'public'), ' ',
1330 h('a', {href: render.toUrl('/private')}, 'private') , ' ',
1331 h('a', {href: render.toUrl('/mentions')}, 'mentions') , ' ',
1332 h('a', {href: render.toUrl('/peers')}, 'peers') , ' ',
1333 self.app.sbot.status ?
1334 [h('a', {href: render.toUrl('/status')}, 'status'), ' '] : '',
1335 h('a', {href: render.toUrl('/channels')}, 'channels') , ' ',
1336 h('a', {href: render.toUrl('/friends')}, 'friends'), ' ',
1337 h('a', {href: render.toUrl('/advsearch')}, 'search'), ' ',
1338 h('a', {href: render.toUrl('/live')}, 'live'), ' ',
1339 h('a', {href: render.toUrl('/compose')}, 'compose'), ' ',
1340 h('a', {href: render.toUrl('/votes')}, 'votes'), ' ',
1341 h('a', {href: render.toUrl('/emojis')}, 'emojis'), ' ',
1342 render.idLink(self.app.sbot.id, done()), ' ',
1343 h('input.search-input', {name: 'q', value: searchQ,
1344 placeholder: 'search'})
1345 // h('a', {href: '/convos'}, 'convos'), ' ',
1346 // h('a', {href: '/friends'}, 'friends'), ' ',
1347 // h('a', {href: '/git'}, 'git')
1348 )),
1349 self.publishedMsg ? h('div',
1350 'published ',
1351 self.app.render.msgLink(self.publishedMsg, done())
1352 ) : '',
1353 // self.note,
1354 content
1355 )))
1356 done(cb)
1357 })
1358 )
1359}
1360
1361Serve.prototype.phIdLink = function (id) {
1362 return pull(
1363 pull.once(id),
1364 this.renderIdsList()
1365 )
1366}
1367
1368Serve.prototype.friends = function (path) {
1369 var self = this
1370 pull(
1371 self.app.sbot.friends.createFriendStream({hops: 1}),
1372 self.renderIdsList(),
1373 u.hyperwrap(function (items, cb) {
1374 cb(null, [
1375 h('section',
1376 h('h3', 'Friends')
1377 ),
1378 h('section', items)
1379 ])
1380 }),
1381 this.wrapPage('friends'),
1382 this.respondSink(200, {
1383 'Content-Type': ctype('html')
1384 })
1385 )
1386}
1387
1388Serve.prototype.renderIdsList = function () {
1389 var self = this
1390 return pull(
1391 paramap(function (id, cb) {
1392 self.app.render.getNameLink(id, cb)
1393 }, 8),
1394 pull.map(function (el) {
1395 return [el, ' ']
1396 }),
1397 pull.map(u.toHTML)
1398 )
1399}
1400
1401var relationships = [
1402 '',
1403 'followed',
1404 'follows you',
1405 'friend'
1406]
1407
1408var relationshipActions = [
1409 'follow',
1410 'unfollow',
1411 'follow back',
1412 'unfriend'
1413]
1414
1415Serve.prototype.wrapUserFeed = function (isScrolled, id) {
1416 var self = this
1417 var myId = self.app.sbot.id
1418 var render = self.app.render
1419 return u.hyperwrap(function (thread, cb) {
1420 var done = multicb({pluck: 1, spread: true})
1421 self.app.getAbout(id, done())
1422 self.app.getFollow(myId, id, done())
1423 self.app.getFollow(id, myId, done())
1424 done(function (err, about, weFollowThem, theyFollowUs) {
1425 if (err) return cb(err)
1426 var relationshipI = weFollowThem | theyFollowUs<<1
1427 var done = multicb({pluck: 1, spread: true})
1428 done()(null, [
1429 h('section.ssb-feed',
1430 h('table', h('tr',
1431 h('td', self.app.render.avatarImage(id, done())),
1432 h('td.feed-about',
1433 h('h3.feed-name',
1434 h('strong', self.app.render.idLink(id, done()))),
1435 h('code', h('small', id)),
1436 about.description ? h('div',
1437 {innerHTML: self.app.render.markdown(about.description)}) : ''
1438 )),
1439 h('tr',
1440 h('td'),
1441 h('td',
1442 h('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ',
1443 h('a', {href: render.toUrl('/about/' + id)}, 'about')
1444 )
1445 ),
1446 h('tr',
1447 h('td'),
1448 h('td',
1449 h('form', {action: render.toUrl('/advsearch'), method: 'get'},
1450 h('input', {type: 'hidden', name: 'source', value: id}),
1451 h('input', {type: 'text', name: 'text', placeholder: 'text'}),
1452 h('input', {type: 'submit', value: 'search'})
1453 )
1454 )
1455 ),
1456 isScrolled ? '' : [
1457 id === myId ? '' : h('tr',
1458 h('td'),
1459 h('td.follow-info', h('form', {action: '', method: 'post'},
1460 relationships[relationshipI], ' ',
1461 h('input', {type: 'hidden', name: 'action', value: 'contact'}),
1462 h('input', {type: 'hidden', name: 'contact', value: id}),
1463 h('input', {type: 'hidden', name: 'following',
1464 value: weFollowThem ? '' : 'following'}),
1465 h('input', {type: 'submit',
1466 value: relationshipActions[relationshipI]})
1467 ))
1468 )
1469 ]
1470 )),
1471 thread
1472 ])
1473 done(cb)
1474 })
1475 })
1476}
1477
1478Serve.prototype.git = function (url) {
1479 var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url)
1480 switch (m[1]) {
1481 case 'commit': return this.gitCommit(m[2])
1482 case 'tag': return this.gitTag(m[2])
1483 case 'tree': return this.gitTree(m[2])
1484 case 'blob': return this.gitBlob(m[2])
1485 case 'raw': return this.gitRaw(m[2])
1486 default: return this.respond(404, 'Not found')
1487 }
1488}
1489
1490Serve.prototype.gitRaw = function (rev) {
1491 var self = this
1492 if (!/[0-9a-f]{24}/.test(rev)) {
1493 return pull(
1494 pull.once('\'' + rev + '\' is not a git object id'),
1495 self.respondSink(400, {'Content-Type': 'text/plain'})
1496 )
1497 }
1498 if (!u.isRef(self.query.msg)) return pull(
1499 ph('div.error', 'missing message id'),
1500 self.wrapPage('git tree ' + rev),
1501 self.respondSink(400)
1502 )
1503
1504 self.app.git.openObject({
1505 obj: rev,
1506 msg: self.query.msg,
1507 }, function (err, obj) {
1508 if (err && err.name === 'BlobNotFoundError')
1509 return self.askWantBlobs(err.links)
1510 if (err) return pull(
1511 pull.once(err.stack),
1512 self.respondSink(400, {'Content-Type': 'text/plain'})
1513 )
1514 pull(
1515 self.app.git.readObject(obj),
1516 catchTextError(),
1517 ident(function (type) {
1518 type = type && mime.lookup(type)
1519 if (type) self.res.setHeader('Content-Type', type)
1520 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
1521 self.res.setHeader('etag', rev)
1522 self.res.writeHead(200)
1523 }),
1524 self.respondSink()
1525 )
1526 })
1527}
1528
1529Serve.prototype.gitAuthorLink = function (author) {
1530 if (author.feed) {
1531 var myName = this.app.getNameSync(author.feed)
1532 var sigil = author.name === author.localpart ? '@' : ''
1533 return ph('a', {
1534 href: this.app.render.toUrl(author.feed),
1535 title: author.localpart + (myName ? ' (' + myName + ')' : '')
1536 }, u.escapeHTML(sigil + author.name))
1537 } else {
1538 return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)},
1539 u.escapeHTML(author.name))
1540 }
1541}
1542
1543Serve.prototype.gitCommit = function (rev) {
1544 var self = this
1545 if (!/[0-9a-f]{24}/.test(rev)) {
1546 return pull(
1547 ph('div.error', 'rev is not a git object id'),
1548 self.wrapPage('git'),
1549 self.respondSink(400)
1550 )
1551 }
1552 if (!u.isRef(self.query.msg)) return pull(
1553 ph('div.error', 'missing message id'),
1554 self.wrapPage('git commit ' + rev),
1555 self.respondSink(400)
1556 )
1557
1558 self.app.git.openObject({
1559 obj: rev,
1560 msg: self.query.msg,
1561 }, function (err, obj) {
1562 if (err && err.name === 'BlobNotFoundError')
1563 return self.askWantBlobs(err.links)
1564 if (err) return pull(
1565 pull.once(u.renderError(err).outerHTML),
1566 self.wrapPage('git commit ' + rev),
1567 self.respondSink(400)
1568 )
1569 var msgDate = new Date(obj.msg.value.timestamp)
1570 self.app.git.getCommit(obj, function (err, commit) {
1571 var missingBlobs
1572 if (err && err.name === 'BlobNotFoundError')
1573 missingBlobs = err.links, err = null
1574 if (err) return pull(
1575 pull.once(u.renderError(err).outerHTML),
1576 self.wrapPage('git commit ' + rev),
1577 self.respondSink(400)
1578 )
1579 pull(
1580 ph('section', [
1581 ph('h3', ph('a', {href: ''}, rev)),
1582 ph('div', [
1583 self.phIdLink(obj.msg.value.author), ' pushed ',
1584 ph('a', {
1585 href: self.app.render.toUrl(obj.msg.key),
1586 title: msgDate.toLocaleString(),
1587 }, htime(msgDate))
1588 ]),
1589 missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
1590 ph('div', [
1591 self.gitAuthorLink(commit.committer),
1592 ' committed ',
1593 ph('span', {title: commit.committer.date.toLocaleString()},
1594 htime(commit.committer.date)),
1595 ' in ', commit.committer.tz
1596 ]),
1597 commit.author ? ph('div', [
1598 self.gitAuthorLink(commit.author),
1599 ' authored ',
1600 ph('span', {title: commit.author.date.toLocaleString()},
1601 htime(commit.author.date)),
1602 ' in ', commit.author.tz
1603 ]) : '',
1604 commit.parents.length ? ph('div', ['parents: ', pull(
1605 pull.values(commit.parents),
1606 self.gitObjectLinks(obj.msg.key, 'commit')
1607 )]) : '',
1608 commit.tree ? ph('div', ['tree: ', pull(
1609 pull.once(commit.tree),
1610 self.gitObjectLinks(obj.msg.key, 'tree')
1611 )]) : '',
1612 h('blockquote',
1613 self.app.render.gitCommitBody(commit.body)).outerHTML,
1614 ph('h4', 'files'),
1615 ph('table', pull(
1616 self.app.git.readCommitChanges(commit),
1617 pull.map(function (file) {
1618 return ph('tr', [
1619 ph('td', ph('code', u.escapeHTML(file.name))),
1620 // ph('td', ph('code', u.escapeHTML(JSON.stringify(file.msg)))),
1621 ph('td', file.deleted ? 'deleted'
1622 : file.created ? 'created'
1623 : file.hash ? 'changed'
1624 : file.mode ? 'mode changed'
1625 : JSON.stringify(file))
1626 ])
1627 })
1628 ))
1629 ]
1630 ]),
1631 self.wrapPage('git commit ' + rev),
1632 self.respondSink(missingBlobs ? 409 : 200)
1633 )
1634 })
1635 })
1636}
1637
1638Serve.prototype.gitTag = function (rev) {
1639 var self = this
1640 if (!/[0-9a-f]{24}/.test(rev)) {
1641 return pull(
1642 ph('div.error', 'rev is not a git object id'),
1643 self.wrapPage('git'),
1644 self.respondSink(400)
1645 )
1646 }
1647 if (!u.isRef(self.query.msg)) return pull(
1648 ph('div.error', 'missing message id'),
1649 self.wrapPage('git tag ' + rev),
1650 self.respondSink(400)
1651 )
1652
1653 self.app.git.openObject({
1654 obj: rev,
1655 msg: self.query.msg,
1656 }, function (err, obj) {
1657 if (err && err.name === 'BlobNotFoundError')
1658 return self.askWantBlobs(err.links)
1659 if (err) return pull(
1660 pull.once(u.renderError(err).outerHTML),
1661 self.wrapPage('git tag ' + rev),
1662 self.respondSink(400)
1663 )
1664 var msgDate = new Date(obj.msg.value.timestamp)
1665 self.app.git.getTag(obj, function (err, tag) {
1666 var missingBlobs
1667 if (err && err.name === 'BlobNotFoundError')
1668 missingBlobs = err.links, err = null
1669 if (err) return pull(
1670 pull.once(u.renderError(err).outerHTML),
1671 self.wrapPage('git tag ' + rev),
1672 self.respondSink(400)
1673 )
1674 pull(
1675 ph('section', [
1676 ph('h3', ph('a', {href: ''}, rev)),
1677 ph('div', [
1678 self.phIdLink(obj.msg.value.author), ' pushed ',
1679 ph('a', {
1680 href: self.app.render.toUrl(obj.msg.key),
1681 title: msgDate.toLocaleString(),
1682 }, htime(msgDate))
1683 ]),
1684 missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
1685 ph('div', [
1686 self.gitAuthorLink(tag.tagger),
1687 ' tagged ',
1688 ph('span', {title: tag.tagger.date.toLocaleString()},
1689 htime(tag.tagger.date)),
1690 ' in ', tag.tagger.tz
1691 ]),
1692 tag.type, ' ',
1693 pull(
1694 pull.once(tag.object),
1695 self.gitObjectLinks(obj.msg.key, tag.type)
1696 ), ' ',
1697 ph('code', u.escapeHTML(tag.tag)),
1698 h('pre', self.app.render.linkify(tag.body)).outerHTML,
1699 ]
1700 ]),
1701 self.wrapPage('git tag ' + rev),
1702 self.respondSink(missingBlobs ? 409 : 200)
1703 )
1704 })
1705 })
1706}
1707
1708Serve.prototype.gitTree = function (rev) {
1709 var self = this
1710 if (!/[0-9a-f]{24}/.test(rev)) {
1711 return pull(
1712 ph('div.error', 'rev is not a git object id'),
1713 self.wrapPage('git'),
1714 self.respondSink(400)
1715 )
1716 }
1717 if (!u.isRef(self.query.msg)) return pull(
1718 ph('div.error', 'missing message id'),
1719 self.wrapPage('git tree ' + rev),
1720 self.respondSink(400)
1721 )
1722
1723 self.app.git.openObject({
1724 obj: rev,
1725 msg: self.query.msg,
1726 }, function (err, obj) {
1727 var missingBlobs
1728 if (err && err.name === 'BlobNotFoundError')
1729 missingBlobs = err.links, err = null
1730 if (err) return pull(
1731 pull.once(u.renderError(err).outerHTML),
1732 self.wrapPage('git tree ' + rev),
1733 self.respondSink(400)
1734 )
1735 var msgDate = new Date(obj.msg.value.timestamp)
1736 pull(
1737 ph('section', [
1738 ph('h3', ph('a', {href: ''}, rev)),
1739 ph('div', [
1740 self.phIdLink(obj.msg.value.author), ' ',
1741 ph('a', {
1742 href: self.app.render.toUrl(obj.msg.key),
1743 title: msgDate.toLocaleString(),
1744 }, htime(msgDate))
1745 ]),
1746 missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [
1747 pull(
1748 self.app.git.readTree(obj),
1749 paramap(function (file, cb) {
1750 self.app.git.getObjectMsg({
1751 obj: file.hash,
1752 headMsgId: obj.msg.key,
1753 }, function (err, msg) {
1754 if (err && err.name === 'ObjectNotFoundError') return cb(null, file)
1755 if (err) return cb(err)
1756 file.msg = msg
1757 cb(null, file)
1758 })
1759 }, 8),
1760 pull.map(function (item) {
1761 if (!item.msg) return ph('tr', [
1762 ph('td',
1763 u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')),
1764 ph('td', u.escapeHTML(item.hash)),
1765 ph('td', 'missing')
1766 ])
1767 var ext = item.name.replace(/.*\./, '')
1768 var path = '/git/' + item.type + '/' + item.hash
1769 + '?msg=' + encodeURIComponent(item.msg.key)
1770 + (ext ? '&ext=' + ext : '')
1771 var fileDate = new Date(item.msg.value.timestamp)
1772 return ph('tr', [
1773 ph('td',
1774 ph('a', {href: self.app.render.toUrl(path)},
1775 u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))),
1776 ph('td',
1777 self.phIdLink(item.msg.value.author)),
1778 ph('td',
1779 ph('a', {
1780 href: self.app.render.toUrl(item.msg.key),
1781 title: fileDate.toLocaleString(),
1782 }, htime(fileDate))
1783 ),
1784 ])
1785 })
1786 )
1787 ]),
1788 ]),
1789 self.wrapPage('git tree ' + rev),
1790 self.respondSink(missingBlobs ? 409 : 200)
1791 )
1792 })
1793}
1794
1795Serve.prototype.gitBlob = function (rev) {
1796 var self = this
1797 if (!/[0-9a-f]{24}/.test(rev)) {
1798 return pull(
1799 ph('div.error', 'rev is not a git object id'),
1800 self.wrapPage('git'),
1801 self.respondSink(400)
1802 )
1803 }
1804 if (!u.isRef(self.query.msg)) return pull(
1805 ph('div.error', 'missing message id'),
1806 self.wrapPage('git object ' + rev),
1807 self.respondSink(400)
1808 )
1809
1810 self.app.getMsgDecrypted(self.query.msg, function (err, msg) {
1811 if (err) return pull(
1812 pull.once(u.renderError(err).outerHTML),
1813 self.wrapPage('git object ' + rev),
1814 self.respondSink(400)
1815 )
1816 var msgDate = new Date(msg.value.timestamp)
1817 self.app.git.openObject({
1818 obj: rev,
1819 msg: msg.key,
1820 }, function (err, obj) {
1821 var missingBlobs
1822 if (err && err.name === 'BlobNotFoundError')
1823 missingBlobs = err.links, err = null
1824 if (err) return pull(
1825 pull.once(u.renderError(err).outerHTML),
1826 self.wrapPage('git object ' + rev),
1827 self.respondSink(400)
1828 )
1829 pull(
1830 ph('section', [
1831 ph('h3', ph('a', {href: ''}, rev)),
1832 ph('div', [
1833 self.phIdLink(msg.value.author), ' ',
1834 ph('a', {
1835 href: self.app.render.toUrl(msg.key),
1836 title: msgDate.toLocaleString(),
1837 }, htime(msgDate))
1838 ]),
1839 missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull(
1840 self.app.git.readObject(obj),
1841 self.wrapBinary({
1842 rawUrl: self.app.render.toUrl('/git/raw/' + rev
1843 + '?msg=' + encodeURIComponent(msg.key)),
1844 ext: self.query.ext
1845 })
1846 ),
1847 ]),
1848 self.wrapPage('git blob ' + rev),
1849 self.respondSink(200)
1850 )
1851 })
1852 })
1853}
1854
1855Serve.prototype.gitObjectLinks = function (headMsgId, type) {
1856 var self = this
1857 return paramap(function (id, cb) {
1858 self.app.git.getObjectMsg({
1859 obj: id,
1860 headMsgId: headMsgId,
1861 type: type,
1862 }, function (err, msg) {
1863 if (err && err.name === 'BlobNotFoundError')
1864 return cb(null, self.askWantBlobsForm(err.links))
1865 if (err && err.name === 'ObjectNotFoundError')
1866 return cb(null, [
1867 ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)'])
1868 if (err) return cb(err)
1869 var path = '/git/' + type + '/' + id
1870 + '?msg=' + encodeURIComponent(msg.key)
1871 cb(null, [ph('code', ph('a', {
1872 href: self.app.render.toUrl(path)
1873 }, u.escapeHTML(id.substr(0, 8)))), ' '])
1874 })
1875 }, 8)
1876}
1877
1878Serve.prototype.npm = function (url) {
1879 var self = this
1880 var parts = url.split('/')
1881 var author = parts[1] && parts[1][0] === '@'
1882 ? u.unescapeId(parts.splice(1, 1)[0]) : null
1883 var name = parts[1]
1884 var version = parts[2]
1885 var distTag = parts[3]
1886 var prefix = 'npm:' +
1887 (name ? name + ':' +
1888 (version ? version + ':' +
1889 (distTag ? distTag + ':' : '') : '') : '')
1890
1891 var render = self.app.render
1892 var base = '/npm/' + (author ? u.escapeId(author) + '/' : '')
1893 var pathWithoutAuthor = '/npm/' +
1894 (name ? name + '/' +
1895 (version ? version + '/' +
1896 (distTag ? distTag + '/' : '') : '') : '')
1897 return pull(
1898 ph('section', {}, [
1899 ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ',
1900 author ? [
1901 self.phIdLink(author), ' ',
1902 ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '&times;')),
1903 ' : '
1904 ] : '',
1905 name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
1906 version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '',
1907 distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : ''
1908 ]),
1909 ph('table', [
1910 ph('thead', ph('tr', [
1911 !author ? ph('td', 'publisher') : '',
1912 ph('td', 'package'),
1913 ph('td', 'version'),
1914 ph('td', 'tag'),
1915 ph('td', 'size'),
1916 ph('td', 'tarball'),
1917 ph('td', 'readme')
1918 ])),
1919 ph('tbody', pull(
1920 self.app.blobMentions({
1921 name: {$prefix: prefix},
1922 author: author,
1923 }),
1924 distTag && !version && pull.filter(function (link) {
1925 return link.name.split(':')[3] === distTag
1926 }),
1927 paramap(function (link, cb) {
1928 self.app.render.npmPackageMention(link, {
1929 withAuthor: !author,
1930 author: author,
1931 name: name,
1932 version: version,
1933 distTag: distTag,
1934 }, cb)
1935 }, 4),
1936 pull.map(u.toHTML)
1937 ))
1938 ])
1939 ]),
1940 self.wrapPage(prefix),
1941 self.respondSink(200)
1942 )
1943}
1944
1945Serve.prototype.npmReadme = function (url) {
1946 var self = this
1947 var id = decodeURIComponent(url.substr(1))
1948 return pull(
1949 ph('section', {}, [
1950 ph('h3', [
1951 'npm readme for ',
1952 ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
1953 ]),
1954 ph('blockquote', u.readNext(function (cb) {
1955 self.app.getNpmReadme(id, function (err, readme, isMarkdown) {
1956 if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
1957 cb(null, isMarkdown
1958 ? ph('div', self.app.render.markdown(readme))
1959 : ph('pre', readme))
1960 })
1961 }))
1962 ]),
1963 self.wrapPage('npm readme'),
1964 self.respondSink(200)
1965 )
1966}
1967
1968// wrap a binary source and render it or turn into an embed
1969Serve.prototype.wrapBinary = function (opts) {
1970 var self = this
1971 var ext = opts.ext
1972 return function (read) {
1973 var readRendered, type
1974 read = ident(function (_ext) {
1975 if (_ext) ext = _ext
1976 type = ext && mime.lookup(ext) || 'text/plain'
1977 })(read)
1978 return function (abort, cb) {
1979 if (readRendered) return readRendered(abort, cb)
1980 if (abort) return read(abort, cb)
1981 if (!type) read(null, function (end, buf) {
1982 if (end) return cb(end)
1983 if (!type) return cb(new Error('unable to get type'))
1984 readRendered = pickSource(type, cat([pull.once(buf), read]))
1985 readRendered(null, cb)
1986 })
1987 }
1988 }
1989 function pickSource(type, read) {
1990 if (/^image\//.test(type)) {
1991 read(true, function (err) {
1992 if (err && err !== true) console.trace(err)
1993 })
1994 return ph('img', {
1995 src: opts.rawUrl
1996 })
1997 }
1998 return ph('pre', pull.map(function (buf) {
1999 return self.app.render.highlight(buf.toString('utf8'), ext)
2000 })(read))
2001 }
2002}
2003
2004Serve.prototype.wrapPublic = function (opts) {
2005 var self = this
2006 return u.hyperwrap(function (thread, cb) {
2007 self.composer({
2008 channel: '',
2009 }, function (err, composer) {
2010 if (err) return cb(err)
2011 cb(null, [
2012 composer,
2013 thread
2014 ])
2015 })
2016 })
2017}
2018
2019Serve.prototype.askWantBlobsForm = function (links) {
2020 var self = this
2021 return ph('form', {action: '', method: 'post'}, [
2022 ph('section', [
2023 ph('h3', 'Missing blobs'),
2024 ph('p', 'The application needs these blobs to continue:'),
2025 ph('table', links.map(u.toLink).map(function (link) {
2026 if (!u.isRef(link.link)) return
2027 return ph('tr', [
2028 ph('td', ph('code', link.link)),
2029 !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '',
2030 ])
2031 })),
2032 ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
2033 ph('input', {type: 'hidden', name: 'blob_ids',
2034 value: links.map(u.linkDest).join(',')}),
2035 ph('p', ph('input', {type: 'submit', value: 'Want Blobs'}))
2036 ])
2037 ])
2038}
2039
2040Serve.prototype.askWantBlobs = function (links) {
2041 var self = this
2042 pull(
2043 self.askWantBlobsForm(links),
2044 self.wrapPage('missing blobs'),
2045 self.respondSink(409)
2046 )
2047}
2048
2049Serve.prototype.wrapPrivate = function (opts) {
2050 var self = this
2051 return u.hyperwrap(function (thread, cb) {
2052 self.composer({
2053 placeholder: 'private message',
2054 private: true,
2055 }, function (err, composer) {
2056 if (err) return cb(err)
2057 cb(null, [
2058 composer,
2059 thread
2060 ])
2061 })
2062 })
2063}
2064
2065Serve.prototype.wrapThread = function (opts) {
2066 var self = this
2067 return u.hyperwrap(function (thread, cb) {
2068 self.app.render.prepareLinks(opts.recps, function (err, recps) {
2069 if (err) return cb(er)
2070 self.composer({
2071 placeholder: recps ? 'private reply' : 'reply',
2072 id: 'reply',
2073 root: opts.root,
2074 post: opts.post,
2075 channel: opts.channel || '',
2076 branches: opts.branches,
2077 postBranches: opts.postBranches,
2078 recps: recps,
2079 }, function (err, composer) {
2080 if (err) return cb(err)
2081 cb(null, [
2082 thread,
2083 composer
2084 ])
2085 })
2086 })
2087 })
2088}
2089
2090Serve.prototype.wrapNew = function (opts) {
2091 var self = this
2092 return u.hyperwrap(function (thread, cb) {
2093 self.composer({
2094 channel: '',
2095 }, function (err, composer) {
2096 if (err) return cb(err)
2097 cb(null, [
2098 composer,
2099 h('table.ssb-msgs',
2100 thread,
2101 h('tr', h('td.paginate.msg-left', {colspan: 3},
2102 h('form', {method: 'get', action: ''},
2103 h('input', {type: 'hidden', name: 'gt', value: opts.gt}),
2104 h('input', {type: 'hidden', name: 'catchup', value: '1'}),
2105 h('input', {type: 'submit', value: 'catchup'})
2106 )
2107 ))
2108 )
2109 ])
2110 })
2111 })
2112}
2113
2114Serve.prototype.wrapChannel = function (channel) {
2115 var self = this
2116 return u.hyperwrap(function (thread, cb) {
2117 self.composer({
2118 placeholder: 'public message in #' + channel,
2119 channel: channel,
2120 }, function (err, composer) {
2121 if (err) return cb(err)
2122 cb(null, [
2123 h('section',
2124 h('h3.feed-name',
2125 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel)
2126 )
2127 ),
2128 composer,
2129 thread
2130 ])
2131 })
2132 })
2133}
2134
2135Serve.prototype.wrapType = function (type) {
2136 var self = this
2137 return u.hyperwrap(function (thread, cb) {
2138 cb(null, [
2139 h('section',
2140 h('h3.feed-name',
2141 h('a', {href: self.app.render.toUrl('/type/' + type)},
2142 h('code', type), 's'))
2143 ),
2144 thread
2145 ])
2146 })
2147}
2148
2149Serve.prototype.wrapLinks = function (dest) {
2150 var self = this
2151 return u.hyperwrap(function (thread, cb) {
2152 cb(null, [
2153 h('section',
2154 h('h3.feed-name', 'links: ',
2155 h('a', {href: self.app.render.toUrl('/links/' + dest)},
2156 h('code', dest)))
2157 ),
2158 thread
2159 ])
2160 })
2161}
2162
2163Serve.prototype.wrapPeers = function (opts) {
2164 var self = this
2165 return u.hyperwrap(function (peers, cb) {
2166 cb(null, [
2167 h('section',
2168 h('h3', 'Peers')
2169 ),
2170 peers
2171 ])
2172 })
2173}
2174
2175Serve.prototype.wrapChannels = function (opts) {
2176 var self = this
2177 return u.hyperwrap(function (channels, cb) {
2178 cb(null, [
2179 h('section',
2180 h('h4', 'Network')
2181 ),
2182 h('section',
2183 channels
2184 )
2185 ])
2186 })
2187}
2188
2189Serve.prototype.wrapMyChannels = function (opts) {
2190 var self = this
2191 return u.hyperwrap(function (channels, cb) {
2192 cb(null, [
2193 h('section',
2194 h('h4', 'Subscribed')
2195 ),
2196 h('section',
2197 channels
2198 )
2199 ])
2200 })
2201}
2202
2203function rows(str) {
2204 return String(str).split(/[^\n]{150}|\n/).length
2205}
2206
2207Serve.prototype.composer = function (opts, cb) {
2208 var self = this
2209 opts = opts || {}
2210 var data = self.data
2211 var myId = self.app.sbot.id
2212
2213 var blobs = u.tryDecodeJSON(data.blobs) || {}
2214 if (data.upload && typeof data.upload === 'object') {
2215 blobs[data.upload.link] = {
2216 type: data.upload.type,
2217 size: data.upload.size,
2218 }
2219 }
2220 if (data.blob_type && blobs[data.blob_link]) {
2221 blobs[data.blob_link].type = data.blob_type
2222 }
2223 var channel = data.channel != null ? data.channel : opts.channel
2224
2225 var formNames = {}
2226 var mentionIds = u.toArray(data.mention_id)
2227 var mentionNames = u.toArray(data.mention_name)
2228 for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) {
2229 formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0]
2230 }
2231
2232 var formEmojiNames = {}
2233 var emojiIds = u.toArray(data.emoji_id)
2234 var emojiNames = u.toArray(data.emoji_name)
2235 for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) {
2236 var upload = data['emoji_upload_' + i]
2237 formEmojiNames[emojiNames[i]] =
2238 (upload && upload.link) || u.extractBlobIds(emojiIds[i])[0]
2239 if (upload) blobs[upload.link] = {
2240 type: upload.type,
2241 size: upload.size,
2242 }
2243 }
2244
2245 if (data.upload) {
2246 // TODO: be able to change the content-type
2247 var isImage = /^image\//.test(data.upload.type)
2248 data.text = (data.text ? data.text + '\n' : '')
2249 + (isImage ? '!' : '')
2250 + '[' + data.upload.name + '](' + data.upload.link + ')'
2251 }
2252
2253 // get bare feed names
2254 var unknownMentionNames = {}
2255 var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
2256 var unknownMentions = mentions
2257 .filter(function (mention) {
2258 return mention.link === '@'
2259 })
2260 .map(function (mention) {
2261 return mention.name
2262 })
2263 .filter(uniques())
2264 .map(function (name) {
2265 var id = formNames[name] || self.app.getReverseNameSync('@' + name)
2266 return {name: name, id: id}
2267 })
2268
2269 var emoji = mentions
2270 .filter(function (mention) { return mention.emoji })
2271 .map(function (mention) { return mention.name })
2272 .filter(uniques())
2273 .map(function (name) {
2274 // 1. check emoji-image mapping for this message
2275 var id = formEmojiNames[name]
2276 if (id) return {name: name, id: id}
2277 // 2. TODO: check user's preferred emoji-image mapping
2278 // 3. check builtin emoji
2279 var link = self.getBuiltinEmojiLink(name)
2280 if (link) {
2281 return {name: name, id: link.link}
2282 blobs[id] = {type: link.type, size: link.size}
2283 }
2284 // 4. check recently seen emoji
2285 id = self.app.getReverseEmojiNameSync(name)
2286 return {name: name, id: id}
2287 })
2288
2289 // strip content other than feed ids from the recps field
2290 if (data.recps) {
2291 data.recps = u.extractFeedIds(data.recps).filter(uniques()).join(', ')
2292 }
2293
2294 var done = multicb({pluck: 1, spread: true})
2295 done()(null, h('section.composer',
2296 h('form', {method: 'post', action: opts.id ? '#' + opts.id : '',
2297 enctype: 'multipart/form-data'},
2298 h('input', {type: 'hidden', name: 'blobs',
2299 value: JSON.stringify(blobs)}),
2300 opts.recps ? self.app.render.privateLine(opts.recps, done()) :
2301 opts.private ? h('div', h('input.recps-input', {name: 'recps',
2302 value: data.recps || '', placeholder: 'recipient ids'})) : '',
2303 channel != null ?
2304 h('div', '#', h('input', {name: 'channel', placeholder: 'channel',
2305 value: channel})) : '',
2306 opts.root !== opts.post ? h('div',
2307 h('label', {for: 'fork_thread'},
2308 h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}),
2309 ' fork thread'
2310 )
2311 ) : '',
2312 h('textarea', {
2313 id: opts.id,
2314 name: 'text',
2315 rows: Math.max(4, rows(data.text)),
2316 cols: 70,
2317 placeholder: opts.placeholder || 'public message',
2318 }, data.text || ''),
2319 unknownMentions.length > 0 ? [
2320 h('div', h('em', 'names:')),
2321 h('ul.mentions', unknownMentions.map(function (mention) {
2322 return h('li',
2323 h('code', '@' + mention.name), ': ',
2324 h('input', {name: 'mention_name', type: 'hidden',
2325 value: mention.name}),
2326 h('input.id-input', {name: 'mention_id', size: 60,
2327 value: mention.id, placeholder: '@id'}))
2328 }))
2329 ] : '',
2330 emoji.length > 0 ? [
2331 h('div', h('em', 'emoji:')),
2332 h('ul.mentions', emoji.map(function (link, i) {
2333 return h('li',
2334 h('code', link.name), ': ',
2335 h('input', {name: 'emoji_name', type: 'hidden',
2336 value: link.name}),
2337 h('input.id-input', {name: 'emoji_id', size: 60,
2338 value: link.id, placeholder: '&id'}), ' ',
2339 h('input', {type: 'file', name: 'emoji_upload_' + i}))
2340 }))
2341 ] : '',
2342 h('table.ssb-msgs',
2343 h('tr.msg-row',
2344 h('td.msg-left', {colspan: 2},
2345 h('input', {type: 'file', name: 'upload'})
2346 ),
2347 h('td.msg-right',
2348 h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ',
2349 h('input', {type: 'submit', name: 'action', value: 'preview'})
2350 )
2351 )
2352 ),
2353 data.action === 'preview' ? preview(false, done()) :
2354 data.action === 'raw' ? preview(true, done()) : ''
2355 )
2356 ))
2357 done(cb)
2358
2359 function prepareContent(cb) {
2360 var done = multicb({pluck: 1})
2361 content = {
2362 type: 'post',
2363 text: String(data.text).replace(/\r\n/g, '\n'),
2364 }
2365 var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
2366 .filter(function (mention) {
2367 if (mention.emoji) {
2368 mention.link = formEmojiNames[mention.name]
2369 if (!mention.link) {
2370 var link = self.getBuiltinEmojiLink(mention.name)
2371 if (link) {
2372 mention.link = link.link
2373 mention.size = link.size
2374 mention.type = link.type
2375 } else {
2376 mention.link = self.app.getReverseEmojiNameSync(mention.name)
2377 if (!mention.link) return false
2378 }
2379 }
2380 }
2381 var blob = blobs[mention.link]
2382 if (blob) {
2383 if (!isNaN(blob.size))
2384 mention.size = blob.size
2385 if (blob.type && blob.type !== 'application/octet-stream')
2386 mention.type = blob.type
2387 } else if (mention.link === '@') {
2388 // bare feed name
2389 var name = mention.name
2390 var id = formNames[name] || self.app.getReverseNameSync('@' + name)
2391 if (id) mention.link = id
2392 else return false
2393 }
2394 if (mention.link && mention.link[0] === '&' && mention.size == null) {
2395 var linkCb = done()
2396 self.app.sbot.blobs.size(mention.link, function (err, size) {
2397 if (!err && size != null) mention.size = size
2398 linkCb()
2399 })
2400 }
2401 return true
2402 })
2403 if (mentions.length) content.mentions = mentions
2404 if (data.recps != null) {
2405 if (opts.recps) return cb(new Error('got recps in opts and data'))
2406 content.recps = [myId]
2407 u.extractFeedIds(data.recps).forEach(function (recp) {
2408 if (content.recps.indexOf(recp) === -1) content.recps.push(recp)
2409 })
2410 } else {
2411 if (opts.recps) content.recps = opts.recps
2412 }
2413 if (data.fork_thread) {
2414 content.root = opts.post || undefined
2415 content.branch = u.fromArray(opts.postBranches) || undefined
2416 } else {
2417 content.root = opts.root || undefined
2418 content.branch = u.fromArray(opts.branches) || undefined
2419 }
2420 if (channel) content.channel = data.channel
2421
2422 done(function (err) {
2423 cb(err, content)
2424 })
2425 }
2426
2427 function preview(raw, cb) {
2428 var msgContainer = h('table.ssb-msgs')
2429 var contentInput = h('input', {type: 'hidden', name: 'content'})
2430 var warningsContainer = h('div')
2431
2432 var content
2433 try { content = JSON.parse(data.text) }
2434 catch (err) {}
2435 if (content) gotContent(null, content)
2436 else prepareContent(gotContent)
2437
2438 function gotContent(err, content) {
2439 if (err) return cb(err)
2440 contentInput.value = JSON.stringify(content)
2441 var msg = {
2442 value: {
2443 author: myId,
2444 timestamp: Date.now(),
2445 content: content
2446 }
2447 }
2448 if (content.recps) msg.value.private = true
2449
2450 var warnings = []
2451 u.toLinkArray(content.mentions).forEach(function (link) {
2452 if (link.emoji && link.size >= 10e3) {
2453 warnings.push(h('li',
2454 'emoji ', h('q', link.name),
2455 ' (', h('code', String(link.link).substr(0, 8) + '…'), ')'
2456 + ' is >10KB'))
2457 } else if (link.link[0] === '&' && link.size >= 10e6 && link.type) {
2458 // if link.type is set, we probably just uploaded this blob
2459 warnings.push(h('li',
2460 'attachment ',
2461 h('code', String(link.link).substr(0, 8) + '…'),
2462 ' is >10MB'))
2463 }
2464 })
2465 if (warnings.length) {
2466 warningsContainer.appendChild(h('div', h('em', 'warning:')))
2467 warningsContainer.appendChild(h('ul.mentions', warnings))
2468 }
2469
2470 pull(
2471 pull.once(msg),
2472 self.app.unboxMessages(),
2473 self.app.render.renderFeeds(raw),
2474 pull.drain(function (el) {
2475 msgContainer.appendChild(h('tbody', el))
2476 }, cb)
2477 )
2478 }
2479
2480 return [
2481 contentInput,
2482 opts.redirectToPublishedMsg ? h('input', {type: 'hidden',
2483 name: 'redirect_to_published_msg', value: '1'}) : '',
2484 warningsContainer,
2485 h('div', h('em', 'draft:')),
2486 msgContainer,
2487 h('div.composer-actions',
2488 h('input', {type: 'submit', name: 'action', value: 'publish'})
2489 )
2490 ]
2491 }
2492
2493}
2494
2495function hashBuf(buf) {
2496 var hash = crypto.createHash('sha256')
2497 hash.update(buf)
2498 return '&' + hash.digest('base64') + '.sha256'
2499}
2500
2501Serve.prototype.getBuiltinEmojiLink = function (name) {
2502 if (!(name in emojis)) return
2503 var file = path.join(emojiDir, name + '.png')
2504 var fileBuf = fs.readFileSync(file)
2505 var id = hashBuf(fileBuf)
2506 // seed the builtin emoji
2507 pull(pull.once(fileBuf), this.app.sbot.blobs.add(id, function (err) {
2508 if (err) console.error('error adding builtin emoji as blob', err)
2509 }))
2510 return {
2511 link: id,
2512 type: 'image/png',
2513 size: fileBuf.length,
2514 }
2515}
2516
2517Serve.prototype.emojis = function (path) {
2518 var self = this
2519 var seen = {}
2520 pull(
2521 ph('section', [
2522 ph('h3', 'Emojis'),
2523 ph('ul', {class: 'mentions'}, pull(
2524 self.app.streamEmojis(),
2525 pull.map(function (emoji) {
2526 if (!seen[emoji.name]) {
2527 // cache the first use, so that our uses take precedence over other feeds'
2528 self.app.reverseEmojiNameCache.set(emoji.name, emoji.link)
2529 seen[emoji.name] = true
2530 }
2531 return ph('li', [
2532 ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)},
2533 ph('img', {
2534 class: 'ssb-emoji',
2535 src: self.app.render.imageUrl(emoji.link),
2536 size: 32,
2537 })
2538 ), ' ',
2539 u.escapeHTML(emoji.name)
2540 ])
2541 })
2542 ))
2543 ]),
2544 this.wrapPage('emojis'),
2545 this.respondSink(200)
2546 )
2547}
2548

Built with git-ssb-web