git ssb

16+

cel / patchfoo



Tree: e6c067de16888102cc4ced4fae9fcd2ffbc98ef9

Files: e6c067de16888102cc4ced4fae9fcd2ffbc98ef9 / lib / serve.js

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

Built with git-ssb-web