git ssb

16+

cel / patchfoo



Tree: b51c32ba41cb9ce7d89a1ef5f70889c750f1bee3

Files: b51c32ba41cb9ce7d89a1ef5f70889c750f1bee3 / lib / serve.js

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

Built with git-ssb-web