git ssb

16+

cel / patchfoo



Tree: 47dda2fa1ff7e9e2cc281278bf33e79e46a38873

Files: 47dda2fa1ff7e9e2cc281278bf33e79e46a38873 / lib / app.js

14218 bytesRaw
1var http = require('http')
2var memo = require('asyncmemo')
3var lru = require('hashlru')
4var pkg = require('../package')
5var u = require('./util')
6var pull = require('pull-stream')
7var hasher = require('pull-hash/ext/ssb')
8var multicb = require('multicb')
9var paramap = require('pull-paramap')
10var Contacts = require('ssb-contact')
11var About = require('./about')
12var Serve = require('./serve')
13var Render = require('./render')
14var Git = require('./git')
15var cat = require('pull-cat')
16
17module.exports = App
18
19function 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
50App.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
70var logPrefix = '[' + pkg.name + ']'
71App.prototype.log = console.log.bind(console, logPrefix)
72App.prototype.error = console.error.bind(console, logPrefix)
73
74App.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
95App.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
101App.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
123function 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
134function 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
144App.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
152App.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
175App.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
186App.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
199App.prototype.pushBlob = function (id, cb) {
200 console.error('pushing blob', id)
201 this.sbot.blobs.push(id, cb)
202}
203
204App.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
212App.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
225App.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
244App.prototype.getReverseNameSync = function (name) {
245 var id = this.reverseNameCache.get(name)
246 return id
247}
248
249App.prototype.getReverseEmojiNameSync = function (name) {
250 return this.reverseEmojiNameCache.get(name)
251}
252
253App.prototype.getNameSync = function (name) {
254 var about = this.aboutCache.get(name)
255 return about && about.name
256}
257
258function 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
266App.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
280App.prototype.pullGetMsg = function (id) {
281 return pull.asyncMap(this.getMsg)(pull.once(id))
282}
283
284App.prototype.createLogStream = function (opts) {
285 opts = opts || {}
286 return opts.sortByTimestamp
287 ? this.sbot.createFeedStream(opts)
288 : this.sbot.createLogStream(opts)
289}
290
291var stateVals = {
292 connected: 3,
293 connecting: 2,
294 disconnecting: 1,
295}
296
297function 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
304App.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
319App.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
336App.prototype.unboxMessages = function () {
337 return paramap(this.unboxMsg, 16)
338}
339
340App.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
354App.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
388App.prototype.createContactStreams = function (id) {
389 return new Contacts(this.sbot).createContactStreams(id)
390}
391
392function compareVoted(a, b) {
393 return b.value - a.value
394}
395
396App.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
452App.prototype.createAboutStreams = function (id) {
453 return this.about.createAboutStreams(id)
454}
455
456App.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
475App.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
505App.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
519App.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
528App.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