git ssb

16+

cel / patchfoo



Tree: c5a95cc1ebe6dd3a4d572220f5cd60c31381de97

Files: c5a95cc1ebe6dd3a4d572220f5cd60c31381de97 / lib / serve.js

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

Built with git-ssb-web