git ssb

30+

cel / git-ssb-web



Tree: d31cae633871e5482af52d039d74ec9f09561689

Files: d31cae633871e5482af52d039d74ec9f09561689 / index.js

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

Built with git-ssb-web