git ssb

30+

cel / git-ssb-web



Tree: a2e4bfb1c3aad80b5a96cc0f888f9af1f6af6021

Files: a2e4bfb1c3aad80b5a96cc0f888f9af1f6af6021 / index.js

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

Built with git-ssb-web