Files: 47dda2fa1ff7e9e2cc281278bf33e79e46a38873 / lib / app.js
14218 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 hasher = require('pull-hash/ext/ssb') |
8 | var multicb = require('multicb') |
9 | var paramap = require('pull-paramap') |
10 | var Contacts = require('ssb-contact') |
11 | var About = require('./about') |
12 | var Serve = require('./serve') |
13 | var Render = require('./render') |
14 | var Git = require('./git') |
15 | var cat = require('pull-cat') |
16 | |
17 | module.exports = App |
18 | |
19 | function App(sbot, config) { |
20 | this.sbot = sbot |
21 | this.config = config |
22 | |
23 | var conf = config.patchfoo || {} |
24 | this.port = conf.port || 8027 |
25 | this.host = conf.host || '::1' |
26 | |
27 | var base = conf.base || '/' |
28 | this.opts = { |
29 | base: base, |
30 | blob_base: conf.blob_base || conf.img_base || base, |
31 | img_base: conf.img_base || base, |
32 | emoji_base: conf.emoji_base || (base + 'emoji/'), |
33 | } |
34 | |
35 | sbot.get = memo({cache: lru(100)}, sbot.get) |
36 | this.about = new About(this, sbot.id) |
37 | this.getMsg = memo({cache: lru(100)}, getMsgWithValue, sbot) |
38 | this.getAbout = memo({cache: this.aboutCache = lru(500)}, |
39 | this._getAbout.bind(this)) |
40 | this.unboxContent = memo({cache: lru(100)}, sbot.private.unbox) |
41 | this.reverseNameCache = lru(500) |
42 | this.reverseEmojiNameCache = lru(500) |
43 | |
44 | this.unboxMsg = this.unboxMsg.bind(this) |
45 | |
46 | this.render = new Render(this, this.opts) |
47 | this.git = new Git(this) |
48 | } |
49 | |
50 | App.prototype.go = function () { |
51 | var self = this |
52 | http.createServer(function (req, res) { |
53 | new Serve(self, req, res).go() |
54 | }).listen(self.port, self.host, function () { |
55 | var host = /:/.test(self.host) ? '[' + self.host + ']' : self.host |
56 | self.log('Listening on http://' + host + ':' + self.port) |
57 | }) |
58 | |
59 | // invalidate cached About info when new About messages come in |
60 | pull( |
61 | self.sbot.links({rel: 'about', old: false, values: true}), |
62 | pull.drain(function (link) { |
63 | self.aboutCache.remove(link.dest) |
64 | }, function (err) { |
65 | if (err) self.error('about:', err) |
66 | }) |
67 | ) |
68 | } |
69 | |
70 | var logPrefix = '[' + pkg.name + ']' |
71 | App.prototype.log = console.log.bind(console, logPrefix) |
72 | App.prototype.error = console.error.bind(console, logPrefix) |
73 | |
74 | App.prototype.unboxMsg = function (msg, cb) { |
75 | var self = this |
76 | var c = msg.value && msg.value.content |
77 | if (typeof c !== 'string') cb(null, msg) |
78 | else self.unboxContent(c, function (err, content) { |
79 | if (err) { |
80 | self.error('unbox:', err) |
81 | return cb(null, msg) |
82 | } else if (!content) { |
83 | return cb(null, msg) |
84 | } |
85 | var m = {} |
86 | for (var k in msg) m[k] = msg[k] |
87 | m.value = {} |
88 | for (var k in msg.value) m.value[k] = msg.value[k] |
89 | m.value.content = content |
90 | m.value.private = true |
91 | cb(null, m) |
92 | }) |
93 | } |
94 | |
95 | App.prototype.search = function (opts) { |
96 | var search = this.sbot.fulltext && this.sbot.fulltext.search |
97 | if (!search) return pull.error(new Error('Missing fulltext search plugin')) |
98 | return search(opts) |
99 | } |
100 | |
101 | App.prototype.advancedSearch = function (opts) { |
102 | return pull( |
103 | opts.dest ? |
104 | this.sbot.links({ |
105 | values: true, |
106 | dest: opts.dest, |
107 | source: opts.source || undefined, |
108 | reverse: true, |
109 | }) |
110 | : opts.source ? |
111 | this.sbot.createUserStream({ |
112 | reverse: true, |
113 | id: opts.source |
114 | }) |
115 | : |
116 | this.sbot.createFeedStream({ |
117 | reverse: true, |
118 | }), |
119 | opts.text && pull.filter(filterByText(opts.text)) |
120 | ) |
121 | } |
122 | |
123 | function forSome(each) { |
124 | return function some(obj) { |
125 | if (obj == null) return false |
126 | if (typeof obj === 'string') return each(obj) |
127 | if (Array.isArray(obj)) return obj.some(some) |
128 | if (typeof obj === 'object') |
129 | for (var k in obj) if (some(obj[k])) return true |
130 | return false |
131 | } |
132 | } |
133 | |
134 | function filterByText(str) { |
135 | if (!str) return function () { return true } |
136 | var search = new RegExp(str, 'i') |
137 | var matches = forSome(search.test.bind(search)) |
138 | return function (msg) { |
139 | var c = msg.value.content |
140 | return c && matches(c) |
141 | } |
142 | } |
143 | |
144 | App.prototype.getMsgDecrypted = function (key, cb) { |
145 | var self = this |
146 | this.getMsg(key, function (err, msg) { |
147 | if (err) return cb(err) |
148 | self.unboxMsg(msg, cb) |
149 | }) |
150 | } |
151 | |
152 | App.prototype.publish = function (content, cb) { |
153 | var self = this |
154 | function tryPublish(triesLeft) { |
155 | if (Array.isArray(content.recps)) { |
156 | recps = content.recps.map(u.linkDest) |
157 | self.sbot.private.publish(content, recps, next) |
158 | } else { |
159 | self.sbot.publish(content, next) |
160 | } |
161 | function next(err, msg) { |
162 | if (err) { |
163 | if (triesLeft > 0) { |
164 | if (/^expected previous:/.test(err.message)) { |
165 | return tryPublish(triesLeft-1) |
166 | } |
167 | } |
168 | } |
169 | return cb(err, msg) |
170 | } |
171 | } |
172 | tryPublish(2) |
173 | } |
174 | |
175 | App.prototype.wantSizeBlob = function (id, cb) { |
176 | var blobs = this.sbot.blobs |
177 | blobs.size(id, function (err, size) { |
178 | if (size != null) return cb(null, size) |
179 | blobs.want(id, function (err) { |
180 | if (err) return cb(err) |
181 | blobs.size(id, cb) |
182 | }) |
183 | }) |
184 | } |
185 | |
186 | App.prototype.addBlob = function (cb) { |
187 | var done = multicb({pluck: 1, spread: true}) |
188 | var hashCb = done() |
189 | var addCb = done() |
190 | done(function (err, hash, add) { |
191 | cb(err, hash) |
192 | }) |
193 | return pull( |
194 | hasher(hashCb), |
195 | this.sbot.blobs.add(addCb) |
196 | ) |
197 | } |
198 | |
199 | App.prototype.pushBlob = function (id, cb) { |
200 | console.error('pushing blob', id) |
201 | this.sbot.blobs.push(id, cb) |
202 | } |
203 | |
204 | App.prototype.readBlob = function (link) { |
205 | link = u.toLink(link) |
206 | return this.sbot.blobs.get({ |
207 | hash: link.link, |
208 | size: link.size, |
209 | }) |
210 | } |
211 | |
212 | App.prototype.readBlobSlice = function (link, opts) { |
213 | if (this.sbot.blobs.getSlice) return this.sbot.blobs.getSlice({ |
214 | hash: link.link, |
215 | size: link.size, |
216 | start: opts.start, |
217 | end: opts.end, |
218 | }) |
219 | return pull( |
220 | this.readBlob(link), |
221 | u.pullSlice(opts.start, opts.end) |
222 | ) |
223 | } |
224 | |
225 | App.prototype.ensureHasBlobs = function (links, cb) { |
226 | var self = this |
227 | var done = multicb({pluck: 1}) |
228 | links.forEach(function (link) { |
229 | var cb = done() |
230 | self.sbot.blobs.size(link.link, function (err, size) { |
231 | if (err) cb(err) |
232 | else if (size == null) cb(null, link) |
233 | else cb() |
234 | }) |
235 | }) |
236 | done(function (err, missingLinks) { |
237 | if (err) console.trace(err) |
238 | missingLinks = missingLinks.filter(Boolean) |
239 | if (missingLinks.length == 0) return cb() |
240 | return cb({name: 'BlobNotFoundError', links: missingLinks}) |
241 | }) |
242 | } |
243 | |
244 | App.prototype.getReverseNameSync = function (name) { |
245 | var id = this.reverseNameCache.get(name) |
246 | return id |
247 | } |
248 | |
249 | App.prototype.getReverseEmojiNameSync = function (name) { |
250 | return this.reverseEmojiNameCache.get(name) |
251 | } |
252 | |
253 | App.prototype.getNameSync = function (name) { |
254 | var about = this.aboutCache.get(name) |
255 | return about && about.name |
256 | } |
257 | |
258 | function getMsgWithValue(sbot, id, cb) { |
259 | if (!id) return cb() |
260 | sbot.get(id, function (err, value) { |
261 | if (err) return cb(err) |
262 | cb(null, {key: id, value: value}) |
263 | }) |
264 | } |
265 | |
266 | App.prototype._getAbout = function (id, cb) { |
267 | var self = this |
268 | if (!u.isRef(id)) return cb(null, {}) |
269 | self.about.get(id, function (err, about) { |
270 | if (err) return cb(err) |
271 | var sigil = id[0] || '@' |
272 | if (about.name && about.name[0] !== sigil) { |
273 | about.name = sigil + about.name |
274 | } |
275 | self.reverseNameCache.set(about.name, id) |
276 | cb(null, about) |
277 | }) |
278 | } |
279 | |
280 | App.prototype.pullGetMsg = function (id) { |
281 | return pull.asyncMap(this.getMsg)(pull.once(id)) |
282 | } |
283 | |
284 | App.prototype.createLogStream = function (opts) { |
285 | opts = opts || {} |
286 | return opts.sortByTimestamp |
287 | ? this.sbot.createFeedStream(opts) |
288 | : this.sbot.createLogStream(opts) |
289 | } |
290 | |
291 | var stateVals = { |
292 | connected: 3, |
293 | connecting: 2, |
294 | disconnecting: 1, |
295 | } |
296 | |
297 | function comparePeers(a, b) { |
298 | var aState = stateVals[a.state] || 0 |
299 | var bState = stateVals[b.state] || 0 |
300 | return (bState - aState) |
301 | || (b.stateChange|0 - a.stateChange|0) |
302 | } |
303 | |
304 | App.prototype.streamPeers = function (opts) { |
305 | var gossip = this.sbot.gossip |
306 | return u.readNext(function (cb) { |
307 | gossip.peers(function (err, peers) { |
308 | if (err) return cb(err) |
309 | if (opts) peers = peers.filter(function (peer) { |
310 | for (var k in opts) if (opts[k] !== peer[k]) return false |
311 | return true |
312 | }) |
313 | peers.sort(comparePeers) |
314 | cb(null, pull.values(peers)) |
315 | }) |
316 | }) |
317 | } |
318 | |
319 | App.prototype.getFollow = function (source, dest, cb) { |
320 | var self = this |
321 | pull( |
322 | self.sbot.links({source: source, dest: dest, rel: 'contact', reverse: true, |
323 | values: true, meta: false, keys: false}), |
324 | pull.filter(function (value) { |
325 | var c = value && value.content |
326 | return c && c.type === 'contact' |
327 | }), |
328 | pull.take(1), |
329 | pull.collect(function (err, msgs) { |
330 | if (err) return cb(err) |
331 | cb(null, msgs[0] && !!msgs[0].content.following) |
332 | }) |
333 | ) |
334 | } |
335 | |
336 | App.prototype.unboxMessages = function () { |
337 | return paramap(this.unboxMsg, 16) |
338 | } |
339 | |
340 | App.prototype.streamChannels = function (opts) { |
341 | return pull( |
342 | this.sbot.messagesByType({type: 'channel', reverse: true}), |
343 | this.unboxMessages(), |
344 | pull.filter(function (msg) { |
345 | return msg.value.content.subscribed |
346 | }), |
347 | pull.map(function (msg) { |
348 | return msg.value.content.channel |
349 | }), |
350 | pull.unique() |
351 | ) |
352 | } |
353 | |
354 | App.prototype.streamMyChannels = function (id, opts) { |
355 | // use ssb-query plugin if it is available, since it has an index for |
356 | // author + type |
357 | if (this.sbot.query) return pull( |
358 | this.sbot.query.read({ |
359 | reverse: true, |
360 | query: [ |
361 | {$filter: { |
362 | value: { |
363 | author: id, |
364 | content: {type: 'channel', subscribed: true} |
365 | } |
366 | }}, |
367 | {$map: ['value', 'content', 'channel']} |
368 | ] |
369 | }), |
370 | pull.unique() |
371 | ) |
372 | |
373 | return pull( |
374 | this.sbot.createUserStream({id: id, reverse: true}), |
375 | this.unboxMessages(), |
376 | pull.filter(function (msg) { |
377 | if (msg.value.content.type == 'channel') { |
378 | return msg.value.content.subscribed |
379 | } |
380 | }), |
381 | pull.map(function (msg) { |
382 | return msg.value.content.channel |
383 | }), |
384 | pull.unique() |
385 | ) |
386 | } |
387 | |
388 | App.prototype.createContactStreams = function (id) { |
389 | return new Contacts(this.sbot).createContactStreams(id) |
390 | } |
391 | |
392 | function compareVoted(a, b) { |
393 | return b.value - a.value |
394 | } |
395 | |
396 | App.prototype.getVoted = function (_opts, cb) { |
397 | if (isNaN(_opts.limit)) return pull.error(new Error('missing limit')) |
398 | var self = this |
399 | var opts = { |
400 | type: 'vote', |
401 | limit: _opts.limit * 100, |
402 | reverse: !!_opts.reverse, |
403 | gt: _opts.gt || undefined, |
404 | lt: _opts.lt || undefined, |
405 | } |
406 | |
407 | var votedObj = {} |
408 | var votedArray = [] |
409 | var numItems = 0 |
410 | var firstTimestamp, lastTimestamp |
411 | pull( |
412 | self.sbot.messagesByType(opts), |
413 | self.unboxMessages(), |
414 | pull.take(function () { |
415 | return numItems < _opts.limit |
416 | }), |
417 | pull.drain(function (msg) { |
418 | if (!firstTimestamp) firstTimestamp = msg.timestamp |
419 | lastTimestamp = msg.timestamp |
420 | var vote = msg.value.content.vote |
421 | if (!vote) return |
422 | var target = u.linkDest(vote) |
423 | var votes = votedObj[target] |
424 | if (!votes) { |
425 | numItems++ |
426 | votes = {id: target, value: 0, feedsObj: {}, feeds: []} |
427 | votedObj[target] = votes |
428 | votedArray.push(votes) |
429 | } |
430 | if (msg.value.author in votes.feedsObj) { |
431 | if (!opts.reverse) return // leave latest vote value as-is |
432 | // remove old vote value |
433 | votes.value -= votes.feedsObj[msg.value.author] |
434 | } else { |
435 | votes.feeds.push(msg.value.author) |
436 | } |
437 | var value = vote.value > 0 ? 1 : vote.value < 0 ? -1 : 0 |
438 | votes.feedsObj[msg.value.author] = value |
439 | votes.value += value |
440 | }, function (err) { |
441 | if (err) return cb(err) |
442 | var items = votedArray |
443 | if (opts.reverse) items.reverse() |
444 | items.sort(compareVoted) |
445 | cb(null, {items: items, |
446 | firstTimestamp: firstTimestamp, |
447 | lastTimestamp: lastTimestamp}) |
448 | }) |
449 | ) |
450 | } |
451 | |
452 | App.prototype.createAboutStreams = function (id) { |
453 | return this.about.createAboutStreams(id) |
454 | } |
455 | |
456 | App.prototype.streamEmojis = function () { |
457 | return pull( |
458 | cat([ |
459 | this.sbot.links({ |
460 | rel: 'mentions', |
461 | source: this.sbot.id, |
462 | dest: '&', |
463 | values: true |
464 | }), |
465 | this.sbot.links({rel: 'mentions', dest: '&', values: true}) |
466 | ]), |
467 | this.unboxMessages(), |
468 | pull.map(function (msg) { return msg.value.content.mentions }), |
469 | pull.flatten(), |
470 | pull.filter('emoji'), |
471 | pull.unique('link') |
472 | ) |
473 | } |
474 | |
475 | App.prototype.filter = function (plugin, opts, filter) { |
476 | // work around flumeview-query not picking the best index. |
477 | // %b+QdyLFQ21UGYwvV3AiD8FEr7mKlB8w9xx3h8WzSUb0=.sha256 |
478 | var index |
479 | if (plugin === this.sbot.backlinks) { |
480 | var c = filter && filter.value && filter.value.content |
481 | var filteringByType = c && c.type |
482 | if (!filteringByType) index = 'DTS' |
483 | } |
484 | // work around flumeview-query not supporting $lt/$gt. |
485 | // %FCIv0D7JQyERznC18p8Dc1KtN6SLeJAl1sR5DAIr/Ek=.sha256 |
486 | return pull( |
487 | plugin.read({ |
488 | index: index, |
489 | reverse: opts.reverse, |
490 | limit: opts.limit && (opts.limit + 1), |
491 | query: [{$filter: u.mergeOpts(filter, { |
492 | timestamp: { |
493 | $gte: opts.gt, |
494 | $lte: opts.lt, |
495 | } |
496 | })}] |
497 | }), |
498 | pull.filter(function (msg) { |
499 | return msg && msg.timestamp !== opts.lt && msg.timestamp !== opts.gt |
500 | }), |
501 | opts.limit && pull.take(opts.limit) |
502 | ) |
503 | } |
504 | |
505 | App.prototype.streamChannel = function (opts) { |
506 | // prefer ssb-backlinks to ssb-query because it also handles hashtag mentions |
507 | if (this.sbot.backlinks) return this.filter(this.sbot.backlinks, opts, { |
508 | dest: '#' + opts.channel, |
509 | }) |
510 | |
511 | if (this.sbot.query) return this.filter(this.sbot.query, opts, { |
512 | value: {content: {channel: opts.channel}}, |
513 | }) |
514 | |
515 | return pull.error(new Error( |
516 | 'Viewing channels/tags requires the ssb-backlinks or ssb-query plugin')) |
517 | } |
518 | |
519 | App.prototype.streamMentions = function (opts) { |
520 | if (!this.sbot.backlinks) return pull.error(new Error( |
521 | 'Viewing mentions requires the ssb-backlinks plugin')) |
522 | |
523 | if (this.sbot.backlinks) return this.filter(this.sbot.backlinks, opts, { |
524 | dest: this.sbot.id, |
525 | }) |
526 | } |
527 | |
528 | App.prototype.streamPrivate = function (opts) { |
529 | if (this.sbot.private.read) return this.filter(this.sbot.private, opts, {}) |
530 | |
531 | return pull( |
532 | this.createLogStream(u.mergeOpts(opts, {limit: null})), |
533 | pull.filter(u.isMsgEncrypted), |
534 | this.unboxMessages(), |
535 | pull.filter(u.isMsgReadable), |
536 | pull.take(opts.limit) |
537 | ) |
538 | } |
539 |
Built with git-ssb-web