git ssb

30+

cel / git-ssb-web



Tree: f57770ffbcfa6ec977a801e02d0a5f97e0ae195f

Files: f57770ffbcfa6ec977a801e02d0a5f97e0ae195f / index.js

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

Built with git-ssb-web