git ssb

16+

cel / patchfoo



Tree: d64cd4d8402357862c983a26b788e40dd32d21a9

Files: d64cd4d8402357862c983a26b788e40dd32d21a9 / lib / serve.js

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

Built with git-ssb-web