git ssb

16+

cel / patchfoo



Tree: b8cd4fd2dcbbaf47e2a4167f169abeaa59d46173

Files: b8cd4fd2dcbbaf47e2a4167f169abeaa59d46173 / lib / serve.js

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

Built with git-ssb-web