git ssb

30+

cel / git-ssb-web



Tree: 6f7dd06d268c0d68a909b14db1147dfea7f3d8b8

Files: 6f7dd06d268c0d68a909b14db1147dfea7f3d8b8 / index.js

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

Built with git-ssb-web