git ssb

16+

cel / patchfoo



Tree: d7c4e48d49294efeb2d0597185956e18d20df928

Files: d7c4e48d49294efeb2d0597185956e18d20df928 / lib / serve.js

136731 bytesRaw
1var fs = require('fs')
2var qs = require('querystring')
3var pull = require('pull-stream')
4var path = require('path')
5var paramap = require('pull-paramap')
6var sort = require('ssb-sort')
7var crypto = require('crypto')
8var toPull = require('stream-to-pull-stream')
9var serveEmoji = require('emoji-server')()
10var u = require('./util')
11var cat = require('pull-cat')
12var h = require('hyperscript')
13var paginate = require('pull-paginate')
14var ssbMentions = require('ssb-mentions')
15var multicb = require('multicb')
16var pkg = require('../package')
17var Busboy = require('busboy')
18var mime = require('mime-types')
19var ident = require('pull-identify-filetype')
20var htime = require('human-time')
21var ph = require('pull-hyperscript')
22var emojis = require('emoji-named-characters')
23var jpeg = require('jpeg-autorotate')
24var Catch = require('pull-catch')
25var Diff = require('diff')
26var split = require('pull-split')
27var utf8 = require('pull-utf8-decoder')
28var webresolve = require('ssb-web-resolver')
29var Url = require('url')
30
31module.exports = Serve
32
33var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
34var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles')
35
36var urlIdRegex = /^(?:\/+(([%&@]|%25|%26)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/
37
38function ctype(name) {
39 switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
40 case 'html': return 'text/html'
41 case 'txt': return 'text/plain'
42 case 'js': return 'text/javascript'
43 case 'css': return 'text/css'
44 case 'png': return 'image/png'
45 case 'json': return 'application/json'
46 case 'ico': return 'image/x-icon'
47 }
48}
49
50function encodeDispositionFilename(fname) {
51 fname = fname.replace(/\/g/, '\\\\').replace(/"/, '\\\"')
52 return '"' + encodeURIComponent(fname) + '"'
53}
54
55function uniques() {
56 var set = {}
57 return function (item) {
58 if (set[item]) return false
59 return set[item] = true
60 }
61}
62
63function Serve(app, req, res) {
64 this.app = app
65 this.req = req
66 this.res = res
67 this.startDate = new Date()
68 var hostname = req.headers.host || app.hostname
69 this.baseUrl = 'http://' + hostname + (app.opts.base || '/')
70}
71
72Serve.prototype.go = function () {
73 console.log(this.req.method, this.req.url)
74 var self = this
75
76 this.res.setTimeout(0)
77 var conf = self.app.config.patchfoo || {}
78 this.conf = conf
79
80 var authtok = conf.auth || null
81 if (authtok) {
82 var auth = this.req.headers['authorization']
83 var tok = null
84 //console.log('Authorization: ',auth)
85
86 if (auth) {
87 var a = auth.split(' ')
88 if (a[0] == 'Basic') {
89 tok = Buffer.from(a[1],'base64').toString('ascii')
90 }
91 }
92 if (tok != authtok) {
93 self.res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Patchfoo"'})
94 self.res.end('Not authorized')
95 return
96 }
97 }
98 var allowAddresses = conf.allowAddresses
99 if (allowAddresses) {
100 var ip = this.req.socket.remoteAddress
101 if (allowAddresses.indexOf(ip) === -1) {
102 this.res.writeHead(401)
103 return this.res.end('Not authorized')
104 }
105 }
106
107 if (this.req.method === 'POST' || this.req.method === 'PUT') {
108 if (/^multipart\/form-data/.test(this.req.headers['content-type'])) {
109 var data = {}
110 var erred
111 var busboy = new Busboy({headers: this.req.headers})
112 var filesCb = multicb({pluck: 1})
113 busboy.on('finish', filesCb())
114 filesCb(function (err) {
115 gotData(err, data)
116 })
117 function addField(name, value) {
118 if (typeof value === 'string') value = value.replace(/\r\n/g, '\n')
119 if (!(name in data)) data[name] = value
120 else if (Array.isArray(data[name])) data[name].push(value)
121 else data[name] = [data[name], value]
122 }
123 busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
124 var cb = filesCb()
125 var size = 0
126 pull(
127 toPull(file),
128 pull.map(function (data) {
129 size += data.length
130 return data
131 }),
132 self.app.addBlob(!!data.private, function (err, link) {
133 if (err) return cb(err)
134 if (size === 0 && !filename) return cb()
135 link.name = filename
136 link.type = mimetype
137 addField(fieldname, link)
138 cb()
139 })
140 )
141 })
142 busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
143 addField(fieldname, val)
144 })
145 this.req.pipe(busboy)
146 } else {
147 pull(
148 toPull(this.req),
149 pull.collect(function (err, bufs) {
150 var data
151 if (!err) try {
152 var str = Buffer.concat(bufs).toString('utf8')
153 str = str.replace(/%0D%0A/ig, '\n')
154 data = qs.parse(str)
155 } catch(e) {
156 err = e
157 }
158 gotData(err, data)
159 })
160 )
161 }
162 } else {
163 gotData(null, {})
164 }
165
166 function gotData(err, data) {
167 self.data = data
168 if (err) next(err)
169 else if (data.action === 'publish') self.publishJSON(next)
170 else if (data.action === 'contact') self.publishContact(next)
171 else if (data.action === 'want-blobs') self.wantBlobs(next)
172 else if (data.action === 'poll-position') self.publishPollPosition(next)
173 else if (data.action_vote) self.publishVote(next)
174 else if (data.action_attend) self.publishAttend(next)
175 else next()
176 }
177
178 function next(err, publishedMsg) {
179 if (err) {
180 if (err.redirectUrl) return self.redirect(err.redirectUrl)
181 self.res.writeHead(400, {'Content-Type': 'text/plain'})
182 self.res.end(err.stack)
183 } else if (publishedMsg) {
184 if (self.data.redirect_to_published_msg) {
185 self.redirect(self.app.render.toUrl(publishedMsg.key))
186 } else {
187 var u = Url.parse(self.req.url)
188 var q = u.query || (u.query = {})
189 q.published = publishedMsg.key
190 self.redirect(Url.format(u))
191 }
192 } else {
193 self.handle()
194 }
195 }
196}
197
198Serve.prototype.saveDraft = function (content, cb) {
199 var self = this
200 var data = self.data
201 var form = {}
202 for (var k in data) {
203 if (k === 'url' || k === 'draft_id' || k === 'content'
204 || k === 'draft_id' || k === 'save_draft') continue
205 form[k] = data[k]
206 }
207 self.app.saveDraft(data.draft_id, data.url, form, content, function (err, id) {
208 if (err) return cb(err)
209 cb(null, id || data.draft_id)
210 })
211}
212
213Serve.prototype.publishJSON = function (cb) {
214 var content
215 try {
216 content = JSON.parse(this.data.content)
217 } catch(e) {
218 return cb(e)
219 }
220 this.publish(content, cb)
221}
222
223Serve.prototype.publishVote = function (next) {
224 var content = {
225 type: 'vote',
226 channel: this.data.channel || undefined,
227 root: this.data.root || undefined,
228 branch: this.data.branches ? this.data.branches.split(',') : undefined,
229 vote: {
230 link: this.data.link,
231 value: Number(this.data.vote_value),
232 expression: this.data.vote_expression || undefined,
233 }
234 }
235 if (this.data.recps) content.recps = this.data.recps.split(',')
236 if (this.app.previewVotes) {
237 var json = JSON.stringify(content, 0, 2)
238 var q = qs.stringify({text: json, action: 'preview'})
239 var url = this.app.render.toUrl('/compose?' + q)
240 this.redirect(url)
241 } else {
242 this.publish(content, next)
243 }
244}
245
246Serve.prototype.requestReplicate = function (id, replicateIt, next) {
247 var self = this
248 var replicate = self.app.sbot.replicate
249 var request = replicate && replicate.request
250 if (!request) return this.respond(500, 'Missing replicate.request method')
251 request(id, replicateIt, function (err) {
252 if (err) return pull(
253 pull.once(u.renderError(err, ext).outerHTML),
254 self.wrapPage('replicate request'),
255 self.respondSink(400)
256 )
257 self.requestedReplicate = replicateIt
258 next()
259 })
260}
261
262Serve.prototype.publishContact = function (next) {
263 if (this.data.replicate) return this.requestReplicate(this.data.contact, true, next)
264 if (this.data.unreplicate) return this.requestReplicate(this.data.contact, false, next)
265 if (this.data.block1) return this.redirect(this.app.render.toUrl('/block/' + this.data.contact))
266 var content = {
267 type: 'contact',
268 contact: this.data.contact,
269 }
270 if (this.data.follow) content.following = true
271 if (this.data.block) content.blocking = true
272 if (this.data.unfollow) content.following = false
273 if (this.data.unblock) content.blocking = false
274 if (this.data.mute) content.blocking = true
275 if (this.data.unmute) content.blocking = false
276 if (this.data.mute || this.data.unmute) content.recps = [this.app.sbot.id]
277 if (this.app.previewContacts) {
278 var json = JSON.stringify(content, 0, 2)
279 var q = qs.stringify({text: json, action: 'preview'})
280 var url = this.app.render.toUrl('/compose?' + q)
281 this.redirect(url)
282 } else {
283 this.publish(content, next)
284 }
285}
286
287Serve.prototype.publishPollPosition = function (cb) {
288 var content = {
289 type: 'position',
290 version: 'v1',
291 channel: this.data.channel || undefined,
292 root: this.data.poll_root,
293 branch: this.data.branches || [],
294 reason: this.data.poll_reason || undefined,
295 details: {
296 type: this.data.poll_type
297 }
298 }
299 if (this.data.poll_choice != null) {
300 content.details.choice = Number(this.data.poll_choice)
301 } else {
302 content.details.choices = u.toArray(this.data.poll_choices).map(Number)
303 }
304 if (this.data.recps) content.recps = this.data.recps.split(',')
305 var json = JSON.stringify(content, 0, 2)
306 var q = qs.stringify({text: json, action: 'preview'})
307 var url = this.app.render.toUrl('/compose?' + q)
308 this.redirect(url)
309 // this.publish(content, cb)
310}
311
312Serve.prototype.publishAttend = function (cb) {
313 var content = {
314 type: 'about',
315 channel: this.data.channel || undefined,
316 about: this.data.link,
317 attendee: {
318 link: this.app.sbot.id
319 }
320 }
321 if (this.data.recps) content.recps = this.data.recps.split(',')
322 this.publish(content, cb)
323}
324
325Serve.prototype.wantBlobs = function (cb) {
326 var self = this
327 if (!self.data.blob_ids) return cb()
328 var ids = self.data.blob_ids.split(',')
329 if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(',')))
330 var done = multicb({pluck: 1})
331 ids.forEach(function (id) {
332 self.app.wantSizeBlob(id, done())
333 })
334 if (self.data.async_want) return cb()
335 done(function (err) {
336 if (err) return cb(err)
337 // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.')
338 cb()
339 })
340}
341
342Serve.prototype.publish = function (content, cb) {
343 var self = this
344 var done = multicb({pluck: 1, spread: true})
345 u.toArray(content && content.mentions).forEach(function (mention) {
346 if (mention.link && mention.link[0] === '&' && !isNaN(mention.size))
347 self.app.pushBlob(mention.link, done())
348 })
349 done(function (err) {
350 if (err) return cb(err)
351 self.publishMayRedirect(content, function (err, msg) {
352 if (err) return cb(err)
353 delete self.data.text
354 delete self.data.recps
355 return cb(null, msg)
356 })
357 })
358}
359
360Serve.prototype.publishMayRedirect = function (content, cb) {
361 var publishguard = this.app.sbot.publishguard
362 if (Array.isArray(content.recps)) {
363 var recps = content.recps.map(u.linkDest)
364 if (publishguard && publishguard.privatePublishGetUrl) {
365 return publishguard.privatePublishGetUrl({
366 content: content,
367 recps: recps,
368 redirectBase: this.baseUrl
369 }, onPublishGetUrl)
370 } else {
371 this.app.privatePublish(content, recps, cb)
372 }
373 } else {
374 if (publishguard && publishguard.publishGetUrl) {
375 publishguard.publishGetUrl({
376 content: content,
377 redirectBase: this.baseUrl
378 }, onPublishGetUrl)
379 } else {
380 this.app.sbot.publish(content, cb)
381 }
382 }
383 function onPublishGetUrl(err, url) {
384 if (err) return cb(err)
385 cb({redirectUrl: url})
386 }
387}
388
389Serve.prototype.handle = function () {
390 var m = urlIdRegex.exec(this.req.url)
391 this.query = m[5] ? qs.parse(m[5]) : {}
392 this.useOoo = this.query.ooo != null ?
393 Boolean(this.query.ooo) : this.app.useOoo
394 switch (m[2]) {
395 case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
396 case '%': return this.id(m[1], m[3])
397 case '@': return this.userFeed(m[1], m[3])
398 case '%26': m[2] = '&'; m[1] = decodeURIComponent(m[1])
399 case '&': return this.blob(m[1], m[3])
400 default: return this.path(m[4])
401 }
402}
403
404Serve.prototype.respond = function (status, message) {
405 this.res.writeHead(status)
406 this.res.end(message)
407}
408
409Serve.prototype.respondSink = function (status, headers, cb) {
410 var self = this
411 if (status || headers)
412 self.res.writeHead(status, headers || {'Content-Type': 'text/html'})
413 return toPull(self.res, cb || function (err) {
414 if (err) self.app.error(err)
415 })
416}
417
418Serve.prototype.redirect = function (dest) {
419 this.res.writeHead(302, {
420 Location: dest
421 })
422 this.res.end()
423}
424
425Serve.prototype.path = function (url) {
426 var m
427 url = url.replace(/^\/+/, '/')
428 switch (url) {
429 case '/': return this.home()
430 case '/robots.txt': return this.res.end('User-agent: *')
431 }
432 if (m = /^\/%23(.*)/.exec(url)) {
433 return this.redirect(this.app.render.toUrl('/channel/'
434 + decodeURIComponent(m[1])))
435 }
436 m = /^([^.]*)(?:\.(.*))?$/.exec(url)
437 switch (m[1]) {
438 case '/new': return this.new(m[2])
439 case '/public': return this.public(m[2])
440 case '/private': return this.private(m[2])
441 case '/mentions': return this.mentions(m[2])
442 case '/search': return this.search(m[2])
443 case '/advsearch': return this.advsearch(m[2])
444 case '/peers': return this.peers(m[2])
445 case '/status': return this.status(m[2])
446 case '/channels': return this.channels(m[2])
447 case '/tags': return this.tags(m[2])
448 case '/friends': return this.friends(m[2])
449 case '/live': return this.live(m[2])
450 case '/compose': return this.compose(m[2])
451 case '/emojis': return this.emojis(m[2])
452 case '/votes': return this.votes(m[2])
453 case '/about-self': return this.aboutSelf(m[2])
454 }
455 m = /^(\/?[^\/]*)(\/.*)?$/.exec(url)
456 switch (m[1]) {
457 case '/channel': return this.channel(m[2])
458 case '/type': return this.type(m[2])
459 case '/links': return this.links(m[2])
460 case '/static': return this.static(m[2])
461 case '/emoji': return this.emoji(m[2])
462 case '/highlight': return this.highlight(m[2])
463 case '/contacts': return this.contacts(m[2])
464 case '/about': return this.about(m[2])
465 case '/pub': return this.pub(m[2])
466 case '/git': return this.git(m[2])
467 case '/image': return this.image(m[2])
468 case '/npm': return this.npm(m[2])
469 case '/npm-prebuilds': return this.npmPrebuilds(m[2])
470 case '/npm-readme': return this.npmReadme(m[2])
471 case '/npm-registry': return this.npmRegistry(m[2])
472 case '/markdown': return this.markdown(m[2])
473 case '/edit-diff': return this.editDiff(m[2])
474 case '/about-diff': return this.aboutDiff(m[2])
475 case '/shard': return this.shard(m[2])
476 case '/zip': return this.zip(m[2])
477 case '/web': return this.web(m[2])
478 case '/block': return this.block(m[2])
479 case '/script': return this.script(m[2])
480 case '/drafts': return this.drafts(m[2])
481 }
482 return this.respond(404, 'Not found')
483}
484
485Serve.prototype.home = function () {
486 pull(
487 pull.empty(),
488 this.wrapPage('/'),
489 this.respondSink(200, {
490 'Content-Type': 'text/html'
491 })
492 )
493}
494
495Serve.prototype.public = function (ext) {
496 var q = this.query
497 var opts = {
498 reverse: !q.forwards,
499 sortByTimestamp: q.sort === 'claimed',
500 lt: Number(q.lt) || Date.now(),
501 gt: Number(q.gt) || -Infinity,
502 filter: q.filter,
503 }
504
505 pull(
506 this.app.createLogStream(opts),
507 this.renderThreadPaginated(opts, null, q),
508 this.wrapMessages(),
509 this.wrapPublic(),
510 this.wrapPage('public'),
511 this.respondSink(200, {
512 'Content-Type': ctype(ext)
513 })
514 )
515}
516
517Serve.prototype.setCookie = function (key, value, options) {
518 var header = key + '=' + value
519 if (options) for (var k in options) {
520 header += '; ' + k + '=' + options[k]
521 }
522 this.res.setHeader('Set-Cookie', header)
523}
524
525Serve.prototype.new = function (ext) {
526 var self = this
527 var q = self.query
528 var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1]
529 var limit = Number(q.limit || self.conf.newLimit || 500)
530 var now = Date.now()
531 var opts = {
532 gt: Number(q.gt) || Number(latest) || now,
533 lte: now,
534 limit: limit
535 }
536
537 if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000})
538
539 var read = self.app.createLogStream(opts)
540 self.req.on('closed', function () {
541 console.error('closing')
542 read(true, function (err) {
543 console.log('closed')
544 if (err && err !== true) console.error(new Error(err.stack))
545 })
546 })
547 pull.collect(function (err, msgs) {
548 if (err) return pull(
549 pull.once(u.renderError(err, ext).outerHTML),
550 self.wrapPage('peers'),
551 self.respondSink(500, {'Content-Type': ctype(ext)})
552 )
553 sort(msgs)
554 var maxTS = msgs.reduce(function (max, msg) {
555 return Math.max(msg.timestamp, max)
556 }, -Infinity)
557 pull(
558 pull.values(msgs),
559 self.renderThread(),
560 self.wrapNew({
561 reachedLimit: msgs.length === limit && limit,
562 gt: isFinite(maxTS) ? maxTS : Date.now()
563 }),
564 self.wrapMessages(),
565 self.wrapPage('new'),
566 self.respondSink(200, {
567 'Content-Type': ctype(ext)
568 })
569 )
570 })(read)
571}
572
573Serve.prototype.private = function (ext) {
574 var q = this.query
575 var opts = {
576 reverse: !q.forwards,
577 lt: Number(q.lt) || Date.now(),
578 gt: Number(q.gt) || -Infinity,
579 filter: q.filter,
580 }
581
582 pull(
583 this.app.streamPrivate(opts),
584 this.renderThreadPaginated(opts, null, q),
585 this.wrapMessages(),
586 this.wrapPrivate(opts),
587 this.wrapPage('private'),
588 this.respondSink(200, {
589 'Content-Type': ctype(ext)
590 })
591 )
592}
593
594Serve.prototype.mentions = function (ext) {
595 var self = this
596 var q = self.query
597 var opts = {
598 reverse: !q.forwards,
599 sortByTimestamp: q.sort === 'claimed',
600 lt: Number(q.lt) || Date.now(),
601 gt: Number(q.gt) || -Infinity,
602 filter: q.filter,
603 }
604
605 return pull(
606 ph('section', {}, [
607 ph('h3', 'Mentions'),
608 pull(
609 self.app.streamMentions(opts),
610 self.app.unboxMessages(),
611 self.renderThreadPaginated(opts, null, q),
612 self.wrapMessages()
613 )
614 ]),
615 self.wrapPage('mentions'),
616 self.respondSink(200)
617 )
618}
619
620Serve.prototype.search = function (ext) {
621 var searchQ = (this.query.q || '').trim()
622 var self = this
623
624 if (/^ssb:\/\//.test(searchQ)) {
625 var maybeId = searchQ.substr(6)
626 if (u.isRef(maybeId)) searchQ = maybeId
627 }
628
629 if (u.isRef(searchQ) || searchQ[0] === '#') {
630 return self.redirect(self.app.render.toUrl(searchQ))
631 }
632
633 pull(
634 self.app.search(searchQ),
635 self.renderThread(),
636 self.wrapMessages(),
637 self.wrapPage('search · ' + searchQ, searchQ),
638 self.respondSink(200, {
639 'Content-Type': ctype(ext),
640 })
641 )
642}
643
644Serve.prototype.advsearch = function (ext) {
645 var self = this
646 var q = this.query || {}
647
648 if (q.source) q.source = u.extractFeedIds(q.source)[0]
649 if (q.dest) q.dest = u.extractFeedIds(q.dest)[0]
650 var hasQuery = q.text || q.source || q.dest || q.channel
651
652 pull(
653 cat([
654 ph('section', {}, [
655 ph('form', {action: '', method: 'get'}, [
656 ph('table', [
657 ph('tr', [
658 ph('td', 'text'),
659 ph('td', ph('input', {name: 'text', placeholder: 'regex',
660 class: 'id-input',
661 value: q.text || ''}))
662 ]),
663 ph('tr', [
664 ph('td', 'author'),
665 ph('td', ph('input', {name: 'source', placeholder: '@id',
666 class: 'id-input',
667 value: q.source || ''}))
668 ]),
669 ph('tr', [
670 ph('td', 'mentions'),
671 ph('td', ph('input', {name: 'dest', placeholder: 'id',
672 class: 'id-input',
673 value: q.dest || ''}))
674 ]),
675 ph('tr', [
676 ph('td', 'channel'),
677 ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel',
678 class: 'id-input',
679 value: q.channel || ''})
680 ])
681 ]),
682 ph('tr', [
683 ph('td', {colspan: 2}, [
684 ph('input', {type: 'submit', value: 'search'})
685 ])
686 ]),
687 ])
688 ])
689 ]),
690 hasQuery && pull(
691 self.app.advancedSearch(q),
692 self.renderThread({
693 feed: q.source,
694 }),
695 self.wrapMessages()
696 )
697 ]),
698 self.wrapPage('advanced search'),
699 self.respondSink(200, {
700 'Content-Type': ctype(ext),
701 })
702 )
703}
704
705Serve.prototype.live = function (ext) {
706 var self = this
707 var q = self.query
708 var opts = {
709 live: true,
710 }
711 var gt = Number(q.gt)
712 if (gt) opts.gt = gt
713 else opts.old = false
714
715 pull(
716 ph('table', {class: 'ssb-msgs'}, pull(
717 self.app.sbot.createLogStream(opts),
718 self.app.render.renderFeeds({
719 serve: self,
720 withGt: true,
721 filter: q.filter,
722 }),
723 pull.map(u.toHTML)
724 )),
725 self.wrapPage('live'),
726 self.respondSink(200, {
727 'Content-Type': ctype(ext),
728 })
729 )
730}
731
732Serve.prototype.compose = function (ext) {
733 var self = this
734 self.composer({
735 channel: '',
736 redirectToPublishedMsg: true,
737 }, function (err, composer) {
738 if (err) return pull(
739 pull.once(u.renderError(err).outerHTML),
740 self.wrapPage('compose'),
741 self.respondSink(500)
742 )
743 pull(
744 pull.once(u.toHTML(composer)),
745 self.wrapPage('compose'),
746 self.respondSink(200, {
747 'Content-Type': ctype(ext)
748 })
749 )
750 })
751}
752
753Serve.prototype.votes = function (path) {
754 if (path) return pull(
755 pull.once(u.renderError(new Error('Not implemented')).outerHTML),
756 this.wrapPage('#' + channel),
757 this.respondSink(404, {'Content-Type': ctype('html')})
758 )
759
760 var self = this
761 var q = self.query
762 var opts = {
763 reverse: !q.forwards,
764 limit: Number(q.limit) || 50,
765 }
766 var gt = Number(q.gt)
767 if (gt) opts.gt = gt
768 var lt = Number(q.lt)
769 if (lt) opts.lt = lt
770
771 self.app.getVoted(opts, function (err, voted) {
772 if (err) return pull(
773 pull.once(u.renderError(err).outerHTML),
774 self.wrapPage('#' + channel),
775 self.respondSink(500, {'Content-Type': ctype('html')})
776 )
777
778 pull(
779 ph('table', [
780 ph('thead', [
781 ph('tr', [
782 ph('td', {colspan: 2}, self.syncPager({
783 first: voted.firstTimestamp,
784 last: voted.lastTimestamp,
785 }))
786 ])
787 ]),
788 ph('tbody', pull(
789 pull.values(voted.items),
790 paramap(function (item, cb) {
791 cb(null, ph('tr', [
792 ph('td', [String(item.value)]),
793 ph('td', [
794 self.phIdLink(item.id),
795 pull.once(' dug by '),
796 self.renderIdsList()(pull.values(item.feeds))
797 ])
798 ]))
799 }, 8)
800 )),
801 ph('tfoot', {}, []),
802 ]),
803 self.wrapPage('votes'),
804 self.respondSink(200, {
805 'Content-Type': ctype('html')
806 })
807 )
808 })
809}
810
811Serve.prototype.syncPager = function (opts) {
812 var q = this.query
813 var reverse = !q.forwards
814 var min = (reverse ? opts.last : opts.first) || Number(q.gt)
815 var max = (reverse ? opts.first : opts.last) || Number(q.lt)
816 var minDate = new Date(min)
817 var maxDate = new Date(max)
818 var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined})
819 var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1})
820 var atNewest = reverse ? !q.lt : !max
821 var atOldest = reverse ? !min : !q.gt
822 if (atNewest && !reverse) qOlder.lt++
823 if (atOldest && reverse) qNewer.gt--
824 return h('div',
825 atOldest ? 'oldest' : [
826 h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ',
827 h('span', {title: minDate.toString()}, htime(minDate)), ' ',
828 ],
829 ' - ',
830 atNewest ? 'now' : [
831 h('span', {title: maxDate.toString()}, htime(maxDate)), ' ',
832 h('a', {href: '?' + qs.stringify(qNewer)}, '>>')
833 ]
834 ).outerHTML
835}
836
837Serve.prototype.peers = function (ext) {
838 var self = this
839 if (self.data.action === 'connect') {
840 return self.app.sbot.gossip.connect(self.data.address, function (err) {
841 if (err) return pull(
842 pull.once(u.renderError(err, ext).outerHTML),
843 self.wrapPage('peers'),
844 self.respondSink(400, {'Content-Type': ctype(ext)})
845 )
846 self.data = {}
847 return self.peers(ext)
848 })
849 }
850
851 pull(
852 self.app.streamPeers(),
853 paramap(function (peer, cb) {
854 var done = multicb({pluck: 1, spread: true})
855 var connectedTime = Date.now() - peer.stateChange
856 var addr = peer.host + ':' + peer.port + ':' + peer.key
857 done()(null, h('section',
858 h('form', {method: 'post', action: ''},
859 peer.client ? '→' : '←', ' ',
860 h('code', peer.host, ':', peer.port, ':'),
861 self.app.render.idLink(peer.key, done()), ' ',
862 peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '',
863 peer.state === 'connected' ? 'connected' : [
864 h('input', {name: 'action', type: 'submit', value: 'connect'}),
865 h('input', {name: 'address', type: 'hidden', value: addr})
866 ]
867 )
868 // h('div', 'source: ', peer.source)
869 // JSON.stringify(peer, 0, 2)).outerHTML
870 ))
871 done(cb)
872 }, 8),
873 pull.map(u.toHTML),
874 self.wrapPeers(),
875 self.wrapPage('peers'),
876 self.respondSink(200, {
877 'Content-Type': ctype(ext)
878 })
879 )
880}
881
882Serve.prototype.status = function (ext) {
883 var self = this
884
885 if (!self.app.sbot.status) return pull(
886 pull.once('missing sbot status method'),
887 this.wrapPage('status'),
888 self.respondSink(400)
889 )
890
891 pull(
892 ph('section', [
893 ph('h3', 'Status'),
894 pull(
895 u.readNext(function (cb) {
896 self.app.sbotStatus(function (err, status) {
897 cb(err, status && pull.once(status))
898 })
899 }),
900 pull.map(function (status) {
901 return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML
902 })
903 )
904 ]),
905 this.wrapPage('status'),
906 this.respondSink(200)
907 )
908}
909
910Serve.prototype.channels = function (ext) {
911 var self = this
912 var id = self.app.sbot.id
913
914 function renderMyChannels() {
915 return pull(
916 self.app.streamMyChannels(id),
917 paramap(function (channel, cb) {
918 // var subscribed = false
919 cb(null, [
920 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
921 ' '
922 ])
923 }, 8),
924 pull.map(u.toHTML),
925 self.wrapMyChannels()
926 )
927 }
928
929 function renderNetworkChannels() {
930 return pull(
931 self.app.streamChannels(),
932 paramap(function (channel, cb) {
933 // var subscribed = false
934 cb(null, [
935 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel),
936 ' '
937 ])
938 }, 8),
939 pull.map(u.toHTML),
940 self.wrapChannels()
941 )
942 }
943
944 pull(
945 cat([
946 ph('section', {}, [
947 ph('h3', {}, 'Channels:'),
948 renderMyChannels(),
949 renderNetworkChannels()
950 ])
951 ]),
952 this.wrapPage('channels'),
953 this.respondSink(200, {
954 'Content-Type': ctype(ext)
955 })
956 )
957}
958
959Serve.prototype.tags = function (path) {
960 var self = this
961 var seen = {}
962 pull(
963 ph('section', [
964 ph('h3', 'Tags'),
965 pull(
966 self.app.streamTags(),
967 pull.map(function (msg) {
968 return [self.phIdLink(msg.key), ' ']
969 })
970 )
971 ]),
972 this.wrapPage('tags'),
973 this.respondSink(200)
974 )
975}
976
977Serve.prototype.contacts = function (path) {
978 var self = this
979 var id = String(path).substr(1)
980 var contacts = self.app.contacts.createContactStreams({
981 id: id,
982 msgIds: true,
983 enemies: true
984 })
985 var render = self.app.render
986
987 pull(
988 cat([
989 ph('section', {}, [
990 ph('h3', {}, ['Contacts: ', self.phIdLink(id)]),
991 ph('h4', {}, 'Friends'),
992 render.friendsList('/contacts/')(contacts.friends),
993 ph('h4', {}, 'Follows'),
994 render.friendsList('/contacts/')(contacts.follows),
995 ph('h4', {}, 'Followers'),
996 render.friendsList('/contacts/')(contacts.followers),
997 ph('h4', {}, 'Blocks'),
998 render.friendsList('/contacts/')(contacts.blocks),
999 ph('h4', {}, 'Blocked by'),
1000 render.friendsList('/contacts/')(contacts.blockers),
1001 contacts.enemies ? [
1002 ph('h4', {}, 'Enemies'),
1003 render.friendsList('/contacts/')(contacts.enemies),
1004 ] : ''
1005 ])
1006 ]),
1007 this.wrapPage('contacts: ' + id),
1008 this.respondSink(200, {
1009 'Content-Type': ctype('html')
1010 })
1011 )
1012}
1013
1014Serve.prototype.about = function (path) {
1015 var self = this
1016 var id = decodeURIComponent(String(path).substr(1))
1017 var abouts = self.app.createAboutStreams(id)
1018 var render = self.app.render
1019
1020 function renderAboutOpImage(link) {
1021 if (!link) return
1022 if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link))
1023 return ph('img', {
1024 class: 'ssb-avatar-image',
1025 src: render.imageUrl(link.link),
1026 alt: link.link
1027 + (link.size ? ' (' + render.formatSize(link.size) + ')' : '')
1028 })
1029 }
1030
1031 function renderAboutOpValue(value) {
1032 if (!value) return
1033 if (u.isRef(value.link)) return self.phIdLink(value.link)
1034 if (value.epoch) return new Date(value.epoch).toUTCString()
1035 return ph('code', {}, JSON.stringify(value))
1036 }
1037
1038 function renderAboutOpContent(op) {
1039 if (op.prop === 'image')
1040 return renderAboutOpImage(u.toLink(op.value))
1041 if (op.prop === 'description')
1042 return h('div', {innerHTML: render.markdown(op.value)}).outerHTML
1043 if (op.prop === 'title')
1044 return h('strong', op.value).outerHTML
1045 if (op.prop === 'name')
1046 return h('u', op.value).outerHTML
1047 return renderAboutOpValue(op.value)
1048 }
1049
1050 function renderAboutOp(op) {
1051 return ph('tr', {}, [
1052 ph('td', self.phIdLink(op.author)),
1053 ph('td',
1054 ph('a', {href: render.toUrl(op.id)},
1055 htime(new Date(op.timestamp)))),
1056 ph('td', op.prop),
1057 ph('td', renderAboutOpContent(op))
1058 ])
1059 }
1060
1061 pull(
1062 cat([
1063 ph('section', {}, [
1064 ph('h3', {}, ['About: ', self.phIdLink(id)]),
1065 ph('table', {},
1066 pull(abouts.scalars, pull.map(renderAboutOp))
1067 ),
1068 pull(
1069 abouts.sets,
1070 pull.map(function (op) {
1071 return h('pre', JSON.stringify(op, 0, 2))
1072 }),
1073 pull.map(u.toHTML)
1074 )
1075 ])
1076 ]),
1077 this.wrapPage('about: ' + id),
1078 this.respondSink(200, {
1079 'Content-Type': ctype('html')
1080 })
1081 )
1082}
1083
1084Serve.prototype.aboutSelf = function (ext) {
1085 var self = this
1086 var id = self.app.sbot.id
1087 var render = self.app.render
1088
1089 self.app.getAbout(id, function gotAbout(err, about) {
1090 if (err) return cb(err)
1091
1092 var data = self.data
1093 var aboutName = about.name ? String(about.name).replace(/^@/, '') : ''
1094 var aboutImageLink = about.imageLink || {}
1095 var name = data.name != null ?
1096 data.name === '' ? null : data.name :
1097 aboutName || null
1098 var image = data.image_upload != null ? {
1099 link: data.image_upload.link,
1100 type: data.image_upload.type,
1101 size: data.image_upload.size
1102 } : data.image_id && data.image_id !== aboutImageLink.link ? {
1103 link: data.image_id,
1104 type: data.image_type,
1105 size: data.image_size
1106 } : aboutImageLink
1107 var imageId = image.link || '/static/fallback.png'
1108 var description = data.description != null ?
1109 data.description === '' ? null : data.description :
1110 about.description || null
1111 var publicWebHosting = data.publicWebHosting != null ?
1112 data.publicWebHosting === 'false' ? false :
1113 data.publicWebHosting === 'null' ? null : !!data.publicWebHosting :
1114 about.publicWebHosting
1115
1116 var content
1117 if (data.preview || data.preview_raw) {
1118 content = {
1119 type: 'about',
1120 about: id
1121 }
1122 if (name != aboutName) content.name = name
1123 if (image.link != about.image) content.image = image
1124 if (description != about.description) content.description = description
1125 if (publicWebHosting != about.publicWebHosting) content.publicWebHosting = publicWebHosting
1126 }
1127
1128 pull(
1129 ph('section', {}, [
1130 ph('h4', 'Your public profile'),
1131 ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
1132 ph('div', [
1133 '@', ph('input', {id: 'name', name: 'name', placeholder: 'name', value: name})
1134 ]),
1135 ph('table', ph('tr', [
1136 ph('td', [
1137 ph('a', {href: render.toUrl(imageId)}, [
1138 ph('img', {
1139 class: 'ssb-avatar-image',
1140 src: render.imageUrl(imageId),
1141 alt: image.link || 'fallback avatar',
1142 title: image.link || 'fallback avatar'
1143 })
1144 ])
1145 ]),
1146 ph('td', [
1147 image.link ? ph('div', [
1148 ph('small', ph('code', u.escapeHTML(image.link))),
1149 ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
1150 ]) : '',
1151 image.size ? [
1152 ph('code', render.formatSize(image.size)),
1153 ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
1154 ] : '',
1155 image.type ? [
1156 ph('input', {type: 'hidden', name: 'image_type', value: image.type})
1157 ] : '',
1158 ph('div', [
1159 ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
1160 ])
1161 ])
1162 ])),
1163 ph('textarea', {
1164 id: 'description', name: 'description', placeholder: 'description',
1165 rows: Math.max(4, u.rows(description))
1166 }, u.escapeHTML(description)),
1167 ph('div', {
1168 title: 'Allow your messages to be hosted on public viewer websites'
1169 }, [
1170 ph('label', {for: 'publicWebHosting'}, 'Public web hosting: '),
1171 ph('select', {name: 'publicWebHosting', id: 'publicWebHosting'}, [
1172 ph('option', {value: 'true', selected: publicWebHosting}, 'yes'),
1173 ph('option', {value: 'false', selected: publicWebHosting === false}, 'no'),
1174 ph('option', {value: 'null', selected: publicWebHosting == null}, '…'),
1175 ])
1176 ]),
1177 self.phMsgActions(content),
1178 ]),
1179 content ? self.phPreview(content, {raw: data.preview_raw}) : ''
1180 ]),
1181 self.wrapPage('about self: ' + id),
1182 self.respondSink(200, {
1183 'Content-Type': ctype('html')
1184 })
1185 )
1186 })
1187}
1188
1189Serve.prototype.block = function (path) {
1190 var self = this
1191 var data = self.data
1192 var id = String(path).substr(1)
1193 try { id = decodeURIComponent(id) }
1194 catch(e) {}
1195
1196 var content
1197 if (data.preview || data.preview_raw) {
1198 content = {
1199 type: 'contact',
1200 contact: id,
1201 blocking: true
1202 }
1203 var reason = typeof data.reason === 'string' ? data.reason : null
1204 if (reason) content.reason = reason
1205 }
1206
1207 function renderDraftLink(draftId) {
1208 return pull.values([
1209 ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)),
1210 title: 'draft link'}, u.escapeHTML(draftId)),
1211 ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ',
1212 ])
1213 }
1214
1215 pull(
1216 ph('section', [
1217 ph('h2', ['Block ', self.phIdLink(id)]),
1218 ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
1219 'Reason: ', ph('input', {name: 'reason', value: reason || '',
1220 className: 'wide',
1221 placeholder: 'spam, abuse, etc.'}),
1222 self.phMsgActions(content),
1223 ]),
1224 content ? self.phPreview(content, {raw: data.preview_raw}) : ''
1225 ]),
1226 self.wrapPage('Block ' + id),
1227 self.respondSink(200)
1228 )
1229}
1230
1231Serve.prototype.type = function (path) {
1232 var q = this.query
1233 var type = decodeURIComponent(path.substr(1))
1234 var opts = {
1235 reverse: !q.forwards,
1236 lt: Number(q.lt) || Date.now(),
1237 gt: Number(q.gt) || -Infinity,
1238 type: type,
1239 filter: q.filter,
1240 }
1241
1242 pull(
1243 this.app.sbotMessagesByType(opts),
1244 this.renderThreadPaginated(opts, null, q),
1245 this.wrapMessages(),
1246 this.wrapType(type),
1247 this.wrapPage('type: ' + type),
1248 this.respondSink(200, {
1249 'Content-Type': ctype('html')
1250 })
1251 )
1252}
1253
1254Serve.prototype.links = function (path) {
1255 var q = this.query
1256 var dest = path.substr(1)
1257 var opts = {
1258 dest: dest,
1259 reverse: true,
1260 values: true,
1261 }
1262 if (q.rel) opts.rel = q.rel
1263
1264 pull(
1265 this.app.sbot.links(opts),
1266 this.renderThread(),
1267 this.wrapMessages(),
1268 this.wrapLinks(dest),
1269 this.wrapPage('links: ' + dest),
1270 this.respondSink(200, {
1271 'Content-Type': ctype('html')
1272 })
1273 )
1274}
1275
1276Serve.prototype.rawId = function (id) {
1277 var self = this
1278
1279 self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
1280 if (err) return pull(
1281 pull.once(u.renderError(err).outerHTML),
1282 self.respondSink(400, {'Content-Type': ctype('html')})
1283 )
1284 return pull(
1285 pull.once(msg),
1286 self.renderRawMsgPage(id),
1287 self.respondSink(200, {
1288 'Content-Type': ctype('html'),
1289 })
1290 )
1291 })
1292}
1293
1294Serve.prototype.channel = function (path) {
1295 var channel = decodeURIComponent(String(path).substr(1))
1296 var q = this.query
1297 var gt = Number(q.gt) || -Infinity
1298 var lt = Number(q.lt) || Date.now()
1299 var opts = {
1300 reverse: !q.forwards,
1301 lt: lt,
1302 gt: gt,
1303 channel: channel,
1304 filter: q.filter,
1305 }
1306
1307 pull(
1308 this.app.streamChannel(opts),
1309 this.renderThreadPaginated(opts, null, q),
1310 this.wrapMessages(),
1311 this.wrapChannel(channel),
1312 this.wrapPage('#' + channel),
1313 this.respondSink(200, {
1314 'Content-Type': ctype('html')
1315 })
1316 )
1317}
1318
1319function threadHeads(msgs, rootId, opts) {
1320 var includeVotes = opts && opts.includeVotes
1321 return sort.heads(msgs.filter(function (msg) {
1322 var c = msg.value && msg.value.content
1323 return (c && (
1324 c.type === 'web-root' ? c.site === rootId :
1325 c.type === 'talenet-idea-comment_reply' ? c.ideaKey === rootId :
1326 c.type === 'vote' ? includeVotes :
1327 c.root === rootId))
1328 || msg.key === rootId
1329 }))
1330}
1331
1332Serve.prototype.streamThreadWithComposer = function (opts) {
1333 var self = this
1334 var id = opts.root
1335 var threadHeadsOpts = {includeVotes: self.app.voteBranches}
1336 return ph('table', {class: 'ssb-msgs'}, u.readNext(next))
1337 function next(cb) {
1338 self.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) {
1339 if (err && err.name === 'NotFoundError') err = null, rootMsg = {
1340 key: id, value: {content: false}}
1341 if (err) return cb(new Error(err.stack || err))
1342 if (!rootMsg) {
1343 console.log('id', id, 'opts', opts)
1344 }
1345 var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
1346 var recps = rootContent && rootContent.recps
1347 || ((rootMsg.value.private || typeof rootMsg.value.content === 'string')
1348 ? [rootMsg.value.author, self.app.sbot.id].filter(uniques())
1349 : undefined)
1350 var threadRootId = rootContent && (
1351 rootContent.type === 'web-root' ? rootContent.site : rootContent.root
1352 ) || id
1353 var channel = opts.channel
1354
1355 pull(
1356 self.app.getThread(rootMsg),
1357 pull.unique('key'),
1358 self.app.unboxMessages(),
1359 pull.through(function (msg) {
1360 var c = msg && msg.value.content
1361 if (!channel && c && c.channel) channel = c.channel
1362 }),
1363 pull.collect(function (err, links) {
1364 if (err) return gotLinks(err)
1365 if (!self.useOoo) return gotLinks(null, links)
1366 self.app.expandOoo({msgs: links, dest: id}, gotLinks)
1367 })
1368 )
1369 function gotLinks(err, links) {
1370 if (err) return cb(new Error(err.stack))
1371 var branches = threadHeads(links, threadRootId, threadHeadsOpts)
1372 cb(null, pull(
1373 pull.values(sort(links)),
1374 self.app.voteBranches && pull.map(function (link) {
1375 var o = {}
1376 for (var k in link) o[k] = link[k]
1377 o.threadBranches = branches
1378 o.threadRoot = threadRootId
1379 return o
1380 }),
1381 self.renderThread({
1382 msgId: id,
1383 branches: branches,
1384 }),
1385 self.wrapMessages(),
1386 self.wrapThread({
1387 recps: recps,
1388 root: threadRootId,
1389 post: id,
1390 branches: branches,
1391 postBranches: threadRootId !== id && threadHeads(links, id, threadHeadsOpts),
1392 placeholder: opts.placeholder,
1393 channel: channel,
1394 })
1395 ))
1396 }
1397 })
1398 }
1399}
1400
1401Serve.prototype.id = function (id, path) {
1402 var self = this
1403 if (self.query.raw != null) return self.rawId(id)
1404 pull(
1405 self.streamThreadWithComposer({root: id}),
1406 self.wrapPage(id),
1407 self.respondSink(200)
1408 )
1409}
1410
1411Serve.prototype.userFeed = function (id, path) {
1412 var self = this
1413 var q = self.query
1414 var opts = {
1415 id: id,
1416 reverse: !q.forwards,
1417 lt: Number(q.lt) || Date.now(),
1418 gt: Number(q.gt) || -Infinity,
1419 feed: id,
1420 filter: q.filter,
1421 }
1422 var isScrolled = q.lt || q.gt
1423
1424 self.app.getAbout(id, function (err, about) {
1425 if (err) self.app.error(err)
1426 pull(
1427 self.app.sbotCreateUserStream(opts),
1428 self.renderThreadPaginated(opts, id, q),
1429 self.wrapMessages(),
1430 self.wrapUserFeed(isScrolled, id),
1431 self.wrapPage(about && about.name || id),
1432 self.respondSink(200)
1433 )
1434 })
1435}
1436
1437Serve.prototype.file = function (file) {
1438 var self = this
1439 fs.stat(file, function (err, stat) {
1440 if (err && err.code === 'ENOENT') return self.respond(404, 'Not found')
1441 if (err) return self.respond(500, err.stack || err)
1442 if (!stat.isFile()) return self.respond(403, 'May only load files')
1443 if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified')
1444 self.res.writeHead(200, {
1445 'Content-Type': ctype(file),
1446 'Content-Length': stat.size,
1447 'Last-Modified': stat.mtime.toGMTString()
1448 })
1449 fs.createReadStream(file).pipe(self.res)
1450 })
1451}
1452
1453Serve.prototype.static = function (file) {
1454 this.file(path.join(__dirname, '../static', file))
1455}
1456
1457Serve.prototype.emoji = function (emoji) {
1458 serveEmoji(this.req, this.res, emoji)
1459}
1460
1461Serve.prototype.highlight = function (dirs) {
1462 this.file(path.join(hlCssDir, dirs))
1463}
1464
1465Serve.prototype.blob = function (id, path) {
1466 var self = this
1467 var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+')
1468 var etag = id + (path || '') + (unbox || '')
1469 if (self.req.headers['if-none-match'] === etag) return self.respond(304)
1470 var key
1471 if (path) {
1472 try { path = decodeURIComponent(path) } catch(e) {}
1473 if (path[0] === '#') {
1474 unbox = path.substr(1)
1475 } else {
1476 return self.respond(400, 'Bad blob request')
1477 }
1478 }
1479 if (unbox) {
1480 try {
1481 key = new Buffer(unbox, 'base64')
1482 } catch(err) {
1483 return self.respond(400, err.message)
1484 }
1485 if (key.length !== 32) {
1486 return self.respond(400, 'Bad blob key')
1487 }
1488 }
1489 self.app.wantSizeBlob(id, function (err, size) {
1490 if (err) {
1491 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
1492 else return self.respond(500, err.message || err)
1493 }
1494 pull(
1495 self.app.getBlob(id, key),
1496 pull.map(Buffer),
1497 ident(gotType),
1498 self.respondSink()
1499 )
1500 function gotType(type) {
1501 type = type && mime.lookup(type)
1502 if (type) self.res.setHeader('Content-Type', type)
1503 // don't serve size for encrypted blob, because it refers to the size of
1504 // the ciphertext
1505 if (typeof size === 'number' && !key)
1506 self.res.setHeader('Content-Length', size)
1507 if (self.query.name) self.res.setHeader('Content-Disposition',
1508 'inline; filename='+encodeDispositionFilename(self.query.name))
1509 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
1510 self.res.setHeader('etag', etag)
1511 self.res.writeHead(200)
1512 }
1513 })
1514}
1515
1516Serve.prototype.image = function (path) {
1517 var self = this
1518 var id, key
1519 var m = urlIdRegex.exec(path)
1520 if (m && m[2] === '&') id = m[1], path = m[3]
1521 var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+')
1522 var etag = 'image-' + id + (path || '') + (unbox || '')
1523 if (self.req.headers['if-none-match'] === etag) return self.respond(304)
1524 if (path) {
1525 try { path = decodeURIComponent(path) } catch(e) {}
1526 if (path[0] === '#') {
1527 unbox = path.substr(1)
1528 } else {
1529 return self.respond(400, 'Bad blob request')
1530 }
1531 }
1532 if (unbox) {
1533 try {
1534 key = new Buffer(unbox, 'base64')
1535 } catch(err) {
1536 return self.respond(400, err.message)
1537 }
1538 if (key.length !== 32) {
1539 return self.respond(400, 'Bad blob key')
1540 }
1541 }
1542 self.app.wantSizeBlob(id, function (err, size) {
1543 if (err) {
1544 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
1545 else return self.respond(500, err.message || err)
1546 }
1547
1548 var done = multicb({pluck: 1, spread: true})
1549 var heresTheData = done()
1550 var heresTheType = done().bind(self, null)
1551
1552 pull(
1553 self.app.getBlob(id, key),
1554 pull.map(Buffer),
1555 ident(heresTheType),
1556 pull.collect(onFullBuffer)
1557 )
1558
1559 function onFullBuffer (err, buffer) {
1560 if (err) return heresTheData(err)
1561 buffer = Buffer.concat(buffer)
1562
1563 try {
1564 jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
1565 if (!err) buffer = rotatedBuffer
1566
1567 heresTheData(null, buffer)
1568 pull(
1569 pull.once(buffer),
1570 self.respondSink()
1571 )
1572 })
1573 } catch (err) {
1574 console.trace(err)
1575 self.respond(500, err.message || err)
1576 }
1577 }
1578
1579 done(function (err, data, type) {
1580 if (err) {
1581 console.trace(err)
1582 self.respond(500, err.message || err)
1583 return
1584 }
1585 type = type && mime.lookup(type)
1586 if (type) self.res.setHeader('Content-Type', type)
1587 self.res.setHeader('Content-Length', data.length)
1588 if (self.query.name) self.res.setHeader('Content-Disposition',
1589 'inline; filename='+encodeDispositionFilename(self.query.name))
1590 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
1591 self.res.setHeader('etag', etag)
1592 self.res.writeHead(200)
1593 })
1594 })
1595}
1596
1597Serve.prototype.ifModified = function (lastMod) {
1598 var ifModSince = this.req.headers['if-modified-since']
1599 if (!ifModSince) return false
1600 var d = new Date(ifModSince)
1601 return d && Math.floor(d/1000) >= Math.floor(lastMod/1000)
1602}
1603
1604Serve.prototype.wrapMessages = function () {
1605 return u.hyperwrap(function (content, cb) {
1606 cb(null, h('table.ssb-msgs', content))
1607 })
1608}
1609
1610Serve.prototype.renderThread = function (opts) {
1611 return pull(
1612 this.app.render.renderFeeds({
1613 raw: false,
1614 full: this.query.full != null,
1615 feed: opts && opts.feed,
1616 msgId: opts && opts.msgId,
1617 filter: this.query.filter,
1618 limit: Number(this.query.limit),
1619 serve: this,
1620 branches: opts && opts.branches,
1621 }),
1622 pull.map(u.toHTML)
1623 )
1624}
1625
1626Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
1627 var self = this
1628 function linkA(opts, name) {
1629 var q1 = u.mergeOpts(q, opts)
1630 return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit)
1631 }
1632 function links(opts) {
1633 var limit = opts.limit || q.limit || 10
1634 return h('tr', h('td.paginate', {colspan: 3},
1635 opts.forwards ? '↑ newer ' : '↓ older ',
1636 linkA(u.mergeOpts(opts, {limit: 1})), ' ',
1637 linkA(u.mergeOpts(opts, {limit: 10})), ' ',
1638 linkA(u.mergeOpts(opts, {limit: 100}))
1639 ))
1640 }
1641
1642 return pull(
1643 self.app.filterMessages({
1644 feed: opts && opts.feed,
1645 msgId: opts && opts.msgId,
1646 filter: this.query.filter,
1647 limit: Number(this.query.limit) || 12,
1648 }),
1649 paginate(
1650 function onFirst(msg, cb) {
1651 var num = feedId ? msg.value.sequence :
1652 opts.sortByTimestamp ? msg.value.timestamp :
1653 msg.timestamp || msg.ts
1654 if (q.forwards) {
1655 cb(null, links({
1656 lt: num,
1657 gt: null,
1658 forwards: null,
1659 filter: opts.filter,
1660 }))
1661 } else {
1662 cb(null, links({
1663 lt: null,
1664 gt: num,
1665 forwards: 1,
1666 filter: opts.filter,
1667 }))
1668 }
1669 },
1670 this.app.render.renderFeeds({
1671 raw: false,
1672 full: this.query.full != null,
1673 feed: opts && opts.feed,
1674 msgId: opts && opts.msgId,
1675 filter: this.query.filter,
1676 serve: this,
1677 limit: Number(this.query.limit) || 12,
1678 }),
1679 function onLast(msg, cb) {
1680 var num = feedId ? msg.value.sequence :
1681 opts.sortByTimestamp ? msg.value.timestamp :
1682 msg.timestamp || msg.ts
1683 if (q.forwards) {
1684 cb(null, links({
1685 lt: null,
1686 gt: num,
1687 forwards: 1,
1688 filter: opts.filter,
1689 }))
1690 } else {
1691 cb(null, links({
1692 lt: num,
1693 gt: null,
1694 forwards: null,
1695 filter: opts.filter,
1696 }))
1697 }
1698 },
1699 function onEmpty(cb) {
1700 if (q.forwards) {
1701 cb(null, links({
1702 gt: null,
1703 lt: opts.gt + 1,
1704 forwards: null,
1705 filter: opts.filter,
1706 }))
1707 } else {
1708 cb(null, links({
1709 gt: opts.lt - 1,
1710 lt: null,
1711 forwards: 1,
1712 filter: opts.filter,
1713 }))
1714 }
1715 }
1716 ),
1717 pull.map(u.toHTML)
1718 )
1719}
1720
1721Serve.prototype.renderRawMsgPage = function (id) {
1722 var showMarkdownSource = (this.query.raw === 'md')
1723 var raw = !showMarkdownSource
1724 return pull(
1725 this.app.render.renderFeeds({
1726 raw: raw,
1727 msgId: id,
1728 filter: this.query.filter,
1729 serve: this,
1730 markdownSource: showMarkdownSource
1731 }),
1732 pull.map(u.toHTML),
1733 this.wrapMessages(),
1734 this.wrapPage(id)
1735 )
1736}
1737
1738function catchHTMLError() {
1739 return function (read) {
1740 var ended
1741 return function (abort, cb) {
1742 if (ended) return cb(ended)
1743 read(abort, function (end, data) {
1744 if (!end || end === true) {
1745 try { return cb(end, data) }
1746 catch(e) { return console.trace(e) }
1747 }
1748 ended = true
1749 cb(null, u.renderError(end).outerHTML)
1750 })
1751 }
1752 }
1753}
1754
1755function catchTextError() {
1756 return function (read) {
1757 var ended
1758 return function (abort, cb) {
1759 if (ended) return cb(ended)
1760 read(abort, function (end, data) {
1761 if (!end || end === true) return cb(end, data)
1762 ended = true
1763 cb(null, end.stack + '\n')
1764 })
1765 }
1766 }
1767}
1768
1769function styles() {
1770 return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8')
1771}
1772
1773Serve.prototype.appendFooter = function () {
1774 var self = this
1775 return function (read) {
1776 return cat([read, u.readNext(function (cb) {
1777 var ms = new Date() - self.startDate
1778 cb(null, pull.once(h('footer',
1779 h('a', {href: pkg.homepage}, pkg.name), ' · ',
1780 ms/1000 + 's'
1781 ).outerHTML))
1782 })])
1783 }
1784}
1785
1786Serve.prototype.wrapPage = function (title, searchQ) {
1787 var self = this
1788 var render = self.app.render
1789 return pull(
1790 catchHTMLError(),
1791 self.appendFooter(),
1792 u.hyperwrap(function (content, cb) {
1793 var done = multicb({pluck: 1, spread: true})
1794 done()(null, h('html', h('head',
1795 h('meta', {charset: 'utf-8'}),
1796 h('meta', {name: 'referrer', content: 'no-referrer'}),
1797 h('title', title),
1798 h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}),
1799 h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}),
1800 h('style', styles()),
1801 h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')})
1802 ),
1803 h('body',
1804 h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'},
1805 self.app.navLinks.map(function (link, i) {
1806 return [i == 0 ? '' : ' ',
1807 link === 'self' ? render.idLink(self.app.sbot.id, done()) :
1808 link === 'searchbox' ? h('input.search-input',
1809 {name: 'q', value: searchQ, placeholder: 'search'}) :
1810 link === 'search' ? h('a', {href: render.toUrl('/advsearch')}, 'search') :
1811 typeof link === 'string' ? h('a', {href: render.toUrl('/' + link)}, link) :
1812 link ? h('a', {href: render.toUrl(link.url)}, link.name) : ''
1813 ]
1814 })
1815 )),
1816 self.query.published ? h('div',
1817 'published ',
1818 render.msgIdLink(self.query.published, done())
1819 ) : '',
1820 // self.note,
1821 content
1822 )))
1823 done(cb)
1824 })
1825 )
1826}
1827
1828Serve.prototype.phIdLink = function (id, opts) {
1829 return pull(
1830 pull.once(id),
1831 this.renderIdsList(opts)
1832 )
1833}
1834
1835Serve.prototype.phIdAvatar = function (id) {
1836 var self = this
1837 return u.readNext(function (cb) {
1838 var el = self.app.render.avatarImage(id, function (err) {
1839 if (err) return cb(err)
1840 cb(null, pull.once(u.toHTML(el)))
1841 })
1842 })
1843}
1844
1845Serve.prototype.friends = function (path) {
1846 var self = this
1847 var friends = self.app.sbot.friends
1848 if (!friends) return pull(
1849 pull.once('missing ssb-friends plugin'),
1850 this.wrapPage('friends'),
1851 self.respondSink(400)
1852 )
1853 if (!friends.createFriendStream) return pull(
1854 pull.once('missing friends.createFriendStream method'),
1855 this.wrapPage('friends'),
1856 self.respondSink(400)
1857 )
1858
1859 pull(
1860 friends.createFriendStream({hops: 1}),
1861 self.renderIdsList(),
1862 u.hyperwrap(function (items, cb) {
1863 cb(null, [
1864 h('section',
1865 h('h3', 'Friends')
1866 ),
1867 h('section', items)
1868 ])
1869 }),
1870 this.wrapPage('friends'),
1871 this.respondSink(200, {
1872 'Content-Type': ctype('html')
1873 })
1874 )
1875}
1876
1877Serve.prototype.renderIdsList = function (opts) {
1878 var self = this
1879 return pull(
1880 paramap(function (id, cb) {
1881 self.app.render.getNameLink(id, opts, cb)
1882 }, 8),
1883 pull.map(function (el) {
1884 return [el, ' ']
1885 }),
1886 pull.map(u.toHTML)
1887 )
1888}
1889
1890Serve.prototype.aboutDescription = function (id) {
1891 var self = this
1892 return u.readNext(function (cb) {
1893 self.app.getAbout(id, function (err, about) {
1894 if (err) return cb(err)
1895 if (!about.description) return cb(null, pull.empty())
1896 cb(null, ph('div', self.app.render.markdown(about.description)))
1897 })
1898 })
1899}
1900
1901Serve.prototype.followInfo = function (id, myId) {
1902 var self = this
1903 return u.readNext(function (cb) {
1904 var done = multicb({pluck: 1, spread: true})
1905 self.app.getContact(myId, id, done())
1906 self.app.getContact(id, myId, done())
1907 self.app.isMuted(id, done())
1908 done(function (err, contactToThem, contactFromThem, isMuted) {
1909 if (err) return cb(err)
1910 cb(null, ph('form', {action: '', method: 'post'}, [
1911 contactFromThem ? contactToThem ? 'friend ' : 'follows you ' :
1912 contactFromThem === false ? 'blocks you ' : '',
1913 ph('input', {type: 'hidden', name: 'action', value: 'contact'}),
1914 ph('input', {type: 'hidden', name: 'contact', value: id}),
1915 ph('input', {type: 'submit',
1916 name: contactToThem ? 'unfollow' : 'follow',
1917 value: contactToThem ? 'unfollow' : 'follow'}), ' ',
1918 contactToThem === false
1919 ? ph('input', {type: 'submit', name: 'unblock', value: 'unblock'})
1920 : ph('input', {type: 'submit', name: 'block1', value: 'block…'}), ' ',
1921 ph('input', {type: 'submit',
1922 name: isMuted ? 'unmute' : 'mute',
1923 value: isMuted ? 'unmute' : 'mute',
1924 title: isMuted ? 'unmute (private unblock)' : 'mute (private block)'}), ' ',
1925 ph('input', {type: 'submit',
1926 name: self.requestedReplicate ? 'unreplicate' : 'replicate',
1927 value: self.requestedReplicate ? 'unreplicate' : 'replicate',
1928 title: self.requestedReplicate
1929 ? 'Temporarily cancel replicating this feed'
1930 : 'Temporarily replicate this feed'})
1931 ]))
1932 })
1933 })
1934}
1935
1936Serve.prototype.friendInfo = function (id, myId) {
1937 var first = false
1938 return pull(
1939 this.app.contacts.createFollowedFollowersStream(myId, id),
1940 this.app.render.friendsList(),
1941 pull.map(function (html) {
1942 if (!first) {
1943 first = true
1944 return 'followed by your friends: ' + html
1945 }
1946 return html
1947 })
1948 )
1949}
1950
1951Serve.prototype.wrapUserFeed = function (isScrolled, id) {
1952 var self = this
1953 var myId = self.app.sbot.id
1954 var render = self.app.render
1955 return function (thread) {
1956 return cat([
1957 ph('section', {class: 'ssb-feed'}, ph('table', [
1958 isScrolled ? '' : ph('tr', [
1959 ph('td', self.phIdAvatar(id)),
1960 ph('td', {class: 'feed-about'}, [
1961 ph('h3', {class: 'feed-name'},
1962 ph('strong', self.phIdLink(id))),
1963 ph('code', ph('small', id)),
1964 self.aboutDescription(id)
1965 ])
1966 ]),
1967 ph('tr', [
1968 ph('td'),
1969 ph('td', [
1970 ph('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ',
1971 ph('a', {href: render.toUrl('/about/' + id)}, 'about'),
1972 id === myId ? [' ',
1973 ph('a', {href: render.toUrl('/about-self')}, 'about-self')] : '',
1974 !isScrolled ? u.readNext(function (cb) {
1975 self.app.isPub(id, function (err, isPub) {
1976 if (err) return cb(err)
1977 if (!isPub) return cb(null, pull.empty())
1978 cb(null, ph('span', [' ', ph('a', {href: render.toUrl('/pub/' + id)}, 'pub')]))
1979 })
1980 }) : ''
1981 ])
1982 ]),
1983 ph('tr', [
1984 ph('td'),
1985 ph('td',
1986 ph('form', {action: render.toUrl('/advsearch'), method: 'get'}, [
1987 ph('input', {type: 'hidden', name: 'source', value: id}),
1988 ph('input', {type: 'text', name: 'text', placeholder: 'text'}),
1989 ph('input', {type: 'submit', value: 'search'})
1990 ])
1991 )
1992 ]),
1993 isScrolled || id === myId ? '' : [
1994 ph('tr', [
1995 ph('td'),
1996 ph('td', {class: 'follow-info'}, self.followInfo(id, myId))
1997 /*
1998 ]),
1999 ph('tr', [
2000 ph('td'),
2001 ph('td', self.friendInfo(id, myId))
2002 */
2003 ])
2004 ]
2005 ])),
2006 thread
2007 ])
2008 }
2009}
2010
2011Serve.prototype.git = function (url) {
2012 var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url)
2013 switch (m[1]) {
2014 case 'object': return this.gitObject(m[2])
2015 case 'commit': return this.gitCommit(m[2])
2016 case 'tag': return this.gitTag(m[2])
2017 case 'tree': return this.gitTree(m[2])
2018 case 'blob': return this.gitBlob(m[2])
2019 case 'raw': return this.gitRaw(m[2])
2020 case 'diff': return this.gitDiff(m[2])
2021 case 'signature': return this.gitSignature(m[2])
2022 case 'line-comment': return this.gitLineComment(m[2])
2023 default: return this.respond(404, 'Not found')
2024 }
2025}
2026
2027Serve.prototype.gitRaw = function (rev) {
2028 var self = this
2029 if (!/[0-9a-f]{24}/.test(rev)) {
2030 return pull(
2031 pull.once('\'' + rev + '\' is not a git object id'),
2032 self.respondSink(400, {'Content-Type': 'text/plain'})
2033 )
2034 }
2035 if (!u.isRef(self.query.msg)) return pull(
2036 ph('div.error', 'missing message id'),
2037 self.wrapPage('git object ' + rev),
2038 self.respondSink(400)
2039 )
2040
2041 self.app.git.openObject({
2042 obj: rev,
2043 msg: self.query.msg,
2044 }, function (err, obj) {
2045 if (err && err.name === 'BlobNotFoundError')
2046 return self.askWantBlobs(err.links)
2047 if (err) return pull(
2048 pull.once(err.stack),
2049 self.respondSink(400, {'Content-Type': 'text/plain'})
2050 )
2051 pull(
2052 self.app.git.readObject(obj),
2053 catchTextError(),
2054 ident(function (type) {
2055 type = type && mime.lookup(type)
2056 if (type) self.res.setHeader('Content-Type', type)
2057 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
2058 self.res.setHeader('etag', rev)
2059 self.res.writeHead(200)
2060 }),
2061 self.respondSink()
2062 )
2063 })
2064}
2065
2066Serve.prototype.gitAuthorLink = function (author) {
2067 if (author.feed) {
2068 var myName = this.app.getNameSync(author.feed)
2069 var sigil = author.name === author.localpart ? '@' : ''
2070 return ph('a', {
2071 href: this.app.render.toUrl(author.feed),
2072 title: author.localpart + (myName ? ' (' + myName + ')' : '')
2073 }, u.escapeHTML(sigil + author.name))
2074 } else {
2075 return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)},
2076 u.escapeHTML(author.name))
2077 }
2078}
2079
2080Serve.prototype.gitObject = function (rev) {
2081 var self = this
2082 if (!/[0-9a-f]{24}/.test(rev)) {
2083 return pull(
2084 ph('div.error', 'rev is not a git object id'),
2085 self.wrapPage('git'),
2086 self.respondSink(400)
2087 )
2088 }
2089 if (!u.isRef(self.query.msg)) return pull(
2090 ph('div.error', 'missing message id'),
2091 self.wrapPage('git object ' + rev),
2092 self.respondSink(400)
2093 )
2094
2095 if (self.query.search) {
2096 return self.app.git.getObjectMsg({
2097 obj: rev,
2098 headMsgId: self.query.msg,
2099 }, function (err, msg) {
2100 if (err && err.name === 'BlobNotFoundError')
2101 return self.askWantBlobs(err.links)
2102 if (err) return pull(
2103 pull.once(u.renderError(err).outerHTML),
2104 self.wrapPage('git object ' + rev),
2105 self.respondSink(400)
2106 )
2107 var path = '/git/object/' + rev
2108 + '?msg=' + encodeURIComponent(msg.key)
2109 return self.redirect(self.app.render.toUrl(path))
2110 })
2111 }
2112
2113 self.app.git.openObject({
2114 obj: rev,
2115 msg: self.query.msg,
2116 }, function (err, obj) {
2117 if (err && err.name === 'BlobNotFoundError')
2118 return self.askWantBlobs(err.links)
2119 if (err) return pull(
2120 pull.once(u.renderError(err).outerHTML),
2121 self.wrapPage('git object ' + rev),
2122 self.respondSink(400)
2123 )
2124 self.app.git.statObject(obj, function (err, stat) {
2125 if (err) return pull(
2126 pull.once(u.renderError(err).outerHTML),
2127 self.wrapPage('git object ' + rev),
2128 self.respondSink(400)
2129 )
2130 var path = '/git/' + stat.type + '/' + rev
2131 + '?msg=' + encodeURIComponent(self.query.msg)
2132 return self.redirect(self.app.render.toUrl(path))
2133 })
2134 })
2135}
2136
2137Serve.prototype.gitSignature = function (id) {
2138 var self = this
2139 if (!/[0-9a-f]{24}/.test(id)) {
2140 return pull(
2141 ph('div.error', 'not a git object id'),
2142 self.wrapPage('git'),
2143 self.respondSink(400)
2144 )
2145 }
2146 if (!u.isRef(self.query.msg)) return pull(
2147 ph('div.error', 'missing message id'),
2148 self.wrapPage('git signature for ' + id),
2149 self.respondSink(400)
2150 )
2151
2152 self.app.git.openObject({
2153 obj: id,
2154 msg: self.query.msg,
2155 type: self.query.type,
2156 }, function (err, obj) {
2157 if (err) return handleError(err)
2158 var msgDate = new Date(obj.msg.value.timestamp)
2159 self.app.verifyGitObjectSignature(obj, function (err, verification) {
2160 if (err) return handleError(err)
2161 var objPath = '/git/object/' + id + '?msg=' + encodeURIComponent(obj.msg.key)
2162 pull(
2163 ph('section', [
2164 ph('h3', [
2165 ph('a', {href: self.app.render.toUrl(objPath)}, id), ': ',
2166 ph('a', {href: ''}, 'signature')
2167 ]),
2168 ph('div', [
2169 self.phIdLink(obj.msg.value.author), ' pushed ',
2170 ph('a', {
2171 href: self.app.render.toUrl(obj.msg.key),
2172 title: msgDate.toLocaleString(),
2173 }, htime(msgDate))
2174 ]),
2175 ph('pre', u.escapeHTML(verification.output))
2176 /*
2177 verification.goodsig ? 'good' : 'bad',
2178 ph('pre', u.escapeHTML(verification.status))
2179 */
2180 ]),
2181 self.wrapPage('git signature for ' + id),
2182 self.respondSink(200)
2183 )
2184 })
2185 })
2186
2187 function handleError(err) {
2188 if (err && err.name === 'BlobNotFoundError')
2189 return self.askWantBlobs(err.links)
2190 if (err) return pull(
2191 pull.once(u.renderError(err).outerHTML),
2192 self.wrapPage('git signature for ' + id),
2193 self.respondSink(400)
2194 )
2195 }
2196}
2197
2198Serve.prototype.gitCommit = function (rev) {
2199 var self = this
2200 if (!/[0-9a-f]{24}/.test(rev)) {
2201 return pull(
2202 ph('div.error', 'rev is not a git object id'),
2203 self.wrapPage('git'),
2204 self.respondSink(400)
2205 )
2206 }
2207 if (!u.isRef(self.query.msg)) return pull(
2208 ph('div.error', 'missing message id'),
2209 self.wrapPage('git commit ' + rev),
2210 self.respondSink(400)
2211 )
2212
2213 if (self.query.search) {
2214 return self.app.git.getObjectMsg({
2215 obj: rev,
2216 headMsgId: self.query.msg,
2217 }, function (err, msg) {
2218 if (err && err.name === 'BlobNotFoundError')
2219 return self.askWantBlobs(err.links)
2220 if (err) return pull(
2221 pull.once(u.renderError(err).outerHTML),
2222 self.wrapPage('git commit ' + rev),
2223 self.respondSink(400)
2224 )
2225 var path = '/git/commit/' + rev
2226 + '?msg=' + encodeURIComponent(msg.key)
2227 return self.redirect(self.app.render.toUrl(path))
2228 })
2229 }
2230
2231 self.app.git.openObject({
2232 obj: rev,
2233 msg: self.query.msg,
2234 type: 'commit',
2235 }, function (err, obj) {
2236 if (err && err.name === 'BlobNotFoundError')
2237 return self.askWantBlobs(err.links)
2238 if (err) return pull(
2239 pull.once(u.renderError(err).outerHTML),
2240 self.wrapPage('git commit ' + rev),
2241 self.respondSink(400)
2242 )
2243 var msgDate = new Date(obj.msg.value.timestamp)
2244 self.app.git.getCommit(obj, function (err, commit) {
2245 var missingBlobs
2246 if (err && err.name === 'BlobNotFoundError')
2247 missingBlobs = err.links, err = null
2248 if (err) return pull(
2249 pull.once(u.renderError(err).outerHTML),
2250 self.wrapPage('git commit ' + rev),
2251 self.respondSink(400)
2252 )
2253 pull(
2254 ph('section', [
2255 ph('h3', ph('a', {href: ''}, rev)),
2256 ph('div', [
2257 self.phIdLink(obj.msg.value.author), ' pushed ',
2258 ph('a', {
2259 href: self.app.render.toUrl(obj.msg.key),
2260 title: msgDate.toLocaleString(),
2261 }, htime(msgDate))
2262 ]),
2263 missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
2264 ph('div', [
2265 self.gitAuthorLink(commit.committer),
2266 ' committed ',
2267 ph('span', {title: commit.committer.date.toLocaleString()},
2268 htime(commit.committer.date)),
2269 ' in ', commit.committer.tz
2270 ]),
2271 commit.author ? ph('div', [
2272 self.gitAuthorLink(commit.author),
2273 ' authored ',
2274 ph('span', {title: commit.author.date.toLocaleString()},
2275 htime(commit.author.date)),
2276 ' in ', commit.author.tz
2277 ]) : '',
2278 commit.parents.length ? ph('div', ['parents: ', pull(
2279 pull.values(commit.parents),
2280 self.gitObjectLinks(obj.msg.key, 'commit')
2281 )]) : '',
2282 commit.tree ? ph('div', ['tree: ', pull(
2283 pull.once(commit.tree),
2284 self.gitObjectLinks(obj.msg.key, 'tree')
2285 )]) : '',
2286 commit.gpgsig ? ph('div', [
2287 ph('a', {href: self.app.render.toUrl(
2288 '/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg)
2289 )}, 'signature'),
2290 commit.signatureVersion ? [' from ', ph('code', u.escapeHTML(commit.signatureVersion))] : ''
2291 ]) : '',
2292 h('blockquote',
2293 self.app.render.gitCommitBody(commit.body)).outerHTML,
2294 ph('h4', 'files'),
2295 ph('table', pull(
2296 self.app.git.readCommitChanges(commit),
2297 pull.map(function (file) {
2298 var msg = file.msg || obj.msg
2299 return ph('tr', [
2300 ph('td', ph('code', u.escapeHTML(file.name))),
2301 ph('td', file.deleted ? 'deleted'
2302 : file.created ?
2303 ph('a', {href:
2304 self.app.render.toUrl('/git/blob/'
2305 + (file.hash[1] || file.hash[0])
2306 + '?msg=' + encodeURIComponent(msg.key))
2307 + '&commit=' + rev
2308 + '&path=' + encodeURIComponent(file.name)
2309 }, 'created')
2310 : file.hash ?
2311 ph('a', {href:
2312 self.app.render.toUrl('/git/diff/'
2313 + file.hash[0] + '..' + file.hash[1]
2314 + '?msg=' + encodeURIComponent(msg.key))
2315 + '&commit=' + rev
2316 + '&path=' + encodeURIComponent(file.name)
2317 }, 'changed')
2318 : file.mode ? 'mode changed'
2319 : JSON.stringify(file))
2320 ])
2321 }),
2322 Catch(function (err) {
2323 if (err && err.name === 'ObjectNotFoundError') return
2324 if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
2325 return false
2326 })
2327 ))
2328 ]
2329 ]),
2330 self.wrapPage('git commit ' + rev),
2331 self.respondSink(missingBlobs ? 409 : 200)
2332 )
2333 })
2334 })
2335}
2336
2337Serve.prototype.gitTag = function (rev) {
2338 var self = this
2339 if (!/[0-9a-f]{24}/.test(rev)) {
2340 return pull(
2341 ph('div.error', 'rev is not a git object id'),
2342 self.wrapPage('git'),
2343 self.respondSink(400)
2344 )
2345 }
2346 if (!u.isRef(self.query.msg)) return pull(
2347 ph('div.error', 'missing message id'),
2348 self.wrapPage('git tag ' + rev),
2349 self.respondSink(400)
2350 )
2351
2352 if (self.query.search) {
2353 return self.app.git.getObjectMsg({
2354 obj: rev,
2355 headMsgId: self.query.msg,
2356 }, function (err, msg) {
2357 if (err && err.name === 'BlobNotFoundError')
2358 return self.askWantBlobs(err.links)
2359 if (err) return pull(
2360 pull.once(u.renderError(err).outerHTML),
2361 self.wrapPage('git tag ' + rev),
2362 self.respondSink(400)
2363 )
2364 var path = '/git/tag/' + rev
2365 + '?msg=' + encodeURIComponent(msg.key)
2366 return self.redirect(self.app.render.toUrl(path))
2367 })
2368 }
2369
2370 self.app.git.openObject({
2371 obj: rev,
2372 msg: self.query.msg,
2373 type: 'tag',
2374 }, function (err, obj) {
2375 if (err && err.name === 'BlobNotFoundError')
2376 return self.askWantBlobs(err.links)
2377 if (err) return pull(
2378 pull.once(u.renderError(err).outerHTML),
2379 self.wrapPage('git tag ' + rev),
2380 self.respondSink(400)
2381 )
2382
2383 var msgDate = new Date(obj.msg.value.timestamp)
2384 self.app.git.getTag(obj, function (err, tag) {
2385 if (err && err.message === 'expected type \'tag\' but found \'commit\'') {
2386 var path = '/git/commit/' + rev
2387 + '?msg=' + encodeURIComponent(self.query.msg)
2388 return self.redirect(self.app.render.toUrl(path))
2389 }
2390 var missingBlobs
2391 if (err && err.name === 'BlobNotFoundError')
2392 missingBlobs = err.links, err = null
2393 if (err) return pull(
2394 pull.once(u.renderError(err).outerHTML),
2395 self.wrapPage('git tag ' + rev),
2396 self.respondSink(400)
2397 )
2398 pull(
2399 ph('section', [
2400 ph('h3', ph('a', {href: ''}, rev)),
2401 ph('div', [
2402 self.phIdLink(obj.msg.value.author), ' pushed ',
2403 ph('a', {
2404 href: self.app.render.toUrl(obj.msg.key),
2405 title: msgDate.toLocaleString(),
2406 }, htime(msgDate))
2407 ]),
2408 missingBlobs ? self.askWantBlobsForm(missingBlobs) : [
2409 ph('div', [
2410 self.gitAuthorLink(tag.tagger),
2411 ' tagged ',
2412 ph('span', {title: tag.tagger.date.toLocaleString()},
2413 htime(tag.tagger.date)),
2414 ' in ', tag.tagger.tz
2415 ]),
2416 tag.type, ' ',
2417 pull(
2418 pull.once(tag.object),
2419 self.gitObjectLinks(obj.msg.key, tag.type)
2420 ), ' ',
2421 ph('code', u.escapeHTML(tag.tag)),
2422 tag.gpgsig ? ph('div', [
2423 ph('a', {href: self.app.render.toUrl(
2424 '/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg)
2425 )}, 'signature'),
2426 tag.signatureVersion ? [' from ', ph('code', u.escapeHTML(tag.signatureVersion))] : ''
2427 ]) : '',
2428 h('pre', self.app.render.linkify(tag.body)).outerHTML,
2429 ]
2430 ]),
2431 self.wrapPage('git tag ' + rev),
2432 self.respondSink(missingBlobs ? 409 : 200)
2433 )
2434 })
2435 })
2436}
2437
2438Serve.prototype.gitTree = function (rev) {
2439 var self = this
2440 if (!/[0-9a-f]{24}/.test(rev)) {
2441 return pull(
2442 ph('div.error', 'rev is not a git object id'),
2443 self.wrapPage('git'),
2444 self.respondSink(400)
2445 )
2446 }
2447 if (!u.isRef(self.query.msg)) return pull(
2448 ph('div.error', 'missing message id'),
2449 self.wrapPage('git tree ' + rev),
2450 self.respondSink(400)
2451 )
2452
2453 self.app.git.openObject({
2454 obj: rev,
2455 msg: self.query.msg,
2456 }, function (err, obj) {
2457 var missingBlobs
2458 if (err && err.name === 'BlobNotFoundError')
2459 missingBlobs = err.links, err = null
2460 if (err) return pull(
2461 pull.once(u.renderError(err).outerHTML),
2462 self.wrapPage('git tree ' + rev),
2463 self.respondSink(400)
2464 )
2465 var msgDate = new Date(obj.msg.value.timestamp)
2466 pull(
2467 ph('section', [
2468 ph('h3', ph('a', {href: ''}, rev)),
2469 ph('div', [
2470 self.phIdLink(obj.msg.value.author), ' ',
2471 ph('a', {
2472 href: self.app.render.toUrl(obj.msg.key),
2473 title: msgDate.toLocaleString(),
2474 }, htime(msgDate))
2475 ]),
2476 missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [
2477 pull(
2478 self.app.git.readTreeFull(obj),
2479 pull.map(function (item) {
2480 if (!item.msg) return ph('tr', [
2481 ph('td',
2482 u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')),
2483 ph('td', u.escapeHTML(item.hash)),
2484 ph('td', 'missing')
2485 ])
2486 var ext = item.name.replace(/.*\./, '')
2487 var path = '/git/' + item.type + '/' + item.hash
2488 + '?msg=' + encodeURIComponent(item.msg.key)
2489 + (ext ? '&ext=' + ext : '')
2490 var fileDate = new Date(item.msg.value.timestamp)
2491 return ph('tr', [
2492 ph('td',
2493 ph('a', {href: self.app.render.toUrl(path)},
2494 u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))),
2495 ph('td',
2496 self.phIdLink(item.msg.value.author)),
2497 ph('td',
2498 ph('a', {
2499 href: self.app.render.toUrl(item.msg.key),
2500 title: fileDate.toLocaleString(),
2501 }, htime(fileDate))
2502 ),
2503 ])
2504 }),
2505 Catch(function (err) {
2506 if (err && err.name === 'ObjectNotFoundError') return
2507 if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
2508 return false
2509 })
2510 )
2511 ]),
2512 ]),
2513 self.wrapPage('git tree ' + rev),
2514 self.respondSink(missingBlobs ? 409 : 200)
2515 )
2516 })
2517}
2518
2519Serve.prototype.gitBlob = function (rev) {
2520 var self = this
2521 if (!/[0-9a-f]{24}/.test(rev)) {
2522 return pull(
2523 ph('div.error', 'rev is not a git object id'),
2524 self.wrapPage('git'),
2525 self.respondSink(400)
2526 )
2527 }
2528 if (!u.isRef(self.query.msg)) return pull(
2529 ph('div.error', 'missing message id'),
2530 self.wrapPage('git object ' + rev),
2531 self.respondSink(400)
2532 )
2533
2534 self.getMsgDecryptedMaybeOoo(self.query.msg, function (err, msg) {
2535 if (err) return pull(
2536 pull.once(u.renderError(err).outerHTML),
2537 self.wrapPage('git object ' + rev),
2538 self.respondSink(400)
2539 )
2540 var msgDate = new Date(msg.value.timestamp)
2541 self.app.git.openObject({
2542 obj: rev,
2543 msg: msg.key,
2544 }, function (err, obj) {
2545 var missingBlobs
2546 if (err && err.name === 'BlobNotFoundError')
2547 missingBlobs = err.links, err = null
2548 if (err) return pull(
2549 pull.once(u.renderError(err).outerHTML),
2550 self.wrapPage('git object ' + rev),
2551 self.respondSink(400)
2552 )
2553 pull(
2554 ph('section', [
2555 ph('h3', ph('a', {href: ''}, rev)),
2556 ph('div', [
2557 self.phIdLink(msg.value.author), ' ',
2558 ph('a', {
2559 href: self.app.render.toUrl(msg.key),
2560 title: msgDate.toLocaleString(),
2561 }, htime(msgDate))
2562 ]),
2563 missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull(
2564 self.app.git.readObject(obj),
2565 self.wrapBinary({
2566 obj: obj,
2567 rawUrl: self.app.render.toUrl('/git/raw/' + rev
2568 + '?msg=' + encodeURIComponent(msg.key)),
2569 ext: self.query.ext
2570 })
2571 ),
2572 ]),
2573 self.wrapPage('git blob ' + rev),
2574 self.respondSink(200)
2575 )
2576 })
2577 })
2578}
2579
2580Serve.prototype.gitDiff = function (revs) {
2581 var self = this
2582 var parts = revs.split('..')
2583 if (parts.length !== 2) return pull(
2584 ph('div.error', 'revs should be <rev1>..<rev2>'),
2585 self.wrapPage('git diff'),
2586 self.respondSink(400)
2587 )
2588 var rev1 = parts[0]
2589 var rev2 = parts[1]
2590 if (!/[0-9a-f]{24}/.test(rev1)) return pull(
2591 ph('div.error', 'rev 1 is not a git object id'),
2592 self.wrapPage('git diff'),
2593 self.respondSink(400)
2594 )
2595 if (!/[0-9a-f]{24}/.test(rev2)) return pull(
2596 ph('div.error', 'rev 2 is not a git object id'),
2597 self.wrapPage('git diff'),
2598 self.respondSink(400)
2599 )
2600
2601 if (!u.isRef(self.query.msg)) return pull(
2602 ph('div.error', 'missing message id'),
2603 self.wrapPage('git diff'),
2604 self.respondSink(400)
2605 )
2606
2607 var done = multicb({pluck: 1, spread: true})
2608 // the msg qs param should point to the message for rev2 object. the msg for
2609 // rev1 object we will have to look up.
2610 self.app.git.getObjectMsg({
2611 obj: rev1,
2612 headMsgId: self.query.msg,
2613 type: 'blob',
2614 }, done())
2615 self.getMsgDecryptedMaybeOoo(self.query.msg, done())
2616 done(function (err, msg1, msg2) {
2617 if (err && err.name === 'BlobNotFoundError')
2618 return self.askWantBlobs(err.links)
2619 if (err) return pull(
2620 pull.once(u.renderError(err).outerHTML),
2621 self.wrapPage('git diff ' + revs),
2622 self.respondSink(400)
2623 )
2624 var msg1Date = new Date(msg1.value.timestamp)
2625 var msg2Date = new Date(msg2.value.timestamp)
2626 var revsShort = rev1.substr(0, 8) + '..' + rev2.substr(0, 8)
2627 var path = self.query.path && String(self.query.path)
2628 var ext = path && path.replace(/^[^.\/]*/, '')
2629 var blob1Url = '/git/blob/' + rev1 +
2630 '?msg=' + encodeURIComponent(msg1.key) +
2631 (ext ? '&ext=' + encodeURIComponent(ext) : '')
2632 var blob2Url = '/git/blob/' + rev2 +
2633 '?msg=' + encodeURIComponent(msg2.key) +
2634 (ext ? '&ext=' + encodeURIComponent(ext) : '')
2635 pull(
2636 ph('section', [
2637 ph('h3', ph('a', {href: ''}, revsShort)),
2638 ph('div', [
2639 ph('a', {
2640 href: self.app.render.toUrl(blob1Url)
2641 }, rev1), ' ',
2642 self.phIdLink(msg1.value.author), ' ',
2643 ph('a', {
2644 href: self.app.render.toUrl(msg1.key),
2645 title: msg1Date.toLocaleString(),
2646 }, htime(msg1Date))
2647 ]),
2648 ph('div', [
2649 ph('a', {
2650 href: self.app.render.toUrl(blob2Url)
2651 }, rev2), ' ',
2652 self.phIdLink(msg2.value.author), ' ',
2653 ph('a', {
2654 href: self.app.render.toUrl(msg2.key),
2655 title: msg2Date.toLocaleString(),
2656 }, htime(msg2Date))
2657 ]),
2658 u.readNext(function (cb) {
2659 var done = multicb({pluck: 1, spread: true})
2660 self.app.git.openObject({
2661 obj: rev1,
2662 msg: msg1.key,
2663 }, done())
2664 self.app.git.openObject({
2665 obj: rev2,
2666 msg: msg2.key,
2667 }, done())
2668 /*
2669 self.app.git.guessCommitAndPath({
2670 obj: rev2,
2671 msg: msg2.key,
2672 }, done())
2673 */
2674 done(function (err, obj1, obj2/*, info2*/) {
2675 if (err && err.name === 'BlobNotFoundError')
2676 return cb(null, self.askWantBlobsForm(err.links))
2677 if (err) return cb(err)
2678
2679 var done = multicb({pluck: 1, spread: true})
2680 pull.collect(done())(self.app.git.readObject(obj1))
2681 pull.collect(done())(self.app.git.readObject(obj2))
2682 self.app.getLineComments({obj: obj2, hash: rev2}, done())
2683 done(function (err, bufs1, bufs2, lineComments) {
2684 if (err) return cb(err)
2685 var str1 = Buffer.concat(bufs1, obj1.length).toString('utf8')
2686 var str2 = Buffer.concat(bufs2, obj2.length).toString('utf8')
2687 var diff = Diff.structuredPatch('', '', str1, str2)
2688 cb(null, self.gitDiffTable(diff, lineComments, {
2689 obj: obj2,
2690 hash: rev2,
2691 commit: self.query.commit, // info2.commit,
2692 path: self.query.path, // info2.path,
2693 }))
2694 })
2695 })
2696 })
2697 ]),
2698 self.wrapPage('git diff'),
2699 self.respondSink(200)
2700 )
2701 })
2702}
2703
2704Serve.prototype.gitDiffTable = function (diff, lineComments, lineCommentInfo) {
2705 var updateMsg = lineCommentInfo.obj.msg
2706 var self = this
2707 return pull(
2708 ph('table', [
2709 pull(
2710 pull.values(diff.hunks),
2711 pull.map(function (hunk) {
2712 var oldLine = hunk.oldStart
2713 var newLine = hunk.newStart
2714 return [
2715 ph('tr', [
2716 ph('td', {colspan: 3}),
2717 ph('td', ph('pre',
2718 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
2719 '+' + newLine + ',' + hunk.newLines + ' @@'))
2720 ]),
2721 pull(
2722 pull.values(hunk.lines),
2723 pull.map(function (line) {
2724 var s = line[0]
2725 if (s == '\\') return
2726 var html = self.app.render.highlight(line)
2727 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
2728 var hash = lineCommentInfo.hash
2729 var newLineNum = lineNums[lineNums.length-1]
2730 var id = hash + '-' + (newLineNum || (lineNums[0] + '-'))
2731 var idEnc = encodeURIComponent(id)
2732 var allowComment = s !== '-'
2733 && self.query.commit && self.query.path
2734 return [
2735 ph('tr', {
2736 class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
2737 }, [
2738 lineNums.map(function (num, i) {
2739 return ph('td', [
2740 ph('a', {
2741 name: i === 0 ? idEnc : undefined,
2742 href: '#' + idEnc
2743 }, String(num))
2744 ])
2745 }),
2746 ph('td',
2747 allowComment ? ph('a', {
2748 href: '?msg=' +
2749 encodeURIComponent(self.query.msg)
2750 + '&comment=' + idEnc
2751 + '&commit=' + encodeURIComponent(self.query.commit)
2752 + '&path=' + encodeURIComponent(self.query.path)
2753 + '#' + idEnc
2754 }, '…') : ''
2755 ),
2756 ph('td', ph('pre', html))
2757 ]),
2758 (lineComments[newLineNum] ?
2759 ph('tr',
2760 ph('td', {colspan: 4},
2761 self.renderLineCommentThread(lineComments[newLineNum], id)
2762 )
2763 )
2764 : newLineNum && lineCommentInfo && self.query.comment === id ?
2765 ph('tr',
2766 ph('td', {colspan: 4},
2767 self.renderLineCommentForm({
2768 id: id,
2769 line: newLineNum,
2770 updateId: updateMsg.key,
2771 blobId: hash,
2772 repoId: updateMsg.value.content.repo,
2773 commitId: lineCommentInfo.commit,
2774 filePath: lineCommentInfo.path,
2775 })
2776 )
2777 )
2778 : '')
2779 ]
2780 })
2781 )
2782 ]
2783 })
2784 )
2785 ])
2786 )
2787}
2788
2789Serve.prototype.renderLineCommentThread = function (lineComment, id) {
2790 return this.streamThreadWithComposer({
2791 root: lineComment.msg.key,
2792 id: id,
2793 placeholder: 'reply to line comment thread'
2794 })
2795}
2796
2797Serve.prototype.renderLineCommentForm = function (opts) {
2798 return [
2799 this.phComposer({
2800 placeholder: 'comment on this line',
2801 id: opts.id,
2802 lineComment: opts
2803 })
2804 ]
2805}
2806
2807// return a composer, pull-hyperscript style
2808Serve.prototype.phComposer = function (opts) {
2809 var self = this
2810 return u.readNext(function (cb) {
2811 self.composer(opts, function (err, composer) {
2812 if (err) return cb(err)
2813 cb(null, pull.once(composer.outerHTML))
2814 })
2815 })
2816}
2817
2818Serve.prototype.gitLineComment = function (path) {
2819 var self = this
2820 var id
2821 try {
2822 id = decodeURIComponent(String(path))
2823 if (id[0] === '%') {
2824 return self.getMsgDecryptedMaybeOoo(id, gotMsg)
2825 } else {
2826 msg = JSON.parse(id)
2827 }
2828 } catch(e) {
2829 return gotMsg(e)
2830 }
2831 gotMsg(null, msg)
2832 function gotMsg(err, msg) {
2833 if (err) return pull(
2834 pull.once(u.renderError(err).outerHTML),
2835 self.respondSink(400, {'Content-Type': ctype('html')})
2836 )
2837 var c = msg && msg.value && msg.value.content
2838 if (!c) return pull(
2839 pull.once('Missing message ' + id),
2840 self.respondSink(500, {'Content-Type': ctype('html')})
2841 )
2842 self.app.git.diffFile({
2843 msg: c.updateId,
2844 commit: c.commitId,
2845 path: c.filePath,
2846 }, function (err, file) {
2847 if (err && err.name === 'BlobNotFoundError')
2848 return self.askWantBlobs(err.links)
2849 if (err) return pull(
2850 pull.once(err.stack),
2851 self.respondSink(400, {'Content-Type': 'text/plain'})
2852 )
2853 var path
2854 if (file.created) {
2855 path = '/git/blob/' + file.hash[1]
2856 + '?msg=' + encodeURIComponent(c.updateId)
2857 + '&commit=' + c.commitId
2858 + '&path=' + encodeURIComponent(c.filePath)
2859 + '#' + file.hash[1] + '-' + c.line
2860 } else {
2861 path = '/git/diff/' + file.hash[0] + '..' + file.hash[1]
2862 + '?msg=' + encodeURIComponent(c.updateId)
2863 + '&commit=' + c.commitId
2864 + '&path=' + encodeURIComponent(c.filePath)
2865 + '#' + file.hash[1] + '-' + c.line
2866 }
2867 var url = self.app.render.toUrl(path)
2868 /*
2869 return pull(
2870 ph('a', {href: url}, path),
2871 self.wrapPage(id),
2872 self.respondSink(200)
2873 )
2874 */
2875 self.redirect(url)
2876 })
2877 }
2878}
2879
2880Serve.prototype.gitObjectLinks = function (headMsgId, type) {
2881 var self = this
2882 return paramap(function (id, cb) {
2883 self.app.git.getObjectMsg({
2884 obj: id,
2885 headMsgId: headMsgId,
2886 type: type,
2887 }, function (err, msg) {
2888 if (err && err.name === 'BlobNotFoundError')
2889 return cb(null, self.askWantBlobsForm(err.links))
2890 if (err && err.name === 'ObjectNotFoundError')
2891 return cb(null, [
2892 ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)'])
2893 if (err) return cb(err)
2894 var path = '/git/' + type + '/' + id
2895 + '?msg=' + encodeURIComponent(msg.key)
2896 cb(null, [ph('code', ph('a', {
2897 href: self.app.render.toUrl(path)
2898 }, u.escapeHTML(id.substr(0, 8)))), ' '])
2899 })
2900 }, 8)
2901}
2902
2903Serve.prototype.npm = function (url) {
2904 var self = this
2905 var parts = url.split('/')
2906 var author = parts[1] && parts[1][0] === '@'
2907 ? u.unescapeId(parts.splice(1, 1)[0]) : null
2908 var name = parts[1]
2909 var version = parts[2]
2910 var distTag = parts[3]
2911 var prefix = 'npm:' +
2912 (name ? name + ':' +
2913 (version ? version + ':' +
2914 (distTag ? distTag + ':' : '') : '') : '')
2915
2916 var render = self.app.render
2917 var base = '/npm/' + (author ? u.escapeId(author) + '/' : '')
2918 var pathWithoutAuthor = '/npm' +
2919 (name ? '/' + name +
2920 (version ? '/' + version +
2921 (distTag ? '/' + distTag : '') : '') : '')
2922 return pull(
2923 ph('section', {}, [
2924 ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ',
2925 author ? [
2926 self.phIdLink(author), ' ',
2927 ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '&times;')),
2928 ' : '
2929 ] : '',
2930 name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
2931 version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '',
2932 distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : ''
2933 ]),
2934 ph('table', [
2935 ph('thead', ph('tr', [
2936 ph('th', 'publisher'),
2937 ph('th', 'package'),
2938 ph('th', 'version'),
2939 ph('th', 'tag'),
2940 ph('th', 'size'),
2941 ph('th', 'tarball'),
2942 ph('th', 'readme')
2943 ])),
2944 ph('tbody', pull(
2945 self.app.blobMentions({
2946 name: {$prefix: prefix},
2947 author: author,
2948 }),
2949 distTag && !version && pull.filter(function (link) {
2950 return link.name.split(':')[3] === distTag
2951 }),
2952 paramap(function (link, cb) {
2953 self.app.render.npmPackageMention(link, {
2954 withAuthor: true,
2955 author: author,
2956 name: name,
2957 version: version,
2958 distTag: distTag,
2959 }, cb)
2960 }, 4),
2961 pull.map(u.toHTML)
2962 ))
2963 ])
2964 ]),
2965 self.wrapPage(prefix),
2966 self.respondSink(200)
2967 )
2968}
2969
2970Serve.prototype.npmPrebuilds = function (url) {
2971 var self = this
2972 var parts = url.split('/')
2973 var author = parts[1] && parts[1][0] === '@'
2974 ? u.unescapeId(parts.splice(1, 1)[0]) : null
2975 var name = parts[1]
2976 var version = parts[2]
2977 var prefix = 'prebuild:' +
2978 (name ? name + '-' +
2979 (version ? version + '-' : '') : '')
2980
2981 var render = self.app.render
2982 var base = '/npm-prebuilds/' + (author ? u.escapeId(author) + '/' : '')
2983 return pull(
2984 ph('section', {}, [
2985 ph('h3', [ph('a', {href: render.toUrl('/npm-prebuilds/')}, 'npm prebuilds'), ' : ',
2986 name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '',
2987 version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version)] : '',
2988 ]),
2989 ph('table', [
2990 ph('thead', ph('tr', [
2991 ph('th', 'publisher'),
2992 ph('th', 'name'),
2993 ph('th', 'version'),
2994 ph('th', 'runtime'),
2995 ph('th', 'abi'),
2996 ph('th', 'platform+libc'),
2997 ph('th', 'arch'),
2998 ph('th', 'size'),
2999 ph('th', 'tarball')
3000 ])),
3001 ph('tbody', pull(
3002 self.app.blobMentions({
3003 name: {$prefix: prefix},
3004 author: author,
3005 }),
3006 paramap(function (link, cb) {
3007 self.app.render.npmPrebuildMention(link, {
3008 withAuthor: true,
3009 author: author,
3010 name: name,
3011 version: version,
3012 }, cb)
3013 }, 4),
3014 pull.map(u.toHTML)
3015 ))
3016 ])
3017 ]),
3018 self.wrapPage(prefix),
3019 self.respondSink(200)
3020 )
3021}
3022
3023Serve.prototype.npmReadme = function (url) {
3024 var self = this
3025 var id = decodeURIComponent(url.substr(1))
3026 return pull(
3027 ph('section', {}, [
3028 ph('h3', [
3029 'npm readme for ',
3030 ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
3031 ]),
3032 ph('blockquote', u.readNext(function (cb) {
3033 self.app.getNpmReadme(id, function (err, readme, isMarkdown) {
3034 if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
3035 cb(null, isMarkdown
3036 ? ph('div', self.app.render.markdown(readme))
3037 : ph('pre', readme))
3038 })
3039 }))
3040 ]),
3041 self.wrapPage('npm readme'),
3042 self.respondSink(200)
3043 )
3044}
3045
3046Serve.prototype.npmRegistry = function (url) {
3047 var self = this
3048 self.req.url = url
3049 self.app.serveSsbNpmRegistry(self.req, self.res)
3050}
3051
3052Serve.prototype.markdown = function (url) {
3053 var self = this
3054 var id = decodeURIComponent(url.substr(1))
3055 if (typeof self.query.unbox === 'string') id += '?unbox=' + self.query.unbox.replace(/\s/g, '+')
3056 return pull(
3057 ph('section', {}, [
3058 ph('h3', [
3059 ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…')
3060 ]),
3061 u.readNext(function (cb) {
3062 self.app.getHasBlob(id, function (err, has) {
3063 if (err) return cb(err)
3064 if (!has) return cb(null, self.askWantBlobsForm([id]))
3065 pull(self.app.getBlob(id), pull.collect(function (err, chunks) {
3066 if (err) return cb(null, ph('div', u.renderError(err).outerHTML))
3067 var text = Buffer.concat(chunks).toString()
3068 cb(null, ph('blockquote', self.app.render.markdown(text)))
3069 }))
3070 })
3071 })
3072 ]),
3073 self.wrapPage('markdown'),
3074 self.respondSink(200)
3075 )
3076}
3077
3078Serve.prototype.zip = function (url) {
3079 var self = this
3080 var parts = url.split('/').slice(1)
3081 var id = decodeURIComponent(parts.shift())
3082 var filename = parts.join('/')
3083 var blobs = self.app.sbot.blobs
3084 var etag = id + filename
3085 var index = filename === '' || /\/$/.test(filename)
3086 var indexFilename = index && (filename + 'index.html')
3087 if (filename === '/' || /\/\/$/.test(filename)) {
3088 // force directory listing if path ends in //
3089 filename = filename.replace(/\/$/, '')
3090 indexFilename = false
3091 }
3092 var files = index && []
3093 if (self.req.headers['if-none-match'] === etag) return self.respond(304)
3094 blobs.size(id, function (err, size) {
3095 if (size == null) return askWantBlobsForm([id])
3096 if (err) {
3097 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
3098 else return self.respond(500, err.message || err)
3099 }
3100 var unzip = require('unzip')
3101 var parseUnzip = unzip.Parse()
3102 var gotEntry = false
3103 parseUnzip.on('entry', function (entry) {
3104 if (index) {
3105 if (!gotEntry) {
3106 if (entry.path === indexFilename) {
3107 gotEntry = true
3108 return serveFile(entry)
3109 } else if (entry.path.substr(0, filename.length) === filename) {
3110 files.push({path: entry.path, type: entry.type, props: entry.props})
3111 }
3112 }
3113 } else {
3114 if (!gotEntry && entry.path === filename) {
3115 gotEntry = true
3116 // if (false && entry.type === 'Directory') return serveDirectory(entry)
3117 return serveFile(entry)
3118 }
3119 }
3120 entry.autodrain()
3121 })
3122 parseUnzip.on('close', function () {
3123 if (gotEntry) return
3124 if (!index) return self.respond(404, 'Entry not found')
3125 pull(
3126 ph('section', {}, [
3127 ph('h3', [
3128 ph('a', {href: self.app.render.toUrl('/links/' + id)}, id.substr(0, 8) + '…'),
3129 ' ',
3130 ph('a', {href: self.app.render.toUrl('/zip/' + encodeURIComponent(id) + '/' + filename)}, filename || '/'),
3131 ]),
3132 pull(
3133 pull.values(files),
3134 pull.map(function (file) {
3135 var path = '/zip/' + encodeURIComponent(id) + '/' + file.path
3136 return ph('li', [
3137 ph('a', {href: self.app.render.toUrl(path)}, file.path)
3138 ])
3139 })
3140 )
3141 ]),
3142 self.wrapPage(id + filename),
3143 self.respondSink(200)
3144 )
3145 gotEntry = true // so that the handler on error event does not run
3146 })
3147 parseUnzip.on('error', function (err) {
3148 if (!gotEntry) return self.respond(400, err.message)
3149 })
3150 var size
3151 function serveFile(entry) {
3152 size = entry.size
3153 pull(
3154 toPull.source(entry),
3155 ident(gotType),
3156 self.respondSink()
3157 )
3158 }
3159 pull(
3160 self.app.getBlob(id),
3161 toPull(parseUnzip)
3162 )
3163 function gotType(type) {
3164 type = type && mime.lookup(type)
3165 if (type) self.res.setHeader('Content-Type', type)
3166 if (size) self.res.setHeader('Content-Length', size)
3167 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
3168 self.res.setHeader('etag', etag)
3169 self.res.writeHead(200)
3170 }
3171 })
3172}
3173
3174Serve.prototype.web = function (url) {
3175 var self = this
3176 var id = url.substr(1)
3177 try { id = decodeURIComponent(id) }
3178 catch(e) {}
3179
3180 var components = url.split('/')
3181 if (components[0] === '') components.shift()
3182 components[0] = decodeURIComponent(components[0])
3183
3184 var type = mime.lookup(components[components.length - 1])
3185 var headers = {}
3186 if (type) headers['Content-Type'] = type
3187 webresolve(this.app.sbot, components, function (err, res) {
3188 if (err) {
3189 return pull(
3190 pull.once(err.toString()),
3191 self.respondSink(404)
3192 )
3193 }
3194 headers['Content-Length'] = res.length
3195 return pull(
3196 pull.once(res),
3197 self.respondSink(200, headers)
3198 )
3199 })
3200}
3201
3202Serve.prototype.script = function (url) {
3203 var self = this
3204 var filepath = url.split('?')[0]
3205 this.app.getScript(filepath, function (err, fn) {
3206 try {
3207 if (err) throw err
3208 fn(self)
3209 } catch(e) {
3210 return pull(
3211 pull.once(u.renderError(e).outerHTML),
3212 self.wrapPage('local: ' + path),
3213 self.respondSink(400)
3214 )
3215 }
3216 })
3217}
3218
3219// wrap a binary source and render it or turn into an embed
3220Serve.prototype.wrapBinary = function (opts) {
3221 var self = this
3222 var ext = opts.ext
3223 var hash = opts.obj.hash
3224 return function (read) {
3225 var readRendered, type
3226 read = ident(function (_ext) {
3227 if (_ext) ext = _ext
3228 type = ext && mime.lookup(ext) || 'text/plain'
3229 })(read)
3230 return function (abort, cb) {
3231 if (readRendered) return readRendered(abort, cb)
3232 if (abort) return read(abort, cb)
3233 if (!type) read(null, function (end, buf) {
3234 if (end) return cb(end)
3235 if (!type) return cb(new Error('unable to get type'))
3236 readRendered = pickSource(type, cat([pull.once(buf), read]))
3237 readRendered(null, cb)
3238 })
3239 }
3240 }
3241 function pickSource(type, read) {
3242 if (/^image\//.test(type)) {
3243 read(true, function (err) {
3244 if (err && err !== true) console.trace(err)
3245 })
3246 return ph('img', {
3247 src: opts.rawUrl
3248 })
3249 }
3250 if (type === 'text/markdown') {
3251 // TODO: rewrite links to files/images to be correct
3252 return ph('blockquote', u.readNext(function (cb) {
3253 pull.collect(function (err, bufs) {
3254 if (err) return cb(pull.error(err))
3255 var text = Buffer.concat(bufs).toString('utf8')
3256 return cb(null, pull.once(self.app.render.markdown(text)))
3257 })(read)
3258 }))
3259 }
3260 var i = 1
3261 var updateMsg = opts.obj.msg
3262 var commitId = self.query.commit
3263 var filePath = self.query.path
3264 var lineComments = opts.lineComments || {}
3265 return u.readNext(function (cb) {
3266 if (commitId && filePath) {
3267 self.app.getLineComments({
3268 obj: opts.obj,
3269 hash: hash,
3270 }, gotLineComments)
3271 } else {
3272 gotLineComments(null, {})
3273 }
3274 function gotLineComments(err, lineComments) {
3275 if (err) return cb(err)
3276 cb(null, ph('table',
3277 pull(
3278 read,
3279 utf8(),
3280 split(),
3281 pull.map(function (line) {
3282 var lineNum = i++
3283 var id = hash + '-' + lineNum
3284 var idEnc = encodeURIComponent(id)
3285 var allowComment = self.query.commit && self.query.path
3286 return [
3287 ph('tr', [
3288 ph('td',
3289 allowComment ? ph('a', {
3290 href: '?msg=' + encodeURIComponent(self.query.msg)
3291 + '&commit=' + encodeURIComponent(self.query.commit)
3292 + '&path=' + encodeURIComponent(self.query.path)
3293 + '&comment=' + idEnc
3294 + '#' + idEnc
3295 }, '…') : ''
3296 ),
3297 ph('td', ph('a', {
3298 name: id,
3299 href: '#' + idEnc
3300 }, String(lineNum))),
3301 ph('td', ph('pre', self.app.render.highlight(line, ext)))
3302 ]),
3303 lineComments[lineNum] ? ph('tr',
3304 ph('td', {colspan: 4},
3305 self.renderLineCommentThread(lineComments[lineNum], id)
3306 )
3307 ) : '',
3308 self.query.comment === id ? ph('tr',
3309 ph('td', {colspan: 4},
3310 self.renderLineCommentForm({
3311 id: id,
3312 line: lineNum,
3313 updateId: updateMsg.key,
3314 repoId: updateMsg.value.content.repo,
3315 commitId: commitId,
3316 blobId: hash,
3317 filePath: filePath,
3318 })
3319 )
3320 ) : ''
3321 ]
3322 })
3323 )
3324 ))
3325 }
3326 })
3327 }
3328}
3329
3330Serve.prototype.wrapPublic = function (opts) {
3331 var self = this
3332 return u.hyperwrap(function (thread, cb) {
3333 self.composer({
3334 channel: '',
3335 }, function (err, composer) {
3336 if (err) return cb(err)
3337 cb(null, [
3338 composer,
3339 thread
3340 ])
3341 })
3342 })
3343}
3344
3345Serve.prototype.askWantBlobsForm = function (links) {
3346 var self = this
3347 return ph('form', {action: '', method: 'post'}, [
3348 ph('section', [
3349 ph('h3', 'Missing blobs'),
3350 ph('p', 'The application needs these blobs to continue:'),
3351 ph('table', links.map(u.toLink).map(function (link) {
3352 if (!u.isRef(link.link)) return
3353 return ph('tr', [
3354 ph('td', ph('code', link.link)),
3355 !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '',
3356 ])
3357 })),
3358 ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
3359 ph('input', {type: 'hidden', name: 'blob_ids',
3360 value: links.map(u.linkDest).join(',')}),
3361 ph('p', ph('input', {type: 'submit', value: 'Want Blobs'}))
3362 ])
3363 ])
3364}
3365
3366Serve.prototype.askWantBlobs = function (links) {
3367 var self = this
3368 pull(
3369 self.askWantBlobsForm(links),
3370 self.wrapPage('missing blobs'),
3371 self.respondSink(409)
3372 )
3373}
3374
3375Serve.prototype.wrapPrivate = function (opts) {
3376 var self = this
3377 return u.hyperwrap(function (thread, cb) {
3378 self.composer({
3379 placeholder: 'private message',
3380 private: true,
3381 }, function (err, composer) {
3382 if (err) return cb(err)
3383 cb(null, [
3384 composer,
3385 thread
3386 ])
3387 })
3388 })
3389}
3390
3391Serve.prototype.wrapThread = function (opts) {
3392 var self = this
3393 return u.hyperwrap(function (thread, cb) {
3394 self.app.render.prepareLinks(opts.recps, function (err, recps) {
3395 if (err) return cb(er)
3396 self.composer({
3397 placeholder: opts.placeholder
3398 || (recps ? 'private reply' : 'reply'),
3399 id: 'reply',
3400 root: opts.root,
3401 post: opts.post,
3402 channel: opts.channel || '',
3403 branches: opts.branches,
3404 postBranches: opts.postBranches,
3405 recps: recps,
3406 private: opts.recps != null,
3407 }, function (err, composer) {
3408 if (err) return cb(err)
3409 cb(null, [
3410 thread,
3411 composer
3412 ])
3413 })
3414 })
3415 })
3416}
3417
3418Serve.prototype.wrapNew = function (opts) {
3419 var self = this
3420 return u.hyperwrap(function (thread, cb) {
3421 self.composer({
3422 channel: '',
3423 }, function (err, composer) {
3424 if (err) return cb(err)
3425 cb(null, [
3426 composer,
3427 h('table.ssb-msgs',
3428 opts.reachedLimit ? h('tr', h('td.paginate.msg-left', {colspan: 3},
3429 'Reached limit of ' + opts.reachedLimit + ' messages'
3430 )) : '',
3431 thread,
3432 h('tr', h('td.paginate.msg-left', {colspan: 3},
3433 h('form', {method: 'get', action: ''},
3434 h('input', {type: 'hidden', name: 'gt', value: opts.gt}),
3435 self.query.limit ? h('input', {type: 'hidden', name: 'limit', value: self.query.limit}) : '',
3436 h('input', {type: 'hidden', name: 'catchup', value: '1'}),
3437 h('input', {type: 'submit', value: 'catchup'})
3438 )
3439 ))
3440 )
3441 ])
3442 })
3443 })
3444}
3445
3446Serve.prototype.wrapChannel = function (channel) {
3447 var self = this
3448 return u.hyperwrap(function (thread, cb) {
3449 self.composer({
3450 placeholder: 'public message in #' + channel,
3451 channel: channel,
3452 }, function (err, composer) {
3453 if (err) return cb(err)
3454 cb(null, [
3455 h('section',
3456 h('h3.feed-name',
3457 h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel)
3458 )
3459 ),
3460 composer,
3461 thread
3462 ])
3463 })
3464 })
3465}
3466
3467Serve.prototype.wrapType = function (type) {
3468 var self = this
3469 return u.hyperwrap(function (thread, cb) {
3470 cb(null, [
3471 h('section',
3472 h('h3.feed-name',
3473 h('a', {href: self.app.render.toUrl('/type/' + type)},
3474 h('code', type), 's'))
3475 ),
3476 thread
3477 ])
3478 })
3479}
3480
3481Serve.prototype.wrapLinks = function (dest) {
3482 var self = this
3483 return u.hyperwrap(function (thread, cb) {
3484 cb(null, [
3485 h('section',
3486 h('h3.feed-name', 'links: ',
3487 h('a', {href: self.app.render.toUrl('/links/' + dest)},
3488 h('code', dest)))
3489 ),
3490 thread
3491 ])
3492 })
3493}
3494
3495Serve.prototype.wrapPeers = function (opts) {
3496 var self = this
3497 return u.hyperwrap(function (peers, cb) {
3498 cb(null, [
3499 h('section',
3500 h('h3', 'Peers')
3501 ),
3502 peers
3503 ])
3504 })
3505}
3506
3507Serve.prototype.wrapChannels = function (opts) {
3508 var self = this
3509 return u.hyperwrap(function (channels, cb) {
3510 cb(null, [
3511 h('section',
3512 h('h4', 'Network')
3513 ),
3514 h('section',
3515 channels
3516 )
3517 ])
3518 })
3519}
3520
3521Serve.prototype.wrapMyChannels = function (opts) {
3522 var self = this
3523 return u.hyperwrap(function (channels, cb) {
3524 cb(null, [
3525 h('section',
3526 h('h4', 'Subscribed')
3527 ),
3528 h('section',
3529 channels
3530 )
3531 ])
3532 })
3533}
3534
3535var blobPrefixesByType = {
3536 audio: 'audio:',
3537 video: 'video:',
3538}
3539
3540var blobPrefixesByExt = {
3541 mp3: 'audio:',
3542 mp4: 'video:',
3543}
3544
3545Serve.prototype.composer = function (opts, cb) {
3546 var self = this
3547 opts = opts || {}
3548 var data = self.data
3549 var myId = self.app.sbot.id
3550
3551 if (opts.id && data.composer_id && opts.id !== data.composer_id) {
3552 // don't share data between multiple composers
3553 data = {}
3554 }
3555
3556 if (!data.text && self.query.text) data.text = self.query.text
3557 if (!data.action && self.query.action) data.action = self.query.action
3558
3559 var blobs = u.tryDecodeJSON(data.blobs) || {}
3560 var upload = data.upload
3561 if (upload && typeof upload === 'object') {
3562 data.upload = null
3563
3564 var href = upload.link + (upload.key ? '?unbox=' + upload.key + '.boxs': '')
3565 var blobType = String(upload.type).split('/')[0]
3566 var blobExt = String(upload.name).split('.').pop()
3567 var blobPrefix = blobPrefixesByType[blobType] || blobPrefixesByExt[blobExt] || ''
3568 var isMedia = blobPrefix || blobType === 'image'
3569 var blobName = blobPrefix + upload.name
3570
3571 blobs[upload.link] = {
3572 type: upload.type,
3573 size: upload.size,
3574 name: blobName,
3575 key: upload.key,
3576 }
3577
3578 data.text = (data.text ? data.text + '\n' : '')
3579 + (isMedia ? '!' : '')
3580 + '[' + blobName + '](' + href + ')'
3581 }
3582
3583 var channel = data.channel != null ? data.channel : opts.channel
3584
3585 var formNames = {}
3586 var mentionIds = u.toArray(data.mention_id)
3587 var mentionNames = u.toArray(data.mention_name)
3588 for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) {
3589 formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0]
3590 }
3591
3592 var formEmojiNames = {}
3593 var emojiIds = u.toArray(data.emoji_id)
3594 var emojiNames = u.toArray(data.emoji_name)
3595 for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) {
3596 var upload = data['emoji_upload_' + i]
3597 formEmojiNames[emojiNames[i]] =
3598 (upload && upload.link) || u.extractBlobIds(emojiIds[i])[0]
3599 if (upload) blobs[upload.link] = {
3600 type: upload.type,
3601 size: upload.size,
3602 name: upload.name,
3603 }
3604 }
3605
3606 var blobIds = u.toArray(data.blob_id)
3607 var blobTypes = u.toArray(data.blob_type)
3608 var blobNames = u.toArray(data.blob_name)
3609 for (var i = 0; i < blobIds.length && i < blobTypes.length; i++) {
3610 var id = blobIds[i]
3611 var blob = blobs[id] || (blobs[id] = {})
3612 blob.type = blobTypes[i]
3613 blob.nameOverride = data['blob_name_override_' + i]
3614 blob.name = blobNames[i]
3615 }
3616
3617 // get bare feed names
3618 var unknownMentionNames = {}
3619 var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
3620 var unknownMentions = mentions
3621 .filter(function (mention) {
3622 return mention.link === '@'
3623 })
3624 .map(function (mention) {
3625 return mention.name
3626 })
3627 .filter(uniques())
3628 .map(function (name) {
3629 var id = formNames[name] || self.app.getReverseNameSync('@' + name)
3630 return {name: name, id: id}
3631 })
3632
3633 var emoji = mentions
3634 .filter(function (mention) { return mention.emoji })
3635 .map(function (mention) { return mention.name })
3636 .filter(uniques())
3637 .map(function (name) {
3638 // 1. check emoji-image mapping for this message
3639 var id = formEmojiNames[name]
3640 if (id) return {name: name, id: id}
3641 // 2. TODO: check user's preferred emoji-image mapping
3642 // 3. check builtin emoji
3643 var link = self.getBuiltinEmojiLink(name)
3644 if (link) {
3645 return {name: name, id: link.link}
3646 blobs[id] = {type: link.type, size: link.size}
3647 }
3648 // 4. check recently seen emoji
3649 id = self.app.getReverseEmojiNameSync(name)
3650 return {name: name, id: id}
3651 })
3652
3653 var blobMentions = mentions
3654 .filter(function (mention) {
3655 return mention
3656 && typeof mention.link === 'string'
3657 && mention.link[0] === '&'
3658 })
3659 blobMentions.forEach(function (mention) {
3660 var blob = blobs[mention.link]
3661 if (blob) {
3662 mention.type = blob.type
3663 if (blob.nameOverride) mention.name = blob.name
3664 }
3665 })
3666
3667 // strip content other than names and feed ids from the recps field
3668 if (data.recps) {
3669 data.recps = recpsToFeedIds(data.recps)
3670 }
3671
3672 var draftLinkContainer
3673 function renderDraftLink(draftId) {
3674 if (!draftId) return []
3675 var draftHref = self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId))
3676 return [
3677 h('a', {href: draftHref, title: 'draft link'}, u.escapeHTML(draftId)),
3678 h('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)})
3679 ]
3680 }
3681
3682 var done = multicb({pluck: 1, spread: true})
3683 done()(null, h('section.composer',
3684 h('form', {method: 'post', action: opts.id ? '#' + opts.id : '',
3685 enctype: 'multipart/form-data'},
3686 h('input', {type: 'hidden', name: 'blobs',
3687 value: JSON.stringify(blobs)}),
3688 h('input', {type: 'hidden', name: 'url', value: self.req.url}),
3689 h('input', {type: 'hidden', name: 'composer_id', value: opts.id}),
3690 opts.recps ? self.app.render.privateLine(opts.recps, true, done()) :
3691 opts.private ? h('div', h('input.wide', {name: 'recps', size: 70,
3692 value: data.recps || '', placeholder: 'recipient ids'})) : '',
3693 channel != null ?
3694 h('div', '#', h('input', {name: 'channel', placeholder: 'channel',
3695 value: channel})) : '',
3696 opts.root !== opts.post ? h('div',
3697 h('label', {for: 'fork_thread'},
3698 h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}),
3699 ' fork thread'
3700 )
3701 ) : '',
3702 closeIssueCheckbox(done()),
3703 h('div', h('input.wide', {name: 'content_warning', size: 70,
3704 value: data.content_warning || '',
3705 placeholder: 'Content Warning'})),
3706 h('textarea', {
3707 id: opts.id,
3708 name: 'text',
3709 rows: Math.max(4, u.rows(data.text)),
3710 cols: 70,
3711 placeholder: opts.placeholder || 'public message',
3712 }, data.text || ''),
3713 unknownMentions.length > 0 ? [
3714 h('div', h('em', 'names:')),
3715 h('ul.mentions', unknownMentions.map(function (mention) {
3716 return h('li',
3717 h('code', '@' + mention.name), ': ',
3718 h('input', {name: 'mention_name', type: 'hidden',
3719 value: mention.name}),
3720 h('input.id-input', {name: 'mention_id', size: 60,
3721 value: mention.id, placeholder: '@id'}))
3722 }))
3723 ] : '',
3724 emoji.length > 0 ? [
3725 h('div', h('em', 'emoji:')),
3726 h('ul.mentions', emoji.map(function (link, i) {
3727 return h('li',
3728 h('code', link.name), ': ',
3729 h('input', {name: 'emoji_name', type: 'hidden',
3730 value: link.name}),
3731 h('input.id-input', {name: 'emoji_id', size: 60,
3732 value: link.id, placeholder: '&id'}), ' ',
3733 h('input', {type: 'file', name: 'emoji_upload_' + i}))
3734 }))
3735 ] : '',
3736 blobMentions.length > 0 ? [
3737 h('div', h('em', 'blobs:')),
3738 h('ul.mentions', blobMentions.map(function (link, i) {
3739 var link1 = blobs[link.link] || {}
3740 var linkHref = self.app.render.toUrl(link.link)
3741 return h('li',
3742 h('a', {href: linkHref}, h('code', String(link.link).substr(0, 8) + '…')), ' ',
3743 h('input', {name: 'blob_id', type: 'hidden',
3744 value: link.link}),
3745 h('input', {name: 'blob_type', size: 24,
3746 value: link.type, placeholder: 'application/octet-stream',
3747 title: 'blob mime-type'}),
3748 h('input', {name: 'blob_name', size: 24,
3749 value: link.name || '', placeholder: 'name',
3750 title: 'blob name'}), ' ',
3751 h('input', {name: 'blob_name_override_' + i, type: 'checkbox',
3752 value: 1, checked: link1.nameOverride ? '1' : undefined,
3753 title: 'override name in markdown'}))
3754 }))
3755 ] : '',
3756 h('table.ssb-msgs',
3757 h('tr.msg-row',
3758 h('td.msg-left', {colspan: 2},
3759 opts.private ?
3760 h('input', {type: 'hidden', name: 'private', value: '1'}) : '',
3761 h('input', {type: 'file', name: 'upload'})
3762 ),
3763 h('td.msg-right',
3764 draftLinkContainer = h('span', renderDraftLink(data.draft_id)), ' ',
3765 h('label', {for: 'save_draft'},
3766 h('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1',
3767 checked: data.save_draft || data.restored_draft ? 'checked' : undefined}),
3768 ' save draft '),
3769 h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ',
3770 h('input', {type: 'submit', name: 'action', value: 'preview'})
3771 )
3772 )
3773 ),
3774 data.action === 'preview' ? preview(false, done()) :
3775 data.action === 'raw' ? preview(true, done()) : ''
3776 )
3777 ))
3778 done(cb)
3779
3780 function recpsToFeedIds (recps) {
3781 var res = data.recps.split(',')
3782 .map(function (str) {
3783 str = str.trim()
3784 var ids = u.extractFeedIds(str).filter(uniques())
3785 if (ids.length >= 1) {
3786 return ids[0]
3787 } else {
3788 ids = u.extractFeedIds(self.app.getReverseNameSync(str))
3789 if (ids.length >= 1) {
3790 return ids[0]
3791 } else {
3792 return null
3793 }
3794 }
3795 })
3796 .filter(Boolean)
3797 return res.join(', ')
3798 }
3799
3800 function prepareContent(cb) {
3801 var done = multicb({pluck: 1})
3802 content = {
3803 type: 'post',
3804 text: String(data.text),
3805 }
3806 if (opts.lineComment) {
3807 content.type = 'line-comment'
3808 content.updateId = opts.lineComment.updateId
3809 content.repo = opts.lineComment.repoId
3810 content.commitId = opts.lineComment.commitId
3811 content.filePath = opts.lineComment.filePath
3812 content.blobId = opts.lineComment.blobId
3813 content.line = opts.lineComment.line
3814 }
3815 var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
3816 .filter(function (mention) {
3817 if (mention.emoji) {
3818 mention.link = formEmojiNames[mention.name]
3819 if (!mention.link) {
3820 var link = self.getBuiltinEmojiLink(mention.name)
3821 if (link) {
3822 mention.link = link.link
3823 mention.size = link.size
3824 mention.type = link.type
3825 } else {
3826 mention.link = self.app.getReverseEmojiNameSync(mention.name)
3827 if (!mention.link) return false
3828 }
3829 }
3830 }
3831 var blob = blobs[mention.link]
3832 if (blob) {
3833 if (!isNaN(blob.size))
3834 mention.size = blob.size
3835 if (blob.type && blob.type !== 'application/octet-stream')
3836 mention.type = blob.type
3837 if (blob.nameOverride)
3838 mention.name = blob.name
3839 } else if (mention.link === '@') {
3840 // bare feed name
3841 var name = mention.name
3842 var id = formNames[name] || self.app.getReverseNameSync('@' + name)
3843 if (id) mention.link = id
3844 else return false
3845 }
3846 if (mention.link && mention.link[0] === '&' && mention.size == null) {
3847 var linkCb = done()
3848 self.app.sbot.blobs.size(mention.link, function (err, size) {
3849 if (!err && size != null) mention.size = size
3850 linkCb()
3851 })
3852 }
3853 return true
3854 })
3855 if (mentions.length) content.mentions = mentions
3856 if (data.recps != null) {
3857 if (opts.recps) return cb(new Error('got recps in opts and data'))
3858 content.recps = [myId]
3859 u.extractFeedIds(data.recps).forEach(function (recp) {
3860 if (content.recps.indexOf(recp) === -1) content.recps.push(recp)
3861 })
3862 } else {
3863 if (opts.recps) content.recps = opts.recps
3864 }
3865 if (data.fork_thread) {
3866 content.root = opts.post || undefined
3867 content.fork = opts.root || undefined
3868 content.branch = u.fromArray(opts.postBranches) || undefined
3869 } else {
3870 content.root = opts.root || undefined
3871 content.branch = u.fromArray(opts.branches) || undefined
3872 }
3873 if (data.close_issue) {
3874 content.issue = opts.root || undefined
3875 content.project = data.project || undefined
3876 content.repo = data.repo || undefined
3877 content.issues = [
3878 {
3879 link: opts.root,
3880 open: false
3881 }
3882 ]
3883 }
3884 if (data.content_warning) content.contentWarning = String(data.content_warning)
3885 if (channel) content.channel = data.channel
3886
3887 done(function (err) {
3888 cb(err, content)
3889 })
3890 }
3891
3892 function closeIssueCheckbox(cb) {
3893 var container = h('div')
3894 if (opts.root) self.getMsgDecryptedMaybeOoo(opts.root, function (err, rootMsg) {
3895 if (err) return console.trace(err), cb(null)
3896 var rootC = rootMsg && rootMsg.value.content && rootMsg.value.content
3897 if (!rootC) return cb(null)
3898 var canCloseIssue = rootC.type === 'issue'
3899 var canClosePR = rootC.type === 'pull-request'
3900 if (canCloseIssue || canClosePR) container.appendChild(h('label', {for: 'close_issue'},
3901 h('input', {id: 'close_issue', type: 'checkbox', name: 'close_issue', value: 'post', checked: data.close_issue || undefined}),
3902 ' close ' + (canClosePR ? 'pull-request' : 'issue'),
3903 rootC.project ? h('input', {type: 'hidden', name: 'project', value: rootC.project}) : '',
3904 rootC.repo ? h('input', {type: 'hidden', name: 'repo', value: rootC.repo}) : ''
3905 ))
3906 cb(null)
3907 })
3908 else cb(null)
3909 return container
3910 }
3911
3912 function preview(raw, cb) {
3913 var msgContainer = h('table.ssb-msgs')
3914 var contentInput = h('input', {type: 'hidden', name: 'content'})
3915 var warningsContainer = h('div')
3916 var sizeEl = h('span')
3917
3918 var content
3919 try { content = JSON.parse(data.text) }
3920 catch (err) {}
3921 if (content) gotContent(null, content)
3922 else prepareContent(gotContent)
3923
3924 function gotContent(err, _content) {
3925 if (err) return cb(err)
3926 content = _content
3927 if (data.save_draft) self.saveDraft(content, saved)
3928 else saved()
3929 }
3930
3931 function saved(err, draftId) {
3932 if (err) return cb(err)
3933
3934 if (draftId) {
3935 draftLinkContainer.childNodes = renderDraftLink(draftId)
3936 }
3937
3938 contentInput.value = JSON.stringify(content)
3939 var msg = {
3940 value: {
3941 author: myId,
3942 timestamp: Date.now(),
3943 content: content
3944 }
3945 }
3946 if (content.recps) msg.value.private = true
3947
3948 var warnings = []
3949 u.toLinkArray(content.mentions).forEach(function (link) {
3950 if (link.emoji && link.size >= 10e3) {
3951 warnings.push(h('li',
3952 'emoji ', h('q', link.name),
3953 ' (', h('code', String(link.link).substr(0, 8) + '…'), ')'
3954 + ' is >10KB'))
3955 } else if (link.link[0] === '&' && link.size >= 10e6 && link.type) {
3956 // if link.type is set, we probably just uploaded this blob
3957 warnings.push(h('li',
3958 'attachment ',
3959 h('code', String(link.link).substr(0, 8) + '…'),
3960 ' is >10MB'))
3961 }
3962 })
3963
3964 var estSize = u.estimateMessageSize(content)
3965 sizeEl.innerHTML = self.app.render.formatSize(estSize)
3966 if (estSize > 8192) warnings.push(h('li', 'message is too long'))
3967
3968 if (warnings.length) {
3969 warningsContainer.appendChild(h('div', h('em', 'warning:')))
3970 warningsContainer.appendChild(h('ul.mentions', warnings))
3971 }
3972
3973 pull(
3974 pull.once(msg),
3975 self.app.unboxMessages(),
3976 self.app.render.renderFeeds({
3977 raw: raw,
3978 dualMarkdown: self.query.dualMd != null ? self.query.dualMd :
3979 self.conf.dualMarkdownPreview,
3980 serve: self,
3981 filter: self.query.filter,
3982 }),
3983 pull.drain(function (el) {
3984 msgContainer.appendChild(h('tbody', el))
3985 }, cb)
3986 )
3987 }
3988
3989 return [
3990 contentInput,
3991 opts.redirectToPublishedMsg ? h('input', {type: 'hidden',
3992 name: 'redirect_to_published_msg', value: '1'}) : '',
3993 warningsContainer,
3994 h('div', h('em', 'draft:'), ' ', sizeEl),
3995 msgContainer,
3996 h('div.composer-actions',
3997 h('input', {type: 'submit', name: 'action', value: 'publish'})
3998 )
3999 ]
4000 }
4001
4002}
4003
4004Serve.prototype.phPreview = function (content, opts) {
4005 var self = this
4006 var msg = {
4007 value: {
4008 author: this.app.sbot.id,
4009 timestamp: Date.now(),
4010 content: content
4011 }
4012 }
4013 opts = opts || {}
4014 if (content.recps) msg.value.private = true
4015 var warnings = []
4016 var estSize = u.estimateMessageSize(content)
4017 if (estSize > 8192) warnings.push(ph('li', 'message is too long'))
4018
4019 return ph('form', {action: '', method: 'post'}, [
4020 ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}),
4021 warnings.length ? [
4022 ph('div', ph('em', 'warning:')),
4023 ph('ul', {class: 'mentions'}, warnings)
4024 ] : '',
4025 ph('div', [
4026 ph('em', 'draft:'), ' ',
4027 u.escapeHTML(this.app.render.formatSize(estSize))
4028 ]),
4029 ph('table', {class: 'ssb-msgs'}, pull(
4030 pull.once(msg),
4031 this.app.unboxMessages(),
4032 this.app.render.renderFeeds({
4033 serve: self,
4034 raw: opts.raw,
4035 filter: this.query.filter,
4036 }),
4037 pull.map(u.toHTML)
4038 )),
4039 ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}),
4040 ph('div', {class: 'composer-actions'}, [
4041 ph('input', {type: 'submit', name: 'action', value: 'publish'})
4042 ])
4043 ])
4044}
4045
4046Serve.prototype.phMsgActions = function (content) {
4047 var self = this
4048 var data = self.data
4049
4050 function renderDraftLink(draftId) {
4051 return pull.values([
4052 ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)),
4053 title: 'draft link'}, u.escapeHTML(draftId)),
4054 ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ',
4055 ])
4056 }
4057
4058 return [
4059 ph('input', {type: 'hidden', name: 'url', value: self.req.url}),
4060 ph('p', {class: 'msg-right'}, [
4061 data.save_draft && content ? u.readNext(function (cb) {
4062 self.saveDraft(content, function (err, draftId) {
4063 if (err) return cb(err)
4064 cb(null, renderDraftLink(draftId))
4065 })
4066 }) : data.draft_id ? renderDraftLink(data.draft_id) : '',
4067 ph('label', {for: 'save_draft'}, [
4068 ph('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1',
4069 checked: data.save_draft || data.restored_draft ? 'checked' : undefined}),
4070 ' save draft '
4071 ]),
4072 ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ',
4073 ph('input', {type: 'submit', name: 'preview', value: 'Preview'}),
4074 ])
4075 ]
4076}
4077
4078function hashBuf(buf) {
4079 var hash = crypto.createHash('sha256')
4080 hash.update(buf)
4081 return '&' + hash.digest('base64') + '.sha256'
4082}
4083
4084Serve.prototype.getBuiltinEmojiLink = function (name) {
4085 if (!(name in emojis)) return
4086 var file = path.join(emojiDir, name + '.png')
4087 var fileBuf = fs.readFileSync(file)
4088 var id = hashBuf(fileBuf)
4089 // seed the builtin emoji
4090 pull(pull.once(fileBuf), this.app.sbot.blobs.add(id, function (err) {
4091 if (err) console.error('error adding builtin emoji as blob', err)
4092 }))
4093 return {
4094 link: id,
4095 type: 'image/png',
4096 size: fileBuf.length,
4097 }
4098}
4099
4100Serve.prototype.getMsgDecryptedMaybeOoo = function (key, cb) {
4101 var self = this
4102 if (this.useOoo) this.app.getMsgDecryptedOoo(key, next)
4103 else this.app.getMsgDecrypted(key, next)
4104 function next(err, msg) {
4105 if (err) return cb(err)
4106 var c = msg && msg.value && msg.value.content
4107 if (typeof c === 'string' && self.query.unbox)
4108 self.app.unboxMsgWithKey(msg, String(self.query.unbox).replace(/ /g, '+'), cb)
4109 else cb(null, msg)
4110 }
4111}
4112
4113Serve.prototype.emojis = function (path) {
4114 var self = this
4115 var seen = {}
4116 pull(
4117 ph('section', [
4118 ph('h3', 'Emojis'),
4119 ph('ul', {class: 'mentions'}, pull(
4120 self.app.streamEmojis(),
4121 pull.map(function (emoji) {
4122 if (!seen[emoji.name]) {
4123 // cache the first use, so that our uses take precedence over other feeds'
4124 self.app.reverseEmojiNameCache.set(emoji.name, emoji.link)
4125 seen[emoji.name] = true
4126 }
4127 return ph('li', [
4128 ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)},
4129 ph('img', {
4130 class: 'ssb-emoji',
4131 src: self.app.render.imageUrl(emoji.link),
4132 size: 32,
4133 })
4134 ), ' ',
4135 u.escapeHTML(emoji.name)
4136 ])
4137 })
4138 ))
4139 ]),
4140 this.wrapPage('emojis'),
4141 this.respondSink(200)
4142 )
4143}
4144
4145Serve.prototype.editDiff = function (url) {
4146 var self = this
4147 var id
4148 try {
4149 id = decodeURIComponent(url.substr(1))
4150 } catch(err) {
4151 return pull(
4152 pull.once(u.renderError(err).outerHTML),
4153 self.wrapPage('diff: ' + id),
4154 self.respondSink(400)
4155 )
4156 }
4157 return pull(
4158 ph('section', {}, [
4159 'diff: ',
4160 ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')),
4161 u.readNext(function (cb) {
4162 self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
4163 if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
4164 var c = msg.value.content || {}
4165 self.getMsgDecryptedMaybeOoo(c.updated, function (err, oldMsg) {
4166 if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
4167 cb(null, self.textEditDiffTable(oldMsg, msg))
4168 })
4169 })
4170 })
4171 ]),
4172 self.wrapPage('diff: ' + id),
4173 self.respondSink(200)
4174 )
4175}
4176
4177function findMsg(msgs, id) {
4178 for (var i = 0; i < msgs.length; i++) {
4179 if (msgs[i].key === id) return i
4180 }
4181 return -1
4182}
4183
4184Serve.prototype.aboutDiff = function (url) {
4185 var self = this
4186 var id
4187 try {
4188 id = decodeURIComponent(url.substr(1))
4189 } catch(err) {
4190 return pull(
4191 pull.once(u.renderError(err).outerHTML),
4192 self.wrapPage('diff: ' + id),
4193 self.respondSink(400)
4194 )
4195 }
4196 return pull(
4197 ph('section', {}, [
4198 'diff: ',
4199 ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')),
4200 u.readNext(function (cb) {
4201 // About messages don't always include branch links. So get the whole thread
4202 // and use ssb-sort to find what to consider the previous message(s).
4203 self.getMsgDecryptedMaybeOoo(id, function (err, msg) {
4204 if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
4205 var c = msg.value.content || {}
4206 var rootId = c.about
4207 if (!rootId) return gotLinks(new Error('Missing about root'))
4208 var msgDate = new Date(msg.value.timestamp)
4209 cb(null, ph('div', [
4210 self.phIdLink(msg.value.author), ' ',
4211 ph('span', {title: msgDate.toLocaleString()}, htime(msgDate)),
4212 ph('div', u.readNext(next.bind(this, rootId, msg)))
4213 ]))
4214 })
4215 })
4216 ]),
4217 self.wrapPage('diff: ' + id),
4218 self.respondSink(200)
4219 )
4220
4221 function next(rootId, msg, cb) {
4222 pull(
4223 self.app.getLinks(rootId),
4224 pull.unique('key'),
4225 self.app.unboxMessages(),
4226 pull.collect(function (err, links) {
4227 if (err) return gotLinks(err)
4228 if (!self.useOoo) return gotLinks(null, links)
4229 self.app.expandOoo({msgs: links, dest: id}, gotLinks)
4230 })
4231 )
4232 function gotLinks(err, links) {
4233 if (err) return cb(null, pull.once(u.renderError(err).outerHTML))
4234
4235 sort(links)
4236 links = links.filter(function (msg) {
4237 var c = msg && msg.value && msg.value.content
4238 return c && c.type === 'about' && c.about === rootId
4239 && typeof c.description === 'string'
4240 })
4241 var i = findMsg(links, id)
4242 if (i < 0) return cb(null, ph('div', 'Unable to find previous message'))
4243 var prevMsg = links[i-1]
4244 var nextMsg = links[i+1]
4245 var prevHref = prevMsg ?
4246 self.app.render.toUrl('/about-diff/' + encodeURIComponent(prevMsg.key)) : null
4247 var nextHref = nextMsg ?
4248 self.app.render.toUrl('/about-diff/' + encodeURIComponent(nextMsg.key)) : null
4249 cb(null, cat([
4250 prevMsg
4251 ? pull.values(['prev: ', ph('a', {href: prevHref}, ph('code',
4252 prevMsg.key.substr(0, 8) + '…')), ', '])
4253 : pull.empty(),
4254 nextMsg
4255 ? pull.values(['next: ', ph('a', {href: nextHref}, ph('code',
4256 nextMsg.key.substr(0, 8) + '…'))])
4257 : pull.empty(),
4258 prevMsg || msg ? self.textEditDiffTable(prevMsg, msg) : pull.empty()
4259 ]))
4260 }
4261 }
4262}
4263
4264Serve.prototype.textEditDiffTable = function (oldMsg, newMsg) {
4265 var oldC = oldMsg && oldMsg.value.content || {}
4266 var newC = newMsg && newMsg.value.content || {}
4267 var oldText = String(oldC.text || oldC.description || '')
4268 var newText = String(newC.text || newC.description || '')
4269 var diff = Diff.structuredPatch('', '', oldText, newText)
4270 var self = this
4271 return pull(
4272 ph('table', [
4273 pull(
4274 pull.values(diff.hunks),
4275 pull.map(function (hunk) {
4276 var oldLine = hunk.oldStart
4277 var newLine = hunk.newStart
4278 return [
4279 ph('tr', [
4280 ph('td', {colspan: 2}),
4281 ph('td', ph('pre',
4282 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
4283 '+' + newLine + ',' + hunk.newLines + ' @@'))
4284 ]),
4285 pull(
4286 pull.values(hunk.lines),
4287 pull.map(function (line) {
4288 var s = line[0]
4289 if (s == '\\') return
4290 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
4291 return [
4292 ph('tr', {
4293 class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
4294 }, [
4295 lineNums.map(function (num, i) {
4296 return ph('td', String(num))
4297 }),
4298 ph('td', [
4299 ph('code', s),
4300 u.unwrapP(self.app.render.markdown(line.substr(1),
4301 s == '-' ? oldC.mentions : newC.mentions))
4302 ])
4303 ])
4304 ]
4305 })
4306 )
4307 ]
4308 })
4309 )
4310 ])
4311 )
4312}
4313
4314Serve.prototype.shard = function (url) {
4315 var self = this
4316 var id
4317 try {
4318 id = decodeURIComponent(url.substr(1))
4319 } catch(err) {
4320 return onError(err)
4321 }
4322 function onError(err) {
4323 pull(
4324 pull.once(u.renderError(err).outerHTML),
4325 self.wrapPage('shard: ' + id),
4326 self.respondSink(400)
4327 )
4328 }
4329 self.app.getShard(id, function (err, shard) {
4330 if (err) return onError(err)
4331 pull(
4332 pull.once(shard),
4333 self.respondSink(200, {'Content-Type': 'text/plain'})
4334 )
4335 })
4336}
4337
4338Serve.prototype.pub = function (path) {
4339 var self = this
4340 var id = String(path).substr(1)
4341 try { id = decodeURIComponent(id) }
4342 catch(e) {}
4343 pull(
4344 ph('section', [
4345 ph('h3', ['Pub addresses: ', self.phIdLink(id)]),
4346 pull(
4347 self.app.getAddresses(id),
4348 pull.map(function (address) {
4349 return ph('div', [
4350 ph('code', self.app.removeDefaultPort(address))
4351 ])
4352 })
4353 )
4354 ]),
4355 self.wrapPage('Addresses: ' + id),
4356 self.respondSink(200)
4357 )
4358}
4359
4360function hiddenInput(key, value) {
4361 return Array.isArray(value) ? value.map(function (value) {
4362 return ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)})
4363 }) : ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)})
4364}
4365
4366Serve.prototype.drafts = function (path) {
4367 var self = this
4368 var id = path && String(path).substr(1)
4369 if (id) try { id = decodeURIComponent(id) }
4370 catch(e) {}
4371
4372 if (id) {
4373 return pull(
4374 ph('section', [
4375 ph('h3', [
4376 ph('a', {href: self.app.render.toUrl('/drafts')}, 'Drafts'), ': ',
4377 ph('a', {href: ''}, u.escapeHTML(id))
4378 ]),
4379 u.readNext(function (cb) {
4380 if (self.data.draft_discard) {
4381 return self.app.discardDraft(id, function (err) {
4382 if (err) return cb(err)
4383 cb(null, ph('div', 'Discarded'))
4384 })
4385 }
4386 self.app.getDraft(id, function (err, draft) {
4387 if (err) return cb(err)
4388 var form = draft.form || {}
4389 var content = draft.content || {type: 'post', text: ''}
4390 var composerUrl = self.app.render.toUrl(draft.url)
4391 + (form.composer_id ? '#' + encodeURIComponent(form.composer_id) : '')
4392 cb(null, ph('div', [
4393 ph('table', ph('tr', [
4394 ph('td', ph('form', {method: 'post', action: u.escapeHTML(composerUrl)}, [
4395 hiddenInput('draft_id', id),
4396 hiddenInput('restored_draft', '1'),
4397 Object.keys(form).map(function (key) {
4398 if (key === 'draft_id' || key === 'save_draft') return ''
4399 return hiddenInput(key, draft.form[key])
4400 }),
4401 ph('input', {type: 'submit', name: 'draft_edit', value: 'Edit'})
4402 ])),
4403 ph('td', ph('form', {method: 'post', action: ''}, [
4404 ph('input', {type: 'submit', name: 'draft_discard', value: 'Discard',
4405 title: 'Discard draft'})
4406 ]))
4407 ])),
4408 self.phPreview(content, {draftId: id})
4409 ]))
4410 })
4411 })
4412 ]),
4413 self.wrapPage('draft: ' + id),
4414 self.respondSink(200)
4415 )
4416 }
4417
4418 return pull(
4419 ph('section', [
4420 ph('h3', 'Drafts'),
4421 ph('ul', pull(
4422 self.app.listDrafts(),
4423 pull.asyncMap(function (draft, cb) {
4424 var form = draft.form || {}
4425 var msg = {
4426 key: '/drafts/' + draft.id,
4427 value: {
4428 author: self.app.sbot.id,
4429 timestamp: Date.now(),
4430 content: draft.content || {type: 'post'}
4431 }
4432 }
4433 cb(null, ph('li', self.app.render.phMsgLink(msg)))
4434 })
4435 ))
4436 ]),
4437 self.wrapPage('drafts'),
4438 self.respondSink(200)
4439 )
4440}
4441

Built with git-ssb-web