git ssb

16+

cel / patchfoo



Tree: 7750ee481b305ff8789ca9813da2c62182b25b06

Files: 7750ee481b305ff8789ca9813da2c62182b25b06 / lib / app.js

39993 bytesRaw
1var http = require('http')
2var memo = require('asyncmemo')
3var lru = require('hashlru')
4var pkg = require('../package')
5var u = require('./util')
6var pull = require('pull-stream')
7var multicb = require('multicb')
8var paramap = require('pull-paramap')
9var Contacts = require('./contacts')
10var PrivateBox = require('private-box')
11var About = require('./about')
12var Follows = require('./follows')
13var Serve = require('./serve')
14var Render = require('./render')
15var Git = require('ssb-git')
16var cat = require('pull-cat')
17var proc = require('child_process')
18var toPull = require('stream-to-pull-stream')
19var BoxStream = require('pull-box-stream')
20var crypto = require('crypto')
21var SsbNpmRegistry = require('ssb-npm-registry')
22var os = require('os')
23var path = require('path')
24var fs = require('fs')
25var mkdirp = require('mkdirp')
26var Base64URL = require('base64-url')
27var ssbKeys = require('ssb-keys')
28
29var zeros = new Buffer(24); zeros.fill(0)
30
31module.exports = App
32
33function App(sbot, config) {
34 this.sbot = sbot
35 this.config = config
36
37 var conf = config.patchfoo || {}
38 this.port = conf.port || 8027
39 this.host = conf.host || 'localhost'
40 this.msgFilter = conf.filter == null ? 'all' : conf.filter
41 this.showPrivates = conf.showPrivates == null ? true : conf.showPrivates
42 this.previewVotes = conf.previewVotes == null ? false : conf.previewVotes
43 this.previewContacts = conf.previewContacts == null ? false : conf.previewContacts
44 this.useOoo = conf.ooo == null ? false : conf.ooo
45 this.ssbPort = 8008
46 this.portRegexp = new RegExp(':' + this.ssbPort + '$')
47 /*
48 var capsConfig = config.caps || {}
49 this.userInviteCap = capsConfig.userInvite
50 || new Buffer('MxiG/9q04kN2C6vJQKa6ZhAj0G/FdfRkmeGTMnpsoek=', 'base64')
51 // sha256('user-invites:DEVELOPMENT')
52 this.peerInviteCap = capsConfig.peerInvite
53 || new Buffer('pmr+IzM+4VAZgi5H5bOopXkwnzqrNussS7DtAJsfbf0=', 'base64')
54 // sha256('peer-invites:DEVELOPMENT')
55 */
56
57 this.hostname = (/:/.test(this.host) ? '[' + this.host + ']' : this.host) + this.port
58 this.dir = path.join(config.path, conf.dir || 'patchfoo')
59 this.scriptDir = path.join(this.dir, conf.scriptDir || 'script')
60 this.draftsDir = path.join(this.dir, conf.draftsDir || 'drafts')
61
62 var base = conf.base || '/'
63 this.opts = {
64 base: base,
65 blob_base: conf.blob_base || conf.img_base || base,
66 img_base: conf.img_base || (base + 'image/'),
67 emoji_base: conf.emoji_base || (base + 'emoji/'),
68 encode_msgids: conf.encode_msgids == null ? true : Boolean(conf.encode_msgids),
69 codeInTextareas: conf.codeInTextareas,
70 }
71
72 this.msgCache = lru(100)
73 this.getMsg = memo({cache: this.msgCache}, getMsgWithValue, sbot)
74 this.getMsgOoo = memo({cache: this.msgCache}, this.getMsgOoo)
75 this.getAbout = memo({cache: this.aboutCache = lru(500)},
76 this._getAbout.bind(this))
77 this.unboxContent = memo({cache: lru(100)}, this.unboxContent.bind(this))
78 this.reverseNameCache = lru(500)
79 this.reverseEmojiNameCache = lru(500)
80 this.getBlobSize = memo({cache: this.blobSizeCache = lru(100)},
81 sbot.blobs.size.bind(sbot.blobs))
82 this.getVotes = memo({cache: lru(100)}, this._getVotes.bind(this))
83 this.getIdeaTitle = memo({cache: lru(100)}, this.getIdeaTitle)
84
85 this.unboxMsg = this.unboxMsg.bind(this)
86
87 this.render = new Render(this, this.opts)
88 this.git = new Git(this.sbot, this.config)
89 this.contacts = new Contacts(this.sbot)
90 this.follows = new Follows(this.sbot, this.contacts)
91 this.about = new About(this, sbot.id, this.follows)
92 this.serveSsbNpmRegistry = SsbNpmRegistry.respond(this.sbot, this.config)
93
94 this.mtimes = {}
95 this.getScript = memo({cache: false}, this.getScript)
96
97 this.monitorBlobWants()
98 this.navLinks = conf.nav || [
99 'new',
100 'public',
101 this.sbot.private && 'private',
102 'mentions',
103 'peers',
104 this.sbot.status && 'status',
105 'channels',
106 'tags',
107 this.sbot.friends && 'friends',
108 'search',
109 'live',
110 'compose',
111 'drafts',
112 'emojis',
113 'self',
114 'searchbox'
115 ]
116}
117
118App.prototype.go = function () {
119 var self = this
120 var server = http.createServer(function (req, res) {
121 new Serve(self, req, res).go()
122 })
123 server.listen(self.port, self.host, onListening)
124 function onListening() {
125 var addr = server.address()
126 var host = addr.family === 'IPv6' ? '[' + addr.address + ']' : addr.address
127 self.log('Listening on http://' + host + ':' + addr.port)
128 }
129
130 // invalidate cached About info when new About messages come in
131 if (!self.sbot.links) return console.error('missing sbot.links')
132 else pull(
133 self.sbot.links({rel: 'about', old: false, values: true}),
134 pull.drain(function (link) {
135 self.aboutCache.remove(link.dest)
136 }, function (err) {
137 if (err) throw err
138 })
139 )
140
141 // keep alive ssb client connection
142 setInterval(self.sbot.whoami, 10e3)
143}
144
145var logPrefix = '[' + pkg.name + ']'
146App.prototype.log = console.log.bind(console, logPrefix)
147App.prototype.error = console.error.bind(console, logPrefix)
148
149App.prototype.unboxContent = function (content, cb) {
150 if (this.sbot.private && this.sbot.private.unbox) return this.sbot.private.unbox(content, cb)
151 if (typeof content !== 'string') return cb(new TypeError('content must be string'))
152 // Missing ssb-private and privat key
153 var keys = this.config.keys || {}
154 if (!keys.private) {
155 // Missing ssb-private and private key, so cannot decrypt here.
156 // ssb-server may do decryption anyway, with private:true RPC option.
157 return cb(null, null)
158 }
159 var data
160 try { data = ssbKeys.unbox(content, this.config.keys.private) }
161 catch(e) { return cb(e) }
162 cb(null, data)
163}
164
165App.prototype.unboxContentWithKey = function (content, key, cb) {
166 if (!key) return this.unboxContent(content, cb)
167 var data
168 try {
169 var contentBuf = new Buffer(content.replace(/\.box.*$/, ''), 'base64')
170 var keyBuf = new Buffer(key, 'base64')
171 data = PrivateBox.multibox_open_body(contentBuf, keyBuf)
172 if (!data) return cb(new Error('failed to decrypt'))
173 data = JSON.parse(data.toString('utf8'))
174 } catch(e) {
175 return cb(new Error(e.stack || e))
176 }
177 cb(null, data)
178}
179
180App.prototype.unboxMsgWithKey = function (msg, key, cb) {
181 var self = this
182 var c = msg && msg.value && msg.value.content
183 if (typeof c !== 'string') cb(null, msg)
184 else self.unboxContentWithKey(c, key, function (err, content) {
185 if (err) {
186 self.error('unbox:', err)
187 return cb(null, msg)
188 } else if (!content) {
189 return cb(null, msg)
190 }
191 var m = {}
192 for (var k in msg) m[k] = msg[k]
193 m.value = {}
194 for (var k in msg.value) m.value[k] = msg.value[k]
195 m.value.content = content
196 m.value.private = true
197 cb(null, m)
198 })
199}
200
201App.prototype.unboxMsg = function (msg, cb) {
202 return this.unboxMsgWithKey(msg, null, cb)
203}
204
205App.prototype.search = function (opts) {
206 var fsearch = this.sbot.fulltext && this.sbot.fulltext.search
207 if (fsearch) return fsearch(opts)
208 var search = this.sbot.search && this.sbot.search.query
209 if (search) return search({query: opts})
210 return pull.error(new Error('Search needs ssb-fulltext or ssb-search plugin'))
211}
212
213App.prototype.advancedSearch = function (opts) {
214 return pull(
215 opts.channel ?
216 this.sbot.backlinks.read({
217 dest: '#' + opts.channel,
218 reverse: true,
219 })
220 : opts.dest ?
221 this.sbot.links({
222 values: true,
223 dest: opts.dest,
224 source: opts.source || undefined,
225 reverse: true,
226 meta: false,
227 })
228 : opts.source ?
229 this.sbotCreateUserStream({
230 reverse: true,
231 id: opts.source
232 })
233 :
234 this.sbot.createFeedStream({
235 reverse: true,
236 }),
237 this.unboxMessages(),
238 opts.text && pull.filter(filterByText(opts.text))
239 )
240}
241
242function forSome(each) {
243 return function some(obj) {
244 if (obj == null) return false
245 if (typeof obj === 'string') return each(obj)
246 if (Array.isArray(obj)) return obj.some(some)
247 if (typeof obj === 'object')
248 for (var k in obj) if (some(obj[k])) return true
249 return false
250 }
251}
252
253function filterByText(str) {
254 if (!str) return function () { return true }
255 var matcher
256 try {
257 var search = new RegExp(str, 'i')
258 matcher = search.test.bind(search)
259 } catch(e) {
260 matcher = function (value) {
261 return String(value).indexOf(str) !== -1
262 }
263 }
264 var matches = forSome(matcher)
265 return function (msg) {
266 var c = msg.value.content
267 return c && matches(c)
268 }
269}
270
271App.prototype.getMsgDecrypted = function (key, cb) {
272 var self = this
273 this.getMsg(key, function (err, msg) {
274 if (err) return cb(err)
275 self.unboxMsg(msg, cb)
276 })
277}
278
279App.prototype.getMsgOoo = function (key, cb) {
280 var ooo = this.sbot.ooo
281 if (!ooo) return cb(new Error('missing ssb-ooo plugin'))
282 ooo.get(key, cb)
283}
284
285App.prototype.getMsgDecryptedOoo = function (key, cb) {
286 var self = this
287 this.getMsgOoo(key, function (err, msg) {
288 if (err) return cb(err)
289 self.unboxMsg(msg, cb)
290 })
291}
292
293App.prototype.privatePublish = function (content, recps, cb) {
294 if (this.private && typeof this.private.publish === 'function') {
295 return this.sbot.private.publish(content, recps, cb)
296 } else {
297 try { content = ssbKeys.box(content, recps) }
298 catch(e) { return cb(e) }
299 return this.sbot.publish(content, cb)
300 }
301}
302
303App.prototype.publish = function (content, cb) {
304 var self = this
305 function tryPublish(triesLeft) {
306 if (Array.isArray(content.recps)) {
307 var recps = content.recps.map(u.linkDest)
308 self.privatePublish(content, recps, next)
309 } else {
310 self.sbot.publish(content, next)
311 }
312 function next(err, msg) {
313 if (err) {
314 if (triesLeft > 0) {
315 if (/^expected previous:/.test(err.message)) {
316 return tryPublish(triesLeft-1)
317 }
318 }
319 }
320 return cb(err, msg)
321 }
322 }
323 tryPublish(2)
324}
325
326App.prototype.wantSizeBlob = function (id, cb) {
327 // only want() the blob if we don't already have it
328 var self = this
329 var blobs = this.sbot.blobs
330 blobs.size(id, function (err, size) {
331 if (size != null) return cb(null, size)
332 self.blobWants[id] = true
333 blobs.want(id, function (err) {
334 if (err) return cb(err)
335 blobs.size(id, cb)
336 })
337 })
338}
339
340App.prototype.addBlobRaw = function (cb) {
341 var done = multicb({pluck: 1, spread: true})
342 var sink = pull(
343 u.pullLength(done()),
344 this.sbot.blobs.add(done())
345 )
346 done(function (err, size, hash) {
347 if (err) return cb(err)
348 cb(null, {link: hash, size: size})
349 })
350 return sink
351}
352
353App.prototype.addBlob = function (isPrivate, cb) {
354 if (!isPrivate) return this.addBlobRaw(cb)
355 else return this.addBlobPrivate(cb)
356}
357
358App.prototype.addBlobPrivate = function (cb) {
359 var bufs = []
360 var self = this
361 // use the hash of the cleartext as the key to encrypt the blob
362 var hash = crypto.createHash('sha256')
363 return pull.drain(function (buf) {
364 bufs.push(buf)
365 hash.update(buf)
366 }, function (err) {
367 if (err) return cb(err)
368 var secret = hash.digest()
369 pull(
370 pull.values(bufs),
371 BoxStream.createBoxStream(secret, zeros),
372 self.addBlobRaw(function (err, link) {
373 if (err) return cb(err)
374 link.key = secret.toString('base64')
375 cb(null, link)
376 })
377 )
378 })
379}
380
381App.prototype.getBlob = function (id, key) {
382 if (!key) return this.sbot.blobs.get(id)
383 if (typeof key === 'string') key = new Buffer(key, 'base64')
384 return pull(
385 this.sbot.blobs.get(id),
386 BoxStream.createUnboxStream(key, zeros)
387 )
388}
389
390App.prototype.pushBlob = function (id, cb) {
391 console.error('pushing blob', id)
392 this.sbot.blobs.push(id, cb)
393}
394
395App.prototype.readBlob = function (link) {
396 link = u.toLink(link)
397 return this.sbot.blobs.get({
398 hash: link.link,
399 size: link.size,
400 })
401}
402
403App.prototype.readBlobSlice = function (link, opts) {
404 if (this.sbot.blobs.getSlice) return this.sbot.blobs.getSlice({
405 hash: link.link,
406 size: link.size,
407 start: opts.start,
408 end: opts.end,
409 })
410 return pull(
411 this.readBlob(link),
412 u.pullSlice(opts.start, opts.end)
413 )
414}
415
416App.prototype.ensureHasBlobs = function (links, cb) {
417 var self = this
418 var done = multicb({pluck: 1})
419 links.filter(Boolean).forEach(function (link) {
420 var cb = done()
421 self.sbot.blobs.size(link.link, function (err, size) {
422 if (err) cb(err)
423 else if (size == null) cb(null, link)
424 else cb()
425 })
426 })
427 done(function (err, missingLinks) {
428 if (err) console.trace(err)
429 missingLinks = missingLinks.filter(Boolean)
430 if (missingLinks.length == 0) return cb()
431 return cb({name: 'BlobNotFoundError', links: missingLinks})
432 })
433}
434
435App.prototype.getReverseNameSync = function (name) {
436 var id = this.reverseNameCache.get(name)
437 return id
438}
439
440App.prototype.getReverseEmojiNameSync = function (name) {
441 return this.reverseEmojiNameCache.get(name)
442}
443
444App.prototype.getNameSync = function (name) {
445 var about = this.aboutCache.get(name)
446 return about && about.name
447}
448
449function sbotGet(sbot, id, cb) {
450 // ssb-ooo@1.0.1 (a50da3928500f3ac0fbead0a1b335a3dd5bbc096): raw=true
451 // ssb-ooo@1.1.0 (f7302d12e56d566b84205bbc0c8b882ae6fd9b12): ooo=false
452 if (sbot.ooo) {
453 sbot.get({id: id, raw: true, ooo: false, private: true}, cb)
454 } else {
455 if (!sbot.private) {
456 // if no sbot.private, assume we have newer sbot that supports private:true
457 return sbot.get({id: id, private: true}, cb)
458 }
459 sbot.get(id, cb)
460 }
461}
462
463function getMsgWithValue(sbot, id, cb) {
464 if (!id) return cb()
465 sbotGet(sbot, id, function (err, value) {
466 if (err) return cb(err)
467 cb(null, {key: id, value: value})
468 })
469}
470
471App.prototype._getAbout = function (id, cb) {
472 var self = this
473 if (!u.isRef(id)) return cb(null, {})
474 self.about.get(id, function (err, about) {
475 if (err) return cb(err)
476 var sigil = id[0] || '@'
477 if (about.name && about.name[0] !== sigil) {
478 about.name = sigil + about.name
479 }
480 self.reverseNameCache.set(about.name, id)
481 cb(null, about)
482 })
483}
484
485App.prototype.pullGetMsg = function (id) {
486 return pull.asyncMap(this.getMsg)(pull.once(id))
487}
488
489App.prototype.createLogStream = function (opts) {
490 opts = opts || {}
491 return opts.sortByTimestamp
492 ? this.createFeedStream(opts)
493 : this.sbot.createLogStream(opts)
494}
495
496App.prototype.createFeedStream = function (opts) {
497 // work around opts.gt being treated as opts.gte sometimes
498 return pull(
499 this.sbot.createFeedStream(opts),
500 pull.filter(function (msg) {
501 var ts = msg && msg.value && msg.value.timestamp
502 return typeof ts === 'number' && ts !== opts.gt && ts !== opts.lt
503 })
504 )
505}
506
507var stateVals = {
508 connected: 3,
509 connecting: 2,
510 disconnecting: 1,
511}
512
513function comparePeers(a, b) {
514 var aState = stateVals[a.state] || 0
515 var bState = stateVals[b.state] || 0
516 return (bState - aState)
517 || (b.stateChange|0 - a.stateChange|0)
518}
519
520App.prototype.streamPeers = function (opts) {
521 var gossip = this.sbot.gossip
522 return u.readNext(function (cb) {
523 gossip.peers(function (err, peers) {
524 if (err) return cb(err)
525 if (opts) peers = peers.filter(function (peer) {
526 for (var k in opts) if (opts[k] !== peer[k]) return false
527 return true
528 })
529 peers.sort(comparePeers)
530 cb(null, pull.values(peers))
531 })
532 })
533}
534
535App.prototype.getContact = function (source, dest, cb) {
536 var self = this
537 pull(
538 self.sbot.links({source: source, dest: dest, rel: 'contact', reverse: true,
539 values: true, meta: false, keys: false}),
540 pull.filter(function (value) {
541 var c = value && !value.private && value.content
542 return c && c.type === 'contact'
543 }),
544 pull.take(1),
545 pull.reduce(function (acc, value) {
546 // trinary logic from ssb-friends
547 return value.content.following ? true
548 : value.content.flagged || value.content.blocking ? false
549 : null
550 }, null, cb)
551 )
552}
553
554App.prototype.isMuted = function (id, cb) {
555 var self = this
556 pull(
557 self.sbot.links({source: self.sbot.id, dest: id, rel: 'contact', reverse: true,
558 values: true, meta: false}), // meta:false to emulate private:true
559 pull.filter(function (msg) {
560 return msg && msg.value && typeof msg.value.content === 'string'
561 }),
562 this.unboxMessages(),
563 pull.filter(function (msg) {
564 var c = msg && msg.value && msg && msg.value.content
565 return c && c.type === 'contact'
566 }),
567 pull.take(1),
568 pull.reduce(function (acc, msg) {
569 var c = msg && msg.value && msg.value.content
570 return c.following ? false : c.flagged || c.blocking ? true : null
571 }, null, cb)
572 )
573}
574
575App.prototype.unboxMessages = function () {
576 return paramap(this.unboxMsg, 16)
577}
578
579App.prototype.streamChannels = function (opts) {
580 return pull(
581 this.sbotMessagesByType({type: 'channel', reverse: true}),
582 this.unboxMessages(),
583 pull.filter(function (msg) {
584 return msg.value.content.subscribed
585 }),
586 pull.map(function (msg) {
587 return msg.value.content.channel
588 }),
589 pull.unique()
590 )
591}
592
593App.prototype.streamMyChannels = function (id, opts) {
594 // use ssb-query plugin if it is available, since it has an index for
595 // author + type
596 if (this.sbot.query) return pull(
597 this.sbot.query.read({
598 reverse: true,
599 query: [
600 {$filter: {
601 value: {
602 author: id,
603 content: {type: 'channel'}
604 }
605 }},
606 {$map: ['value', 'content']}
607 ]
608 }),
609 pull.unique('channel'),
610 pull.filter('subscribed'),
611 pull.map('channel')
612 )
613
614 return pull(
615 this.sbotCreateUserStream({id: id, reverse: true}),
616 this.unboxMessages(),
617 pull.map(function (msg) {
618 return msg.value.content
619 }),
620 pull.filter(function (c) {
621 return c.type === 'channel'
622 }),
623 pull.unique('channel'),
624 pull.filter('subscribed'),
625 pull.map('channel')
626 )
627}
628
629App.prototype.streamTags = function () {
630 return pull(
631 this.sbotMessagesByType({type: 'tag', reverse: true}),
632 this.unboxMessages(),
633 pull.filter(function (msg) {
634 return !msg.value.content.message
635 })
636 )
637}
638
639function compareVoted(a, b) {
640 return b.value - a.value
641}
642
643App.prototype.getVoted = function (_opts, cb) {
644 if (isNaN(_opts.limit)) return pull.error(new Error('missing limit'))
645 var self = this
646 var opts = {
647 type: 'vote',
648 limit: _opts.limit * 100,
649 reverse: !!_opts.reverse,
650 gt: _opts.gt || undefined,
651 lt: _opts.lt || undefined,
652 }
653
654 var votedObj = {}
655 var votedArray = []
656 var numItems = 0
657 var firstTimestamp, lastTimestamp
658 pull(
659 self.sbotMessagesByType(opts),
660 self.unboxMessages(),
661 pull.take(function () {
662 return numItems < _opts.limit
663 }),
664 pull.drain(function (msg) {
665 if (!firstTimestamp) firstTimestamp = msg.timestamp
666 lastTimestamp = msg.timestamp
667 var vote = msg.value.content.vote
668 if (!vote) return
669 var target = u.linkDest(vote)
670 var votes = votedObj[target]
671 if (!votes) {
672 numItems++
673 votes = {id: target, value: 0, feedsObj: {}, feeds: []}
674 votedObj[target] = votes
675 votedArray.push(votes)
676 }
677 if (msg.value.author in votes.feedsObj) {
678 if (!opts.reverse) return // leave latest vote value as-is
679 // remove old vote value
680 votes.value -= votes.feedsObj[msg.value.author]
681 } else {
682 votes.feeds.push(msg.value.author)
683 }
684 var value = vote.value > 0 ? 1 : vote.value < 0 ? -1 : 0
685 votes.feedsObj[msg.value.author] = value
686 votes.value += value
687 }, function (err) {
688 if (err && err !== true) return cb(err)
689 var items = votedArray
690 if (opts.reverse) items.reverse()
691 items.sort(compareVoted)
692 cb(null, {items: items,
693 firstTimestamp: firstTimestamp,
694 lastTimestamp: lastTimestamp})
695 })
696 )
697}
698
699App.prototype.createAboutStreams = function (id) {
700 return this.about.createAboutStreams(id)
701}
702
703App.prototype.streamEmojis = function () {
704 return pull(
705 cat([
706 this.sbot.links({
707 rel: 'mentions',
708 source: this.sbot.id,
709 dest: '&',
710 values: true,
711 meta: false // emulate private:true
712 }),
713 this.sbot.links({rel: 'mentions', dest: '&', values: true, meta: false})
714 ]),
715 this.unboxMessages(),
716 pull.map(function (msg) { return msg.value.content.mentions }),
717 pull.flatten(),
718 pull.filter('emoji'),
719 pull.unique('link')
720 )
721}
722
723App.prototype.filter = function (plugin, opts, filter) {
724 // work around flumeview-query not picking the best index.
725 // %b+QdyLFQ21UGYwvV3AiD8FEr7mKlB8w9xx3h8WzSUb0=.sha256
726 var limit = Number(opts.limit)
727 var index
728 if (plugin === this.sbot.backlinks) {
729 var c = filter && filter.value && filter.value.content
730 var filteringByType = c && c.type
731 if (opts.sortByTimestamp) index = 'DTA'
732 else if (filteringByType) index = 'DTS'
733 }
734 var filterOpts = {
735 $gt: opts.gt,
736 $lt: opts.lt,
737 }
738 return plugin.read({
739 index: index,
740 reverse: opts.reverse,
741 limit: limit || undefined,
742 query: [{$filter: u.mergeOpts(filter, opts.sortByTimestamp ? {
743 value: {
744 timestamp: filterOpts
745 }
746 } : {
747 timestamp: filterOpts
748 })}]
749 })
750}
751
752App.prototype.filterMessages = function (opts) {
753 var self = this
754 var limit = Number(opts.limit)
755 return pull(
756 paramap(function (msg, cb) {
757 self.filterMsg(msg, opts, function (err, show) {
758 if (err) return cb(err)
759 cb(null, show ? msg : null)
760 })
761 }, 4),
762 pull.filter(Boolean),
763 limit && pull.take(limit)
764 )
765}
766
767App.prototype.streamChannel = function (opts) {
768 // prefer ssb-backlinks to ssb-query because it also handles hashtag mentions
769 if (this.sbot.backlinks) return this.filter(this.sbot.backlinks, opts, {
770 dest: '#' + opts.channel,
771 })
772
773 if (this.sbot.query) return this.filter(this.sbot.query, opts, {
774 value: {content: {channel: opts.channel}},
775 })
776
777 return pull.error(new Error(
778 'Viewing channels/tags requires the ssb-backlinks or ssb-query plugin'))
779}
780
781App.prototype.streamMentions = function (opts) {
782 if (!this.sbot.backlinks) return pull.error(new Error(
783 'Viewing mentions requires the ssb-backlinks plugin'))
784
785 if (this.sbot.backlinks) return this.filter(this.sbot.backlinks, opts, {
786 dest: this.sbot.id,
787 })
788}
789
790App.prototype.streamPrivate = function (opts) {
791 if (this.sbot.private && this.sbot.private.read)
792 return this.filter(this.sbot.private, opts, {})
793
794 return pull(
795 this.createLogStream(u.mergeOpts(opts, {private: true})),
796 pull.filter(u.isMsgEncrypted),
797 this.unboxMessages(),
798 pull.filter(u.isMsgReadable)
799 )
800}
801
802App.prototype.blobMentions = function (opts) {
803 if (!this.sbot.links2) return pull.error(new Error(
804 'missing ssb-links plugin'))
805 var filter = {rel: ['mentions', opts.name]}
806 if (opts.author) filter.source = opts.author
807 return this.sbot.links2.read({
808 query: [
809 {$filter: filter},
810 {$filter: {dest: {$prefix: '&'}}},
811 {$map: {
812 name: ['rel', 1],
813 size: ['rel', 2],
814 link: 'dest',
815 author: 'source',
816 time: 'ts'
817 }}
818 ]
819 })
820}
821
822App.prototype.monitorBlobWants = function () {
823 var self = this
824 self.blobWants = {}
825 pull(
826 this.sbot.blobs.createWants(),
827 pull.drain(function (wants) {
828 for (var id in wants) {
829 if (wants[id] < 0) self.blobWants[id] = true
830 else delete self.blobWants[id]
831 self.blobSizeCache.remove(id)
832 }
833 }, function (err) {
834 if (err) console.trace(err)
835 })
836 )
837}
838
839App.prototype.getBlobState = function (id, cb) {
840 var self = this
841 if (self.blobWants[id]) return cb(null, 'wanted')
842 self.getBlobSize(id, function (err, size) {
843 if (err) return cb(err)
844 cb(null, size != null)
845 })
846}
847
848App.prototype.getNpmReadme = function (tarballId, cb) {
849 var self = this
850 // TODO: make this portable, and handle plaintext readmes
851 var tar = proc.spawn('tar', ['--ignore-case', '-Oxz',
852 'package/README.md', 'package/readme.markdown', 'package/readme.mkd'])
853 var done = multicb({pluck: 1, spread: true})
854 pull(
855 self.sbot.blobs.get(tarballId),
856 toPull.sink(tar.stdin, done())
857 )
858 pull(
859 toPull.source(tar.stdout),
860 pull.collect(done())
861 )
862 done(function (err, _, bufs) {
863 if (err) return cb(err)
864 var text = Buffer.concat(bufs).toString('utf8')
865 cb(null, text, true)
866 })
867}
868
869App.prototype.filterMsg = function (msg, opts, cb) {
870 var self = this
871 var myId = self.sbot.id
872 var author = msg.value && msg.value.author
873 var filter = opts.filter || self.msgFilter
874 if (filter === 'all') return cb(null, true)
875 var show = (filter !== 'invert')
876 var isPrivate = msg.value && typeof msg.value.content === 'string'
877 if (isPrivate && !self.showPrivates) return cb(null, !show)
878 if (author === myId
879 || author === opts.feed
880 || msg.key === opts.msgId) return cb(null, show)
881 self.follows.getFollows(myId, function (err, follows) {
882 if (err) return cb(err)
883 if (follows[author]) return cb(null, show)
884 self.getVotes(msg.key, function (err, votes) {
885 if (err) return cb(err)
886 for (var author in votes) {
887 if (follows[author] && votes[author] > 0) {
888 return cb(null, show)
889 }
890 }
891 return cb(null, !show)
892 })
893 })
894}
895
896App.prototype.isFollowing = function (src, dest, cb) {
897 var self = this
898 self.follows.getFollows(src, function (err, follows) {
899 if (err) return cb(err)
900 return cb(null, follows[dest])
901 })
902}
903
904App.prototype.getVotesStream = function (id) {
905 var links2 = this.sbot.links2
906 if (links2 && links2.read) return links2.read({
907 query: [
908 {$filter: {
909 dest: id,
910 rel: [{$prefix: 'vote'}]
911 }},
912 {$map: {
913 value: ['rel', 1],
914 author: 'source'
915 }}
916 ]
917 })
918
919 var backlinks = this.sbot.backlinks
920 if (backlinks && backlinks.read) return backlinks.read({
921 query: [
922 {$filter: {
923 dest: id,
924 value: {
925 content: {
926 type: 'vote',
927 vote: {
928 link: id
929 }
930 }
931 }
932 }},
933 {$map: {
934 author: ['value', 'author'],
935 value: ['value', 'content', 'vote', 'value']
936 }}
937 ]
938 })
939
940 return pull(
941 this.sbot.links({
942 dest: id,
943 rel: 'vote',
944 keys: false,
945 meta: false,
946 values: true
947 }),
948 pull.map(function (value) {
949 var vote = value && value.content && value.content.vote
950 return {
951 author: value && value.author,
952 vote: vote && vote.value
953 }
954 })
955 )
956}
957
958App.prototype._getVotes = function (id, cb) {
959 var votes = {}
960 pull(
961 this.getVotesStream(),
962 pull.drain(function (vote) {
963 votes[vote.author] = vote.value
964 }, function (err) {
965 cb(err, votes)
966 })
967 )
968}
969
970App.prototype.getAddresses = function (id) {
971 if (!this.sbot.backlinks) {
972 if (!this.warned1) {
973 this.warned1 = true
974 console.trace('Getting peer addresses requires the ssb-backlinks plugin')
975 }
976 return pull.empty()
977 }
978
979 var pubMsgAddresses = pull(
980 this.sbot.backlinks.read({
981 reverse: true,
982 query: [
983 {$filter: {
984 dest: id,
985 value: {
986 content: {
987 type: 'pub',
988 address: {
989 key: id,
990 host: {$truthy: true},
991 port: {$truthy: true},
992 }
993 }
994 }
995 }},
996 {$map: ['value', 'content', 'address']}
997 ]
998 }),
999 pull.map(function (addr) {
1000 return addr.host + ':' + addr.port
1001 })
1002 )
1003
1004 var newerAddresses = pull(
1005 cat([
1006 this.sbot.query.read({
1007 reverse: true,
1008 query: [
1009 {$filter: {
1010 value: {
1011 author: id,
1012 content: {
1013 type: 'address',
1014 address: {$truthy: true}
1015 }
1016 }
1017 }},
1018 {$map: ['value', 'content', 'address']}
1019 ]
1020 }),
1021 this.sbot.query.read({
1022 reverse: true,
1023 query: [
1024 {$filter: {
1025 value: {
1026 author: id,
1027 content: {
1028 type: 'pub-owner-confirm',
1029 address: {$truthy: true}
1030 }
1031 }
1032 }},
1033 {$map: ['value', 'content', 'address']}
1034 ]
1035 })
1036 ]),
1037 pull.map(u.extractHostPort.bind(this, id))
1038 )
1039
1040 return pull(
1041 cat([newerAddresses, pubMsgAddresses]),
1042 pull.unique()
1043 )
1044}
1045
1046App.prototype.isPub = function (id, cb) {
1047 if (!this.sbot.backlinks) {
1048 return pull(
1049 this.sbot.links({
1050 dest: id,
1051 rel: 'pub'
1052 }),
1053 pull.take(1),
1054 pull.collect(function (err, links) {
1055 if (err) return cb(err)
1056 cb(null, links.length == 1)
1057 })
1058 )
1059 }
1060 return pull(
1061 this.sbot.backlinks.read({
1062 limit: 1,
1063 query: [
1064 {$filter: {
1065 dest: id,
1066 value: {
1067 content: {
1068 type: 'pub',
1069 address: {
1070 key: id,
1071 host: {$truthy: true},
1072 port: {$truthy: true},
1073 }
1074 }
1075 }
1076 }}
1077 ]
1078 }),
1079 pull.collect(function (err, msgs) {
1080 if (err) return cb(err)
1081 cb(null, msgs.length == 1)
1082 })
1083 )
1084}
1085
1086App.prototype.getIdeaTitle = function (id, cb) {
1087 if (!this.sbot.backlinks) return cb(null, String(id).substr(0, 8) + '…')
1088 pull(
1089 this.sbot.backlinks.read({
1090 reverse: true,
1091 query: [
1092 {$filter: {
1093 dest: id,
1094 value: {
1095 content: {
1096 type: 'talenet-idea-update',
1097 ideaKey: id,
1098 title: {$truthy: true}
1099 }
1100 }
1101 }},
1102 {$map: ['value', 'content', 'title']}
1103 ]
1104 }),
1105 pull.take(1),
1106 pull.collect(function (err, titles) {
1107 if (err) return cb(err)
1108 var title = titles && titles[0]
1109 || (String(id).substr(0, 8) + '…')
1110 cb(null, title)
1111 })
1112 )
1113}
1114
1115function traverse(obj, emit) {
1116 emit(obj)
1117 if (obj !== null && typeof obj === 'object') {
1118 for (var k in obj) {
1119 traverse(obj[k], emit)
1120 }
1121 }
1122}
1123
1124App.prototype.expandOoo = function (opts, cb) {
1125 var self = this
1126 var dest = opts.dest
1127 var msgs = opts.msgs
1128 if (!Array.isArray(msgs)) return cb(new TypeError('msgs should be array'))
1129
1130 // algorithm:
1131 // traverse all links in the initial message set.
1132 // find linked-to messages not in the set.
1133 // fetch those messages.
1134 // if one links to the dest, add it to the set
1135 // and look for more missing links to fetch.
1136 // done when no more links to fetch
1137
1138 var msgsO = {}
1139 var getting = {}
1140 var waiting = 0
1141
1142 function checkDone() {
1143 if (waiting) return
1144 var msgs = Object.keys(msgsO).map(function (key) {
1145 return msgsO[key]
1146 })
1147 cb(null, msgs)
1148 }
1149
1150 function getMsg(id) {
1151 if (msgsO[id] || getting[id]) return
1152 getting[id] = true
1153 waiting++
1154 self.getMsgDecryptedOoo(id, function (err, msg) {
1155 waiting--
1156 if (err) console.trace(err)
1157 else gotMsg(msg)
1158 checkDone()
1159 })
1160 }
1161
1162 var links = {}
1163 function addLink(id) {
1164 if (typeof id === 'string' && id[0] === '%' && u.isRef(id)) {
1165 links[id] = true
1166 }
1167 }
1168
1169 msgs.forEach(function (msg) {
1170 if (msgs[msg.key]) return
1171 if (msg.value.content === false) return // missing root
1172 msgsO[msg.key] = msg
1173 traverse(msg, addLink)
1174 })
1175 waiting++
1176 for (var id in links) {
1177 getMsg(id)
1178 }
1179 waiting--
1180 checkDone()
1181
1182 function gotMsg(msg) {
1183 if (msgsO[msg.key]) return
1184 var links = []
1185 var linkedToDest = msg.key === dest
1186 traverse(msg, function (id) {
1187 if (id === dest) linkedToDest = true
1188 links.push(id)
1189 })
1190 if (linkedToDest) {
1191 msgsO[msg.key] = msg
1192 links.forEach(addLink)
1193 }
1194 }
1195}
1196
1197App.prototype.getLineComments = function (opts, cb) {
1198 // get line comments for a git-update message and git object id.
1199 // line comments include message id, commit id and path
1200 // but we have message id and git object hash.
1201 // look up the git object hash for each line-comment
1202 // to verify that it is for the git object file we want
1203 var updateId = opts.obj.msg.key
1204 var objId = opts.hash
1205 var self = this
1206 var lineComments = {}
1207 pull(
1208 self.sbot.backlinks ? self.sbot.backlinks.read({
1209 query: [
1210 {$filter: {
1211 dest: updateId,
1212 value: {
1213 content: {
1214 type: 'line-comment',
1215 updateId: updateId,
1216 }
1217 }
1218 }}
1219 ]
1220 }) : pull(
1221 self.sbot.links({
1222 dest: updateId,
1223 rel: 'updateId',
1224 values: true,
1225 meta: false,
1226 }),
1227 pull.filter(function (msg) {
1228 var c = msg && msg.value && msg.value.content
1229 return c && c.type === 'line-comment'
1230 && c.updateId === updateId
1231 })
1232 ),
1233 paramap(function (msg, cb) {
1234 var c = msg.value.content
1235 self.git.getObjectAtPath({
1236 msg: updateId,
1237 obj: c.commitId,
1238 path: c.filePath,
1239 }, function (err, info) {
1240 if (err) return cb(err)
1241 cb(null, {
1242 obj: info.obj,
1243 hash: info.hash,
1244 msg: msg,
1245 })
1246 })
1247 }, 4),
1248 pull.filter(function (info) {
1249 return info.hash === objId
1250 }),
1251 pull.drain(function (info) {
1252 lineComments[info.msg.value.content.line] = info
1253 }, function (err) {
1254 cb(err, lineComments)
1255 })
1256 )
1257}
1258
1259App.prototype.sbotLinks = function (opts) {
1260 if (!this.sbot.links) return pull.error(new Error('missing sbot.links'))
1261 return this.sbot.links(opts)
1262}
1263
1264App.prototype.sbotCreateUserStream = function (opts) {
1265 if (!this.sbot.createUserStream) return pull.error(new Error('missing sbot.createUserStream'))
1266 return this.sbot.createUserStream(opts)
1267}
1268
1269App.prototype.sbotMessagesByType = function (opts) {
1270 if (!this.sbot.messagesByType) return pull.error(new Error('missing sbot.messagesByType'))
1271 return this.sbot.messagesByType(opts)
1272}
1273
1274App.prototype.getThread = function (msg) {
1275 return cat([
1276 pull.once(msg),
1277 this.sbot.backlinks ? this.sbot.backlinks.read({
1278 query: [
1279 {$filter: {dest: msg.key}}
1280 ]
1281 }) : this.sbotLinks({
1282 dest: msg.key,
1283 meta: false,
1284 values: true
1285 })
1286 ])
1287}
1288
1289App.prototype.getLinks = function (id) {
1290 return this.sbot.backlinks ? this.sbot.backlinks.read({
1291 query: [
1292 {$filter: {dest: id}}
1293 ]
1294 }) : this.sbotLinks({
1295 dest: id,
1296 meta: false,
1297 values: true
1298 })
1299}
1300
1301App.prototype.getShard = function (id, cb) {
1302 var self = this
1303 this.getMsgDecrypted(id, function (err, msg) {
1304 if (err) return cb(new Error('Unable to get shard message: ' + err.stack))
1305 var c = msg.value.content || {}
1306 if (!c.shard) return cb(new Error('Message missing shard: ' + id))
1307 self.unboxContent(c.shard, function (err, shard) {
1308 if (err) return cb(new Error('Unable to decrypt shard: ' + err.stack))
1309 cb(null, shard)
1310 })
1311 })
1312}
1313
1314App.prototype.sbotStatus = function (cb) {
1315 /* sbot.status is a "sync" method. if we are a plugin, it is sync. if we are
1316 * calling over muxrpc, it is async. */
1317 var status
1318 try {
1319 status = this.sbot.status(cb)
1320 } catch(err) {
1321 return cb(err)
1322 }
1323 if (typeof status === 'object' && status !== null) return cb(null, status)
1324}
1325
1326function writeAll(fd, buf, cb) {
1327 var offset = 0
1328 var remaining = buf.length
1329 fs.write(fd, buf, function onWrite(err, bytesWritten) {
1330 if (err) return cb(err)
1331 offset += bytesWritten
1332 remaining -= bytesWritten
1333 if (remaining > 0) fs.write(fd, buf, offset, remaining, null, onWrite)
1334 else cb()
1335 })
1336}
1337
1338App.prototype.verifyGitObjectSignature = function (obj, cb) {
1339 var self = this
1340 var tmpPath = path.join(os.tmpdir(), '.git_vtag_tmp' + Math.random().toString('36'))
1341 // use a temp file to work around https://github.com/nodejs/node/issues/13542
1342 function closeEnd(err) {
1343 fs.close(fd, function (err1) {
1344 fs.unlink(tmpPath, function (err2) {
1345 cb(err2 || err1 || err)
1346 })
1347 })
1348 }
1349 fs.open(tmpPath, 'w+', function (err, fd) {
1350 if (err) return cb(err)
1351 self.git.extractSignature(obj, function (err, parts) {
1352 if (err) return closeEnd(err)
1353 writeAll(fd, parts.signature, function (err) {
1354 if (err) return closeEnd(err)
1355 try { next(fd, parts) }
1356 catch(e) { closeEnd(e) }
1357 })
1358 })
1359 })
1360 function next(fd, parts) {
1361 var readSig = fs.createReadStream(null, {fd: fd, start: 0})
1362 var done = multicb({pluck: 1, spread: true})
1363 var gpg = proc.spawn('gpg', ['--status-fd=1', '--keyid-format=long',
1364 '--verify', '/dev/fd/3', '-'], {
1365 stdio: ['pipe', 'pipe', 'pipe', readSig]
1366 }).on('close', done().bind(null, null))
1367 .on('error', console.error.bind(console, 'gpg'))
1368 gpg.stdin.end(parts.payload)
1369 pull(toPull.source(gpg.stdout), u.pullConcat(done()))
1370 pull(toPull.source(gpg.stderr), u.pullConcat(done()))
1371 done(function (err, code, status, output) {
1372 if (err) return closeEnd(err)
1373 fs.unlink(tmpPath, function (err) {
1374 if (err) return cb(err)
1375 cb(null, {
1376 goodsig: status.includes('\n[GNUPG:] GOODSIG '),
1377 status: status.toString(),
1378 output: output.toString()
1379 })
1380 })
1381 })
1382 }
1383}
1384
1385App.prototype.getScript = function (filepath, cb) {
1386 var filename = path.join(this.scriptDir, filepath)
1387 var self = this
1388 fs.stat(filename, function (err, stat) {
1389 if (err) return cb(err)
1390 var resolved
1391 try { resolved = require.resolve(filename) }
1392 catch(e) { return cb(e) }
1393 var prevMtime = self.mtimes[resolved]
1394 var mtime = stat.mtime.getTime()
1395 if (mtime !== prevMtime) {
1396 delete require.cache[resolved]
1397 self.mtimes[filename] = mtime
1398 }
1399 var module
1400 try { module = require(resolved) }
1401 catch(e) { return cb(e) }
1402 cb(null, module)
1403 })
1404}
1405
1406function writeNewFile(dir, data, tries, cb) {
1407 var id = Base64URL.encode(crypto.randomBytes(8))
1408 fs.writeFile(path.join(dir, id), data, {flag: 'wx'}, function (err) {
1409 if (err && err.code === 'EEXIST' && tries > 0) return writeNewFile(dir, data, tries-1, cb)
1410 if (err) return cb(err)
1411 cb(null, id)
1412 })
1413}
1414
1415App.prototype.saveDraft = function (id, url, form, content, cb) {
1416 var self = this
1417 if (!self.madeDraftsDir) {
1418 mkdirp.sync(self.draftsDir)
1419 self.madeDraftsDir = true
1420 }
1421 if (/[\/:\\]/.test(id)) return cb(new Error('draft id cannot contain path seperators'))
1422 var draft = {
1423 url: url,
1424 form: form,
1425 content: content
1426 }
1427 var data = JSON.stringify(draft)
1428 if (id) fs.writeFile(path.join(self.draftsDir, id), data, cb)
1429 else writeNewFile(self.draftsDir, data, 32, cb)
1430}
1431
1432App.prototype.getDraft = function (id, cb) {
1433 var self = this
1434 fs.readFile(path.join(self.draftsDir, id), 'utf8', function (err, data) {
1435 if (err) return cb(err)
1436 var draft
1437 try { draft = JSON.parse(data) }
1438 catch(e) { return cb(e) }
1439 draft.id = id
1440 cb(null, draft)
1441 })
1442}
1443
1444App.prototype.discardDraft = function (id, cb) {
1445 fs.unlink(path.join(this.draftsDir, id), cb)
1446}
1447
1448function compareMtime(a, b) {
1449 return b.mtime.getTime() - a.mtime.getTime()
1450}
1451
1452function statAll(files, dir, cb) {
1453 pull(
1454 pull.values(files),
1455 paramap(function (file, cb) {
1456 fs.stat(path.join(dir, file), function (err, stats) {
1457 if (err) return cb(err)
1458 stats.name = file
1459 cb(null, stats)
1460 })
1461 }, 8),
1462 pull.collect(cb)
1463 )
1464}
1465
1466App.prototype.listDrafts = function () {
1467 var self = this
1468 return u.readNext(function (cb) {
1469 fs.readdir(self.draftsDir, function (err, files) {
1470 if (err && err.code === 'ENOENT') return cb(null, pull.empty())
1471 if (err) return cb(err)
1472 statAll(files, self.draftsDir, function (err, stats) {
1473 if (err) return cb(err)
1474 stats.sort(compareMtime)
1475 cb(null, pull(
1476 pull.values(stats),
1477 paramap(function (stat, cb) {
1478 self.getDraft(stat.name, cb)
1479 }, 4)
1480 ))
1481 })
1482 })
1483 })
1484}
1485
1486App.prototype.removeDefaultPort = function (addr) {
1487 return addr.replace(this.portRegexp, '')
1488}
1489

Built with git-ssb-web