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