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