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