git ssb

16+

cel / patchfoo



Tree: 67dfa43e8df1dbf3a3238a452531c10da46325b8

Files: 67dfa43e8df1dbf3a3238a452531c10da46325b8 / lib / serve.js

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

Built with git-ssb-web