git ssb

30+

cel / git-ssb-web



Tree: 51231092e514abc2c2cf7af606e1bf4e510f7f75

Files: 51231092e514abc2c2cf7af606e1bf4e510f7f75 / index.js

31096 bytesRaw
1var fs = require('fs')
2var http = require('http')
3var path = require('path')
4var url = require('url')
5var qs = require('querystring')
6var util = require('util')
7var ref = require('ssb-ref')
8var pull = require('pull-stream')
9var ssbGit = require('ssb-git-repo')
10var toPull = require('stream-to-pull-stream')
11var cat = require('pull-cat')
12var GitRepo = require('pull-git-repo')
13var u = require('./lib/util')
14var markdown = require('./lib/markdown')
15var paginate = require('pull-paginate')
16var asyncMemo = require('asyncmemo')
17var multicb = require('multicb')
18var schemas = require('ssb-msg-schemas')
19var Issues = require('ssb-issues')
20var PullRequests = require('ssb-pull-requests')
21var paramap = require('pull-paramap')
22var Mentions = require('ssb-mentions')
23var many = require('pull-many')
24var ident = require('pull-identify-filetype')
25var mime = require('mime-types')
26var moment = require('moment')
27var LRUCache = require('lrucache')
28
29var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
30var emojiPath = path.resolve(require.resolve('emoji-named-characters'), '../pngs')
31
32function ParamError(msg) {
33 var err = Error.call(this, msg)
34 err.name = ParamError.name
35 return err
36}
37util.inherits(ParamError, Error)
38
39function parseAddr(str, def) {
40 if (!str) return def
41 var i = str.lastIndexOf(':')
42 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
43 if (isNaN(str)) return {host: str, port: def.port}
44 return {host: def.host, port: str}
45}
46
47function tryDecodeURIComponent(str) {
48 if (!str || (str[0] == '%' && ref.isBlobId(str)))
49 return str
50 try {
51 str = decodeURIComponent(str)
52 } finally {
53 return str
54 }
55}
56
57function getContentType(filename) {
58 var ext = u.getExtension(filename)
59 return contentTypes[ext] || u.imgMimes[ext] || 'text/plain; charset=utf-8'
60}
61
62var contentTypes = {
63 css: 'text/css'
64}
65
66function readReqForm(req, cb) {
67 pull(
68 toPull(req),
69 pull.collect(function (err, bufs) {
70 if (err) return cb(err)
71 var data
72 try {
73 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
74 } catch(e) {
75 return cb(e)
76 }
77 cb(null, data)
78 })
79 )
80}
81
82var msgTypes = {
83 'git-repo': true,
84 'git-update': true,
85 'issue': true,
86 'pull-request': true
87}
88
89module.exports = {
90 name: 'git-ssb-web',
91 version: require('./package').version,
92 manifest: {},
93 init: function (ssb, config) {
94 var web = new GitSSBWeb(ssb, config)
95 return {}
96 }
97}
98
99function GitSSBWeb(ssb, config) {
100 this.ssb = ssb
101 this.config = config
102
103 if (config.logging && config.logging.level)
104 this.logLevel = this.logLevels.indexOf(config.logging.level)
105 this.ssbAppname = config.appname || 'ssb'
106 this.isPublic = config.public
107 this.getVotes = require('./lib/votes')(ssb)
108 this.getMsg = asyncMemo({cache: new LRUCache(100)}, this.getMsgRaw)
109 this.issues = Issues.init(ssb)
110 this.pullReqs = PullRequests.init(ssb)
111 this.getRepo = asyncMemo({
112 cache: new LRUCache(32)
113 }, function (id, cb) {
114 if (id[0] === '#') return ssbGit.getRepo(ssb, id, {live: true}, cb)
115 this.getMsg(id, function (err, msg) {
116 if (err) return cb(err)
117 if (msg.private && this.isPublic) return cb(new Error('Private Repo'))
118 ssbGit.getRepo(ssb, msg, {live: true}, cb)
119 })
120 })
121
122 this.about = function (id, cb) { cb(null, {name: id}) }
123 ssb.whoami(function (err, feed) {
124 this.myId = feed.id
125 this.about = require('./lib/about')(ssb, this.myId)
126 }.bind(this))
127
128 this.i18n = require('./lib/i18n')(path.join(__dirname, 'locale'), 'en')
129 this.users = require('./lib/users')(this)
130 this.repos = require('./lib/repos')(this)
131
132 var webConfig = config['git-ssb-web'] || {}
133
134 if (webConfig.computeIssueCounts !== false) {
135 this.indexCache = require('./lib/index-cache')(ssb)
136 }
137
138 this.serveAcmeChallenge = require('./lib/acme-challenge')(ssb)
139
140 var addr = parseAddr(config.listenAddr, {
141 host: webConfig.host || 'localhost',
142 port: webConfig.port || 7718
143 })
144 this.listen(addr.host, addr.port)
145
146 this.monitorSsbClient()
147}
148
149var G = GitSSBWeb.prototype
150
151G.logLevels = ['error', 'warning', 'notice', 'info']
152G.logLevel = G.logLevels.indexOf('notice')
153
154G.log = function (level) {
155 if (this.logLevels.indexOf(level) > this.logLevel) return
156 console.log.apply(console, [].slice.call(arguments, 1))
157}
158
159G.listen = function (host, port) {
160 this.httpServer = http.createServer(G_onRequest.bind(this))
161 this.httpServer.listen(port, host, function () {
162 var hostName = ~host.indexOf(':') ? '[' + host + ']' : host
163 this.log('notice', 'Listening on http://' + hostName + ':' + port + '/')
164 }.bind(this))
165}
166
167G.getRepoName = function (ownerId, repoId, cb) {
168 if (!repoId) return cb(null, '?')
169 if (repoId[0] === '#') return cb(null, repoId)
170 this.about.getName({
171 owner: ownerId,
172 target: repoId
173 }, cb)
174}
175
176G.getRepoFullName = function (author, repoId, cb) {
177 var done = multicb({ pluck: 1, spread: true })
178 this.getRepoName(author, repoId, done())
179 this.about.getName(author, done())
180 done(cb)
181}
182
183G.addAuthorName = function () {
184 var about = this.about
185 return paramap(function (msg, cb) {
186 var author = msg && msg.value && msg.value.author
187 if (!author) return cb(null, msg)
188 about.getName(author, function (err, authorName) {
189 msg.authorName = authorName
190 cb(err, msg)
191 })
192 }, 8)
193}
194
195/* Serving a request */
196
197function serve(req, res) {
198 return pull(
199 pull.filter(function (data) {
200 if (Array.isArray(data)) {
201 res.writeHead.apply(res, data)
202 return false
203 }
204 return true
205 }),
206 toPull(res)
207 )
208}
209
210function G_onRequest(req, res) {
211 this.log('info', req.method, req.url)
212
213 if (req.url.startsWith('/.well-known/acme-challenge'))
214 return this.serveAcmeChallenge(req, res)
215
216 req._u = url.parse(req.url, true)
217 var locale = req._u.query.locale ||
218 (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
219 var reqLocales = req.headers['accept-language']
220 var locales = reqLocales ? reqLocales.split(/, */).map(function (item) {
221 return item.split(';')[0]
222 }) : []
223 req._locale = locales[0] || locale || this.i18n.fallback
224
225 this.i18n.pickCatalog(reqLocales, locale, function (err, t) {
226 if (err) return pull(this.serveError(req, err, 500), serve(req, res))
227 req._t = t
228 pull(this.handleRequest(req), serve(req, res))
229 }.bind(this))
230}
231
232G.handleRequest = function (req) {
233 var path = req._u.pathname.slice(1)
234 var dirs = ref.isLink(path) ? [path] :
235 path.split(/\/+/).map(tryDecodeURIComponent)
236 var dir = dirs[0]
237
238 if (req.method == 'POST')
239 return this.handlePOST(req, dir)
240
241 if (dir == '')
242 return this.serveIndex(req)
243 else if (dir == 'search')
244 return this.serveSearch(req)
245 else if (dir[0] === '#')
246 return this.serveChannel(req, dir, dirs.slice(1))
247 else if (ref.isBlobId(dir))
248 return this.serveBlob(req, dir)
249 else if (ref.isMsgId(dir))
250 return this.serveMessage(req, dir, dirs.slice(1))
251 else if (ref.isFeedId(dir))
252 return this.users.serveUserPage(req, dir, dirs.slice(1))
253 else if (dir == 'static')
254 return this.serveFile(req, dirs)
255 else if (dir == 'highlight')
256 return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
257 else if (dir == 'emoji')
258 return this.serveFile(req, [emojiPath].concat(dirs.slice(1)), true)
259 else
260 return this.serve404(req)
261}
262
263G.handlePOST = function (req, dir) {
264 var self = this
265 if (self.isPublic)
266 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
267 return u.readNext(function (cb) {
268 readReqForm(req, function (err, data) {
269 if (err) return cb(null, self.serveError(req, err, 400))
270 if (!data) return cb(null, self.serveError(req,
271 new ParamError(req._t('error.MissingData')), 400))
272
273 switch (data.action) {
274 case 'fork-prompt':
275 return cb(null, self.serveRedirect(req,
276 u.encodeLink([data.id, 'fork'])))
277
278 case 'fork':
279 if (!data.id)
280 return cb(null, self.serveError(req,
281 new ParamError(req._t('error.MissingId')), 400))
282 return ssbGit.createRepo(self.ssb, {upstream: data.id},
283 function (err, repo) {
284 if (err) return cb(null, self.serveError(req, err))
285 cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
286 })
287
288 case 'vote':
289 var voteValue = +data.value || 0
290 if (!data.id)
291 return cb(null, self.serveError(req,
292 new ParamError(req._t('error.MissingId')), 400))
293 var msg = schemas.vote(data.id, voteValue)
294 return self.ssb.publish(msg, function (err) {
295 if (err) return cb(null, self.serveError(req, err))
296 cb(null, self.serveRedirect(req, req.url))
297 })
298
299 case 'repo-name':
300 if (!data.id)
301 return cb(null, self.serveError(req,
302 new ParamError(req._t('error.MissingId')), 400))
303 if (!data.name)
304 return cb(null, self.serveError(req,
305 new ParamError(req._t('error.MissingName')), 400))
306 var msg = schemas.name(data.id, data.name)
307 return self.ssb.publish(msg, function (err) {
308 if (err) return cb(null, self.serveError(req, err))
309 cb(null, self.serveRedirect(req, req.url))
310 })
311
312 case 'comment':
313 if (!data.id)
314 return cb(null, self.serveError(req,
315 new ParamError(req._t('error.MissingId')), 400))
316 var msg = schemas.post(data.text, data.id, data.branch || data.id)
317 msg.issue = data.issue
318 msg.repo = data.repo
319 if (data.open != null)
320 Issues.schemas.reopens(msg, data.id)
321 if (data.close != null)
322 Issues.schemas.closes(msg, data.id)
323 var mentions = Mentions(data.text)
324 if (mentions.length)
325 msg.mentions = mentions
326 return self.ssb.publish(msg, function (err) {
327 if (err) return cb(null, self.serveError(req, err))
328 cb(null, self.serveRedirect(req, req.url))
329 })
330
331 case 'new-issue':
332 var msg = Issues.schemas.new(dir, data.text)
333 var mentions = Mentions(data.text)
334 if (mentions.length)
335 msg.mentions = mentions
336 return self.ssb.publish(msg, function (err, msg) {
337 if (err) return cb(null, self.serveError(req, err))
338 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
339 })
340
341 case 'new-pull':
342 var msg = PullRequests.schemas.new(dir, data.branch,
343 data.head_repo, data.head_branch, data.text)
344 var mentions = Mentions(data.text)
345 if (mentions.length)
346 msg.mentions = mentions
347 return self.ssb.publish(msg, function (err, msg) {
348 if (err) return cb(null, self.serveError(req, err))
349 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
350 })
351
352 case 'markdown':
353 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
354
355 default:
356 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
357 }
358 })
359 })
360}
361
362G.serveFile = function (req, dirs, outside) {
363 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
364 // prevent escaping base dir
365 if (!outside && filename.indexOf('../') === 0)
366 return this.serveBuffer(403, req._t("error.403Forbidden"))
367
368 return u.readNext(function (cb) {
369 fs.stat(filename, function (err, stats) {
370 cb(null, err ?
371 err.code == 'ENOENT' ? this.serve404(req)
372 : this.serveBuffer(500, err.message)
373 : u.ifModifiedSince(req, stats.mtime) ?
374 pull.once([304])
375 : stats.isDirectory() ?
376 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
377 : cat([
378 pull.once([200, {
379 'Content-Type': getContentType(filename),
380 'Content-Length': stats.size,
381 'Last-Modified': stats.mtime.toGMTString()
382 }]),
383 toPull(fs.createReadStream(filename))
384 ]))
385 }.bind(this))
386 }.bind(this))
387}
388
389G.serveBuffer = function (code, buf, contentType, headers) {
390 headers = headers || {}
391 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
392 headers['Content-Length'] = Buffer.byteLength(buf)
393 return pull.values([
394 [code, headers],
395 buf
396 ])
397}
398
399G.serve404 = function (req) {
400 return this.serveBuffer(404, req._t("error.404NotFound"))
401}
402
403G.serveRedirect = function (req, path) {
404 return this.serveBuffer(302,
405 '<!doctype><html><head>' +
406 '<title>' + req._t('Redirect') + '</title></head><body>' +
407 '<p><a href="' + u.escape(path) + '">' +
408 req._t('Continue') + '</a></p>' +
409 '</body></html>', 'text/html; charset=utf-8', {Location: path})
410}
411
412G.serveMarkdown = function (text, repo) {
413 return this.serveBuffer(200, markdown(text, repo),
414 'text/html; charset=utf-8')
415}
416
417G.renderError = function (err, tag) {
418 tag = tag || 'h3'
419 return '<' + tag + '>' + err.name + '</' + tag + '>' +
420 '<pre>' + u.escape(err.stack) + '</pre>'
421}
422
423G.renderTry = function (read) {
424 var self = this
425 var ended
426 return function (end, cb) {
427 if (ended) return cb(ended)
428 read(end, function (err, data) {
429 if (err === true)
430 cb(true)
431 else if (err) {
432 ended = true
433 cb(null, self.renderError(err))
434 } else
435 cb(null, data)
436 })
437 }
438}
439
440G.serveTemplate = function (req, title, code, read) {
441 var self = this
442 if (read === undefined)
443 return this.serveTemplate.bind(this, req, title, code)
444 var q = req._u.query.q && u.escape(req._u.query.q) || ''
445 var app = 'git ssb'
446 var appName = this.ssbAppname
447 if (req._t) app = req._t(app)
448 return cat([
449 pull.values([
450 [code || 200, {
451 'Content-Type': 'text/html'
452 }],
453 '<!doctype html><html><head><meta charset=utf-8>',
454 '<title>' + app + (title != undefined ? ' - ' + title : '') + '</title>',
455 '<link rel=stylesheet href="/static/styles.css"/>',
456 '<link rel=stylesheet href="/highlight/foundation.css"/>',
457 '</head>\n',
458 '<body>',
459 '<header>'
460 ]),
461 self.isPublic ? null : u.readOnce(function (cb) {
462 self.about(self.myId, function (err, about) {
463 if (err) return cb(err)
464 cb(null,
465 '<a href="' + u.encodeLink(self.myId) + '">' +
466 (about.image ?
467 '<img class="profile-icon icon-right"' +
468 ' src="/' + encodeURIComponent(about.image) + '"' +
469 ' alt="' + u.escape(about.name) + '">' : u.escape(about.name)) +
470 '</a>')
471 })
472 }),
473 pull.once(
474 '<form action="/search" method="get">' +
475 '<h1><a href="/">' + app +
476 (appName == 'ssb' ? '' : ' <sub>' + appName + '</sub>') +
477 '</a></h1> ' +
478 '<input class="search-bar" name="q" size="60"' +
479 ' placeholder=" Search" value="' + q + '" />' +
480 '</form>' +
481 '</header>' +
482 '<article><hr />'),
483 this.renderTry(read),
484 pull.once('<hr/><p style="font-size: .8em;">Built with <a href="http://git-ssb.celehner.com">git-ssb-web</a></p></article></body></html>')
485 ])
486}
487
488G.serveError = function (req, err, status) {
489 return pull(
490 pull.once(this.renderError(err, 'h2')),
491 this.serveTemplate(req, err.name, status || 500)
492 )
493}
494
495G.renderObjectData = function (obj, filename, repo, rev, path) {
496 var ext = u.getExtension(filename)
497 return u.readOnce(function (cb) {
498 u.readObjectString(obj, function (err, buf) {
499 buf = buf.toString('utf8')
500 if (err) return cb(err)
501 cb(null, (ext == 'md' || ext == 'markdown')
502 ? markdown(buf, {repo: repo, rev: rev, path: path})
503 : buf.length > 1000000 ? ''
504 : renderCodeTable(buf, ext))
505 })
506 })
507}
508
509function renderCodeTable(buf, ext) {
510 return '<pre><table class="code">' +
511 u.highlight(buf, ext).split('\n').map(function (line, i) {
512 i++
513 return '<tr id="L' + i + '">' +
514 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
515 '<td class="code-text">' + line + '</td></tr>'
516 }).join('') +
517 '</table></pre>'
518}
519
520/* Feed */
521
522G.renderFeed = function (req, feedId, filter) {
523 var query = req._u.query
524 var opts = {
525 reverse: !query.forwards,
526 lt: query.lt && +query.lt || Date.now(),
527 gt: query.gt ? +query.gt : -Infinity,
528 id: feedId
529 }
530 return pull(
531 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
532 u.decryptMessages(this.ssb),
533 u.readableMessages(),
534 pull.filter(function (msg) {
535 var c = msg.value.content
536 return c.type in msgTypes
537 || (c.type == 'post' && c.repo && c.issue)
538 }),
539 typeof filter == 'function' ? filter(opts) : filter,
540 pull.take(100),
541 this.addAuthorName(),
542 query.forwards && u.pullReverse(),
543 paginate(
544 function (first, cb) {
545 if (!query.lt && !query.gt) return cb(null, '')
546 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
547 query.gt = gt
548 query.forwards = 1
549 delete query.lt
550 cb(null, '<a href="?' + qs.stringify(query) + '">' +
551 req._t('Next') + '</a>')
552 },
553 paramap(this.renderFeedItem.bind(this, req), 8),
554 function (last, cb) {
555 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
556 delete query.gt
557 delete query.forwards
558 cb(null, '<a href="?' + qs.stringify(query) + '">' +
559 req._t('Previous') + '</a>')
560 },
561 function (cb) {
562 if (query.forwards) {
563 delete query.gt
564 delete query.forwards
565 query.lt = opts.gt + 1
566 } else {
567 delete query.lt
568 query.gt = opts.lt - 1
569 query.forwards = 1
570 }
571 cb(null, '<a href="?' + qs.stringify(query) + '">' +
572 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
573 }
574 )
575 )
576}
577
578G.renderFeedItem = function (req, msg, cb) {
579 var self = this
580 var c = msg.value.content
581 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
582 var msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
583 var author = msg.value.author
584 var authorLink = u.link([msg.value.author], msg.authorName)
585 var privateIconMaybe = msg.value.private ? ' ' + u.privateIcon(req) : ''
586 switch (c.type) {
587 case 'git-repo':
588 var done = multicb({ pluck: 1, spread: true })
589 self.getRepoName(author, msg.key, done())
590 if (c.upstream) {
591 return self.getMsg(c.upstream, function (err, upstreamMsg) {
592 if (err) return cb(null, self.serveError(req, err))
593 self.getRepoName(upstreamMsg.value.author, c.upstream, done())
594 done(function (err, repoName, upstreamName) {
595 cb(null, '<section class="collapse">' +
596 req._t('Forked', {
597 name: authorLink,
598 upstream: u.link([c.upstream], upstreamName),
599 repo: u.link([msg.key], repoName)
600 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
601 })
602 })
603 } else {
604 return done(function (err, repoName) {
605 if (err) return cb(err)
606 var repoLink = u.link([msg.key], repoName)
607 cb(null, '<section class="collapse">' +
608 req._t('CreatedRepo', {
609 name: authorLink,
610 repo: repoLink
611 }) + ' ' + msgDateLink + privateIconMaybe +
612 (msg.value.private ?
613 '<br>' + req._t('repo.Recipients') + '<ul>' +
614 (Array.isArray(c.recps) ? c.recps : []).map(function (feed) {
615 return '<li>' + u.link([feed], feed) + '</li>'
616 }).join('') + '</ul>'
617 : '') +
618 '</section>')
619 })
620 }
621 case 'git-update':
622 return self.getRepoName(author, c.repo, function (err, repoName) {
623 if (err) return cb(err)
624 var repoLink = u.link([c.repo], repoName)
625 cb(null, '<section class="collapse">' +
626 req._t('Pushed', {
627 name: authorLink,
628 repo: repoLink
629 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
630 })
631 case 'issue':
632 case 'pull-request':
633 var issueLink = u.link([msg.key], u.messageTitle(msg))
634 // TODO: handle hashtag in project property
635 return self.getMsg(c.project, function (err, projectMsg) {
636 if (err) return cb(null,
637 self.repos.serveRepoNotFound(req, c.repo, err))
638 self.getRepoName(projectMsg.value.author, c.project,
639 function (err, repoName) {
640 if (err) return cb(err)
641 var repoLink = u.link([c.project], repoName)
642 cb(null, '<section class="collapse">' +
643 req._t('OpenedIssue', {
644 name: authorLink,
645 type: req._t(c.type == 'pull-request' ?
646 'pull request' : 'issue.'),
647 title: issueLink,
648 project: repoLink
649 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
650 })
651 })
652 case 'about':
653 return cb(null, '<section class="collapse">' +
654 req._t('Named', {
655 author: authorLink,
656 target: '<tt>' + u.escape(c.about) + '</tt>',
657 name: u.link([c.about], c.name)
658 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
659 case 'post':
660 return this.pullReqs.get(c.issue, function (err, pr) {
661 if (err) return cb(err)
662 var type = pr.msg.value.content.type == 'pull-request' ?
663 'pull request' : 'issue.'
664 var changed = self.issues.isStatusChanged(msg, pr)
665 return cb(null, '<section class="collapse">' +
666 req._t(changed == null ? 'CommentedOn' :
667 changed ? 'ReopenedIssue' : 'ClosedIssue', {
668 name: authorLink,
669 type: req._t(type),
670 title: u.link([pr.id], pr.title, true)
671 }) + ' ' + msgDateLink + privateIconMaybe +
672 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
673 '</section>')
674 })
675 default:
676 return cb(null, u.json(msg))
677 }
678}
679
680/* Index */
681
682G.serveIndex = function (req) {
683 return this.serveTemplate(req)(this.renderFeed(req))
684}
685
686G.serveChannel = function (req, id, path) {
687 var self = this
688 return u.readNext(function (cb) {
689 self.getRepo(id, function (err, repo) {
690 if (err) return cb(null, self.serveError(req, err))
691 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
692 })
693 })
694}
695
696G.serveMessage = function (req, id, path) {
697 var self = this
698 return u.readNext(function (cb) {
699 self.getMsg(id, function (err, msg) {
700 if (err) return cb(null, self.serveError(req, err))
701 var c = msg && msg.value && msg.value.content || {}
702 switch (c.type) {
703 case 'git-repo':
704 return self.getRepo(id, function (err, repo) {
705 if (err) return cb(null, self.serveError(req, err))
706 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
707 })
708 case 'git-update':
709 return self.getRepo(c.repo, function (err, repo) {
710 if (err) return cb(null,
711 self.repos.serveRepoNotFound(req, c.repo, err))
712 cb(null, self.repos.serveRepoUpdate(req,
713 GitRepo(repo), msg, path))
714 })
715 case 'issue':
716 return self.getRepo(c.project, function (err, repo) {
717 if (err) return cb(null,
718 self.repos.serveRepoNotFound(req, c.project, err))
719 self.issues.get(id, function (err, issue) {
720 if (err) return cb(null, self.serveError(req, err))
721 cb(null, self.repos.issues.serveRepoIssue(req,
722 GitRepo(repo), issue, path))
723 })
724 })
725 case 'pull-request':
726 return self.getRepo(c.repo, function (err, repo) {
727 if (err) return cb(null,
728 self.repos.serveRepoNotFound(req, c.project, err))
729 self.pullReqs.get(id, function (err, pr) {
730 if (err) return cb(null, self.serveError(req, err))
731 cb(null, self.repos.pulls.serveRepoPullReq(req,
732 GitRepo(repo), pr, path))
733 })
734 })
735 case 'issue-edit':
736 if (ref.isMsgId(c.issue)) {
737 return self.pullReqs.get(c.issue, function (err, issue) {
738 if (err) return cb(err)
739 self.getRepo(issue.project, function (err, repo) {
740 if (err) {
741 if (!repo) return cb(null,
742 self.repos.serveRepoNotFound(req, c.repo, err))
743 return cb(null, self.serveError(req, err))
744 }
745 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
746 issue, path, id))
747 })
748 })
749 }
750 // fallthrough
751 case 'post':
752 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
753 // comment on an issue
754 var done = multicb({ pluck: 1, spread: true })
755 self.getRepo(c.repo, done())
756 self.pullReqs.get(c.issue, done())
757 return done(function (err, repo, issue) {
758 if (err) {
759 if (!repo) return cb(null,
760 self.repos.serveRepoNotFound(req, c.repo, err))
761 return cb(null, self.serveError(req, err))
762 }
763 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
764 issue, path, id))
765 })
766 } else if (ref.isMsgId(c.root)) {
767 // comment on issue from patchwork?
768 return self.getMsg(c.root, function (err, root) {
769 var rc = root.value && root.value.content && root.value.content
770 if (err) return cb(null, self.serveError(req, err))
771 var repoId = rc.repo || rc.project
772 if (!ref.isMsgId(repoId))
773 return cb(null, self.serveGenericMessage(req, msg, path))
774 self.getRepo(repoId, function (err, repo) {
775 if (err) return cb(null, self.serveError(req, err))
776 switch (rc && rc.type) {
777 case 'issue':
778 return self.issues.get(c.root, function (err, issue) {
779 if (err) return cb(null, self.serveError(req, err))
780 return cb(null,
781 self.repos.issues.serveRepoIssue(req,
782 GitRepo(repo), issue, path, id))
783 })
784 case 'pull-request':
785 return self.pullReqs.get(c.root, function (err, pr) {
786 if (err) return cb(null, self.serveError(req, err))
787 return cb(null,
788 self.repos.pulls.serveRepoPullReq(req,
789 GitRepo(repo), pr, path, id))
790 })
791 }
792 })
793 })
794 }
795 // fallthrough
796 default:
797 if (ref.isMsgId(c.repo))
798 return self.getRepo(c.repo, function (err, repo) {
799 if (err) return cb(null,
800 self.repos.serveRepoNotFound(req, c.repo, err))
801 cb(null, self.repos.serveRepoSomething(req,
802 GitRepo(repo), id, msg, path))
803 })
804 else
805 return cb(null, self.serveGenericMessage(req, msg, path))
806 }
807 })
808 })
809}
810
811G.serveGenericMessage = function (req, msg, path) {
812 return this.serveTemplate(req, msg.key)(pull.once(
813 '<section><h2>' + u.link([msg.key]) + '</h2>' +
814 u.json(msg.value) +
815 '</section>'))
816}
817
818/* Search */
819
820G.serveSearch = function (req) {
821 var self = this
822 var q = String(req._u.query.q || '')
823 if (!q) return this.serveIndex(req)
824 var qId = q.replace(/^ssb:\/*/, '')
825 if (ref.type(qId))
826 return this.serveRedirect(req, encodeURIComponent(qId))
827
828 var search = new RegExp(q, 'i')
829 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
830 this.renderFeed(req, null, function (opts) {
831 return function (read) {
832 return pull(
833 many([
834 self.getMsgs('about', opts),
835 read
836 ]),
837 pull.filter(function (msg) {
838 var c = msg.value.content
839 return (
840 search.test(msg.key) ||
841 c.text && search.test(c.text) ||
842 c.name && search.test(c.name) ||
843 c.title && search.test(c.title))
844 })
845 )
846 }
847 })
848 )
849}
850
851G.getMsgRaw = function (key, cb) {
852 var self = this
853 this.ssb.get(key, function (err, value) {
854 if (err) return cb(err)
855 u.decryptMessage(self.ssb, {key: key, value: value}, cb)
856 })
857}
858
859G.getMsgs = function (type, opts) {
860 return this.ssb.messagesByType({
861 type: type,
862 reverse: opts.reverse,
863 lt: opts.lt,
864 gt: opts.gt,
865 })
866}
867
868G.serveBlobNotFound = function (req, repoId, err) {
869 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
870 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
871 '<p>' + req._t('error.BlobNotFoundInRepo', {
872 repo: u.link([repoId])
873 }) + '</p>' +
874 '<pre>' + u.escape(err.stack) + '</pre>'
875 ))
876}
877
878G.serveRaw = function (length, contentType) {
879 var headers = {
880 'Content-Type': contentType || 'text/plain; charset=utf-8',
881 'Cache-Control': 'max-age=31536000'
882 }
883 if (length != null)
884 headers['Content-Length'] = length
885 return function (read) {
886 return cat([pull.once([200, headers]), read])
887 }
888}
889
890G.getBlob = function (req, key, cb) {
891 var blobs = this.ssb.blobs
892 // use size to check for blob's presence, since has or want may broadcast
893 blobs.size(key, function (err, size) {
894 if (typeof size === 'number') cb(null, blobs.get(key))
895 else blobs.want(key, function (err, got) {
896 if (err) cb(err)
897 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
898 else cb(null, blobs.get(key))
899 })
900 })
901}
902
903G.serveBlob = function (req, key) {
904 var self = this
905 return u.readNext(function (cb) {
906 self.getBlob(req, key, function (err, read) {
907 if (err) cb(null, self.serveError(req, err))
908 else if (!read) cb(null, self.serve404(req))
909 else cb(null, identToResp(read))
910 })
911 })
912}
913
914function identToResp(read) {
915 var ended, type, queue
916 var id = ident(function (_type) {
917 type = _type && mime.lookup(_type)
918 })(read)
919 return function (end, cb) {
920 if (ended) return cb(ended)
921 if (end) id(end, function (end) {
922 cb(end === true ? null : end)
923 })
924 else if (queue) {
925 var _queue = queue
926 queue = null
927 cb(null, _queue)
928 }
929 else if (!type)
930 id(null, function (end, data) {
931 if (ended = end) return cb(end)
932 queue = data
933 cb(null, [200, {
934 'Content-Type': type || 'text/plain; charset=utf-8',
935 'Cache-Control': 'max-age=31536000'
936 }])
937 })
938 else
939 id(null, cb)
940 }
941}
942
943G.monitorSsbClient = function () {
944 pull(
945 function (abort, cb) {
946 if (abort) throw abort
947 setTimeout(function () {
948 cb(null, 'keepalive')
949 }, 15e3)
950 },
951 this.ssb.gossip.ping(),
952 pull.drain(null, function (err) {
953 // exit when the rpc connection ends
954 if (err) console.error(err)
955 console.error('sbot client connection closed. aborting')
956 process.exit(1)
957 })
958 )
959}
960

Built with git-ssb-web