git ssb

30+

cel / git-ssb-web



Tree: e279da804093d1c3a4958b93bec995d7da77fd5b

Files: e279da804093d1c3a4958b93bec995d7da77fd5b / index.js

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

Built with git-ssb-web