git ssb

16+

cel / patchfoo



Tree: f0952377664cc671d536cff9d13918e967964e04

Files: f0952377664cc671d536cff9d13918e967964e04 / lib / serve.js

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

Built with git-ssb-web