git ssb

16+

cel / patchfoo



Tree: b614cb87e82036808f31e0940b9a67f609212fe4

Files: b614cb87e82036808f31e0940b9a67f609212fe4 / lib / app.js

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

Built with git-ssb-web