git ssb

30+

cel / git-ssb-web



Tree: 1a34953425dcdb8f86d953dd87bc0b7e4fa03b13

Files: 1a34953425dcdb8f86d953dd87bc0b7e4fa03b13 / index.js

34225 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 'line-comment':
332 if (!data.repo)
333 return cb(null, self.serveError(req,
334 new ParamError('missing repo id'), 400))
335 if (!data.commitId)
336 return cb(null, self.serveError(req,
337 new ParamError('missing commit id'), 400))
338 if (!data.updateId)
339 return cb(null, self.serveError(req,
340 new ParamError('missing update id'), 400))
341 if (!data.filePath)
342 return cb(null, self.serveError(req,
343 new ParamError('missing file path'), 400))
344 if (!data.line)
345 return cb(null, self.serveError(req,
346 new ParamError('missing line number'), 400))
347 var lineNumber = Number(data.line)
348 if (isNaN(lineNumber))
349 return cb(null, self.serveError(req,
350 new ParamError('bad line number'), 400))
351 var msg = {
352 type: 'line-comment',
353 text: data.text,
354 repo: data.repo,
355 updateId: data.updateId,
356 commitId: data.commitId,
357 filePath: data.filePath,
358 line: lineNumber,
359 }
360 msg.issue = data.issue
361 var mentions = Mentions(data.text)
362 if (mentions.length)
363 msg.mentions = mentions
364 return self.ssb.publish(msg, function (err) {
365 if (err) return cb(null, self.serveError(req, err))
366 cb(null, self.serveRedirect(req, req.url))
367 })
368
369 case 'line-comment-reply':
370 if (!data.root)
371 return cb(null, self.serveError(req,
372 new ParamError('missing thread root'), 400))
373 if (!data.branch)
374 return cb(null, self.serveError(req,
375 new ParamError('missing thread branch'), 400))
376 if (!data.text)
377 return cb(null, self.serveError(req,
378 new ParamError('missing post text'), 400))
379 var msg = {
380 type: 'post',
381 root: data.root,
382 branch: data.branch,
383 text: data.text,
384 }
385 var mentions = Mentions(data.text)
386 if (mentions.length)
387 msg.mentions = mentions
388 return self.ssb.publish(msg, function (err) {
389 if (err) return cb(null, self.serveError(req, err))
390
391 cb(null, self.serveRedirect(req, req.url))
392 })
393
394 case 'new-issue':
395 var msg = Issues.schemas.new(dir, data.text)
396 var mentions = Mentions(data.text)
397 if (mentions.length)
398 msg.mentions = mentions
399 return self.ssb.publish(msg, function (err, msg) {
400 if (err) return cb(null, self.serveError(req, err))
401 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
402 })
403
404 case 'new-pull':
405 var msg = PullRequests.schemas.new(dir, data.branch,
406 data.head_repo, data.head_branch, data.text)
407 var mentions = Mentions(data.text)
408 if (mentions.length)
409 msg.mentions = mentions
410 return self.ssb.publish(msg, function (err, msg) {
411 if (err) return cb(null, self.serveError(req, err))
412 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
413 })
414
415 case 'markdown':
416 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
417
418 default:
419 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
420 }
421 })
422 })
423}
424
425G.serveFile = function (req, dirs, outside) {
426 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
427 // prevent escaping base dir
428 if (!outside && filename.indexOf('../') === 0)
429 return this.serveBuffer(403, req._t("error.403Forbidden"))
430
431 return u.readNext(function (cb) {
432 fs.stat(filename, function (err, stats) {
433 cb(null, err ?
434 err.code == 'ENOENT' ? this.serve404(req)
435 : this.serveBuffer(500, err.message)
436 : u.ifModifiedSince(req, stats.mtime) ?
437 pull.once([304])
438 : stats.isDirectory() ?
439 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
440 : cat([
441 pull.once([200, {
442 'Content-Type': getContentType(filename),
443 'Content-Length': stats.size,
444 'Last-Modified': stats.mtime.toGMTString()
445 }]),
446 toPull(fs.createReadStream(filename))
447 ]))
448 }.bind(this))
449 }.bind(this))
450}
451
452G.serveBuffer = function (code, buf, contentType, headers) {
453 headers = headers || {}
454 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
455 headers['Content-Length'] = Buffer.byteLength(buf)
456 return pull.values([
457 [code, headers],
458 buf
459 ])
460}
461
462G.serve404 = function (req) {
463 return this.serveBuffer(404, req._t("error.404NotFound"))
464}
465
466G.serveRedirect = function (req, path) {
467 return this.serveBuffer(302,
468 '<!doctype><html><head>' +
469 '<title>' + req._t('Redirect') + '</title></head><body>' +
470 '<p><a href="' + u.escape(path) + '">' +
471 req._t('Continue') + '</a></p>' +
472 '</body></html>', 'text/html; charset=utf-8', {Location: path})
473}
474
475G.serveMarkdown = function (text, repo) {
476 return this.serveBuffer(200, markdown(text, repo),
477 'text/html; charset=utf-8')
478}
479
480G.renderError = function (err, tag) {
481 tag = tag || 'h3'
482 return '<' + tag + '>' + err.name + '</' + tag + '>' +
483 '<pre>' + u.escape(err.stack) + '</pre>'
484}
485
486G.renderTry = function (read) {
487 var self = this
488 var ended
489 return function (end, cb) {
490 if (ended) return cb(ended)
491 read(end, function (err, data) {
492 if (err === true)
493 cb(true)
494 else if (err) {
495 ended = true
496 cb(null, self.renderError(err))
497 } else
498 cb(null, data)
499 })
500 }
501}
502
503G.serveTemplate = function (req, title, code, read) {
504 var self = this
505 if (read === undefined)
506 return this.serveTemplate.bind(this, req, title, code)
507 var q = req._u.query.q && u.escape(req._u.query.q) || ''
508 var app = 'git ssb'
509 var appName = this.ssbAppname
510 if (req._t) app = req._t(app)
511 return cat([
512 pull.values([
513 [code || 200, {
514 'Content-Type': 'text/html'
515 }],
516 '<!doctype html><html><head><meta charset=utf-8>',
517 '<title>' + app + (title != undefined ? ' - ' + title : '') + '</title>',
518 '<link rel=stylesheet href="/static/styles.css"/>',
519 '<link rel=stylesheet href="/highlight/foundation.css"/>',
520 '</head>\n',
521 '<body>',
522 '<header>'
523 ]),
524 self.isPublic ? null : u.readOnce(function (cb) {
525 self.about(self.myId, function (err, about) {
526 if (err) return cb(err)
527 cb(null,
528 '<a href="' + u.encodeLink(self.myId) + '">' +
529 (about.image ?
530 '<img class="profile-icon icon-right"' +
531 ' src="/' + encodeURIComponent(about.image) + '"' +
532 ' alt="' + u.escape(about.name) + '">' : u.escape(about.name)) +
533 '</a>')
534 })
535 }),
536 pull.once(
537 '<form action="/search" method="get">' +
538 '<h1><a href="/">' + app +
539 (appName == 'ssb' ? '' : ' <sub>' + appName + '</sub>') +
540 '</a></h1> ' +
541 '<input class="search-bar" name="q" size="60"' +
542 ' placeholder=" Search" value="' + q + '" />' +
543 '</form>' +
544 '</header>' +
545 '<article><hr />'),
546 this.renderTry(read),
547 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>')
548 ])
549}
550
551G.serveError = function (req, err, status) {
552 return pull(
553 pull.once(this.renderError(err, 'h2')),
554 this.serveTemplate(req, err.name, status || 500)
555 )
556}
557
558G.renderObjectData = function (obj, filename, repo, rev, path) {
559 var ext = u.getExtension(filename)
560 return u.readOnce(function (cb) {
561 u.readObjectString(obj, function (err, buf) {
562 buf = buf.toString('utf8')
563 if (err) return cb(err)
564 cb(null, (ext == 'md' || ext == 'markdown')
565 ? markdown(buf, {repo: repo, rev: rev, path: path})
566 : buf.length > 1000000 ? ''
567 : renderCodeTable(buf, ext))
568 })
569 })
570}
571
572function renderCodeTable(buf, ext) {
573 return '<pre><table class="code">' +
574 u.highlight(buf, ext).split('\n').map(function (line, i) {
575 i++
576 return '<tr id="L' + i + '">' +
577 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
578 '<td class="code-text">' + line + '</td></tr>'
579 }).join('') +
580 '</table></pre>'
581}
582
583/* Feed */
584
585G.renderFeed = function (req, feedId, filter) {
586 var query = req._u.query
587 var opts = {
588 reverse: !query.forwards,
589 lt: query.lt && +query.lt || Date.now(),
590 gt: query.gt ? +query.gt : -Infinity,
591 values: true,
592 id: feedId
593 }
594
595 // use git index, if present
596 var source
597 if (this.ssb.gitindex) {
598 source = pull(
599 feedId ? this.ssb.gitindex.author(opts) : this.ssb.gitindex.read(opts),
600 pull.map(function (msg) { return msg.value })
601 )
602 } else {
603 source = feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts)
604 }
605
606 return pull(
607 source,
608 u.decryptMessages(this.ssb),
609 u.readableMessages(),
610 pull.filter(function (msg) {
611 var c = msg.value.content
612 return c.type in msgTypes
613 || (c.type == 'post' && c.repo && c.issue)
614 }),
615 typeof filter == 'function' ? filter(opts) : filter,
616 pull.take(100),
617 this.addAuthorName(),
618 query.forwards && u.pullReverse(),
619 paginate(
620 function (first, cb) {
621 if (!query.lt && !query.gt) return cb(null, '')
622 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
623 query.gt = gt
624 query.forwards = 1
625 delete query.lt
626 cb(null, '<a href="?' + qs.stringify(query) + '">' +
627 req._t('Next') + '</a>')
628 },
629 paramap(this.renderFeedItem.bind(this, req), 8),
630 function (last, cb) {
631 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
632 delete query.gt
633 delete query.forwards
634 cb(null, '<a href="?' + qs.stringify(query) + '">' +
635 req._t('Previous') + '</a>')
636 },
637 function (cb) {
638 if (query.forwards) {
639 delete query.gt
640 delete query.forwards
641 query.lt = opts.gt + 1
642 } else {
643 delete query.lt
644 query.gt = opts.lt - 1
645 query.forwards = 1
646 }
647 cb(null, '<a href="?' + qs.stringify(query) + '">' +
648 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
649 }
650 )
651 )
652}
653
654G.renderFeedItem = function (req, msg, cb) {
655 var self = this
656 var c = msg.value.content
657 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
658 var msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
659 var author = msg.value.author
660 var authorLink = u.link([msg.value.author], msg.authorName)
661 var privateIconMaybe = msg.value.private ? ' ' + u.privateIcon(req) : ''
662 switch (c.type) {
663 case 'git-repo':
664 var done = multicb({ pluck: 1, spread: true })
665 self.getRepoName(author, msg.key, done())
666 if (c.upstream) {
667 return self.getMsg(c.upstream, function (err, upstreamMsg) {
668 if (err) return cb(null, self.serveError(req, err))
669 self.getRepoName(upstreamMsg.value.author, c.upstream, done())
670 done(function (err, repoName, upstreamName) {
671 cb(null, '<section class="collapse">' +
672 req._t('Forked', {
673 name: authorLink,
674 upstream: u.link([c.upstream], upstreamName),
675 repo: u.link([msg.key], repoName)
676 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
677 })
678 })
679 } else {
680 return done(function (err, repoName) {
681 if (err) return cb(err)
682 var repoLink = u.link([msg.key], repoName)
683 cb(null, '<section class="collapse">' +
684 req._t('CreatedRepo', {
685 name: authorLink,
686 repo: repoLink
687 }) + ' ' + msgDateLink + privateIconMaybe +
688 (msg.value.private ?
689 '<br>' + req._t('repo.Recipients') + '<ul>' +
690 (Array.isArray(c.recps) ? c.recps : []).map(function (feed) {
691 return '<li>' + u.link([feed], feed) + '</li>'
692 }).join('') + '</ul>'
693 : '') +
694 '</section>')
695 })
696 }
697 case 'git-update':
698 return self.getRepoName(author, c.repo, function (err, repoName) {
699 if (err) return cb(err)
700 var repoLink = u.link([c.repo], repoName)
701 cb(null, '<section class="collapse">' +
702 req._t('Pushed', {
703 name: authorLink,
704 repo: repoLink
705 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
706 })
707 case 'issue':
708 case 'pull-request':
709 var issueLink = u.link([msg.key], u.messageTitle(msg))
710 // TODO: handle hashtag in project property
711 return self.getMsg(c.project, function (err, projectMsg) {
712 if (err) return cb(null,
713 self.repos.serveRepoNotFound(req, c.repo, err))
714 self.getRepoName(projectMsg.value.author, c.project,
715 function (err, repoName) {
716 if (err) return cb(err)
717 var repoLink = u.link([c.project], repoName)
718 cb(null, '<section class="collapse">' +
719 req._t('OpenedIssue', {
720 name: authorLink,
721 type: req._t(c.type == 'pull-request' ?
722 'pull request' : 'issue.'),
723 title: issueLink,
724 project: repoLink
725 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
726 })
727 })
728 case 'about':
729 return cb(null, '<section class="collapse">' +
730 req._t('Named', {
731 author: authorLink,
732 target: '<tt>' + u.escape(c.about) + '</tt>',
733 name: u.link([c.about], c.name)
734 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
735 case 'post':
736 return this.pullReqs.get(c.issue, function (err, pr) {
737 if (err) return cb(err)
738 var type = pr.msg.value.content.type == 'pull-request' ?
739 'pull request' : 'issue.'
740 var changed = self.issues.isStatusChanged(msg, pr)
741 return cb(null, '<section class="collapse">' +
742 req._t(changed == null ? 'CommentedOn' :
743 changed ? 'ReopenedIssue' : 'ClosedIssue', {
744 name: authorLink,
745 type: req._t(type),
746 title: u.link([pr.id], pr.title, true)
747 }) + ' ' + msgDateLink + privateIconMaybe +
748 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
749 '</section>')
750 })
751 default:
752 return cb(null, u.json(msg))
753 }
754}
755
756/* Index */
757
758G.serveIndex = function (req) {
759 return this.serveTemplate(req)(this.renderFeed(req))
760}
761
762G.serveChannel = function (req, id, path) {
763 var self = this
764 return u.readNext(function (cb) {
765 self.getRepo(id, function (err, repo) {
766 if (err) return cb(null, self.serveError(req, err))
767 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
768 })
769 })
770}
771
772G.serveMessage = function (req, id, path) {
773 var self = this
774 return u.readNext(function (cb) {
775 self.getMsg(id, function (err, msg) {
776 if (err) return cb(null, self.serveError(req, err))
777 var c = msg && msg.value && msg.value.content || {}
778 switch (c.type) {
779 case 'git-repo':
780 return self.getRepo(id, function (err, repo) {
781 if (err) return cb(null, self.serveError(req, err))
782 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
783 })
784 case 'git-update':
785 return self.getRepo(c.repo, function (err, repo) {
786 if (err) return cb(null,
787 self.repos.serveRepoNotFound(req, c.repo, err))
788 cb(null, self.repos.serveRepoUpdate(req,
789 GitRepo(repo), msg, path))
790 })
791 case 'issue':
792 return self.getRepo(c.project, function (err, repo) {
793 if (err) return cb(null,
794 self.repos.serveRepoNotFound(req, c.project, err))
795 self.issues.get(id, function (err, issue) {
796 if (err) return cb(null, self.serveError(req, err))
797 cb(null, self.repos.issues.serveRepoIssue(req,
798 GitRepo(repo), issue, path))
799 })
800 })
801 case 'pull-request':
802 return self.getRepo(c.repo, function (err, repo) {
803 if (err) return cb(null,
804 self.repos.serveRepoNotFound(req, c.project, err))
805 self.pullReqs.get(id, function (err, pr) {
806 if (err) return cb(null, self.serveError(req, err))
807 cb(null, self.repos.pulls.serveRepoPullReq(req,
808 GitRepo(repo), pr, path))
809 })
810 })
811 case 'line-comment':
812 return self.getRepo(c.repo, function (err, repo) {
813 if (err) return cb(null,
814 self.repos.serveRepoNotFound(req, c.repo, err))
815 return cb(null,
816 self.repos.serveRepoCommit(req, GitRepo(repo), c.commitId, c.filename))
817 })
818 case 'issue-edit':
819 if (ref.isMsgId(c.issue)) {
820 return self.pullReqs.get(c.issue, function (err, issue) {
821 if (err) return cb(err)
822 self.getRepo(issue.project, function (err, repo) {
823 if (err) {
824 if (!repo) return cb(null,
825 self.repos.serveRepoNotFound(req, c.repo, err))
826 return cb(null, self.serveError(req, err))
827 }
828 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
829 issue, path, id))
830 })
831 })
832 }
833 // fallthrough
834 case 'post':
835 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
836 // comment on an issue
837 var done = multicb({ pluck: 1, spread: true })
838 self.getRepo(c.repo, done())
839 self.pullReqs.get(c.issue, done())
840 return done(function (err, repo, issue) {
841 if (err) {
842 if (!repo) return cb(null,
843 self.repos.serveRepoNotFound(req, c.repo, err))
844 return cb(null, self.serveError(req, err))
845 }
846 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
847 issue, path, id))
848 })
849 } else if (ref.isMsgId(c.root)) {
850 // comment on issue from patchwork?
851 return self.getMsg(c.root, function (err, root) {
852 var rc = root.value && root.value.content && root.value.content
853 if (err) return cb(null, self.serveError(req, err))
854 var repoId = rc.repo || rc.project
855 if (!ref.isMsgId(repoId))
856 return cb(null, self.serveGenericMessage(req, msg, path))
857 self.getRepo(repoId, function (err, repo) {
858 if (err) return cb(null, self.serveError(req, err))
859 switch (rc && rc.type) {
860 case 'issue':
861 return self.issues.get(c.root, function (err, issue) {
862 if (err) return cb(null, self.serveError(req, err))
863 return cb(null,
864 self.repos.issues.serveRepoIssue(req,
865 GitRepo(repo), issue, path, id))
866 })
867 case 'pull-request':
868 return self.pullReqs.get(c.root, function (err, pr) {
869 if (err) return cb(null, self.serveError(req, err))
870 return cb(null,
871 self.repos.pulls.serveRepoPullReq(req,
872 GitRepo(repo), pr, path, id))
873 })
874 case 'line-comment':
875 return cb(null,
876 self.repos.serveRepoCommit(req, GitRepo(repo), rc.commitId, rc.filename))
877 default:
878 return cb(null, self.serveGenericMessage(req, msg, path))
879 }
880 })
881 })
882 }
883 // fallthrough
884 default:
885 if (ref.isMsgId(c.repo))
886 return self.getRepo(c.repo, function (err, repo) {
887 if (err) return cb(null,
888 self.repos.serveRepoNotFound(req, c.repo, err))
889 cb(null, self.repos.serveRepoSomething(req,
890 GitRepo(repo), id, msg, path))
891 })
892 else
893 return cb(null, self.serveGenericMessage(req, msg, path))
894 }
895 })
896 })
897}
898
899G.serveGenericMessage = function (req, msg, path) {
900 return this.serveTemplate(req, msg.key)(pull.once(
901 '<section><h2>' + u.link([msg.key]) + '</h2>' +
902 u.json(msg.value) +
903 '</section>'))
904}
905
906/* Search */
907
908G.serveSearch = function (req) {
909 var self = this
910 var q = String(req._u.query.q || '')
911 if (!q) return this.serveIndex(req)
912 var qId = q.replace(/^ssb:\/*/, '')
913 if (ref.type(qId))
914 return this.serveRedirect(req, encodeURIComponent(qId))
915
916 var search = new RegExp(q, 'i')
917 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
918 this.renderFeed(req, null, function (opts) {
919 return function (read) {
920 return pull(
921 many([
922 self.getMsgs('about', opts),
923 read
924 ]),
925 pull.filter(function (msg) {
926 var c = msg.value.content
927 return (
928 search.test(msg.key) ||
929 c.text && search.test(c.text) ||
930 c.name && search.test(c.name) ||
931 c.title && search.test(c.title))
932 })
933 )
934 }
935 })
936 )
937}
938
939G.getMsgRaw = function (key, cb) {
940 var self = this
941 this.ssb.get(key, function (err, value) {
942 if (err) return cb(err)
943 u.decryptMessage(self.ssb, {key: key, value: value}, cb)
944 })
945}
946
947G.getMsgs = function (type, opts) {
948 return this.ssb.messagesByType({
949 type: type,
950 reverse: opts.reverse,
951 lt: opts.lt,
952 gt: opts.gt,
953 })
954}
955
956G.serveBlobNotFound = function (req, repoId, err) {
957 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
958 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
959 '<p>' + req._t('error.BlobNotFoundInRepo', {
960 repo: u.link([repoId])
961 }) + '</p>' +
962 '<pre>' + u.escape(err.stack) + '</pre>'
963 ))
964}
965
966G.serveRaw = function (length, contentType) {
967 var headers = {
968 'Content-Type': contentType || 'text/plain; charset=utf-8',
969 'Cache-Control': 'max-age=31536000'
970 }
971 if (length != null)
972 headers['Content-Length'] = length
973 return function (read) {
974 return cat([pull.once([200, headers]), read])
975 }
976}
977
978G.getBlob = function (req, key, cb) {
979 var blobs = this.ssb.blobs
980 // use size to check for blob's presence, since has or want may broadcast
981 blobs.size(key, function (err, size) {
982 if (typeof size === 'number') cb(null, blobs.get(key))
983 else blobs.want(key, function (err, got) {
984 if (err) cb(err)
985 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
986 else cb(null, blobs.get(key))
987 })
988 })
989}
990
991G.serveBlob = function (req, key) {
992 var self = this
993 return u.readNext(function (cb) {
994 self.getBlob(req, key, function (err, read) {
995 if (err) cb(null, self.serveError(req, err))
996 else if (!read) cb(null, self.serve404(req))
997 else cb(null, identToResp(read))
998 })
999 })
1000}
1001
1002function identToResp(read) {
1003 var ended, type, queue
1004 var id = ident(function (_type) {
1005 type = _type && mime.lookup(_type)
1006 })(read)
1007 return function (end, cb) {
1008 if (ended) return cb(ended)
1009 if (end) id(end, function (end) {
1010 cb(end === true ? null : end)
1011 })
1012 else if (queue) {
1013 var _queue = queue
1014 queue = null
1015 cb(null, _queue)
1016 }
1017 else if (!type)
1018 id(null, function (end, data) {
1019 if (ended = end) return cb(end)
1020 queue = data
1021 cb(null, [200, {
1022 'Content-Type': type || 'text/plain; charset=utf-8',
1023 'Cache-Control': 'max-age=31536000'
1024 }])
1025 })
1026 else
1027 id(null, cb)
1028 }
1029}
1030
1031G.monitorSsbClient = function () {
1032 pull(
1033 function (abort, cb) {
1034 if (abort) throw abort
1035 setTimeout(function () {
1036 cb(null, 'keepalive')
1037 }, 15e3)
1038 },
1039 this.ssb.gossip.ping(),
1040 pull.drain(null, function (err) {
1041 // exit when the rpc connection ends
1042 if (err) console.error(err)
1043 console.error('sbot client connection closed. aborting')
1044 process.exit(1)
1045 })
1046 )
1047}
1048

Built with git-ssb-web