git ssb

16+

cel / patchfoo



Tree: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018

Files: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018 / lib / app.js

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

Built with git-ssb-web