git ssb

30+

cel / git-ssb-web



Tree: f2a29c8443af6d8b2826e9119cc208e19479f015

Files: f2a29c8443af6d8b2826e9119cc208e19479f015 / index.js

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

Built with git-ssb-web