git ssb

30+

cel / git-ssb-web



Tree: afbb585e6580d4380af3a1b490efb8e6d05feeda

Files: afbb585e6580d4380af3a1b490efb8e6d05feeda / index.js

33944 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 id: feedId
592 }
593 return pull(
594 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
595 u.decryptMessages(this.ssb),
596 u.readableMessages(),
597 pull.filter(function (msg) {
598 var c = msg.value.content
599 return c.type in msgTypes
600 || (c.type == 'post' && c.repo && c.issue)
601 }),
602 typeof filter == 'function' ? filter(opts) : filter,
603 pull.take(100),
604 this.addAuthorName(),
605 query.forwards && u.pullReverse(),
606 paginate(
607 function (first, cb) {
608 if (!query.lt && !query.gt) return cb(null, '')
609 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
610 query.gt = gt
611 query.forwards = 1
612 delete query.lt
613 cb(null, '<a href="?' + qs.stringify(query) + '">' +
614 req._t('Next') + '</a>')
615 },
616 paramap(this.renderFeedItem.bind(this, req), 8),
617 function (last, cb) {
618 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
619 delete query.gt
620 delete query.forwards
621 cb(null, '<a href="?' + qs.stringify(query) + '">' +
622 req._t('Previous') + '</a>')
623 },
624 function (cb) {
625 if (query.forwards) {
626 delete query.gt
627 delete query.forwards
628 query.lt = opts.gt + 1
629 } else {
630 delete query.lt
631 query.gt = opts.lt - 1
632 query.forwards = 1
633 }
634 cb(null, '<a href="?' + qs.stringify(query) + '">' +
635 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
636 }
637 )
638 )
639}
640
641G.renderFeedItem = function (req, msg, cb) {
642 var self = this
643 var c = msg.value.content
644 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
645 var msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
646 var author = msg.value.author
647 var authorLink = u.link([msg.value.author], msg.authorName)
648 var privateIconMaybe = msg.value.private ? ' ' + u.privateIcon(req) : ''
649 switch (c.type) {
650 case 'git-repo':
651 var done = multicb({ pluck: 1, spread: true })
652 self.getRepoName(author, msg.key, done())
653 if (c.upstream) {
654 return self.getMsg(c.upstream, function (err, upstreamMsg) {
655 if (err) return cb(null, self.serveError(req, err))
656 self.getRepoName(upstreamMsg.value.author, c.upstream, done())
657 done(function (err, repoName, upstreamName) {
658 cb(null, '<section class="collapse">' +
659 req._t('Forked', {
660 name: authorLink,
661 upstream: u.link([c.upstream], upstreamName),
662 repo: u.link([msg.key], repoName)
663 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
664 })
665 })
666 } else {
667 return done(function (err, repoName) {
668 if (err) return cb(err)
669 var repoLink = u.link([msg.key], repoName)
670 cb(null, '<section class="collapse">' +
671 req._t('CreatedRepo', {
672 name: authorLink,
673 repo: repoLink
674 }) + ' ' + msgDateLink + privateIconMaybe +
675 (msg.value.private ?
676 '<br>' + req._t('repo.Recipients') + '<ul>' +
677 (Array.isArray(c.recps) ? c.recps : []).map(function (feed) {
678 return '<li>' + u.link([feed], feed) + '</li>'
679 }).join('') + '</ul>'
680 : '') +
681 '</section>')
682 })
683 }
684 case 'git-update':
685 return self.getRepoName(author, c.repo, function (err, repoName) {
686 if (err) return cb(err)
687 var repoLink = u.link([c.repo], repoName)
688 cb(null, '<section class="collapse">' +
689 req._t('Pushed', {
690 name: authorLink,
691 repo: repoLink
692 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
693 })
694 case 'issue':
695 case 'pull-request':
696 var issueLink = u.link([msg.key], u.messageTitle(msg))
697 // TODO: handle hashtag in project property
698 return self.getMsg(c.project, function (err, projectMsg) {
699 if (err) return cb(null,
700 self.repos.serveRepoNotFound(req, c.repo, err))
701 self.getRepoName(projectMsg.value.author, c.project,
702 function (err, repoName) {
703 if (err) return cb(err)
704 var repoLink = u.link([c.project], repoName)
705 cb(null, '<section class="collapse">' +
706 req._t('OpenedIssue', {
707 name: authorLink,
708 type: req._t(c.type == 'pull-request' ?
709 'pull request' : 'issue.'),
710 title: issueLink,
711 project: repoLink
712 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
713 })
714 })
715 case 'about':
716 return cb(null, '<section class="collapse">' +
717 req._t('Named', {
718 author: authorLink,
719 target: '<tt>' + u.escape(c.about) + '</tt>',
720 name: u.link([c.about], c.name)
721 }) + ' ' + msgDateLink + privateIconMaybe + '</section>')
722 case 'post':
723 return this.pullReqs.get(c.issue, function (err, pr) {
724 if (err) return cb(err)
725 var type = pr.msg.value.content.type == 'pull-request' ?
726 'pull request' : 'issue.'
727 var changed = self.issues.isStatusChanged(msg, pr)
728 return cb(null, '<section class="collapse">' +
729 req._t(changed == null ? 'CommentedOn' :
730 changed ? 'ReopenedIssue' : 'ClosedIssue', {
731 name: authorLink,
732 type: req._t(type),
733 title: u.link([pr.id], pr.title, true)
734 }) + ' ' + msgDateLink + privateIconMaybe +
735 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
736 '</section>')
737 })
738 default:
739 return cb(null, u.json(msg))
740 }
741}
742
743/* Index */
744
745G.serveIndex = function (req) {
746 return this.serveTemplate(req)(this.renderFeed(req))
747}
748
749G.serveChannel = function (req, id, path) {
750 var self = this
751 return u.readNext(function (cb) {
752 self.getRepo(id, function (err, repo) {
753 if (err) return cb(null, self.serveError(req, err))
754 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
755 })
756 })
757}
758
759G.serveMessage = function (req, id, path) {
760 var self = this
761 return u.readNext(function (cb) {
762 self.getMsg(id, function (err, msg) {
763 if (err) return cb(null, self.serveError(req, err))
764 var c = msg && msg.value && msg.value.content || {}
765 switch (c.type) {
766 case 'git-repo':
767 return self.getRepo(id, function (err, repo) {
768 if (err) return cb(null, self.serveError(req, err))
769 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
770 })
771 case 'git-update':
772 return self.getRepo(c.repo, function (err, repo) {
773 if (err) return cb(null,
774 self.repos.serveRepoNotFound(req, c.repo, err))
775 cb(null, self.repos.serveRepoUpdate(req,
776 GitRepo(repo), msg, path))
777 })
778 case 'issue':
779 return self.getRepo(c.project, function (err, repo) {
780 if (err) return cb(null,
781 self.repos.serveRepoNotFound(req, c.project, err))
782 self.issues.get(id, function (err, issue) {
783 if (err) return cb(null, self.serveError(req, err))
784 cb(null, self.repos.issues.serveRepoIssue(req,
785 GitRepo(repo), issue, path))
786 })
787 })
788 case 'pull-request':
789 return self.getRepo(c.repo, function (err, repo) {
790 if (err) return cb(null,
791 self.repos.serveRepoNotFound(req, c.project, err))
792 self.pullReqs.get(id, function (err, pr) {
793 if (err) return cb(null, self.serveError(req, err))
794 cb(null, self.repos.pulls.serveRepoPullReq(req,
795 GitRepo(repo), pr, path))
796 })
797 })
798 case 'line-comment':
799 return self.getRepo(c.repo, function (err, repo) {
800 if (err) return cb(null,
801 self.repos.serveRepoNotFound(req, c.repo, err))
802 return cb(null,
803 self.repos.serveRepoCommit(req, GitRepo(repo), c.commitId, c.filename))
804 })
805 case 'issue-edit':
806 if (ref.isMsgId(c.issue)) {
807 return self.pullReqs.get(c.issue, function (err, issue) {
808 if (err) return cb(err)
809 self.getRepo(issue.project, function (err, repo) {
810 if (err) {
811 if (!repo) return cb(null,
812 self.repos.serveRepoNotFound(req, c.repo, err))
813 return cb(null, self.serveError(req, err))
814 }
815 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
816 issue, path, id))
817 })
818 })
819 }
820 // fallthrough
821 case 'post':
822 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
823 // comment on an issue
824 var done = multicb({ pluck: 1, spread: true })
825 self.getRepo(c.repo, done())
826 self.pullReqs.get(c.issue, done())
827 return done(function (err, repo, issue) {
828 if (err) {
829 if (!repo) return cb(null,
830 self.repos.serveRepoNotFound(req, c.repo, err))
831 return cb(null, self.serveError(req, err))
832 }
833 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
834 issue, path, id))
835 })
836 } else if (ref.isMsgId(c.root)) {
837 // comment on issue from patchwork?
838 return self.getMsg(c.root, function (err, root) {
839 var rc = root.value && root.value.content && root.value.content
840 if (err) return cb(null, self.serveError(req, err))
841 var repoId = rc.repo || rc.project
842 if (!ref.isMsgId(repoId))
843 return cb(null, self.serveGenericMessage(req, msg, path))
844 self.getRepo(repoId, function (err, repo) {
845 if (err) return cb(null, self.serveError(req, err))
846 switch (rc && rc.type) {
847 case 'issue':
848 return self.issues.get(c.root, function (err, issue) {
849 if (err) return cb(null, self.serveError(req, err))
850 return cb(null,
851 self.repos.issues.serveRepoIssue(req,
852 GitRepo(repo), issue, path, id))
853 })
854 case 'pull-request':
855 return self.pullReqs.get(c.root, function (err, pr) {
856 if (err) return cb(null, self.serveError(req, err))
857 return cb(null,
858 self.repos.pulls.serveRepoPullReq(req,
859 GitRepo(repo), pr, path, id))
860 })
861 case 'line-comment':
862 return cb(null,
863 self.repos.serveRepoCommit(req, GitRepo(repo), rc.commitId, rc.filename))
864 default:
865 return cb(null, self.serveGenericMessage(req, msg, path))
866 }
867 })
868 })
869 }
870 // fallthrough
871 default:
872 if (ref.isMsgId(c.repo))
873 return self.getRepo(c.repo, function (err, repo) {
874 if (err) return cb(null,
875 self.repos.serveRepoNotFound(req, c.repo, err))
876 cb(null, self.repos.serveRepoSomething(req,
877 GitRepo(repo), id, msg, path))
878 })
879 else
880 return cb(null, self.serveGenericMessage(req, msg, path))
881 }
882 })
883 })
884}
885
886G.serveGenericMessage = function (req, msg, path) {
887 return this.serveTemplate(req, msg.key)(pull.once(
888 '<section><h2>' + u.link([msg.key]) + '</h2>' +
889 u.json(msg.value) +
890 '</section>'))
891}
892
893/* Search */
894
895G.serveSearch = function (req) {
896 var self = this
897 var q = String(req._u.query.q || '')
898 if (!q) return this.serveIndex(req)
899 var qId = q.replace(/^ssb:\/*/, '')
900 if (ref.type(qId))
901 return this.serveRedirect(req, encodeURIComponent(qId))
902
903 var search = new RegExp(q, 'i')
904 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
905 this.renderFeed(req, null, function (opts) {
906 return function (read) {
907 return pull(
908 many([
909 self.getMsgs('about', opts),
910 read
911 ]),
912 pull.filter(function (msg) {
913 var c = msg.value.content
914 return (
915 search.test(msg.key) ||
916 c.text && search.test(c.text) ||
917 c.name && search.test(c.name) ||
918 c.title && search.test(c.title))
919 })
920 )
921 }
922 })
923 )
924}
925
926G.getMsgRaw = function (key, cb) {
927 var self = this
928 this.ssb.get(key, function (err, value) {
929 if (err) return cb(err)
930 u.decryptMessage(self.ssb, {key: key, value: value}, cb)
931 })
932}
933
934G.getMsgs = function (type, opts) {
935 return this.ssb.messagesByType({
936 type: type,
937 reverse: opts.reverse,
938 lt: opts.lt,
939 gt: opts.gt,
940 })
941}
942
943G.serveBlobNotFound = function (req, repoId, err) {
944 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
945 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
946 '<p>' + req._t('error.BlobNotFoundInRepo', {
947 repo: u.link([repoId])
948 }) + '</p>' +
949 '<pre>' + u.escape(err.stack) + '</pre>'
950 ))
951}
952
953G.serveRaw = function (length, contentType) {
954 var headers = {
955 'Content-Type': contentType || 'text/plain; charset=utf-8',
956 'Cache-Control': 'max-age=31536000'
957 }
958 if (length != null)
959 headers['Content-Length'] = length
960 return function (read) {
961 return cat([pull.once([200, headers]), read])
962 }
963}
964
965G.getBlob = function (req, key, cb) {
966 var blobs = this.ssb.blobs
967 // use size to check for blob's presence, since has or want may broadcast
968 blobs.size(key, function (err, size) {
969 if (typeof size === 'number') cb(null, blobs.get(key))
970 else blobs.want(key, function (err, got) {
971 if (err) cb(err)
972 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
973 else cb(null, blobs.get(key))
974 })
975 })
976}
977
978G.serveBlob = function (req, key) {
979 var self = this
980 return u.readNext(function (cb) {
981 self.getBlob(req, key, function (err, read) {
982 if (err) cb(null, self.serveError(req, err))
983 else if (!read) cb(null, self.serve404(req))
984 else cb(null, identToResp(read))
985 })
986 })
987}
988
989function identToResp(read) {
990 var ended, type, queue
991 var id = ident(function (_type) {
992 type = _type && mime.lookup(_type)
993 })(read)
994 return function (end, cb) {
995 if (ended) return cb(ended)
996 if (end) id(end, function (end) {
997 cb(end === true ? null : end)
998 })
999 else if (queue) {
1000 var _queue = queue
1001 queue = null
1002 cb(null, _queue)
1003 }
1004 else if (!type)
1005 id(null, function (end, data) {
1006 if (ended = end) return cb(end)
1007 queue = data
1008 cb(null, [200, {
1009 'Content-Type': type || 'text/plain; charset=utf-8',
1010 'Cache-Control': 'max-age=31536000'
1011 }])
1012 })
1013 else
1014 id(null, cb)
1015 }
1016}
1017
1018G.monitorSsbClient = function () {
1019 pull(
1020 function (abort, cb) {
1021 if (abort) throw abort
1022 setTimeout(function () {
1023 cb(null, 'keepalive')
1024 }, 15e3)
1025 },
1026 this.ssb.gossip.ping(),
1027 pull.drain(null, function (err) {
1028 // exit when the rpc connection ends
1029 if (err) console.error(err)
1030 console.error('sbot client connection closed. aborting')
1031 process.exit(1)
1032 })
1033 )
1034}
1035

Built with git-ssb-web