git ssb

16+

cel / patchfoo



Tree: 487a99ede50bdbbd5308b1b446b9fc354d0a8c27

Files: 487a99ede50bdbbd5308b1b446b9fc354d0a8c27 / lib / serve.js

29019 bytesRaw
1var fs = require('fs')
2var qs = require('querystring')
3var pull = require('pull-stream')
4var path = require('path')
5var paramap = require('pull-paramap')
6var sort = require('ssb-sort')
7var crypto = require('crypto')
8var toPull = require('stream-to-pull-stream')
9var serveEmoji = require('emoji-server')()
10var u = require('./util')
11var cat = require('pull-cat')
12var h = require('hyperscript')
13var paginate = require('pull-paginate')
14var ssbMentions = require('ssb-mentions')
15var multicb = require('multicb')
16var pkg = require('../package')
17var Busboy = require('busboy')
18var mime = require('mime-types')
19var ident = require('pull-identify-filetype')
20var htime = require('human-time')
21
22module.exports = Serve
23
24var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
25
26var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
27
28function isMsgReadable(msg) {
29 var c = msg && msg.value.content
30 return typeof c === 'object' && c !== null
31}
32
33function isMsgEncrypted(msg) {
34 var c = msg && msg.value.content
35 return typeof c === 'string'
36}
37
38function ctype(name) {
39 switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
40 case 'html': return 'text/html'
41 case 'txt': return 'text/plain'
42 case 'js': return 'text/javascript'
43 case 'css': return 'text/css'
44 case 'png': return 'image/png'
45 case 'json': return 'application/json'
46 }
47}
48
49function encodeDispositionFilename(fname) {
50 return '"' + fname.replace(/\/g/, '\\\\').replace(/"/, '\\\"') + '"'
51}
52
53function Serve(app, req, res) {
54 this.app = app
55 this.req = req
56 this.res = res
57 this.startDate = new Date()
58}
59
60Serve.prototype.go = function () {
61 console.log(this.req.method, this.req.url)
62 var self = this
63
64 if (this.req.method === 'POST' || this.req.method === 'PUT') {
65 if (/^multipart\/form-data/.test(this.req.headers['content-type'])) {
66 var data = {}
67 var erred
68 var busboy = new Busboy({headers: this.req.headers})
69 var filesCb = multicb({pluck: 1})
70 busboy.on('finish', filesCb())
71 filesCb(function (err) {
72 gotData(err, data)
73 })
74 busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
75 var done = multicb({pluck: 1, spread: true})
76 var cb = filesCb()
77 pull(
78 toPull(file),
79 u.pullLength(done()),
80 self.app.addBlob(done())
81 )
82 done(function (err, size, id) {
83 if (err) return cb(err)
84 if (size === 0 && !filename) return cb()
85 data[fieldname] = {link: id, name: filename, type: mimetype, size: size}
86 cb()
87 })
88 })
89 busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
90 if (!(fieldname in data)) data[fieldname] = val
91 else if (Array.isArray(data[fieldname])) data[fieldname].push(val)
92 else data[fieldname] = [data[fieldname], val]
93 })
94 this.req.pipe(busboy)
95 } else {
96 pull(
97 toPull(this.req),
98 pull.collect(function (err, bufs) {
99 var data
100 if (!err) try {
101 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
102 } catch(e) {
103 err = e
104 }
105 gotData(err, data)
106 })
107 )
108 }
109 } else {
110 gotData(null, {})
111 }
112
113 function gotData(err, data) {
114 self.data = data
115 if (err) next(err)
116 else if (data.action === 'publish') self.publishJSON(next)
117 else if (data.action === 'vote') self.publishVote(next)
118 else next()
119 }
120
121 function next(err) {
122 if (err) {
123 self.res.writeHead(400, {'Content-Type': 'text/plain'})
124 self.res.end(err.stack)
125 } else {
126 self.handle()
127 }
128 }
129}
130
131Serve.prototype.publishJSON = function (cb) {
132 var content
133 try {
134 content = JSON.parse(this.data.content)
135 } catch(e) {
136 return cb(e)
137 }
138 this.publish(content, cb)
139}
140
141Serve.prototype.publishVote = function (cb) {
142 var content = {
143 type: 'vote',
144 vote: {
145 link: this.data.link,
146 value: Number(this.data.value),
147 expression: this.data.expression,
148 }
149 }
150 if (this.data.recps) content.recps = this.data.recps.split(',')
151 this.publish(content, cb)
152}
153
154Serve.prototype.publish = function (content, cb) {
155 var self = this
156 var done = multicb({pluck: 1, spread: true})
157 u.toArray(content && content.mentions).forEach(function (mention) {
158 if (mention.link && mention.link[0] === '&' && !isNaN(mention.size))
159 self.app.pushBlob(mention.link, done())
160 })
161 done(function (err) {
162 if (err) return cb(err)
163 self.app.publish(content, function (err, msg) {
164 if (err) return cb(err)
165 delete self.data.text
166 delete self.data.recps
167 self.publishedMsg = msg
168 return cb()
169 })
170 })
171}
172
173Serve.prototype.handle = function () {
174 var m = urlIdRegex.exec(this.req.url)
175 this.query = m[5] ? qs.parse(m[5]) : {}
176 switch (m[2]) {
177 case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
178 case '%': return this.id(m[1], m[3])
179 case '@': return this.userFeed(m[1], m[3])
180 case '&': return this.blob(m[1])
181 default: return this.path(m[4])
182 }
183}
184
185Serve.prototype.respond = function (status, message) {
186 this.res.writeHead(status)
187 this.res.end(message)
188}
189
190Serve.prototype.respondSink = function (status, headers, cb) {
191 var self = this
192 if (status && headers) self.res.writeHead(status, headers)
193 return toPull(self.res, cb || function (err) {
194 if (err) self.app.error(err)
195 })
196}
197
198Serve.prototype.path = function (url) {
199 var m
200 url = url.replace(/^\/+/, '/')
201 switch (url) {
202 case '/': return this.home()
203 case '/robots.txt': return this.res.end('User-agent: *')
204 }
205 if (m = /^\/%23(.*)/.exec(url)) {
206 return this.channel(decodeURIComponent(m[1]))
207 }
208 m = /^([^.]*)(?:\.(.*))?$/.exec(url)
209 switch (m[1]) {
210 case '/public': return this.public(m[2])
211 case '/private': return this.private(m[2])
212 case '/search': return this.search(m[2])
213 case '/vote': return this.vote(m[2])
214 case '/peers': return this.peers(m[2])
215 }
216 m = /^(\/?[^\/]*)(\/.*)?$/.exec(url)
217 switch (m[1]) {
218 case '/type': return this.type(m[2])
219 case '/links': return this.links(m[2])
220 case '/static': return this.static(m[2])
221 case '/emoji': return this.emoji(m[2])
222 }
223 return this.respond(404, 'Not found')
224}
225
226Serve.prototype.home = function () {
227 pull(
228 pull.empty(),
229 this.wrapPage('/'),
230 this.respondSink(200, {
231 'Content-Type': 'text/html'
232 })
233 )
234}
235
236Serve.prototype.public = function (ext) {
237 var q = this.query
238 var opts = {
239 reverse: !q.forwards,
240 sortByTimestamp: q.sort === 'claimed',
241 lt: Number(q.lt) || Date.now(),
242 gt: Number(q.gt) || -Infinity,
243 limit: Number(q.limit) || 12
244 }
245
246 pull(
247 this.app.createLogStream(opts),
248 this.renderThreadPaginated(opts, null, q),
249 this.wrapMessages(),
250 this.wrapPublic(),
251 this.wrapPage('public'),
252 this.respondSink(200, {
253 'Content-Type': ctype(ext)
254 })
255 )
256}
257
258Serve.prototype.private = function (ext) {
259 var q = this.query
260 var opts = {
261 reverse: !q.forwards,
262 sortByTimestamp: q.sort === 'claimed',
263 lt: Number(q.lt) || Date.now(),
264 gt: Number(q.gt) || -Infinity,
265 }
266 var limit = Number(q.limit) || 12
267
268 pull(
269 this.app.createLogStream(opts),
270 pull.filter(isMsgEncrypted),
271 paramap(this.app.unboxMsg, 16),
272 pull.filter(isMsgReadable),
273 pull.take(limit),
274 this.renderThreadPaginated(opts, null, q),
275 this.wrapMessages(),
276 this.wrapPrivate(opts),
277 this.wrapPage('private'),
278 this.respondSink(200, {
279 'Content-Type': ctype(ext)
280 })
281 )
282}
283
284Serve.prototype.search = function (ext) {
285 var searchQ = (this.query.q || '').trim()
286 var self = this
287
288 if (/^ssb:\/\//.test(searchQ)) {
289 var maybeId = searchQ.substr(6)
290 if (u.isRef(maybeId)) searchQ = maybeId
291 }
292
293 if (u.isRef(searchQ) || searchQ[0] === '#') {
294 self.res.writeHead(302, {
295 Location: self.app.render.toUrl(searchQ)
296 })
297 return self.res.end()
298 }
299
300 pull(
301 self.app.search(searchQ),
302 self.renderThread(),
303 self.wrapMessages(),
304 self.wrapPage('search · ' + searchQ, searchQ),
305 self.respondSink(200, {
306 'Content-Type': ctype(ext),
307 })
308 )
309}
310
311Serve.prototype.peers = function (ext) {
312 var self = this
313 if (self.data.action === 'connect') {
314 return self.app.sbot.gossip.connect(self.data.address, function (err) {
315 if (err) return pull(
316 pull.once(u.renderError(err, ext).outerHTML),
317 self.wrapPage('peers'),
318 self.respondSink(400, {'Content-Type': ctype(ext)})
319 )
320 self.data = {}
321 return self.peers(ext)
322 })
323 }
324
325 pull(
326 self.app.streamPeers(),
327 paramap(function (peer, cb) {
328 var done = multicb({pluck: 1, spread: true})
329 var connectedTime = Date.now() - peer.stateChange
330 var addr = peer.host + ':' + peer.port + ':' + peer.key
331 done()(null, h('section',
332 h('form', {method: 'post', action: ''},
333 peer.client ? '→' : '←', ' ',
334 h('code', peer.host, ':', peer.port, ':'),
335 self.app.render.idLink(peer.key, done()), ' ',
336 htime(new Date(peer.stateChange)), ' ',
337 peer.state === 'connected' ? 'connected' : [
338 h('input', {name: 'action', type: 'submit', value: 'connect'}),
339 h('input', {name: 'address', type: 'hidden', value: addr})
340 ]
341 )
342 // h('div', 'source: ', peer.source)
343 // JSON.stringify(peer, 0, 2)).outerHTML
344 ))
345 done(cb)
346 }, 8),
347 pull.map(u.toHTML),
348 self.wrapPeers(),
349 self.wrapPage('peers'),
350 self.respondSink(200, {
351 'Content-Type': ctype(ext)
352 })
353 )
354}
355
356
357Serve.prototype.type = function (path) {
358 var q = this.query
359 var type = path.substr(1)
360 var opts = {
361 reverse: !q.forwards,
362 lt: Number(q.lt) || Date.now(),
363 gt: Number(q.gt) || -Infinity,
364 limit: Number(q.limit) || 12,
365 type: type,
366 }
367
368 pull(
369 this.app.sbot.messagesByType(opts),
370 this.renderThreadPaginated(opts, null, q),
371 this.wrapMessages(),
372 this.wrapType(type),
373 this.wrapPage('type: ' + type),
374 this.respondSink(200, {
375 'Content-Type': ctype('html')
376 })
377 )
378}
379
380Serve.prototype.links = function (path) {
381 var q = this.query
382 var dest = path.substr(1)
383 var opts = {
384 dest: dest,
385 reverse: true,
386 values: true,
387 }
388
389 pull(
390 this.app.sbot.links(opts),
391 this.renderThread(opts, null, q),
392 this.wrapMessages(),
393 this.wrapLinks(dest),
394 this.wrapPage('links: ' + dest),
395 this.respondSink(200, {
396 'Content-Type': ctype('html')
397 })
398 )
399}
400
401Serve.prototype.rawId = function (id) {
402 var self = this
403
404 self.app.getMsgDecrypted(id, function (err, msg) {
405 if (err) return pull(
406 pull.once(u.renderError(err).outerHTML),
407 self.respondSink(400, {'Content-Type': ctype('html')})
408 )
409 return pull(
410 pull.once(msg),
411 self.renderRawMsgPage(id),
412 self.respondSink(200, {
413 'Content-Type': ctype('html'),
414 })
415 )
416 })
417}
418
419Serve.prototype.channel = function (channel) {
420 var q = this.query
421 var gt = Number(q.gt) || -Infinity
422 var lt = Number(q.lt) || Date.now()
423 var opts = {
424 reverse: !q.forwards,
425 lt: lt,
426 gt: gt,
427 limit: Number(q.limit) || 12,
428 query: [{$filter: {
429 value: {content: {channel: channel}},
430 timestamp: {
431 $gt: gt,
432 $lt: lt,
433 }
434 }}]
435 }
436
437 if (!this.app.sbot.query) return pull(
438 pull.once(u.renderError(new Error('Missing ssb-query plugin')).outerHTML),
439 this.wrapPage('#' + channel),
440 this.respondSink(400, {'Content-Type': ctype('html')})
441 )
442
443 pull(
444 this.app.sbot.query.read(opts),
445 this.renderThreadPaginated(opts, null, q),
446 this.wrapMessages(),
447 this.wrapChannel(channel),
448 this.wrapPage('#' + channel),
449 this.respondSink(200, {
450 'Content-Type': ctype('html')
451 })
452 )
453}
454
455function threadHeads(msgs, rootId) {
456 return sort.heads(msgs.filter(function (msg) {
457 var c = msg.value && msg.value.content
458 return (c && c.root === rootId)
459 || msg.key === rootId
460 }))
461}
462
463
464Serve.prototype.id = function (id, ext) {
465 var self = this
466 if (self.query.raw != null) return self.rawId(id)
467
468 this.app.getMsgDecrypted(id, function (err, rootMsg) {
469 if (err && err.name === 'NotFoundError') err = null, rootMsg = {key: id}
470 if (err) return self.respond(500, err.stack || err)
471 var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
472 var recps = rootContent && rootContent.recps
473 var threadRootId = rootContent && rootContent.root || id
474 var channel = rootContent && rootContent.channel
475
476 pull(
477 cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]),
478 pull.unique('key'),
479 paramap(self.app.unboxMsg, 16),
480 pull.collect(function (err, links) {
481 if (err) return self.respond(500, err.stack || err)
482 pull(
483 pull.values(sort(links)),
484 self.renderThread(),
485 self.wrapMessages(),
486 self.wrapThread({
487 recps: recps,
488 root: threadRootId,
489 branches: id === threadRootId ? threadHeads(links, id) : id,
490 channel: channel,
491 }),
492 self.wrapPage(id),
493 self.respondSink(200, {
494 'Content-Type': ctype(ext),
495 })
496 )
497 })
498 )
499 })
500}
501
502Serve.prototype.userFeed = function (id, ext) {
503 var self = this
504 var q = self.query
505 var opts = {
506 id: id,
507 reverse: !q.forwards,
508 lt: Number(q.lt) || Date.now(),
509 gt: Number(q.gt) || -Infinity,
510 limit: Number(q.limit) || 20
511 }
512
513 self.app.getAbout(id, function (err, about) {
514 if (err) self.app.error(err)
515 pull(
516 self.app.sbot.createUserStream(opts),
517 self.renderThreadPaginated(opts, id, q),
518 self.wrapMessages(),
519 self.wrapUserFeed(id),
520 self.wrapPage(about.name),
521 self.respondSink(200, {
522 'Content-Type': ctype(ext)
523 })
524 )
525 })
526}
527
528Serve.prototype.file = function (file) {
529 var self = this
530 fs.stat(file, function (err, stat) {
531 if (err && err.code === 'ENOENT') return self.respond(404, 'Not found')
532 if (err) return self.respond(500, err.stack || err)
533 if (!stat.isFile()) return self.respond(403, 'May only load files')
534 if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified')
535 self.res.writeHead(200, {
536 'Content-Type': ctype(file),
537 'Content-Length': stat.size,
538 'Last-Modified': stat.mtime.toGMTString()
539 })
540 fs.createReadStream(file).pipe(self.res)
541 })
542}
543
544Serve.prototype.static = function (file) {
545 this.file(path.join(__dirname, '../static', file))
546}
547
548Serve.prototype.emoji = function (emoji) {
549 serveEmoji(this.req, this.res, emoji)
550}
551
552Serve.prototype.blob = function (id) {
553 var self = this
554 var blobs = self.app.sbot.blobs
555 if (self.req.headers['if-none-match'] === id) return self.respond(304)
556 blobs.want(id, function (err, has) {
557 if (err) {
558 if (/^invalid/.test(err.message)) return self.respond(400, err.message)
559 else return self.respond(500, err.message || err)
560 }
561 if (!has) return self.respond(404, 'Not found')
562 pull(
563 blobs.get(id),
564 pull.map(Buffer),
565 ident(function (type) {
566 type = type && mime.lookup(type)
567 if (type) self.res.setHeader('Content-Type', type)
568 if (self.query.name) self.res.setHeader('Content-Disposition',
569 'inline; filename='+encodeDispositionFilename(self.query.name))
570 self.res.setHeader('Cache-Control', 'public, max-age=315360000')
571 self.res.setHeader('etag', id)
572 self.res.writeHead(200)
573 }),
574 self.respondSink()
575 )
576 })
577}
578
579Serve.prototype.ifModified = function (lastMod) {
580 var ifModSince = this.req.headers['if-modified-since']
581 if (!ifModSince) return false
582 var d = new Date(ifModSince)
583 return d && Math.floor(d/1000) >= Math.floor(lastMod/1000)
584}
585
586Serve.prototype.wrapMessages = function () {
587 return u.hyperwrap(function (content, cb) {
588 cb(null, h('table.ssb-msgs', content))
589 })
590}
591
592Serve.prototype.renderThread = function () {
593 return pull(
594 this.app.render.renderFeeds(false),
595 pull.map(u.toHTML)
596 )
597}
598
599function mergeOpts(a, b) {
600 var obj = {}, k
601 for (k in a) {
602 obj[k] = a[k]
603 }
604 for (k in b) {
605 if (b[k] != null) obj[k] = b[k]
606 else delete obj[k]
607 }
608 return obj
609}
610
611Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
612 var self = this
613 function linkA(opts, name) {
614 var q1 = mergeOpts(q, opts)
615 return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit)
616 }
617 function links(opts) {
618 var limit = opts.limit || q.limit || 10
619 return h('tr', h('td.paginate', {colspan: 3},
620 opts.forwards ? '↑ newer ' : '↓ older ',
621 linkA(mergeOpts(opts, {limit: 1})), ' ',
622 linkA(mergeOpts(opts, {limit: 10})), ' ',
623 linkA(mergeOpts(opts, {limit: 100}))
624 ))
625 }
626
627 return pull(
628 paginate(
629 function onFirst(msg, cb) {
630 var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts
631 if (q.forwards) {
632 cb(null, links({
633 lt: num,
634 gt: null,
635 forwards: null,
636 }))
637 } else {
638 cb(null, links({
639 lt: null,
640 gt: num,
641 forwards: 1,
642 }))
643 }
644 },
645 this.app.render.renderFeeds(),
646 function onLast(msg, cb) {
647 var num = feedId ? msg.value.sequence : msg.timestamp || msg.ts
648 if (q.forwards) {
649 cb(null, links({
650 lt: null,
651 gt: num,
652 forwards: 1,
653 }))
654 } else {
655 cb(null, links({
656 lt: num,
657 gt: null,
658 forwards: null,
659 }))
660 }
661 },
662 function onEmpty(cb) {
663 if (q.forwards) {
664 cb(null, links({
665 gt: null,
666 lt: opts.gt + 1,
667 forwards: null,
668 }))
669 } else {
670 cb(null, links({
671 gt: opts.lt - 1,
672 lt: null,
673 forwards: 1,
674 }))
675 }
676 }
677 ),
678 pull.map(u.toHTML)
679 )
680}
681
682Serve.prototype.renderRawMsgPage = function (id) {
683 return pull(
684 this.app.render.renderFeeds(true),
685 pull.map(u.toHTML),
686 this.wrapMessages(),
687 this.wrapPage(id)
688 )
689}
690
691function catchHTMLError() {
692 return function (read) {
693 var ended
694 return function (abort, cb) {
695 if (ended) return cb(ended)
696 read(abort, function (end, data) {
697 if (!end || end === true) return cb(end, data)
698 ended = true
699 cb(null, u.renderError(end).outerHTML)
700 })
701 }
702 }
703}
704
705function styles() {
706 return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8')
707}
708
709Serve.prototype.appendFooter = function () {
710 var self = this
711 return function (read) {
712 return cat([read, u.readNext(function (cb) {
713 var ms = new Date() - self.startDate
714 cb(null, pull.once(h('footer',
715 h('a', {href: pkg.homepage}, pkg.name), ' · ',
716 ms/1000 + 's'
717 ).outerHTML))
718 })])
719 }
720}
721
722Serve.prototype.wrapPage = function (title, searchQ) {
723 var self = this
724 var render = self.app.render
725 return pull(
726 catchHTMLError(),
727 self.appendFooter(),
728 u.hyperwrap(function (content, cb) {
729 var done = multicb({pluck: 1, spread: true})
730 done()(null, h('html', h('head',
731 h('meta', {charset: 'utf-8'}),
732 h('title', title),
733 h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}),
734 h('style', styles())
735 ),
736 h('body',
737 h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'},
738 h('a', {href: render.toUrl('/public')}, 'public'), ' ',
739 h('a', {href: render.toUrl('/private')}, 'private') , ' ',
740 h('a', {href: render.toUrl('/peers')}, 'peers') , ' ',
741 render.idLink(self.app.sbot.id, done()), ' ',
742 h('input.search-input', {name: 'q', value: searchQ,
743 placeholder: 'search'})
744 // h('a', {href: '/convos'}, 'convos'), ' ',
745 // h('a', {href: '/friends'}, 'friends'), ' ',
746 // h('a', {href: '/git'}, 'git')
747 )),
748 self.publishedMsg ? h('div',
749 'published ',
750 self.app.render.msgLink(self.publishedMsg, done())
751 ) : '',
752 content
753 )))
754 done(cb)
755 })
756 )
757}
758
759Serve.prototype.wrapUserFeed = function (id) {
760 var self = this
761 return u.hyperwrap(function (thread, cb) {
762 self.app.getAbout(id, function (err, about) {
763 if (err) return cb(err)
764 var done = multicb({pluck: 1, spread: true})
765 done()(null, [
766 h('section.ssb-feed',
767 h('table', h('tr',
768 h('td', self.app.render.avatarImage(id, done())),
769 h('td.feed-about',
770 h('h3.feed-name',
771 h('strong', self.app.render.idLink(id, done()))),
772 h('code', h('small', id)),
773 about.description ? h('div',
774 {innerHTML: self.app.render.markdown(about.description)}) : ''
775 )))
776 ),
777 thread
778 ])
779 done(cb)
780 })
781 })
782}
783
784Serve.prototype.wrapPublic = function (opts) {
785 var self = this
786 return u.hyperwrap(function (thread, cb) {
787 self.composer({
788 channel: '',
789 }, function (err, composer) {
790 if (err) return cb(err)
791 cb(null, [
792 composer,
793 thread
794 ])
795 })
796 })
797}
798
799Serve.prototype.wrapPrivate = function (opts) {
800 var self = this
801 return u.hyperwrap(function (thread, cb) {
802 self.composer({
803 placeholder: 'private message',
804 private: true,
805 }, function (err, composer) {
806 if (err) return cb(err)
807 cb(null, [
808 composer,
809 thread
810 ])
811 })
812 })
813}
814
815Serve.prototype.wrapThread = function (opts) {
816 var self = this
817 return u.hyperwrap(function (thread, cb) {
818 self.app.render.prepareLinks(opts.recps, function (err, recps) {
819 if (err) return cb(er)
820 self.composer({
821 placeholder: recps ? 'private reply' : 'reply',
822 id: 'reply',
823 root: opts.root,
824 channel: opts.channel || '',
825 branches: opts.branches,
826 recps: recps,
827 }, function (err, composer) {
828 if (err) return cb(err)
829 cb(null, [
830 thread,
831 composer
832 ])
833 })
834 })
835 })
836}
837
838Serve.prototype.wrapChannel = function (channel) {
839 var self = this
840 return u.hyperwrap(function (thread, cb) {
841 self.composer({
842 placeholder: 'public message in #' + channel,
843 channel: channel,
844 }, function (err, composer) {
845 if (err) return cb(err)
846 cb(null, [
847 h('section',
848 h('h3.feed-name',
849 h('a', {href: self.app.render.toUrl('#' + channel)}, '#' + channel)
850 )
851 ),
852 composer,
853 thread
854 ])
855 })
856 })
857}
858
859Serve.prototype.wrapType = function (type) {
860 var self = this
861 return u.hyperwrap(function (thread, cb) {
862 cb(null, [
863 h('section',
864 h('h3.feed-name',
865 h('a', {href: self.app.render.toUrl('/type/' + type)},
866 h('code', type), 's'))
867 ),
868 thread
869 ])
870 })
871}
872
873Serve.prototype.wrapLinks = function (dest) {
874 var self = this
875 return u.hyperwrap(function (thread, cb) {
876 cb(null, [
877 h('section',
878 h('h3.feed-name', 'links: ',
879 h('a', {href: self.app.render.toUrl('/links/' + dest)},
880 h('code', dest)))
881 ),
882 thread
883 ])
884 })
885}
886
887Serve.prototype.wrapPeers = function (opts) {
888 var self = this
889 return u.hyperwrap(function (peers, cb) {
890 cb(null, [
891 h('section',
892 h('h3', 'Peers')
893 ),
894 peers
895 ])
896 })
897}
898
899function rows(str) {
900 return String(str).split(/[^\n]{150}|\n/).length
901}
902
903Serve.prototype.composer = function (opts, cb) {
904 var self = this
905 opts = opts || {}
906 var data = self.data
907
908 var blobs = u.tryDecodeJSON(data.blobs) || {}
909 if (data.upload && typeof data.upload === 'object') {
910 blobs[data.upload.link] = {
911 type: data.upload.type,
912 size: data.upload.size,
913 }
914 }
915 if (data.blob_type && blobs[data.blob_link]) {
916 blobs[data.blob_link].type = data.blob_type
917 }
918 var channel = data.channel != null ? data.channel : opts.channel
919
920 var formNames = {}
921 var mentionIds = u.toArray(data.mention_id)
922 var mentionNames = u.toArray(data.mention_name)
923 for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) {
924 formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0]
925 }
926
927 var done = multicb({pluck: 1, spread: true})
928 done()(null, h('section.composer',
929 h('form', {method: 'post', action: opts.id ? '#' + opts.id : '',
930 enctype: 'multipart/form-data'},
931 h('input', {type: 'hidden', name: 'blobs',
932 value: JSON.stringify(blobs)}),
933 opts.recps ? self.app.render.privateLine(opts.recps, done()) :
934 opts.private ? h('div', h('input.recps-input', {name: 'recps',
935 value: data.recps || '', placeholder: 'recipient ids'})) : '',
936 channel != null ?
937 h('div', '#', h('input', {name: 'channel', placeholder: 'channel',
938 value: channel})) : '',
939 h('textarea', {
940 id: opts.id,
941 name: 'text',
942 rows: Math.max(4, rows(data.text)),
943 cols: 70,
944 placeholder: opts.placeholder || 'public message',
945 }, data.text || ''),
946 h('table.ssb-msgs',
947 h('tr.msg-row',
948 h('td.msg-left', {colspan: 2},
949 h('input', {type: 'file', name: 'upload'}), ' ',
950 h('input', {type: 'submit', name: 'action', value: 'attach'})
951 ),
952 h('td.msg-right',
953 h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ',
954 h('input', {type: 'submit', name: 'action', value: 'preview'})
955 )
956 )
957 ),
958 data.upload ? [
959 h('div', h('em', 'attach:')),
960 h('code', '[' + data.upload.name + '](' + data.upload.link + ')'), ' ',
961 h('input', {name: 'blob_link', value: data.upload.link, type: 'hidden'}),
962 h('input', {name: 'blob_type', value: data.upload.type})
963 ] : '',
964 data.action === 'preview' ? preview(false, done()) :
965 data.action === 'raw' ? preview(true, done()) : ''
966 )
967 ))
968 done(cb)
969
970 function preview(raw, cb) {
971 var myId = self.app.sbot.id
972 var content
973 var unknownMentions = []
974 try {
975 content = JSON.parse(data.text)
976 } catch (err) {
977 data.text = String(data.text).replace(/\r\n/g, '\n')
978 content = {
979 type: 'post',
980 text: data.text,
981 }
982 var mentions = ssbMentions(data.text, {bareFeedNames: true})
983 if (mentions.length) {
984 content.mentions = mentions.filter(function (mention) {
985 var blob = blobs[mention.link]
986 if (blob) {
987 if (!isNaN(blob.size))
988 mention.size = blob.size
989 if (blob.type && blob.type !== 'application/octet-stream')
990 mention.type = blob.type
991 } else if (mention.link === '@') {
992 // bare feed name
993 var name = mention.name
994 var fullName = mention.link + name
995 var id = formNames[name] || self.app.getReverseNameSync(fullName)
996 unknownMentions.push({name: name, fullName: fullName, id: id})
997 if (id) mention.link = id
998 else return false
999 }
1000 return true
1001 })
1002 }
1003 if (data.recps != null) {
1004 if (opts.recps) return cb(new Error('got recps in opts and data'))
1005 content.recps = [myId]
1006 u.extractFeedIds(data.recps).forEach(function (recp) {
1007 if (content.recps.indexOf(recp) === -1) content.recps.push(recp)
1008 })
1009 } else {
1010 if (opts.recps) content.recps = opts.recps
1011 }
1012 if (opts.root) content.root = opts.root
1013 if (opts.branches) content.branch = u.fromArray(opts.branches)
1014 if (channel) content.channel = data.channel
1015 }
1016 var msg = {
1017 value: {
1018 author: myId,
1019 timestamp: Date.now(),
1020 content: content
1021 }
1022 }
1023 if (content.recps) msg.value.private = true
1024 var msgContainer = h('table.ssb-msgs')
1025 pull(
1026 pull.once(msg),
1027 pull.asyncMap(self.app.unboxMsg),
1028 self.app.render.renderFeeds(raw),
1029 pull.drain(function (el) {
1030 msgContainer.appendChild(h('tbody', el))
1031 }, cb)
1032 )
1033 return [
1034 h('input', {type: 'hidden', name: 'content',
1035 value: JSON.stringify(content)}),
1036 h('div', h('em', 'draft:')),
1037 msgContainer,
1038 unknownMentions.length > 0 ? [
1039 h('div', h('em', 'names:')),
1040 h('ul', unknownMentions.map(function (mention) {
1041 return h('li',
1042 h('code', mention.fullName), ': ',
1043 h('input', {name: 'mention_name', type: 'hidden',
1044 value: mention.name}),
1045 h('input.mention-id-input', {name: 'mention_id',
1046 value: mention.id, placeholder: 'id'}))
1047 }))
1048 ] : '',
1049 h('div.composer-actions',
1050 h('input', {type: 'submit', name: 'action', value: 'publish'})
1051 )
1052 ]
1053 }
1054
1055}
1056

Built with git-ssb-web