Files: a36543131c127121355391b4c743f49b7280b2aa / lib / app.js
41558 bytesRaw
1 | var http = require('http') |
2 | var memo = require('asyncmemo') |
3 | var lru = require('hashlru') |
4 | var pkg = require('../package') |
5 | var u = require('./util') |
6 | var pull = require('pull-stream') |
7 | var multicb = require('multicb') |
8 | var paramap = require('pull-paramap') |
9 | var Contacts = require('./contacts') |
10 | var PrivateBox = require('private-box') |
11 | var About = require('./about') |
12 | var Follows = require('./follows') |
13 | var Serve = require('./serve') |
14 | var Render = require('./render') |
15 | var Git = require('ssb-git') |
16 | var cat = require('pull-cat') |
17 | var proc = require('child_process') |
18 | var toPull = require('stream-to-pull-stream') |
19 | var BoxStream = require('pull-box-stream') |
20 | var crypto = require('crypto') |
21 | var SsbNpmRegistry = require('ssb-npm-registry') |
22 | var os = require('os') |
23 | var path = require('path') |
24 | var fs = require('fs') |
25 | var mkdirp = require('mkdirp') |
26 | var Base64URL = require('base64-url') |
27 | var ssbKeys = require('ssb-keys') |
28 | |
29 | var zeros = new Buffer(24); zeros.fill(0) |
30 | |
31 | module.exports = App |
32 | |
33 | function 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 | |
115 | App.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 | |
142 | var logPrefix = '[' + pkg.name + ']' |
143 | App.prototype.log = console.log.bind(console, logPrefix) |
144 | App.prototype.error = console.error.bind(console, logPrefix) |
145 | |
146 | App.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 | |
162 | App.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 | |
177 | App.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 | |
198 | App.prototype.unboxMsg = function (msg, cb) { |
199 | return this.unboxMsgWithKey(msg, null, cb) |
200 | } |
201 | |
202 | App.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 | |
210 | App.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 | |
251 | function 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 | |
264 | function 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 | |
282 | App.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 | |
290 | App.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 | |
296 | App.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 | |
304 | App.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 | |
314 | App.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 | |
337 | App.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 | |
351 | App.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 | |
364 | App.prototype.addBlob = function (isPrivate, cb) { |
365 | if (!isPrivate) return this.addBlobRaw(cb) |
366 | else return this.addBlobPrivate(cb) |
367 | } |
368 | |
369 | App.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 | |
392 | App.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 | |
408 | App.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 | |
418 | App.prototype.pushBlob = function (id, cb) { |
419 | console.error('pushing blob', id) |
420 | this.sbot.blobs.push(id, cb) |
421 | } |
422 | |
423 | App.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 | |
431 | App.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 | |
444 | App.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 | |
463 | App.prototype.getReverseNameSync = function (name) { |
464 | var id = this.reverseNameCache.get(name) |
465 | return id |
466 | } |
467 | |
468 | App.prototype.getReverseEmojiNameSync = function (name) { |
469 | return this.reverseEmojiNameCache.get(name) |
470 | } |
471 | |
472 | App.prototype.getNameSync = function (name) { |
473 | var about = this.aboutCache.get(name) |
474 | return about && about.name |
475 | } |
476 | |
477 | function 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 | |
491 | function 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 | |
510 | App.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 | |
524 | App.prototype.pullGetMsg = function (id) { |
525 | return pull.asyncMap(this.getMsg)(pull.once(id)) |
526 | } |
527 | |
528 | App.prototype.createLogStream = function (opts) { |
529 | opts = opts || {} |
530 | return opts.sortByTimestamp |
531 | ? this.createFeedStream(opts) |
532 | : this.sbot.createLogStream(opts) |
533 | } |
534 | |
535 | App.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 | |
546 | var stateVals = { |
547 | connected: 3, |
548 | connecting: 2, |
549 | disconnecting: 1, |
550 | } |
551 | |
552 | function 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 | |
559 | App.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 | |
574 | App.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 | |
593 | App.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 | |
614 | App.prototype.unboxMessages = function () { |
615 | return paramap(this.unboxMsg, 16) |
616 | } |
617 | |
618 | App.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 | |
632 | App.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 | |
668 | App.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 | |
678 | function compareVoted(a, b) { |
679 | return b.value - a.value |
680 | } |
681 | |
682 | App.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 | |
738 | App.prototype.createAboutStreams = function (id) { |
739 | return this.about.createAboutStreams(id) |
740 | } |
741 | |
742 | App.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 | |
762 | App.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 | |
791 | App.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 | |
806 | App.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 | |
820 | App.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 | |
829 | App.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 | |
841 | App.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 | |
861 | App.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 | |
878 | App.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 | |
887 | App.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 | |
908 | App.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 | |
935 | App.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 | |
943 | App.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 | |
997 | App.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 | |
1009 | App.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 | |
1085 | App.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 | |
1125 | App.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 | |
1154 | function 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 | |
1163 | App.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 | |
1236 | App.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 | |
1298 | App.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 | |
1303 | App.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 | |
1308 | App.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 | |
1313 | App.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 | |
1328 | App.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 | |
1340 | App.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 | |
1360 | App.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 | |
1373 | App.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 | |
1385 | function 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 | |
1397 | App.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 | |
1444 | App.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 | |
1465 | function 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 | |
1474 | App.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 | |
1491 | App.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 | |
1503 | App.prototype.discardDraft = function (id, cb) { |
1504 | fs.unlink(path.join(this.draftsDir, id), cb) |
1505 | } |
1506 | |
1507 | function compareMtime(a, b) { |
1508 | return b.mtime.getTime() - a.mtime.getTime() |
1509 | } |
1510 | |
1511 | function 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 | |
1525 | App.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 | |
1545 | App.prototype.removeDefaultPort = function (addr) { |
1546 | return addr.replace(this.portRegexp, '') |
1547 | } |
1548 |
Built with git-ssb-web