Files: 24417ffb7f896971664053a13101154e40d6adce / lib / app.js
8205 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 ssbAvatar = require('ssb-avatar') |
8 | var hasher = require('pull-hash/ext/ssb') |
9 | var multicb = require('multicb') |
10 | var paramap = require('pull-paramap') |
11 | var Contacts = require('ssb-contact') |
12 | |
13 | var Serve = require('./serve') |
14 | var Render = require('./render') |
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.getMsg = memo({cache: lru(100)}, getMsgWithValue, sbot) |
36 | this.getAbout = memo({cache: this.aboutCache = lru(500)}, |
37 | getAbout.bind(this), sbot, sbot.id) |
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 | function getMsgWithValue(sbot, id, cb) { |
195 | if (!id) return cb() |
196 | sbot.get(id, function (err, value) { |
197 | if (err) return cb(err) |
198 | cb(null, {key: id, value: value}) |
199 | }) |
200 | } |
201 | |
202 | function getAbout(sbot, src, id, cb) { |
203 | var self = this |
204 | ssbAvatar(sbot, src, id, function (err, about) { |
205 | if (err) return cb(err) |
206 | var sigil = id && id[0] || '@' |
207 | if (about.name && about.name[0] !== sigil) { |
208 | about.name = sigil + about.name |
209 | } |
210 | self.reverseNameCache.set(about.name, id) |
211 | cb(null, about) |
212 | }) |
213 | } |
214 | |
215 | App.prototype.createLogStream = function (opts) { |
216 | opts = opts || {} |
217 | return opts.sortByTimestamp |
218 | ? this.sbot.createFeedStream(opts) |
219 | : this.sbot.createLogStream(opts) |
220 | } |
221 | |
222 | var stateVals = { |
223 | connected: 3, |
224 | connecting: 2, |
225 | disconnecting: 1, |
226 | } |
227 | |
228 | function comparePeers(a, b) { |
229 | var aState = stateVals[a.state] || 0 |
230 | var bState = stateVals[b.state] || 0 |
231 | return (bState - aState) |
232 | || (b.stateChange|0 - a.stateChange|0) |
233 | } |
234 | |
235 | App.prototype.streamPeers = function (opts) { |
236 | var gossip = this.sbot.gossip |
237 | return u.readNext(function (cb) { |
238 | gossip.peers(function (err, peers) { |
239 | if (err) return cb(err) |
240 | if (opts) peers = peers.filter(function (peer) { |
241 | for (var k in opts) if (opts[k] !== peer[k]) return false |
242 | return true |
243 | }) |
244 | peers.sort(comparePeers) |
245 | cb(null, pull.values(peers)) |
246 | }) |
247 | }) |
248 | } |
249 | |
250 | App.prototype.getFollow = function (source, dest, cb) { |
251 | var self = this |
252 | pull( |
253 | self.sbot.links({source: source, dest: dest, rel: 'contact', reverse: true, |
254 | values: true, meta: false, keys: false}), |
255 | pull.filter(function (value) { |
256 | var c = value && value.content |
257 | return c && c.type === 'contact' |
258 | }), |
259 | pull.take(1), |
260 | pull.collect(function (err, msgs) { |
261 | if (err) return cb(err) |
262 | cb(null, msgs[0] && !!msgs[0].content.following) |
263 | }) |
264 | ) |
265 | } |
266 | |
267 | App.prototype.unboxMessages = function () { |
268 | return paramap(this.unboxMsg, 16) |
269 | } |
270 | |
271 | App.prototype.streamChannels = function (opts) { |
272 | return pull( |
273 | this.sbot.messagesByType({type: 'channel', reverse: true}), |
274 | this.unboxMessages(), |
275 | pull.filter(function (msg) { |
276 | return msg.value.content.subscribed |
277 | }), |
278 | pull.map(function (msg) { |
279 | return msg.value.content.channel |
280 | }), |
281 | pull.unique() |
282 | ) |
283 | } |
284 | |
285 | App.prototype.streamMyChannels = function (id, opts) { |
286 | // use ssb-query plugin if it is available, since it has an index for |
287 | // author + type |
288 | if (this.sbot.query) return pull( |
289 | this.sbot.query.read({ |
290 | reverse: true, |
291 | query: [ |
292 | {$filter: { |
293 | value: { |
294 | author: id, |
295 | content: {type: 'channel', subscribed: true} |
296 | } |
297 | }}, |
298 | {$map: ['value', 'content', 'channel']} |
299 | ] |
300 | }), |
301 | pull.unique() |
302 | ) |
303 | |
304 | return pull( |
305 | this.sbot.createUserStream({id: id, reverse: true}), |
306 | this.unboxMessages(), |
307 | pull.filter(function (msg) { |
308 | if (msg.value.content.type == 'channel') { |
309 | return msg.value.content.subscribed |
310 | } |
311 | }), |
312 | pull.map(function (msg) { |
313 | return msg.value.content.channel |
314 | }), |
315 | pull.unique() |
316 | ) |
317 | } |
318 | |
319 | App.prototype.createContactStreams = function (id) { |
320 | return new Contacts(this.sbot).createContactStreams(id) |
321 | } |
322 |
Built with git-ssb-web