git ssb

16+

cel / patchfoo



Tree: 0bd830cf4ebf9cf3e4df1f2dba4b851b9361fc14

Files: 0bd830cf4ebf9cf3e4df1f2dba4b851b9361fc14 / lib / serve.js

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

Built with git-ssb-web