Files: b66bcecec258b0a2631ec338501afa9409882fe8 / lib / app.js
9564 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 | |
16 | module.exports = App |
17 | |
18 | function App(sbot, config) { |
19 | this.sbot = sbot |
20 | this.config = config |
21 | |
22 | var conf = config.patchfoo || {} |
23 | this.port = conf.port || 8027 |
24 | this.host = conf.host || '::1' |
25 | |
26 | var base = conf.base || '/' |
27 | this.opts = { |
28 | base: base, |
29 | blob_base: conf.blob_base || conf.img_base || base, |
30 | img_base: conf.img_base || base, |
31 | emoji_base: conf.emoji_base || (base + 'emoji/'), |
32 | } |
33 | |
34 | sbot.get = memo({cache: lru(100)}, sbot.get) |
35 | this.about = new About(this, sbot.id) |
36 | this.getMsg = memo({cache: lru(100)}, getMsgWithValue, sbot) |
37 | this.getAbout = memo({cache: this.aboutCache = lru(500)}, |
38 | this._getAbout.bind(this)) |
39 | this.unboxContent = memo({cache: lru(100)}, sbot.private.unbox) |
40 | this.reverseNameCache = lru(500) |
41 | |
42 | this.unboxMsg = this.unboxMsg.bind(this) |
43 | |
44 | this.render = new Render(this, this.opts) |
45 | this.git = new Git(this) |
46 | } |
47 | |
48 | App.prototype.go = function () { |
49 | var self = this |
50 | http.createServer(function (req, res) { |
51 | new Serve(self, req, res).go() |
52 | }).listen(self.port, self.host, function () { |
53 | var host = /:/.test(self.host) ? '[' + self.host + ']' : self.host |
54 | self.log('Listening on http://' + host + ':' + self.port) |
55 | }) |
56 | |
57 | // invalidate cached About info when new About messages come in |
58 | pull( |
59 | self.sbot.links({rel: 'about', old: false, values: true}), |
60 | pull.drain(function (link) { |
61 | self.aboutCache.remove(link.dest) |
62 | }, function (err) { |
63 | if (err) self.error('about:', err) |
64 | }) |
65 | ) |
66 | } |
67 | |
68 | var logPrefix = '[' + pkg.name + ']' |
69 | App.prototype.log = console.log.bind(console, logPrefix) |
70 | App.prototype.error = console.error.bind(console, logPrefix) |
71 | |
72 | App.prototype.unboxMsg = function (msg, cb) { |
73 | var self = this |
74 | var c = msg.value && msg.value.content |
75 | if (typeof c !== 'string') cb(null, msg) |
76 | else self.unboxContent(c, function (err, content) { |
77 | if (err) { |
78 | self.error('unbox:', err) |
79 | return cb(null, msg) |
80 | } else if (!content) { |
81 | return cb(null, msg) |
82 | } |
83 | var m = {} |
84 | for (var k in msg) m[k] = msg[k] |
85 | m.value = {} |
86 | for (var k in msg.value) m.value[k] = msg.value[k] |
87 | m.value.content = content |
88 | m.value.private = true |
89 | cb(null, m) |
90 | }) |
91 | } |
92 | |
93 | App.prototype.search = function (opts) { |
94 | var search = this.sbot.fulltext && this.sbot.fulltext.search |
95 | if (!search) return pull.error(new Error('Missing fulltext search plugin')) |
96 | return search(opts) |
97 | } |
98 | |
99 | App.prototype.advancedSearch = function (opts) { |
100 | return pull( |
101 | opts.dest ? |
102 | this.sbot.links({ |
103 | values: true, |
104 | dest: opts.dest, |
105 | source: opts.source || undefined, |
106 | reverse: true, |
107 | }) |
108 | : opts.source ? |
109 | this.sbot.createUserStream({ |
110 | reverse: true, |
111 | id: opts.source |
112 | }) |
113 | : |
114 | this.sbot.createFeedStream({ |
115 | reverse: true, |
116 | }), |
117 | opts.text && pull.filter(filterByText(opts.text)) |
118 | ) |
119 | } |
120 | |
121 | function forSome(each) { |
122 | return function some(obj) { |
123 | if (obj == null) return false |
124 | if (typeof obj === 'string') return each(obj) |
125 | if (Array.isArray(obj)) return obj.some(some) |
126 | if (typeof obj === 'object') |
127 | for (var k in obj) if (some(obj[k])) return true |
128 | return false |
129 | } |
130 | } |
131 | |
132 | function filterByText(str) { |
133 | if (!str) return function () { return true } |
134 | var search = new RegExp(str, 'i') |
135 | var matches = forSome(search.test.bind(search)) |
136 | return function (msg) { |
137 | var c = msg.value.content |
138 | return c && matches(c) |
139 | } |
140 | } |
141 | |
142 | App.prototype.getMsgDecrypted = function (key, cb) { |
143 | var self = this |
144 | this.getMsg(key, function (err, msg) { |
145 | if (err) return cb(err) |
146 | self.unboxMsg(msg, cb) |
147 | }) |
148 | } |
149 | |
150 | App.prototype.publish = function (content, cb) { |
151 | var self = this |
152 | function tryPublish(triesLeft) { |
153 | if (Array.isArray(content.recps)) { |
154 | recps = content.recps.map(u.linkDest) |
155 | self.sbot.private.publish(content, recps, next) |
156 | } else { |
157 | self.sbot.publish(content, next) |
158 | } |
159 | function next(err, msg) { |
160 | if (err) { |
161 | if (triesLeft > 0) { |
162 | if (/^expected previous:/.test(err.message)) { |
163 | return tryPublish(triesLeft-1) |
164 | } |
165 | } |
166 | } |
167 | return cb(err, msg) |
168 | } |
169 | } |
170 | tryPublish(2) |
171 | } |
172 | |
173 | App.prototype.addBlob = function (cb) { |
174 | var done = multicb({pluck: 1, spread: true}) |
175 | var hashCb = done() |
176 | var addCb = done() |
177 | done(function (err, hash, add) { |
178 | cb(err, hash) |
179 | }) |
180 | return sink |
181 | } |
182 | |
183 | App.prototype.pushBlob = function (id, cb) { |
184 | console.error('pushing blob', id) |
185 | this.sbot.blobs.push(id, cb) |
186 | } |
187 | |
188 | App.prototype.readBlob = function (link) { |
189 | link = u.toLink(link) |
190 | return this.sbot.blobs.get({ |
191 | hash: link.link, |
192 | size: link.size, |
193 | }) |
194 | } |
195 | |
196 | App.prototype.readBlobSlice = function (link, opts) { |
197 | if (this.sbot.blobs.getSlice) return this.sbot.blobs.getSlice({ |
198 | hash: link.link, |
199 | size: link.size, |
200 | start: opts.start, |
201 | end: opts.end, |
202 | }) |
203 | return pull( |
204 | this.readBlob(link), |
205 | u.pullSlice(opts.start, opts.end) |
206 | ) |
207 | } |
208 | |
209 | App.prototype.ensureHasBlobs = function (links, cb) { |
210 | var self = this |
211 | var done = multicb({pluck: 1}) |
212 | links.forEach(function (link) { |
213 | var cb = done() |
214 | self.sbot.blobs.size(link.link, function (err, size) { |
215 | if (err) cb(err) |
216 | else if (size == null) cb(null, link) |
217 | else cb() |
218 | }) |
219 | }) |
220 | done(function (err, missingLinks) { |
221 | if (err) console.trace(err) |
222 | missingLinks = missingLinks.filter(Boolean) |
223 | if (missingLinks.length == 0) return cb() |
224 | return cb({name: 'BlobNotFoundError', links: missingLinks}) |
225 | }) |
226 | } |
227 | |
228 | App.prototype.getReverseNameSync = function (name) { |
229 | var id = this.reverseNameCache.get(name) |
230 | return id |
231 | } |
232 | |
233 | App.prototype.getNameSync = function (name) { |
234 | var about = this.aboutCache.get(name) |
235 | return about && about.name |
236 | } |
237 | |
238 | function getMsgWithValue(sbot, id, cb) { |
239 | if (!id) return cb() |
240 | sbot.get(id, function (err, value) { |
241 | if (err) return cb(err) |
242 | cb(null, {key: id, value: value}) |
243 | }) |
244 | } |
245 | |
246 | App.prototype._getAbout = function (id, cb) { |
247 | var self = this |
248 | if (!u.isRef(id)) return cb(null, {}) |
249 | self.about.get(id, function (err, about) { |
250 | if (err) return cb(err) |
251 | var sigil = id[0] || '@' |
252 | if (about.name && about.name[0] !== sigil) { |
253 | about.name = sigil + about.name |
254 | } |
255 | self.reverseNameCache.set(about.name, id) |
256 | cb(null, about) |
257 | }) |
258 | } |
259 | |
260 | App.prototype.pullGetMsg = function (id) { |
261 | return pull.asyncMap(this.getMsg)(pull.once(id)) |
262 | } |
263 | |
264 | App.prototype.createLogStream = function (opts) { |
265 | opts = opts || {} |
266 | return opts.sortByTimestamp |
267 | ? this.sbot.createFeedStream(opts) |
268 | : this.sbot.createLogStream(opts) |
269 | } |
270 | |
271 | var stateVals = { |
272 | connected: 3, |
273 | connecting: 2, |
274 | disconnecting: 1, |
275 | } |
276 | |
277 | function comparePeers(a, b) { |
278 | var aState = stateVals[a.state] || 0 |
279 | var bState = stateVals[b.state] || 0 |
280 | return (bState - aState) |
281 | || (b.stateChange|0 - a.stateChange|0) |
282 | } |
283 | |
284 | App.prototype.streamPeers = function (opts) { |
285 | var gossip = this.sbot.gossip |
286 | return u.readNext(function (cb) { |
287 | gossip.peers(function (err, peers) { |
288 | if (err) return cb(err) |
289 | if (opts) peers = peers.filter(function (peer) { |
290 | for (var k in opts) if (opts[k] !== peer[k]) return false |
291 | return true |
292 | }) |
293 | peers.sort(comparePeers) |
294 | cb(null, pull.values(peers)) |
295 | }) |
296 | }) |
297 | } |
298 | |
299 | App.prototype.getFollow = function (source, dest, cb) { |
300 | var self = this |
301 | pull( |
302 | self.sbot.links({source: source, dest: dest, rel: 'contact', reverse: true, |
303 | values: true, meta: false, keys: false}), |
304 | pull.filter(function (value) { |
305 | var c = value && value.content |
306 | return c && c.type === 'contact' |
307 | }), |
308 | pull.take(1), |
309 | pull.collect(function (err, msgs) { |
310 | if (err) return cb(err) |
311 | cb(null, msgs[0] && !!msgs[0].content.following) |
312 | }) |
313 | ) |
314 | } |
315 | |
316 | App.prototype.unboxMessages = function () { |
317 | return paramap(this.unboxMsg, 16) |
318 | } |
319 | |
320 | App.prototype.streamChannels = function (opts) { |
321 | return pull( |
322 | this.sbot.messagesByType({type: 'channel', reverse: true}), |
323 | this.unboxMessages(), |
324 | pull.filter(function (msg) { |
325 | return msg.value.content.subscribed |
326 | }), |
327 | pull.map(function (msg) { |
328 | return msg.value.content.channel |
329 | }), |
330 | pull.unique() |
331 | ) |
332 | } |
333 | |
334 | App.prototype.streamMyChannels = function (id, opts) { |
335 | // use ssb-query plugin if it is available, since it has an index for |
336 | // author + type |
337 | if (this.sbot.query) return pull( |
338 | this.sbot.query.read({ |
339 | reverse: true, |
340 | query: [ |
341 | {$filter: { |
342 | value: { |
343 | author: id, |
344 | content: {type: 'channel', subscribed: true} |
345 | } |
346 | }}, |
347 | {$map: ['value', 'content', 'channel']} |
348 | ] |
349 | }), |
350 | pull.unique() |
351 | ) |
352 | |
353 | return pull( |
354 | this.sbot.createUserStream({id: id, reverse: true}), |
355 | this.unboxMessages(), |
356 | pull.filter(function (msg) { |
357 | if (msg.value.content.type == 'channel') { |
358 | return msg.value.content.subscribed |
359 | } |
360 | }), |
361 | pull.map(function (msg) { |
362 | return msg.value.content.channel |
363 | }), |
364 | pull.unique() |
365 | ) |
366 | } |
367 | |
368 | App.prototype.createContactStreams = function (id) { |
369 | return new Contacts(this.sbot).createContactStreams(id) |
370 | } |
371 | |
372 | App.prototype.createAboutStreams = function (id) { |
373 | return this.about.createAboutStreams(id) |
374 | } |
375 |
Built with git-ssb-web