git ssb

16+

cel / patchfoo



Tree: 01ea88684375793b3e009d8cefb9722384e2c88b

Files: 01ea88684375793b3e009d8cefb9722384e2c88b / lib / serve.js

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

Built with git-ssb-web