git ssb

16+

cel / patchfoo



Tree: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018

Files: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018 / lib / serve.js

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

Built with git-ssb-web