git ssb

16+

cel / patchfoo



Tree: f34f08104b9613ba5f0ac43eb229ae992116224a

Files: f34f08104b9613ba5f0ac43eb229ae992116224a / lib / serve.js

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

Built with git-ssb-web