git ssb

16+

cel / patchfoo



Tree: 8667de56c81425100b274655b183def02e92725c

Files: 8667de56c81425100b274655b183def02e92725c / lib / serve.js

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

Built with git-ssb-web