Files: d51b2ca77b945cef037d2da636d80ee9fa0cc4ce / lib / serve.js
64663 bytesRaw
1 | var fs = require('fs') |
2 | var qs = require('querystring') |
3 | var pull = require('pull-stream') |
4 | var path = require('path') |
5 | var paramap = require('pull-paramap') |
6 | var sort = require('ssb-sort') |
7 | var crypto = require('crypto') |
8 | var toPull = require('stream-to-pull-stream') |
9 | var serveEmoji = require('emoji-server')() |
10 | var u = require('./util') |
11 | var cat = require('pull-cat') |
12 | var h = require('hyperscript') |
13 | var paginate = require('pull-paginate') |
14 | var ssbMentions = require('ssb-mentions') |
15 | var multicb = require('multicb') |
16 | var pkg = require('../package') |
17 | var Busboy = require('busboy') |
18 | var mime = require('mime-types') |
19 | var ident = require('pull-identify-filetype') |
20 | var htime = require('human-time') |
21 | var ph = require('pull-hyperscript') |
22 | var emojis = require('emoji-named-characters') |
23 | |
24 | module.exports = Serve |
25 | |
26 | var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs') |
27 | |
28 | var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/ |
29 | |
30 | function ctype(name) { |
31 | switch (name && /[^.\/]*$/.exec(name)[0] || 'html') { |
32 | case 'html': return 'text/html' |
33 | case 'txt': return 'text/plain' |
34 | case 'js': return 'text/javascript' |
35 | case 'css': return 'text/css' |
36 | case 'png': return 'image/png' |
37 | case 'json': return 'application/json' |
38 | case 'ico': return 'image/x-icon' |
39 | } |
40 | } |
41 | |
42 | function encodeDispositionFilename(fname) { |
43 | fname = fname.replace(/\/g/, '\\\\').replace(/"/, '\\\"') |
44 | return '"' + encodeURIComponent(fname) + '"' |
45 | } |
46 | |
47 | function uniques() { |
48 | var set = {} |
49 | return function (item) { |
50 | if (set[item]) return false |
51 | return set[item] = true |
52 | } |
53 | } |
54 | |
55 | function Serve(app, req, res) { |
56 | this.app = app |
57 | this.req = req |
58 | this.res = res |
59 | this.startDate = new Date() |
60 | } |
61 | |
62 | Serve.prototype.go = function () { |
63 | console.log(this.req.method, this.req.url) |
64 | var self = this |
65 | |
66 | this.res.setTimeout(0) |
67 | |
68 | if (this.req.method === 'POST' || this.req.method === 'PUT') { |
69 | if (/^multipart\/form-data/.test(this.req.headers['content-type'])) { |
70 | var data = {} |
71 | var erred |
72 | var busboy = new Busboy({headers: this.req.headers}) |
73 | var filesCb = multicb({pluck: 1}) |
74 | busboy.on('finish', filesCb()) |
75 | filesCb(function (err) { |
76 | gotData(err, data) |
77 | }) |
78 | function addField(name, value) { |
79 | if (!(name in data)) data[name] = value |
80 | else if (Array.isArray(data[name])) data[name].push(value) |
81 | else data[name] = [data[name], value] |
82 | } |
83 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { |
84 | var done = multicb({pluck: 1, spread: true}) |
85 | var cb = filesCb() |
86 | pull( |
87 | toPull(file), |
88 | u.pullLength(done()), |
89 | self.app.addBlob(done()) |
90 | ) |
91 | done(function (err, size, id) { |
92 | if (err) return cb(err) |
93 | if (size === 0 && !filename) return cb() |
94 | addField(fieldname, |
95 | {link: id, name: filename, type: mimetype, size: size}) |
96 | cb() |
97 | }) |
98 | }) |
99 | busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { |
100 | addField(fieldname, val) |
101 | }) |
102 | this.req.pipe(busboy) |
103 | } else { |
104 | pull( |
105 | toPull(this.req), |
106 | pull.collect(function (err, bufs) { |
107 | var data |
108 | if (!err) try { |
109 | data = qs.parse(Buffer.concat(bufs).toString('ascii')) |
110 | } catch(e) { |
111 | err = e |
112 | } |
113 | gotData(err, data) |
114 | }) |
115 | ) |
116 | } |
117 | } else { |
118 | gotData(null, {}) |
119 | } |
120 | |
121 | function gotData(err, data) { |
122 | self.data = data |
123 | if (err) next(err) |
124 | else if (data.action === 'publish') self.publishJSON(next) |
125 | else if (data.action === 'contact') self.publishContact(next) |
126 | else if (data.action === 'want-blobs') self.wantBlobs(next) |
127 | else if (data.action_vote) self.publishVote(next) |
128 | else if (data.action_attend) self.publishAttend(next) |
129 | else next() |
130 | } |
131 | |
132 | function next(err, publishedMsg) { |
133 | if (err) { |
134 | self.res.writeHead(400, {'Content-Type': 'text/plain'}) |
135 | self.res.end(err.stack) |
136 | } else if (publishedMsg) { |
137 | if (self.data.redirect_to_published_msg) { |
138 | self.redirect(self.app.render.toUrl(publishedMsg.key)) |
139 | } else { |
140 | self.publishedMsg = publishedMsg |
141 | self.handle() |
142 | } |
143 | } else { |
144 | self.handle() |
145 | } |
146 | } |
147 | } |
148 | |
149 | Serve.prototype.publishJSON = function (cb) { |
150 | var content |
151 | try { |
152 | content = JSON.parse(this.data.content) |
153 | } catch(e) { |
154 | return cb(e) |
155 | } |
156 | this.publish(content, cb) |
157 | } |
158 | |
159 | Serve.prototype.publishVote = function (cb) { |
160 | var content = { |
161 | type: 'vote', |
162 | channel: this.data.channel || undefined, |
163 | vote: { |
164 | link: this.data.link, |
165 | value: Number(this.data.vote_value), |
166 | expression: this.data.vote_expression, |
167 | } |
168 | } |
169 | if (this.data.recps) content.recps = this.data.recps.split(',') |
170 | this.publish(content, cb) |
171 | } |
172 | |
173 | Serve.prototype.publishContact = function (cb) { |
174 | var content = { |
175 | type: 'contact', |
176 | contact: this.data.contact, |
177 | following: !!this.data.following |
178 | } |
179 | this.publish(content, cb) |
180 | } |
181 | |
182 | Serve.prototype.publishAttend = function (cb) { |
183 | var content = { |
184 | type: 'about', |
185 | channel: this.data.channel || undefined, |
186 | about: this.data.link, |
187 | attendee: { |
188 | link: this.app.sbot.id |
189 | } |
190 | } |
191 | if (this.data.recps) content.recps = this.data.recps.split(',') |
192 | this.publish(content, cb) |
193 | } |
194 | |
195 | Serve.prototype.wantBlobs = function (cb) { |
196 | var self = this |
197 | if (!self.data.blob_ids) return cb() |
198 | var ids = self.data.blob_ids.split(',') |
199 | if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(','))) |
200 | var done = multicb({pluck: 1}) |
201 | ids.forEach(function (id) { |
202 | self.app.sbot.blobs.want(id, done()) |
203 | }) |
204 | done(function (err) { |
205 | if (err) return cb(err) |
206 | // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.') |
207 | cb() |
208 | }) |
209 | } |
210 | |
211 | Serve.prototype.publish = function (content, cb) { |
212 | var self = this |
213 | var done = multicb({pluck: 1, spread: true}) |
214 | u.toArray(content && content.mentions).forEach(function (mention) { |
215 | if (mention.link && mention.link[0] === '&' && !isNaN(mention.size)) |
216 | self.app.pushBlob(mention.link, done()) |
217 | }) |
218 | done(function (err) { |
219 | if (err) return cb(err) |
220 | self.app.publish(content, function (err, msg) { |
221 | if (err) return cb(err) |
222 | delete self.data.text |
223 | delete self.data.recps |
224 | return cb(null, msg) |
225 | }) |
226 | }) |
227 | } |
228 | |
229 | Serve.prototype.handle = function () { |
230 | var m = urlIdRegex.exec(this.req.url) |
231 | this.query = m[5] ? qs.parse(m[5]) : {} |
232 | switch (m[2]) { |
233 | case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1]) |
234 | case '%': return this.id(m[1], m[3]) |
235 | case '@': return this.userFeed(m[1], m[3]) |
236 | case '&': return this.blob(m[1]) |
237 | default: return this.path(m[4]) |
238 | } |
239 | } |
240 | |
241 | Serve.prototype.respond = function (status, message) { |
242 | this.res.writeHead(status) |
243 | this.res.end(message) |
244 | } |
245 | |
246 | Serve.prototype.respondSink = function (status, headers, cb) { |
247 | var self = this |
248 | if (status || headers) |
249 | self.res.writeHead(status, headers || {'Content-Type': 'text/html'}) |
250 | return toPull(self.res, cb || function (err) { |
251 | if (err) self.app.error(err) |
252 | }) |
253 | } |
254 | |
255 | Serve.prototype.redirect = function (dest) { |
256 | this.res.writeHead(302, { |
257 | Location: dest |
258 | }) |
259 | this.res.end() |
260 | } |
261 | |
262 | Serve.prototype.path = function (url) { |
263 | var m |
264 | url = url.replace(/^\/+/, '/') |
265 | switch (url) { |
266 | case '/': return this.home() |
267 | case '/robots.txt': return this.res.end('User-agent: *') |
268 | } |
269 | if (m = /^\/%23(.*)/.exec(url)) { |
270 | return this.redirect(this.app.render.toUrl('/channel/' |
271 | + decodeURIComponent(m[1]))) |
272 | } |
273 | m = /^([^.]*)(?:\.(.*))?$/.exec(url) |
274 | switch (m[1]) { |
275 | case '/new': return this.new(m[2]) |
276 | case '/public': return this.public(m[2]) |
277 | case '/private': return this.private(m[2]) |
278 | case '/search': return this.search(m[2]) |
279 | case '/advsearch': return this.advsearch(m[2]) |
280 | case '/vote': return this.vote(m[2]) |
281 | case '/peers': return this.peers(m[2]) |
282 | case '/channels': return this.channels(m[2]) |
283 | case '/friends': return this.friends(m[2]) |
284 | case '/live': return this.live(m[2]) |
285 | case '/compose': return this.compose(m[2]) |
286 | case '/emojis': return this.emojis(m[2]) |
287 | } |
288 | m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) |
289 | switch (m[1]) { |
290 | case '/channel': return this.channel(m[2]) |
291 | case '/type': return this.type(m[2]) |
292 | case '/links': return this.links(m[2]) |
293 | case '/static': return this.static(m[2]) |
294 | case '/emoji': return this.emoji(m[2]) |
295 | case '/contacts': return this.contacts(m[2]) |
296 | case '/about': return this.about(m[2]) |
297 | case '/git': return this.git(m[2]) |
298 | } |
299 | return this.respond(404, 'Not found') |
300 | } |
301 | |
302 | Serve.prototype.home = function () { |
303 | pull( |
304 | pull.empty(), |
305 | this.wrapPage('/'), |
306 | this.respondSink(200, { |
307 | 'Content-Type': 'text/html' |
308 | }) |
309 | ) |
310 | } |
311 | |
312 | Serve.prototype.public = function (ext) { |
313 | var q = this.query |
314 | var opts = { |
315 | reverse: !q.forwards, |
316 | sortByTimestamp: q.sort === 'claimed', |
317 | lt: Number(q.lt) || Date.now(), |
318 | gt: Number(q.gt) || -Infinity, |
319 | limit: Number(q.limit) || 12 |
320 | } |
321 | |
322 | pull( |
323 | this.app.createLogStream(opts), |
324 | this.renderThreadPaginated(opts, null, q), |
325 | this.wrapMessages(), |
326 | this.wrapPublic(), |
327 | this.wrapPage('public'), |
328 | this.respondSink(200, { |
329 | 'Content-Type': ctype(ext) |
330 | }) |
331 | ) |
332 | } |
333 | |
334 | Serve.prototype.setCookie = function (key, value, options) { |
335 | var header = key + '=' + value |
336 | if (options) for (var k in options) { |
337 | header += '; ' + k + '=' + options[k] |
338 | } |
339 | this.res.setHeader('Set-Cookie', header) |
340 | } |
341 | |
342 | Serve.prototype.new = function (ext) { |
343 | var self = this |
344 | var q = self.query |
345 | var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1] |
346 | var opts = { |
347 | gt: Number(q.gt) || Number(latest) || Date.now(), |
348 | } |
349 | |
350 | if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000}) |
351 | |
352 | var read = self.app.createLogStream(opts) |
353 | self.req.on('closed', function () { |
354 | console.error('closing') |
355 | read(true, function (err) { |
356 | console.log('closed') |
357 | if (err && err !== true) console.error(new Error(err.stack)) |
358 | }) |
359 | }) |
360 | pull.collect(function (err, msgs) { |
361 | if (err) return pull( |
362 | pull.once(u.renderError(err, ext).outerHTML), |
363 | self.wrapPage('peers'), |
364 | self.respondSink(500, {'Content-Type': ctype(ext)}) |
365 | ) |
366 | sort(msgs) |
367 | var maxTS = msgs.reduce(function (max, msg) { |
368 | return Math.max(msg.timestamp, max) |
369 | }, -Infinity) |
370 | pull( |
371 | pull.values(msgs), |
372 | self.renderThread(opts, null, q), |
373 | self.wrapNew({ |
374 | gt: isFinite(maxTS) ? maxTS : Date.now() |
375 | }), |
376 | self.wrapMessages(), |
377 | self.wrapPage('new'), |
378 | self.respondSink(200, { |
379 | 'Content-Type': ctype(ext) |
380 | }) |
381 | ) |
382 | })(read) |
383 | } |
384 | |
385 | Serve.prototype.private = function (ext) { |
386 | var q = this.query |
387 | var opts = { |
388 | reverse: !q.forwards, |
389 | sortByTimestamp: q.sort === 'claimed', |
390 | lt: Number(q.lt) || Date.now(), |
391 | gt: Number(q.gt) || -Infinity, |
392 | } |
393 | var limit = Number(q.limit) || 12 |
394 | |
395 | pull( |
396 | this.app.createLogStream(opts), |
397 | pull.filter(u.isMsgEncrypted), |
398 | this.app.unboxMessages(), |
399 | pull.filter(u.isMsgReadable), |
400 | pull.take(limit), |
401 | this.renderThreadPaginated(opts, null, q), |
402 | this.wrapMessages(), |
403 | this.wrapPrivate(opts), |
404 | this.wrapPage('private'), |
405 | this.respondSink(200, { |
406 | 'Content-Type': ctype(ext) |
407 | }) |
408 | ) |
409 | } |
410 | |
411 | Serve.prototype.search = function (ext) { |
412 | var searchQ = (this.query.q || '').trim() |
413 | var self = this |
414 | |
415 | if (/^ssb:\/\//.test(searchQ)) { |
416 | var maybeId = searchQ.substr(6) |
417 | if (u.isRef(maybeId)) searchQ = maybeId |
418 | } |
419 | |
420 | if (u.isRef(searchQ) || searchQ[0] === '#') { |
421 | return self.redirect(self.app.render.toUrl(searchQ)) |
422 | } |
423 | |
424 | pull( |
425 | self.app.search(searchQ), |
426 | self.renderThread(), |
427 | self.wrapMessages(), |
428 | self.wrapPage('search · ' + searchQ, searchQ), |
429 | self.respondSink(200, { |
430 | 'Content-Type': ctype(ext), |
431 | }) |
432 | ) |
433 | } |
434 | |
435 | Serve.prototype.advsearch = function (ext) { |
436 | var self = this |
437 | var q = this.query || {} |
438 | |
439 | if (q.source) q.source = u.extractFeedIds(q.source)[0] |
440 | if (q.dest) q.dest = u.extractFeedIds(q.dest)[0] |
441 | var hasQuery = q.text || q.source || q.dest |
442 | |
443 | pull( |
444 | cat([ |
445 | ph('section', {}, [ |
446 | ph('form', {action: '', method: 'get'}, [ |
447 | ph('table', [ |
448 | ph('tr', [ |
449 | ph('td', 'text'), |
450 | ph('td', ph('input', {name: 'text', placeholder: 'regex', |
451 | class: 'id-input', |
452 | value: q.text || ''})) |
453 | ]), |
454 | ph('tr', [ |
455 | ph('td', 'author'), |
456 | ph('td', ph('input', {name: 'source', placeholder: '@id', |
457 | class: 'id-input', |
458 | value: q.source || ''})) |
459 | ]), |
460 | ph('tr', [ |
461 | ph('td', 'mentions'), |
462 | ph('td', ph('input', {name: 'dest', placeholder: 'id', |
463 | class: 'id-input', |
464 | value: q.dest || ''})) |
465 | ]), |
466 | ph('tr', [ |
467 | ph('td', {colspan: 2}, [ |
468 | ph('input', {type: 'submit', value: 'search'}) |
469 | ]) |
470 | ]), |
471 | ]) |
472 | ]) |
473 | ]), |
474 | hasQuery && pull( |
475 | self.app.advancedSearch(q), |
476 | self.renderThread(), |
477 | self.wrapMessages() |
478 | ) |
479 | ]), |
480 | self.wrapPage('advanced search'), |
481 | self.respondSink(200, { |
482 | 'Content-Type': ctype(ext), |
483 | }) |
484 | ) |
485 | } |
486 | |
487 | Serve.prototype.live = function (ext) { |
488 | var self = this |
489 | var q = self.query |
490 | var opts = { |
491 | live: true, |
492 | } |
493 | var gt = Number(q.gt) |
494 | if (gt) opts.gt = gt |
495 | else opts.old = false |
496 | |
497 | pull( |
498 | ph('table', {class: 'ssb-msgs'}, pull( |
499 | self.app.sbot.createLogStream(opts), |
500 | self.app.render.renderFeeds({ |
501 | withGt: true, |
502 | }), |
503 | pull.map(u.toHTML) |
504 | )), |
505 | self.wrapPage('live'), |
506 | self.respondSink(200, { |
507 | 'Content-Type': ctype(ext), |
508 | }) |
509 | ) |
510 | } |
511 | |
512 | Serve.prototype.compose = function (ext) { |
513 | var self = this |
514 | self.composer({ |
515 | channel: '', |
516 | redirectToPublishedMsg: true, |
517 | }, function (err, composer) { |
518 | if (err) return cb(err) |
519 | pull( |
520 | pull.once(u.toHTML(composer)), |
521 | self.wrapPage('compose'), |
522 | self.respondSink(200, { |
523 | 'Content-Type': ctype(ext) |
524 | }) |
525 | ) |
526 | }) |
527 | } |
528 | |
529 | Serve.prototype.peers = function (ext) { |
530 | var self = this |
531 | if (self.data.action === 'connect') { |
532 | return self.app.sbot.gossip.connect(self.data.address, function (err) { |
533 | if (err) return pull( |
534 | pull.once(u.renderError(err, ext).outerHTML), |
535 | self.wrapPage('peers'), |
536 | self.respondSink(400, {'Content-Type': ctype(ext)}) |
537 | ) |
538 | self.data = {} |
539 | return self.peers(ext) |
540 | }) |
541 | } |
542 | |
543 | pull( |
544 | self.app.streamPeers(), |
545 | paramap(function (peer, cb) { |
546 | var done = multicb({pluck: 1, spread: true}) |
547 | var connectedTime = Date.now() - peer.stateChange |
548 | var addr = peer.host + ':' + peer.port + ':' + peer.key |
549 | done()(null, h('section', |
550 | h('form', {method: 'post', action: ''}, |
551 | peer.client ? '→' : '←', ' ', |
552 | h('code', peer.host, ':', peer.port, ':'), |
553 | self.app.render.idLink(peer.key, done()), ' ', |
554 | peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '', |
555 | peer.state === 'connected' ? 'connected' : [ |
556 | h('input', {name: 'action', type: 'submit', value: 'connect'}), |
557 | h('input', {name: 'address', type: 'hidden', value: addr}) |
558 | ] |
559 | ) |
560 | // h('div', 'source: ', peer.source) |
561 | // JSON.stringify(peer, 0, 2)).outerHTML |
562 | )) |
563 | done(cb) |
564 | }, 8), |
565 | pull.map(u.toHTML), |
566 | self.wrapPeers(), |
567 | self.wrapPage('peers'), |
568 | self.respondSink(200, { |
569 | 'Content-Type': ctype(ext) |
570 | }) |
571 | ) |
572 | } |
573 | |
574 | Serve.prototype.channels = function (ext) { |
575 | var self = this |
576 | var id = self.app.sbot.id |
577 | |
578 | function renderMyChannels() { |
579 | return pull( |
580 | self.app.streamMyChannels(id), |
581 | paramap(function (channel, cb) { |
582 | // var subscribed = false |
583 | cb(null, [ |
584 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
585 | ' ' |
586 | ]) |
587 | }, 8), |
588 | pull.map(u.toHTML), |
589 | self.wrapMyChannels() |
590 | ) |
591 | } |
592 | |
593 | function renderNetworkChannels() { |
594 | return pull( |
595 | self.app.streamChannels(), |
596 | paramap(function (channel, cb) { |
597 | // var subscribed = false |
598 | cb(null, [ |
599 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
600 | ' ' |
601 | ]) |
602 | }, 8), |
603 | pull.map(u.toHTML), |
604 | self.wrapChannels() |
605 | ) |
606 | } |
607 | |
608 | pull( |
609 | cat([ |
610 | ph('section', {}, [ |
611 | ph('h3', {}, 'Channels:'), |
612 | renderMyChannels(), |
613 | renderNetworkChannels() |
614 | ]) |
615 | ]), |
616 | this.wrapPage('channels'), |
617 | this.respondSink(200, { |
618 | 'Content-Type': ctype(ext) |
619 | }) |
620 | ) |
621 | } |
622 | |
623 | Serve.prototype.phIdLink = function (id) { |
624 | return pull( |
625 | pull.once(id), |
626 | pull.asyncMap(this.renderIdLink.bind(this)), |
627 | pull.map(u.toHTML) |
628 | ) |
629 | } |
630 | |
631 | Serve.prototype.contacts = function (path) { |
632 | var self = this |
633 | var id = String(path).substr(1) |
634 | var contacts = self.app.createContactStreams(id) |
635 | |
636 | function renderFriendsList() { |
637 | return pull( |
638 | paramap(function (id, cb) { |
639 | self.app.getAbout(id, function (err, about) { |
640 | var name = about && about.name || id.substr(0, 8) + '…' |
641 | cb(null, h('a', {href: self.app.render.toUrl('/contacts/' + id)}, name)) |
642 | }) |
643 | }, 8), |
644 | pull.map(function (el) { |
645 | return [el, ' '] |
646 | }), |
647 | pull.flatten(), |
648 | pull.map(u.toHTML) |
649 | ) |
650 | } |
651 | |
652 | pull( |
653 | cat([ |
654 | ph('section', {}, [ |
655 | ph('h3', {}, ['Contacts: ', self.phIdLink(id)]), |
656 | ph('h4', {}, 'Friends'), |
657 | renderFriendsList()(contacts.friends), |
658 | ph('h4', {}, 'Follows'), |
659 | renderFriendsList()(contacts.follows), |
660 | ph('h4', {}, 'Followers'), |
661 | renderFriendsList()(contacts.followers) |
662 | ]) |
663 | ]), |
664 | this.wrapPage('contacts: ' + id), |
665 | this.respondSink(200, { |
666 | 'Content-Type': ctype('html') |
667 | }) |
668 | ) |
669 | } |
670 | |
671 | Serve.prototype.about = function (path) { |
672 | var self = this |
673 | var id = decodeURIComponent(String(path).substr(1)) |
674 | var abouts = self.app.createAboutStreams(id) |
675 | var render = self.app.render |
676 | |
677 | function renderAboutOpImage(link) { |
678 | if (!link) return |
679 | if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link)) |
680 | return ph('img', { |
681 | class: 'ssb-avatar-image', |
682 | src: render.imageUrl(link.link), |
683 | alt: link.link |
684 | + (link.size ? ' (' + render.formatSize(link.size) + ')' : '') |
685 | }) |
686 | } |
687 | |
688 | function renderAboutOpValue(value) { |
689 | if (!value) return |
690 | if (u.isRef(value.link)) return self.phIdLink(value.link) |
691 | if (value.epoch) return new Date(value.epoch).toUTCString() |
692 | return ph('code', {}, JSON.stringify(value)) |
693 | } |
694 | |
695 | function renderAboutOpContent(op) { |
696 | if (op.prop === 'image') |
697 | return renderAboutOpImage(op.value) |
698 | if (op.prop === 'description') |
699 | return h('div', {innerHTML: render.markdown(op.value)}).outerHTML |
700 | if (op.prop === 'title') |
701 | return h('strong', op.value).outerHTML |
702 | if (op.prop === 'name') |
703 | return h('u', op.value).outerHTML |
704 | return renderAboutOpValue(op.value) |
705 | } |
706 | |
707 | function renderAboutOp(op) { |
708 | return ph('tr', {}, [ |
709 | ph('td', self.phIdLink(op.author)), |
710 | ph('td', |
711 | ph('a', {href: render.toUrl(op.id)}, |
712 | htime(new Date(op.timestamp)))), |
713 | ph('td', op.prop), |
714 | ph('td', renderAboutOpContent(op)) |
715 | ]) |
716 | } |
717 | |
718 | pull( |
719 | cat([ |
720 | ph('section', {}, [ |
721 | ph('h3', {}, ['About: ', self.phIdLink(id)]), |
722 | ph('table', {}, |
723 | pull(abouts.scalars, pull.map(renderAboutOp)) |
724 | ), |
725 | pull( |
726 | abouts.sets, |
727 | pull.map(function (op) { |
728 | return h('pre', JSON.stringify(op, 0, 2)) |
729 | }), |
730 | pull.map(u.toHTML) |
731 | ) |
732 | ]) |
733 | ]), |
734 | this.wrapPage('about: ' + id), |
735 | this.respondSink(200, { |
736 | 'Content-Type': ctype('html') |
737 | }) |
738 | ) |
739 | } |
740 | |
741 | Serve.prototype.type = function (path) { |
742 | var q = this.query |
743 | var type = decodeURIComponent(path.substr(1)) |
744 | var opts = { |
745 | reverse: !q.forwards, |
746 | lt: Number(q.lt) || Date.now(), |
747 | gt: Number(q.gt) || -Infinity, |
748 | limit: Number(q.limit) || 12, |
749 | type: type, |
750 | } |
751 | |
752 | pull( |
753 | this.app.sbot.messagesByType(opts), |
754 | this.renderThreadPaginated(opts, null, q), |
755 | this.wrapMessages(), |
756 | this.wrapType(type), |
757 | this.wrapPage('type: ' + type), |
758 | this.respondSink(200, { |
759 | 'Content-Type': ctype('html') |
760 | }) |
761 | ) |
762 | } |
763 | |
764 | Serve.prototype.links = function (path) { |
765 | var q = this.query |
766 | var dest = path.substr(1) |
767 | var opts = { |
768 | dest: dest, |
769 | reverse: true, |
770 | values: true, |
771 | } |
772 | if (q.rel) opts.rel = q.rel |
773 | |
774 | pull( |
775 | this.app.sbot.links(opts), |
776 | this.renderThread(opts, null, q), |
777 | this.wrapMessages(), |
778 | this.wrapLinks(dest), |
779 | this.wrapPage('links: ' + dest), |
780 | this.respondSink(200, { |
781 | 'Content-Type': ctype('html') |
782 | }) |
783 | ) |
784 | } |
785 | |
786 | Serve.prototype.rawId = function (id) { |
787 | var self = this |
788 | |
789 | self.app.getMsgDecrypted(id, function (err, msg) { |
790 | if (err) return pull( |
791 | pull.once(u.renderError(err).outerHTML), |
792 | self.respondSink(400, {'Content-Type': ctype('html')}) |
793 | ) |
794 | return pull( |
795 | pull.once(msg), |
796 | self.renderRawMsgPage(id), |
797 | self.respondSink(200, { |
798 | 'Content-Type': ctype('html'), |
799 | }) |
800 | ) |
801 | }) |
802 | } |
803 | |
804 | Serve.prototype.channel = function (path) { |
805 | var channel = decodeURIComponent(String(path).substr(1)) |
806 | var q = this.query |
807 | var gt = Number(q.gt) || -Infinity |
808 | var lt = Number(q.lt) || Date.now() |
809 | var opts = { |
810 | reverse: !q.forwards, |
811 | lt: lt, |
812 | gt: gt, |
813 | limit: Number(q.limit) || 12, |
814 | query: [{$filter: { |
815 | value: {content: {channel: channel}}, |
816 | timestamp: { |
817 | $gt: gt, |
818 | $lt: lt, |
819 | } |
820 | }}] |
821 | } |
822 | |
823 | if (!this.app.sbot.query) return pull( |
824 | pull.once(u.renderError(new Error('Missing ssb-query plugin')).outerHTML), |
825 | this.wrapPage('#' + channel), |
826 | this.respondSink(400, {'Content-Type': ctype('html')}) |
827 | ) |
828 | |
829 | pull( |
830 | this.app.sbot.query.read(opts), |
831 | this.renderThreadPaginated(opts, null, q), |
832 | this.wrapMessages(), |
833 | this.wrapChannel(channel), |
834 | this.wrapPage('#' + channel), |
835 | this.respondSink(200, { |
836 | 'Content-Type': ctype('html') |
837 | }) |
838 | ) |
839 | } |
840 | |
841 | function threadHeads(msgs, rootId) { |
842 | return sort.heads(msgs.filter(function (msg) { |
843 | var c = msg.value && msg.value.content |
844 | return (c && c.root === rootId) |
845 | || msg.key === rootId |
846 | })) |
847 | } |
848 | |
849 | |
850 | Serve.prototype.id = function (id, ext) { |
851 | var self = this |
852 | if (self.query.raw != null) return self.rawId(id) |
853 | |
854 | this.app.getMsgDecrypted(id, function (err, rootMsg) { |
855 | if (err && err.name === 'NotFoundError') err = null, rootMsg = { |
856 | key: id, value: {content: false}} |
857 | if (err) return self.respond(500, err.stack || err) |
858 | var rootContent = rootMsg && rootMsg.value && rootMsg.value.content |
859 | var recps = rootContent && rootContent.recps |
860 | var threadRootId = rootContent && rootContent.root || id |
861 | var channel |
862 | |
863 | pull( |
864 | cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]), |
865 | pull.unique('key'), |
866 | self.app.unboxMessages(), |
867 | pull.through(function (msg) { |
868 | var c = msg && msg.value.content |
869 | if (!channel && c.channel) channel = c.channel |
870 | }), |
871 | pull.collect(function (err, links) { |
872 | if (err) return self.respond(500, err.stack || err) |
873 | pull( |
874 | pull.values(sort(links)), |
875 | self.renderThread(), |
876 | self.wrapMessages(), |
877 | self.wrapThread({ |
878 | recps: recps, |
879 | root: threadRootId, |
880 | post: id, |
881 | branches: threadHeads(links, threadRootId), |
882 | postBranches: threadRootId !== id && threadHeads(links, id), |
883 | channel: channel, |
884 | }), |
885 | self.wrapPage(id), |
886 | self.respondSink(200, { |
887 | 'Content-Type': ctype(ext), |
888 | }) |
889 | ) |
890 | }) |
891 | ) |
892 | }) |
893 | } |
894 | |
895 | Serve.prototype.userFeed = function (id, ext) { |
896 | var self = this |
897 | var q = self.query |
898 | var opts = { |
899 | id: id, |
900 | reverse: !q.forwards, |
901 | lt: Number(q.lt) || Date.now(), |
902 | gt: Number(q.gt) || -Infinity, |
903 | limit: Number(q.limit) || 20 |
904 | } |
905 | var isScrolled = q.lt || q.gt |
906 | |
907 | self.app.getAbout(id, function (err, about) { |
908 | if (err) self.app.error(err) |
909 | pull( |
910 | self.app.sbot.createUserStream(opts), |
911 | self.renderThreadPaginated(opts, id, q), |
912 | self.wrapMessages(), |
913 | self.wrapUserFeed(isScrolled, id), |
914 | self.wrapPage(about.name || id), |
915 | self.respondSink(200, { |
916 | 'Content-Type': ctype(ext) |
917 | }) |
918 | ) |
919 | }) |
920 | } |
921 | |
922 | Serve.prototype.file = function (file) { |
923 | var self = this |
924 | fs.stat(file, function (err, stat) { |
925 | if (err && err.code === 'ENOENT') return self.respond(404, 'Not found') |
926 | if (err) return self.respond(500, err.stack || err) |
927 | if (!stat.isFile()) return self.respond(403, 'May only load files') |
928 | if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified') |
929 | self.res.writeHead(200, { |
930 | 'Content-Type': ctype(file), |
931 | 'Content-Length': stat.size, |
932 | 'Last-Modified': stat.mtime.toGMTString() |
933 | }) |
934 | fs.createReadStream(file).pipe(self.res) |
935 | }) |
936 | } |
937 | |
938 | Serve.prototype.static = function (file) { |
939 | this.file(path.join(__dirname, '../static', file)) |
940 | } |
941 | |
942 | Serve.prototype.emoji = function (emoji) { |
943 | serveEmoji(this.req, this.res, emoji) |
944 | } |
945 | |
946 | Serve.prototype.blob = function (id) { |
947 | var self = this |
948 | var blobs = self.app.sbot.blobs |
949 | if (self.req.headers['if-none-match'] === id) return self.respond(304) |
950 | var done = multicb({pluck: 1, spread: true}) |
951 | blobs.want(id, function (err, has) { |
952 | if (err) { |
953 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
954 | else return self.respond(500, err.message || err) |
955 | } |
956 | if (!has) return self.respond(404, 'Not found') |
957 | blobs.size(id, done()) |
958 | pull( |
959 | blobs.get(id), |
960 | pull.map(Buffer), |
961 | ident(done().bind(self, null)), |
962 | self.respondSink() |
963 | ) |
964 | done(function (err, size, type) { |
965 | if (err) console.trace(err) |
966 | type = type && mime.lookup(type) |
967 | if (type) self.res.setHeader('Content-Type', type) |
968 | if (typeof size === 'number') self.res.setHeader('Content-Length', size) |
969 | if (self.query.name) self.res.setHeader('Content-Disposition', |
970 | 'inline; filename='+encodeDispositionFilename(self.query.name)) |
971 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
972 | self.res.setHeader('etag', id) |
973 | self.res.writeHead(200) |
974 | }) |
975 | }) |
976 | } |
977 | |
978 | Serve.prototype.ifModified = function (lastMod) { |
979 | var ifModSince = this.req.headers['if-modified-since'] |
980 | if (!ifModSince) return false |
981 | var d = new Date(ifModSince) |
982 | return d && Math.floor(d/1000) >= Math.floor(lastMod/1000) |
983 | } |
984 | |
985 | Serve.prototype.wrapMessages = function () { |
986 | return u.hyperwrap(function (content, cb) { |
987 | cb(null, h('table.ssb-msgs', content)) |
988 | }) |
989 | } |
990 | |
991 | Serve.prototype.renderThread = function () { |
992 | return pull( |
993 | this.app.render.renderFeeds(false), |
994 | pull.map(u.toHTML) |
995 | ) |
996 | } |
997 | |
998 | function mergeOpts(a, b) { |
999 | var obj = {}, k |
1000 | for (k in a) { |
1001 | obj[k] = a[k] |
1002 | } |
1003 | for (k in b) { |
1004 | if (b[k] != null) obj[k] = b[k] |
1005 | else delete obj[k] |
1006 | } |
1007 | return obj |
1008 | } |
1009 | |
1010 | Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { |
1011 | var self = this |
1012 | function linkA(opts, name) { |
1013 | var q1 = mergeOpts(q, opts) |
1014 | return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit) |
1015 | } |
1016 | function links(opts) { |
1017 | var limit = opts.limit || q.limit || 10 |
1018 | return h('tr', h('td.paginate', {colspan: 3}, |
1019 | opts.forwards ? '↑ newer ' : '↓ older ', |
1020 | linkA(mergeOpts(opts, {limit: 1})), ' ', |
1021 | linkA(mergeOpts(opts, {limit: 10})), ' ', |
1022 | linkA(mergeOpts(opts, {limit: 100})) |
1023 | )) |
1024 | } |
1025 | |
1026 | return pull( |
1027 | paginate( |
1028 | function onFirst(msg, cb) { |
1029 | var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts |
1030 | if (q.forwards) { |
1031 | cb(null, links({ |
1032 | lt: num, |
1033 | gt: null, |
1034 | forwards: null, |
1035 | })) |
1036 | } else { |
1037 | cb(null, links({ |
1038 | lt: null, |
1039 | gt: num, |
1040 | forwards: 1, |
1041 | })) |
1042 | } |
1043 | }, |
1044 | this.app.render.renderFeeds(), |
1045 | function onLast(msg, cb) { |
1046 | var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts |
1047 | if (q.forwards) { |
1048 | cb(null, links({ |
1049 | lt: null, |
1050 | gt: num, |
1051 | forwards: 1, |
1052 | })) |
1053 | } else { |
1054 | cb(null, links({ |
1055 | lt: num, |
1056 | gt: null, |
1057 | forwards: null, |
1058 | })) |
1059 | } |
1060 | }, |
1061 | function onEmpty(cb) { |
1062 | if (q.forwards) { |
1063 | cb(null, links({ |
1064 | gt: null, |
1065 | lt: opts.gt + 1, |
1066 | forwards: null, |
1067 | })) |
1068 | } else { |
1069 | cb(null, links({ |
1070 | gt: opts.lt - 1, |
1071 | lt: null, |
1072 | forwards: 1, |
1073 | })) |
1074 | } |
1075 | } |
1076 | ), |
1077 | pull.map(u.toHTML) |
1078 | ) |
1079 | } |
1080 | |
1081 | Serve.prototype.renderRawMsgPage = function (id) { |
1082 | var showMarkdownSource = (this.query.raw === 'md') |
1083 | var raw = !showMarkdownSource |
1084 | return pull( |
1085 | this.app.render.renderFeeds({ |
1086 | raw: raw, |
1087 | markdownSource: showMarkdownSource |
1088 | }), |
1089 | pull.map(u.toHTML), |
1090 | this.wrapMessages(), |
1091 | this.wrapPage(id) |
1092 | ) |
1093 | } |
1094 | |
1095 | function catchHTMLError() { |
1096 | return function (read) { |
1097 | var ended |
1098 | return function (abort, cb) { |
1099 | if (ended) return cb(ended) |
1100 | read(abort, function (end, data) { |
1101 | if (!end || end === true) return cb(end, data) |
1102 | ended = true |
1103 | cb(null, u.renderError(end).outerHTML) |
1104 | }) |
1105 | } |
1106 | } |
1107 | } |
1108 | |
1109 | function catchTextError() { |
1110 | return function (read) { |
1111 | var ended |
1112 | return function (abort, cb) { |
1113 | if (ended) return cb(ended) |
1114 | read(abort, function (end, data) { |
1115 | if (!end || end === true) return cb(end, data) |
1116 | ended = true |
1117 | cb(null, end.stack + '\n') |
1118 | }) |
1119 | } |
1120 | } |
1121 | } |
1122 | |
1123 | function styles() { |
1124 | return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8') |
1125 | } |
1126 | |
1127 | Serve.prototype.appendFooter = function () { |
1128 | var self = this |
1129 | return function (read) { |
1130 | return cat([read, u.readNext(function (cb) { |
1131 | var ms = new Date() - self.startDate |
1132 | cb(null, pull.once(h('footer', |
1133 | h('a', {href: pkg.homepage}, pkg.name), ' · ', |
1134 | ms/1000 + 's' |
1135 | ).outerHTML)) |
1136 | })]) |
1137 | } |
1138 | } |
1139 | |
1140 | Serve.prototype.wrapPage = function (title, searchQ) { |
1141 | var self = this |
1142 | var render = self.app.render |
1143 | return pull( |
1144 | catchHTMLError(), |
1145 | self.appendFooter(), |
1146 | u.hyperwrap(function (content, cb) { |
1147 | var done = multicb({pluck: 1, spread: true}) |
1148 | done()(null, h('html', h('head', |
1149 | h('meta', {charset: 'utf-8'}), |
1150 | h('title', title), |
1151 | h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}), |
1152 | h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}), |
1153 | h('style', styles()) |
1154 | ), |
1155 | h('body', |
1156 | h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'}, |
1157 | h('a', {href: render.toUrl('/new')}, 'new') , ' ', |
1158 | h('a', {href: render.toUrl('/public')}, 'public'), ' ', |
1159 | h('a', {href: render.toUrl('/private')}, 'private') , ' ', |
1160 | h('a', {href: render.toUrl('/peers')}, 'peers') , ' ', |
1161 | h('a', {href: render.toUrl('/channels')}, 'channels') , ' ', |
1162 | h('a', {href: render.toUrl('/friends')}, 'friends'), ' ', |
1163 | h('a', {href: render.toUrl('/advsearch')}, 'search'), ' ', |
1164 | h('a', {href: render.toUrl('/live')}, 'live'), ' ', |
1165 | h('a', {href: render.toUrl('/compose')}, 'compose'), ' ', |
1166 | h('a', {href: render.toUrl('/emojis')}, 'emojis'), ' ', |
1167 | render.idLink(self.app.sbot.id, done()), ' ', |
1168 | h('input.search-input', {name: 'q', value: searchQ, |
1169 | placeholder: 'search'}) |
1170 | // h('a', {href: '/convos'}, 'convos'), ' ', |
1171 | // h('a', {href: '/friends'}, 'friends'), ' ', |
1172 | // h('a', {href: '/git'}, 'git') |
1173 | )), |
1174 | self.publishedMsg ? h('div', |
1175 | 'published ', |
1176 | self.app.render.msgLink(self.publishedMsg, done()) |
1177 | ) : '', |
1178 | // self.note, |
1179 | content |
1180 | ))) |
1181 | done(cb) |
1182 | }) |
1183 | ) |
1184 | } |
1185 | |
1186 | Serve.prototype.renderIdLink = function (id, cb) { |
1187 | var render = this.app.render |
1188 | var el = render.idLink(id, function (err) { |
1189 | if (err || !el) { |
1190 | el = h('a', {href: render.toUrl(id)}, id) |
1191 | } |
1192 | cb(null, el) |
1193 | }) |
1194 | } |
1195 | |
1196 | Serve.prototype.friends = function (path) { |
1197 | var self = this |
1198 | pull( |
1199 | self.app.sbot.friends.createFriendStream({hops: 1}), |
1200 | self.renderFriends(), |
1201 | pull.map(function (el) { |
1202 | return [el, ' '] |
1203 | }), |
1204 | pull.map(u.toHTML), |
1205 | u.hyperwrap(function (items, cb) { |
1206 | cb(null, [ |
1207 | h('section', |
1208 | h('h3', 'Friends') |
1209 | ), |
1210 | h('section', items) |
1211 | ]) |
1212 | }), |
1213 | this.wrapPage('friends'), |
1214 | this.respondSink(200, { |
1215 | 'Content-Type': ctype('html') |
1216 | }) |
1217 | ) |
1218 | } |
1219 | |
1220 | Serve.prototype.renderFriends = function () { |
1221 | var self = this |
1222 | return paramap(function (id, cb) { |
1223 | self.renderIdLink(id, function (err, el) { |
1224 | if (err) el = u.renderError(err, ext) |
1225 | cb(null, el) |
1226 | }) |
1227 | }, 8) |
1228 | } |
1229 | |
1230 | var relationships = [ |
1231 | '', |
1232 | 'followed', |
1233 | 'follows you', |
1234 | 'friend' |
1235 | ] |
1236 | |
1237 | var relationshipActions = [ |
1238 | 'follow', |
1239 | 'unfollow', |
1240 | 'follow back', |
1241 | 'unfriend' |
1242 | ] |
1243 | |
1244 | Serve.prototype.wrapUserFeed = function (isScrolled, id) { |
1245 | var self = this |
1246 | var myId = self.app.sbot.id |
1247 | var render = self.app.render |
1248 | return u.hyperwrap(function (thread, cb) { |
1249 | var done = multicb({pluck: 1, spread: true}) |
1250 | self.app.getAbout(id, done()) |
1251 | self.app.getFollow(myId, id, done()) |
1252 | self.app.getFollow(id, myId, done()) |
1253 | done(function (err, about, weFollowThem, theyFollowUs) { |
1254 | if (err) return cb(err) |
1255 | var relationshipI = weFollowThem | theyFollowUs<<1 |
1256 | var done = multicb({pluck: 1, spread: true}) |
1257 | done()(null, [ |
1258 | h('section.ssb-feed', |
1259 | h('table', h('tr', |
1260 | h('td', self.app.render.avatarImage(id, done())), |
1261 | h('td.feed-about', |
1262 | h('h3.feed-name', |
1263 | h('strong', self.app.render.idLink(id, done()))), |
1264 | h('code', h('small', id)), |
1265 | about.description ? h('div', |
1266 | {innerHTML: self.app.render.markdown(about.description)}) : '' |
1267 | )), |
1268 | h('tr', |
1269 | h('td'), |
1270 | h('td', |
1271 | h('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ', |
1272 | h('a', {href: render.toUrl('/about/' + id)}, 'about') |
1273 | ) |
1274 | ), |
1275 | h('tr', |
1276 | h('td'), |
1277 | h('td', |
1278 | h('form', {action: render.toUrl('/advsearch'), method: 'get'}, |
1279 | h('input', {type: 'hidden', name: 'source', value: id}), |
1280 | h('input', {type: 'text', name: 'text', placeholder: 'text'}), |
1281 | h('input', {type: 'submit', value: 'search'}) |
1282 | ) |
1283 | ) |
1284 | ), |
1285 | isScrolled ? '' : [ |
1286 | id === myId ? '' : h('tr', |
1287 | h('td'), |
1288 | h('td.follow-info', h('form', {action: '', method: 'post'}, |
1289 | relationships[relationshipI], ' ', |
1290 | h('input', {type: 'hidden', name: 'action', value: 'contact'}), |
1291 | h('input', {type: 'hidden', name: 'contact', value: id}), |
1292 | h('input', {type: 'hidden', name: 'following', |
1293 | value: weFollowThem ? '' : 'following'}), |
1294 | h('input', {type: 'submit', |
1295 | value: relationshipActions[relationshipI]}) |
1296 | )) |
1297 | ) |
1298 | ] |
1299 | )), |
1300 | thread |
1301 | ]) |
1302 | done(cb) |
1303 | }) |
1304 | }) |
1305 | } |
1306 | |
1307 | Serve.prototype.git = function (url) { |
1308 | var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url) |
1309 | switch (m[1]) { |
1310 | case 'commit': return this.gitCommit(m[2]) |
1311 | case 'tag': return this.gitTag(m[2]) |
1312 | case 'tree': return this.gitTree(m[2]) |
1313 | case 'blob': return this.gitBlob(m[2]) |
1314 | case 'raw': return this.gitRaw(m[2]) |
1315 | default: return this.respond(404, 'Not found') |
1316 | } |
1317 | } |
1318 | |
1319 | Serve.prototype.gitRaw = function (rev) { |
1320 | var self = this |
1321 | if (!/[0-9a-f]{24}/.test(rev)) { |
1322 | return pull( |
1323 | pull.once('\'' + rev + '\' is not a git object id'), |
1324 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
1325 | ) |
1326 | } |
1327 | if (!u.isRef(self.query.msg)) return pull( |
1328 | ph('div.error', 'missing message id'), |
1329 | self.wrapPage('git tree ' + rev), |
1330 | self.respondSink(400) |
1331 | ) |
1332 | |
1333 | self.app.git.openObject({ |
1334 | obj: rev, |
1335 | msg: self.query.msg, |
1336 | }, function (err, obj) { |
1337 | if (err && err.name === 'BlobNotFoundError') |
1338 | return self.askWantBlobs(err.links) |
1339 | if (err) return pull( |
1340 | pull.once(err.stack), |
1341 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
1342 | ) |
1343 | pull( |
1344 | self.app.git.readObject(obj), |
1345 | catchTextError(), |
1346 | ident(function (type) { |
1347 | type = type && mime.lookup(type) |
1348 | if (type) self.res.setHeader('Content-Type', type) |
1349 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
1350 | self.res.setHeader('etag', rev) |
1351 | self.res.writeHead(200) |
1352 | }), |
1353 | self.respondSink() |
1354 | ) |
1355 | }) |
1356 | } |
1357 | |
1358 | Serve.prototype.gitAuthorLink = function (author) { |
1359 | if (author.feed) { |
1360 | var myName = this.app.getNameSync(author.feed) |
1361 | var sigil = author.name === author.localpart ? '@' : '' |
1362 | return ph('a', { |
1363 | href: this.app.render.toUrl(author.feed), |
1364 | title: author.localpart + (myName ? ' (' + myName + ')' : '') |
1365 | }, u.escapeHTML(sigil + author.name)) |
1366 | } else { |
1367 | return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)}, |
1368 | u.escapeHTML(author.name)) |
1369 | } |
1370 | } |
1371 | |
1372 | Serve.prototype.gitCommit = function (rev) { |
1373 | var self = this |
1374 | if (!/[0-9a-f]{24}/.test(rev)) { |
1375 | return pull( |
1376 | ph('div.error', 'rev is not a git object id'), |
1377 | self.wrapPage('git'), |
1378 | self.respondSink(400) |
1379 | ) |
1380 | } |
1381 | if (!u.isRef(self.query.msg)) return pull( |
1382 | ph('div.error', 'missing message id'), |
1383 | self.wrapPage('git commit ' + rev), |
1384 | self.respondSink(400) |
1385 | ) |
1386 | |
1387 | self.app.git.openObject({ |
1388 | obj: rev, |
1389 | msg: self.query.msg, |
1390 | }, function (err, obj) { |
1391 | if (err && err.name === 'BlobNotFoundError') |
1392 | return self.askWantBlobs(err.links) |
1393 | if (err) return pull( |
1394 | pull.once(u.renderError(err).outerHTML), |
1395 | self.wrapPage('git commit ' + rev), |
1396 | self.respondSink(400) |
1397 | ) |
1398 | var msgDate = new Date(obj.msg.value.timestamp) |
1399 | self.app.git.getCommit(obj, function (err, commit) { |
1400 | var missingBlobs |
1401 | if (err && err.name === 'BlobNotFoundError') |
1402 | missingBlobs = err.links, err = null |
1403 | if (err) return pull( |
1404 | pull.once(u.renderError(err).outerHTML), |
1405 | self.wrapPage('git commit ' + rev), |
1406 | self.respondSink(400) |
1407 | ) |
1408 | pull( |
1409 | ph('section', [ |
1410 | ph('h3', ph('a', {href: ''}, rev)), |
1411 | ph('div', [ |
1412 | self.phIdLink(obj.msg.value.author), ' pushed ', |
1413 | ph('a', { |
1414 | href: self.app.render.toUrl(obj.msg.key), |
1415 | title: msgDate.toLocaleString(), |
1416 | }, htime(msgDate)) |
1417 | ]), |
1418 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
1419 | ph('div', [ |
1420 | self.gitAuthorLink(commit.committer), |
1421 | ' committed ', |
1422 | ph('span', {title: commit.committer.date.toLocaleString()}, |
1423 | htime(commit.committer.date)), |
1424 | ' in ', commit.committer.tz |
1425 | ]), |
1426 | commit.author ? ph('div', [ |
1427 | self.gitAuthorLink(commit.author), |
1428 | ' authored ', |
1429 | ph('span', {title: commit.author.date.toLocaleString()}, |
1430 | htime(commit.author.date)), |
1431 | ' in ', commit.author.tz |
1432 | ]) : '', |
1433 | commit.parents.length ? ph('div', ['parents: ', pull( |
1434 | pull.values(commit.parents), |
1435 | self.gitObjectLinks(obj.msg.key, 'commit') |
1436 | )]) : '', |
1437 | commit.tree ? ph('div', ['tree: ', pull( |
1438 | pull.once(commit.tree), |
1439 | self.gitObjectLinks(obj.msg.key, 'tree') |
1440 | )]) : '', |
1441 | h('pre', self.app.render.linkify(commit.body)).outerHTML, |
1442 | ] |
1443 | ]), |
1444 | self.wrapPage('git commit ' + rev), |
1445 | self.respondSink(missingBlobs ? 409 : 200) |
1446 | ) |
1447 | }) |
1448 | }) |
1449 | } |
1450 | |
1451 | Serve.prototype.gitTag = function (rev) { |
1452 | var self = this |
1453 | if (!/[0-9a-f]{24}/.test(rev)) { |
1454 | return pull( |
1455 | ph('div.error', 'rev is not a git object id'), |
1456 | self.wrapPage('git'), |
1457 | self.respondSink(400) |
1458 | ) |
1459 | } |
1460 | if (!u.isRef(self.query.msg)) return pull( |
1461 | ph('div.error', 'missing message id'), |
1462 | self.wrapPage('git tag ' + rev), |
1463 | self.respondSink(400) |
1464 | ) |
1465 | |
1466 | self.app.git.openObject({ |
1467 | obj: rev, |
1468 | msg: self.query.msg, |
1469 | }, function (err, obj) { |
1470 | if (err && err.name === 'BlobNotFoundError') |
1471 | return self.askWantBlobs(err.links) |
1472 | if (err) return pull( |
1473 | pull.once(u.renderError(err).outerHTML), |
1474 | self.wrapPage('git tag ' + rev), |
1475 | self.respondSink(400) |
1476 | ) |
1477 | var msgDate = new Date(obj.msg.value.timestamp) |
1478 | self.app.git.getTag(obj, function (err, tag) { |
1479 | var missingBlobs |
1480 | if (err && err.name === 'BlobNotFoundError') |
1481 | missingBlobs = err.links, err = null |
1482 | if (err) return pull( |
1483 | pull.once(u.renderError(err).outerHTML), |
1484 | self.wrapPage('git tag ' + rev), |
1485 | self.respondSink(400) |
1486 | ) |
1487 | pull( |
1488 | ph('section', [ |
1489 | ph('h3', ph('a', {href: ''}, rev)), |
1490 | ph('div', [ |
1491 | self.phIdLink(obj.msg.value.author), ' pushed ', |
1492 | ph('a', { |
1493 | href: self.app.render.toUrl(obj.msg.key), |
1494 | title: msgDate.toLocaleString(), |
1495 | }, htime(msgDate)) |
1496 | ]), |
1497 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
1498 | ph('div', [ |
1499 | self.gitAuthorLink(tag.tagger), |
1500 | ' tagged ', |
1501 | ph('span', {title: tag.tagger.date.toLocaleString()}, |
1502 | htime(tag.tagger.date)), |
1503 | ' in ', tag.tagger.tz |
1504 | ]), |
1505 | tag.type, ' ', |
1506 | pull( |
1507 | pull.once(tag.object), |
1508 | self.gitObjectLinks(obj.msg.key, tag.type) |
1509 | ), ' ', |
1510 | ph('code', u.escapeHTML(tag.tag)), |
1511 | h('pre', self.app.render.linkify(tag.body)).outerHTML, |
1512 | ] |
1513 | ]), |
1514 | self.wrapPage('git tag ' + rev), |
1515 | self.respondSink(missingBlobs ? 409 : 200) |
1516 | ) |
1517 | }) |
1518 | }) |
1519 | } |
1520 | |
1521 | Serve.prototype.gitTree = function (rev) { |
1522 | var self = this |
1523 | if (!/[0-9a-f]{24}/.test(rev)) { |
1524 | return pull( |
1525 | ph('div.error', 'rev is not a git object id'), |
1526 | self.wrapPage('git'), |
1527 | self.respondSink(400) |
1528 | ) |
1529 | } |
1530 | if (!u.isRef(self.query.msg)) return pull( |
1531 | ph('div.error', 'missing message id'), |
1532 | self.wrapPage('git tree ' + rev), |
1533 | self.respondSink(400) |
1534 | ) |
1535 | |
1536 | self.app.git.openObject({ |
1537 | obj: rev, |
1538 | msg: self.query.msg, |
1539 | }, function (err, obj) { |
1540 | var missingBlobs |
1541 | if (err && err.name === 'BlobNotFoundError') |
1542 | missingBlobs = err.links, err = null |
1543 | if (err) return pull( |
1544 | pull.once(u.renderError(err).outerHTML), |
1545 | self.wrapPage('git tree ' + rev), |
1546 | self.respondSink(400) |
1547 | ) |
1548 | var msgDate = new Date(obj.msg.value.timestamp) |
1549 | pull( |
1550 | ph('section', [ |
1551 | ph('h3', ph('a', {href: ''}, rev)), |
1552 | ph('div', [ |
1553 | self.phIdLink(obj.msg.value.author), ' ', |
1554 | ph('a', { |
1555 | href: self.app.render.toUrl(obj.msg.key), |
1556 | title: msgDate.toLocaleString(), |
1557 | }, htime(msgDate)) |
1558 | ]), |
1559 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [ |
1560 | pull( |
1561 | self.app.git.readTree(obj), |
1562 | paramap(function (file, cb) { |
1563 | self.app.git.getObjectMsg({ |
1564 | obj: file.hash, |
1565 | headMsgId: obj.msg.key, |
1566 | }, function (err, msg) { |
1567 | if (err && err.name === 'ObjectNotFoundError') return cb(null, file) |
1568 | if (err) return cb(err) |
1569 | file.msg = msg |
1570 | cb(null, file) |
1571 | }) |
1572 | }, 8), |
1573 | pull.map(function (item) { |
1574 | var type = item.mode === 0040000 ? 'tree' : |
1575 | item.mode === 0160000 ? 'commit' : 'blob' |
1576 | if (!item.msg) return ph('tr', [ |
1577 | ph('td', |
1578 | u.escapeHTML(item.name) + (type === 'tree' ? '/' : '')), |
1579 | ph('td', 'missing') |
1580 | ]) |
1581 | var path = '/git/' + type + '/' + item.hash |
1582 | + '?msg=' + encodeURIComponent(item.msg.key) |
1583 | var fileDate = new Date(item.msg.value.timestamp) |
1584 | return ph('tr', [ |
1585 | ph('td', |
1586 | ph('a', {href: self.app.render.toUrl(path)}, |
1587 | u.escapeHTML(item.name) + (type === 'tree' ? '/' : ''))), |
1588 | ph('td', |
1589 | self.phIdLink(item.msg.value.author)), |
1590 | ph('td', |
1591 | ph('a', { |
1592 | href: self.app.render.toUrl(item.msg.key), |
1593 | title: fileDate.toLocaleString(), |
1594 | }, htime(fileDate)) |
1595 | ), |
1596 | ]) |
1597 | }) |
1598 | ) |
1599 | ]), |
1600 | ]), |
1601 | self.wrapPage('git tree ' + rev), |
1602 | self.respondSink(missingBlobs ? 409 : 200) |
1603 | ) |
1604 | }) |
1605 | } |
1606 | |
1607 | Serve.prototype.gitBlob = function (rev) { |
1608 | var self = this |
1609 | if (!/[0-9a-f]{24}/.test(rev)) { |
1610 | return pull( |
1611 | ph('div.error', 'rev is not a git object id'), |
1612 | self.wrapPage('git'), |
1613 | self.respondSink(400) |
1614 | ) |
1615 | } |
1616 | if (!u.isRef(self.query.msg)) return pull( |
1617 | ph('div.error', 'missing message id'), |
1618 | self.wrapPage('git object ' + rev), |
1619 | self.respondSink(400) |
1620 | ) |
1621 | |
1622 | self.app.getMsgDecrypted(self.query.msg, function (err, msg) { |
1623 | if (err) return pull( |
1624 | pull.once(u.renderError(err).outerHTML), |
1625 | self.wrapPage('git object ' + rev), |
1626 | self.respondSink(400) |
1627 | ) |
1628 | var msgDate = new Date(msg.value.timestamp) |
1629 | self.app.git.openObject({ |
1630 | obj: rev, |
1631 | msg: msg.key, |
1632 | }, function (err, obj) { |
1633 | var missingBlobs |
1634 | if (err && err.name === 'BlobNotFoundError') |
1635 | missingBlobs = err.links, err = null |
1636 | if (err) return pull( |
1637 | pull.once(u.renderError(err).outerHTML), |
1638 | self.wrapPage('git object ' + rev), |
1639 | self.respondSink(400) |
1640 | ) |
1641 | pull( |
1642 | ph('section', [ |
1643 | ph('h3', ph('a', {href: ''}, rev)), |
1644 | ph('div', [ |
1645 | self.phIdLink(msg.value.author), ' ', |
1646 | ph('a', { |
1647 | href: self.app.render.toUrl(msg.key), |
1648 | title: msgDate.toLocaleString(), |
1649 | }, htime(msgDate)) |
1650 | ]), |
1651 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull( |
1652 | self.app.git.readObject(obj), |
1653 | self.wrapBinary({ |
1654 | rawUrl: self.app.render.toUrl('/git/raw/' + rev |
1655 | + '?msg=' + encodeURIComponent(msg.key)) |
1656 | }) |
1657 | ), |
1658 | ]), |
1659 | self.wrapPage('git blob ' + rev), |
1660 | self.respondSink(200) |
1661 | ) |
1662 | }) |
1663 | }) |
1664 | } |
1665 | |
1666 | Serve.prototype.gitObjectLinks = function (headMsgId, type) { |
1667 | var self = this |
1668 | return paramap(function (id, cb) { |
1669 | self.app.git.getObjectMsg({ |
1670 | obj: id, |
1671 | headMsgId: headMsgId, |
1672 | type: type, |
1673 | }, function (err, msg) { |
1674 | if (err && err.name === 'BlobNotFoundError') |
1675 | return cb(null, self.askWantBlobsForm(err.links)) |
1676 | if (err && err.name === 'ObjectNotFoundError') |
1677 | return cb(null, [ |
1678 | ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)']) |
1679 | if (err) return cb(err) |
1680 | var path = '/git/' + type + '/' + id |
1681 | + '?msg=' + encodeURIComponent(msg.key) |
1682 | cb(null, [ph('code', ph('a', { |
1683 | href: self.app.render.toUrl(path) |
1684 | }, u.escapeHTML(id.substr(0, 8)))), ' ']) |
1685 | }) |
1686 | }, 8) |
1687 | } |
1688 | |
1689 | // wrap a binary source and render it or turn into an embed |
1690 | Serve.prototype.wrapBinary = function (opts) { |
1691 | var self = this |
1692 | return function (read) { |
1693 | var readRendered, type |
1694 | read = ident(function (ext) { |
1695 | type = ext && mime.lookup(ext) || 'text/plain' |
1696 | })(read) |
1697 | return function (abort, cb) { |
1698 | if (readRendered) return readRendered(abort, cb) |
1699 | if (abort) return read(abort, cb) |
1700 | if (!type) read(null, function (end, buf) { |
1701 | if (end) return cb(end) |
1702 | if (!type) return cb(new Error('unable to get type')) |
1703 | readRendered = pickSource(type, cat([pull.once(buf), read])) |
1704 | readRendered(null, cb) |
1705 | }) |
1706 | } |
1707 | } |
1708 | function pickSource(type, read) { |
1709 | if (/^image\//.test(type)) { |
1710 | read(true, function (err) { |
1711 | if (err && err !== true) console.trace(err) |
1712 | }) |
1713 | return ph('img', { |
1714 | src: opts.rawUrl |
1715 | }) |
1716 | } |
1717 | return ph('pre', pull.map(function (buf) { |
1718 | return h('div', |
1719 | self.app.render.linkify(buf.toString('utf8')) |
1720 | ).innerHTML |
1721 | })(read)) |
1722 | } |
1723 | } |
1724 | |
1725 | Serve.prototype.wrapPublic = function (opts) { |
1726 | var self = this |
1727 | return u.hyperwrap(function (thread, cb) { |
1728 | self.composer({ |
1729 | channel: '', |
1730 | }, function (err, composer) { |
1731 | if (err) return cb(err) |
1732 | cb(null, [ |
1733 | composer, |
1734 | thread |
1735 | ]) |
1736 | }) |
1737 | }) |
1738 | } |
1739 | |
1740 | Serve.prototype.askWantBlobsForm = function (links) { |
1741 | var self = this |
1742 | return ph('form', {action: '', method: 'post'}, [ |
1743 | ph('section', [ |
1744 | ph('h3', 'Missing blobs'), |
1745 | ph('p', 'The application needs these blobs to continue:'), |
1746 | ph('table', links.map(u.toLink).map(function (link) { |
1747 | if (!u.isRef(link.link)) return |
1748 | return ph('tr', [ |
1749 | ph('td', ph('code', link.link)), |
1750 | ph('td', self.app.render.formatSize(link.size)), |
1751 | ]) |
1752 | })), |
1753 | ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), |
1754 | ph('input', {type: 'hidden', name: 'blob_ids', |
1755 | value: links.map(u.linkDest).join(',')}), |
1756 | ph('p', ph('input', {type: 'submit', value: 'Want Blobs'})) |
1757 | ]) |
1758 | ]) |
1759 | } |
1760 | |
1761 | Serve.prototype.askWantBlobs = function (links) { |
1762 | var self = this |
1763 | pull( |
1764 | self.askWantBlobsForm(links), |
1765 | self.wrapPage('missing blobs'), |
1766 | self.respondSink(409) |
1767 | ) |
1768 | } |
1769 | |
1770 | Serve.prototype.wrapPrivate = function (opts) { |
1771 | var self = this |
1772 | return u.hyperwrap(function (thread, cb) { |
1773 | self.composer({ |
1774 | placeholder: 'private message', |
1775 | private: true, |
1776 | }, function (err, composer) { |
1777 | if (err) return cb(err) |
1778 | cb(null, [ |
1779 | composer, |
1780 | thread |
1781 | ]) |
1782 | }) |
1783 | }) |
1784 | } |
1785 | |
1786 | Serve.prototype.wrapThread = function (opts) { |
1787 | var self = this |
1788 | return u.hyperwrap(function (thread, cb) { |
1789 | self.app.render.prepareLinks(opts.recps, function (err, recps) { |
1790 | if (err) return cb(er) |
1791 | self.composer({ |
1792 | placeholder: recps ? 'private reply' : 'reply', |
1793 | id: 'reply', |
1794 | root: opts.root, |
1795 | post: opts.post, |
1796 | channel: opts.channel || '', |
1797 | branches: opts.branches, |
1798 | postBranches: opts.postBranches, |
1799 | recps: recps, |
1800 | }, function (err, composer) { |
1801 | if (err) return cb(err) |
1802 | cb(null, [ |
1803 | thread, |
1804 | composer |
1805 | ]) |
1806 | }) |
1807 | }) |
1808 | }) |
1809 | } |
1810 | |
1811 | Serve.prototype.wrapNew = function (opts) { |
1812 | var self = this |
1813 | return u.hyperwrap(function (thread, cb) { |
1814 | self.composer({ |
1815 | channel: '', |
1816 | }, function (err, composer) { |
1817 | if (err) return cb(err) |
1818 | cb(null, [ |
1819 | composer, |
1820 | h('table.ssb-msgs', |
1821 | thread, |
1822 | h('tr', h('td.paginate.msg-left', {colspan: 3}, |
1823 | h('form', {method: 'get', action: ''}, |
1824 | h('input', {type: 'hidden', name: 'gt', value: opts.gt}), |
1825 | h('input', {type: 'hidden', name: 'catchup', value: '1'}), |
1826 | h('input', {type: 'submit', value: 'catchup'}) |
1827 | ) |
1828 | )) |
1829 | ) |
1830 | ]) |
1831 | }) |
1832 | }) |
1833 | } |
1834 | |
1835 | Serve.prototype.wrapChannel = function (channel) { |
1836 | var self = this |
1837 | return u.hyperwrap(function (thread, cb) { |
1838 | self.composer({ |
1839 | placeholder: 'public message in #' + channel, |
1840 | channel: channel, |
1841 | }, function (err, composer) { |
1842 | if (err) return cb(err) |
1843 | cb(null, [ |
1844 | h('section', |
1845 | h('h3.feed-name', |
1846 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel) |
1847 | ) |
1848 | ), |
1849 | composer, |
1850 | thread |
1851 | ]) |
1852 | }) |
1853 | }) |
1854 | } |
1855 | |
1856 | Serve.prototype.wrapType = function (type) { |
1857 | var self = this |
1858 | return u.hyperwrap(function (thread, cb) { |
1859 | cb(null, [ |
1860 | h('section', |
1861 | h('h3.feed-name', |
1862 | h('a', {href: self.app.render.toUrl('/type/' + type)}, |
1863 | h('code', type), 's')) |
1864 | ), |
1865 | thread |
1866 | ]) |
1867 | }) |
1868 | } |
1869 | |
1870 | Serve.prototype.wrapLinks = function (dest) { |
1871 | var self = this |
1872 | return u.hyperwrap(function (thread, cb) { |
1873 | cb(null, [ |
1874 | h('section', |
1875 | h('h3.feed-name', 'links: ', |
1876 | h('a', {href: self.app.render.toUrl('/links/' + dest)}, |
1877 | h('code', dest))) |
1878 | ), |
1879 | thread |
1880 | ]) |
1881 | }) |
1882 | } |
1883 | |
1884 | Serve.prototype.wrapPeers = function (opts) { |
1885 | var self = this |
1886 | return u.hyperwrap(function (peers, cb) { |
1887 | cb(null, [ |
1888 | h('section', |
1889 | h('h3', 'Peers') |
1890 | ), |
1891 | peers |
1892 | ]) |
1893 | }) |
1894 | } |
1895 | |
1896 | Serve.prototype.wrapChannels = function (opts) { |
1897 | var self = this |
1898 | return u.hyperwrap(function (channels, cb) { |
1899 | cb(null, [ |
1900 | h('section', |
1901 | h('h4', 'Network') |
1902 | ), |
1903 | h('section', |
1904 | channels |
1905 | ) |
1906 | ]) |
1907 | }) |
1908 | } |
1909 | |
1910 | Serve.prototype.wrapMyChannels = function (opts) { |
1911 | var self = this |
1912 | return u.hyperwrap(function (channels, cb) { |
1913 | cb(null, [ |
1914 | h('section', |
1915 | h('h4', 'Subscribed') |
1916 | ), |
1917 | h('section', |
1918 | channels |
1919 | ) |
1920 | ]) |
1921 | }) |
1922 | } |
1923 | |
1924 | function rows(str) { |
1925 | return String(str).split(/[^\n]{150}|\n/).length |
1926 | } |
1927 | |
1928 | Serve.prototype.composer = function (opts, cb) { |
1929 | var self = this |
1930 | opts = opts || {} |
1931 | var data = self.data |
1932 | var myId = self.app.sbot.id |
1933 | |
1934 | var blobs = u.tryDecodeJSON(data.blobs) || {} |
1935 | if (data.upload && typeof data.upload === 'object') { |
1936 | blobs[data.upload.link] = { |
1937 | type: data.upload.type, |
1938 | size: data.upload.size, |
1939 | } |
1940 | } |
1941 | if (data.blob_type && blobs[data.blob_link]) { |
1942 | blobs[data.blob_link].type = data.blob_type |
1943 | } |
1944 | var channel = data.channel != null ? data.channel : opts.channel |
1945 | |
1946 | var formNames = {} |
1947 | var mentionIds = u.toArray(data.mention_id) |
1948 | var mentionNames = u.toArray(data.mention_name) |
1949 | for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) { |
1950 | formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0] |
1951 | } |
1952 | |
1953 | var formEmojiNames = {} |
1954 | var emojiIds = u.toArray(data.emoji_id) |
1955 | var emojiNames = u.toArray(data.emoji_name) |
1956 | for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) { |
1957 | var upload = data['emoji_upload_' + i] |
1958 | formEmojiNames[emojiNames[i]] = |
1959 | (upload && upload.link) || u.extractBlobIds(emojiIds[i])[0] |
1960 | if (upload) blobs[upload.link] = { |
1961 | type: upload.type, |
1962 | size: upload.size, |
1963 | } |
1964 | } |
1965 | |
1966 | if (data.upload) { |
1967 | // TODO: be able to change the content-type |
1968 | var isImage = /^image\//.test(data.upload.type) |
1969 | data.text = (data.text ? data.text + '\n' : '') |
1970 | + (isImage ? '!' : '') |
1971 | + '[' + data.upload.name + '](' + data.upload.link + ')' |
1972 | } |
1973 | |
1974 | // get bare feed names |
1975 | var unknownMentionNames = {} |
1976 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
1977 | var unknownMentions = mentions |
1978 | .filter(function (mention) { |
1979 | return mention.link === '@' |
1980 | }) |
1981 | .map(function (mention) { |
1982 | return mention.name |
1983 | }) |
1984 | .filter(uniques()) |
1985 | .map(function (name) { |
1986 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
1987 | return {name: name, id: id} |
1988 | }) |
1989 | |
1990 | var emoji = mentions |
1991 | .filter(function (mention) { return mention.emoji }) |
1992 | .map(function (mention) { return mention.name }) |
1993 | .filter(uniques()) |
1994 | .map(function (name) { |
1995 | // 1. check emoji-image mapping for this message |
1996 | var id = formEmojiNames[name] |
1997 | if (id) return {name: name, id: id} |
1998 | // 2. TODO: check user's preferred emoji-image mapping |
1999 | // 3. check builtin emoji |
2000 | var link = self.getBuiltinEmojiLink(name) |
2001 | if (link) { |
2002 | return {name: name, id: link.link} |
2003 | blobs[id] = {type: link.type, size: link.size} |
2004 | } |
2005 | // 4. check recently seen emoji |
2006 | id = self.app.getReverseEmojiNameSync(name) |
2007 | return {name: name, id: id} |
2008 | }) |
2009 | |
2010 | // strip content other than feed ids from the recps field |
2011 | if (data.recps) { |
2012 | data.recps = u.extractFeedIds(data.recps).filter(uniques()).join(', ') |
2013 | } |
2014 | |
2015 | var done = multicb({pluck: 1, spread: true}) |
2016 | done()(null, h('section.composer', |
2017 | h('form', {method: 'post', action: opts.id ? '#' + opts.id : '', |
2018 | enctype: 'multipart/form-data'}, |
2019 | h('input', {type: 'hidden', name: 'blobs', |
2020 | value: JSON.stringify(blobs)}), |
2021 | opts.recps ? self.app.render.privateLine(opts.recps, done()) : |
2022 | opts.private ? h('div', h('input.recps-input', {name: 'recps', |
2023 | value: data.recps || '', placeholder: 'recipient ids'})) : '', |
2024 | channel != null ? |
2025 | h('div', '#', h('input', {name: 'channel', placeholder: 'channel', |
2026 | value: channel})) : '', |
2027 | opts.root !== opts.post ? h('div', |
2028 | h('label', {for: 'fork_thread'}, |
2029 | h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}), |
2030 | ' fork thread' |
2031 | ) |
2032 | ) : '', |
2033 | h('textarea', { |
2034 | id: opts.id, |
2035 | name: 'text', |
2036 | rows: Math.max(4, rows(data.text)), |
2037 | cols: 70, |
2038 | placeholder: opts.placeholder || 'public message', |
2039 | }, data.text || ''), |
2040 | unknownMentions.length > 0 ? [ |
2041 | h('div', h('em', 'names:')), |
2042 | h('ul.mentions', unknownMentions.map(function (mention) { |
2043 | return h('li', |
2044 | h('code', '@' + mention.name), ': ', |
2045 | h('input', {name: 'mention_name', type: 'hidden', |
2046 | value: mention.name}), |
2047 | h('input.id-input', {name: 'mention_id', size: 60, |
2048 | value: mention.id, placeholder: '@id'})) |
2049 | })) |
2050 | ] : '', |
2051 | emoji.length > 0 ? [ |
2052 | h('div', h('em', 'emoji:')), |
2053 | h('ul.mentions', emoji.map(function (link, i) { |
2054 | return h('li', |
2055 | h('code', link.name), ': ', |
2056 | h('input', {name: 'emoji_name', type: 'hidden', |
2057 | value: link.name}), |
2058 | h('input.id-input', {name: 'emoji_id', size: 60, |
2059 | value: link.id, placeholder: '&id'}), ' ', |
2060 | h('input', {type: 'file', name: 'emoji_upload_' + i})) |
2061 | })) |
2062 | ] : '', |
2063 | h('table.ssb-msgs', |
2064 | h('tr.msg-row', |
2065 | h('td.msg-left', {colspan: 2}, |
2066 | h('input', {type: 'file', name: 'upload'}) |
2067 | ), |
2068 | h('td.msg-right', |
2069 | h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ', |
2070 | h('input', {type: 'submit', name: 'action', value: 'preview'}) |
2071 | ) |
2072 | ) |
2073 | ), |
2074 | data.action === 'preview' ? preview(false, done()) : |
2075 | data.action === 'raw' ? preview(true, done()) : '' |
2076 | ) |
2077 | )) |
2078 | done(cb) |
2079 | |
2080 | function prepareContent(cb) { |
2081 | var done = multicb({pluck: 1}) |
2082 | content = { |
2083 | type: 'post', |
2084 | text: String(data.text).replace(/\r\n/g, '\n'), |
2085 | } |
2086 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
2087 | .filter(function (mention) { |
2088 | if (mention.emoji) { |
2089 | mention.link = formEmojiNames[mention.name] |
2090 | if (!mention.link) { |
2091 | var link = self.getBuiltinEmojiLink(mention.name) |
2092 | if (link) { |
2093 | mention.link = link.link |
2094 | mention.size = link.size |
2095 | mention.type = link.type |
2096 | } else { |
2097 | mention.link = self.app.getReverseEmojiNameSync(mention.name) |
2098 | if (!mention.link) return false |
2099 | } |
2100 | } |
2101 | } |
2102 | var blob = blobs[mention.link] |
2103 | if (blob) { |
2104 | if (!isNaN(blob.size)) |
2105 | mention.size = blob.size |
2106 | if (blob.type && blob.type !== 'application/octet-stream') |
2107 | mention.type = blob.type |
2108 | } else if (mention.link === '@') { |
2109 | // bare feed name |
2110 | var name = mention.name |
2111 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
2112 | if (id) mention.link = id |
2113 | else return false |
2114 | } |
2115 | if (mention.link && mention.link[0] === '&' && mention.size == null) { |
2116 | var linkCb = done() |
2117 | self.app.sbot.blobs.size(mention.link, function (err, size) { |
2118 | if (!err && size != null) mention.size = size |
2119 | linkCb() |
2120 | }) |
2121 | } |
2122 | return true |
2123 | }) |
2124 | if (mentions.length) content.mentions = mentions |
2125 | if (data.recps != null) { |
2126 | if (opts.recps) return cb(new Error('got recps in opts and data')) |
2127 | content.recps = [myId] |
2128 | u.extractFeedIds(data.recps).forEach(function (recp) { |
2129 | if (content.recps.indexOf(recp) === -1) content.recps.push(recp) |
2130 | }) |
2131 | } else { |
2132 | if (opts.recps) content.recps = opts.recps |
2133 | } |
2134 | if (data.fork_thread) { |
2135 | content.root = opts.post || undefined |
2136 | content.branch = u.fromArray(opts.postBranches) || undefined |
2137 | } else { |
2138 | content.root = opts.root || undefined |
2139 | content.branch = u.fromArray(opts.branches) || undefined |
2140 | } |
2141 | if (channel) content.channel = data.channel |
2142 | |
2143 | done(function (err) { |
2144 | cb(err, content) |
2145 | }) |
2146 | } |
2147 | |
2148 | function preview(raw, cb) { |
2149 | var msgContainer = h('table.ssb-msgs') |
2150 | var contentInput = h('input', {type: 'hidden', name: 'content'}) |
2151 | var warningsContainer = h('div') |
2152 | |
2153 | var content |
2154 | try { content = JSON.parse(data.text) } |
2155 | catch (err) {} |
2156 | if (content) gotContent(null, content) |
2157 | else prepareContent(gotContent) |
2158 | |
2159 | function gotContent(err, content) { |
2160 | if (err) return cb(err) |
2161 | contentInput.value = JSON.stringify(content) |
2162 | var msg = { |
2163 | value: { |
2164 | author: myId, |
2165 | timestamp: Date.now(), |
2166 | content: content |
2167 | } |
2168 | } |
2169 | if (content.recps) msg.value.private = true |
2170 | |
2171 | var warnings = [] |
2172 | u.toLinkArray(content.mentions).forEach(function (link) { |
2173 | if (link.emoji && link.size >= 10e3) { |
2174 | warnings.push(h('li', |
2175 | 'emoji ', h('q', link.name), |
2176 | ' (', h('code', String(link.link).substr(0, 8) + '…'), ')' |
2177 | + ' is >10KB')) |
2178 | } else if (link.link[0] === '&' && link.size >= 10e6 && link.type) { |
2179 | // if link.type is set, we probably just uploaded this blob |
2180 | warnings.push(h('li', |
2181 | 'attachment ', |
2182 | h('code', String(link.link).substr(0, 8) + '…'), |
2183 | ' is >10MB')) |
2184 | } |
2185 | }) |
2186 | if (warnings.length) { |
2187 | warningsContainer.appendChild(h('div', h('em', 'warning:'))) |
2188 | warningsContainer.appendChild(h('ul.mentions', warnings)) |
2189 | } |
2190 | |
2191 | pull( |
2192 | pull.once(msg), |
2193 | self.app.unboxMessages(), |
2194 | self.app.render.renderFeeds(raw), |
2195 | pull.drain(function (el) { |
2196 | msgContainer.appendChild(h('tbody', el)) |
2197 | }, cb) |
2198 | ) |
2199 | } |
2200 | |
2201 | return [ |
2202 | contentInput, |
2203 | opts.redirectToPublishedMsg ? h('input', {type: 'hidden', |
2204 | name: 'redirect_to_published_msg', value: '1'}) : '', |
2205 | warningsContainer, |
2206 | h('div', h('em', 'draft:')), |
2207 | msgContainer, |
2208 | h('div.composer-actions', |
2209 | h('input', {type: 'submit', name: 'action', value: 'publish'}) |
2210 | ) |
2211 | ] |
2212 | } |
2213 | |
2214 | } |
2215 | |
2216 | function hashBuf(buf) { |
2217 | var hash = crypto.createHash('sha256') |
2218 | hash.update(buf) |
2219 | return '&' + hash.digest('base64') + '.sha256' |
2220 | } |
2221 | |
2222 | Serve.prototype.getBuiltinEmojiLink = function (name) { |
2223 | if (!(name in emojis)) return |
2224 | var file = path.join(emojiDir, name + '.png') |
2225 | var fileBuf = fs.readFileSync(file) |
2226 | var id = hashBuf(fileBuf) |
2227 | // seed the builtin emoji |
2228 | pull(pull.once(fileBuf), this.app.sbot.blobs.add(id, function (err) { |
2229 | if (err) console.error('error adding builtin emoji as blob', err) |
2230 | })) |
2231 | return { |
2232 | link: id, |
2233 | type: 'image/png', |
2234 | size: fileBuf.length, |
2235 | } |
2236 | } |
2237 | |
2238 | Serve.prototype.emojis = function (path) { |
2239 | var self = this |
2240 | pull( |
2241 | ph('section', [ |
2242 | ph('h3', 'Emojis'), |
2243 | ph('ul', {class: 'mentions'}, pull( |
2244 | self.app.streamEmojis(), |
2245 | pull.map(function (emoji) { |
2246 | return ph('li', [ |
2247 | ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)}, |
2248 | ph('img', { |
2249 | class: 'ssb-emoji', |
2250 | src: self.app.render.imageUrl(emoji.link), |
2251 | size: 32, |
2252 | }) |
2253 | ), ' ', |
2254 | u.escapeHTML(emoji.name) |
2255 | ]) |
2256 | }) |
2257 | )) |
2258 | ]), |
2259 | this.wrapPage('emojis'), |
2260 | this.respondSink(200) |
2261 | ) |
2262 | } |
2263 |
Built with git-ssb-web