Files: f0952377664cc671d536cff9d13918e967964e04 / lib / serve.js
102920 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 | var jpeg = require('jpeg-autorotate') |
24 | var Catch = require('pull-catch') |
25 | var Diff = require('diff') |
26 | var split = require('pull-split') |
27 | var utf8 = require('pull-utf8-decoder') |
28 | var webresolve = require('ssb-web-resolver') |
29 | |
30 | module.exports = Serve |
31 | |
32 | var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs') |
33 | var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles') |
34 | |
35 | var urlIdRegex = /^(?:\/+(([%&@]|%25|%26)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/ |
36 | |
37 | function ctype(name) { |
38 | switch (name && /[^.\/]*$/.exec(name)[0] || 'html') { |
39 | case 'html': return 'text/html' |
40 | case 'txt': return 'text/plain' |
41 | case 'js': return 'text/javascript' |
42 | case 'css': return 'text/css' |
43 | case 'png': return 'image/png' |
44 | case 'json': return 'application/json' |
45 | case 'ico': return 'image/x-icon' |
46 | } |
47 | } |
48 | |
49 | function encodeDispositionFilename(fname) { |
50 | fname = fname.replace(/\/g/, '\\\\').replace(/"/, '\\\"') |
51 | return '"' + encodeURIComponent(fname) + '"' |
52 | } |
53 | |
54 | function uniques() { |
55 | var set = {} |
56 | return function (item) { |
57 | if (set[item]) return false |
58 | return set[item] = true |
59 | } |
60 | } |
61 | |
62 | function Serve(app, req, res) { |
63 | this.app = app |
64 | this.req = req |
65 | this.res = res |
66 | this.startDate = new Date() |
67 | } |
68 | |
69 | Serve.prototype.go = function () { |
70 | console.log(this.req.method, this.req.url) |
71 | var self = this |
72 | |
73 | this.res.setTimeout(0) |
74 | var conf = self.app.config.patchfoo || {} |
75 | var authtok = conf.auth || null |
76 | if (authtok) { |
77 | var auth = this.req.headers['authorization'] |
78 | var tok = null |
79 | //console.log('Authorization: ',auth) |
80 | |
81 | if (auth) { |
82 | var a = auth.split(' ') |
83 | if (a[0] == 'Basic') { |
84 | tok = Buffer.from(a[1],'base64').toString('ascii') |
85 | } |
86 | } |
87 | if (tok != authtok) { |
88 | self.res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Patchfoo"'}) |
89 | self.res.end('Not authorized') |
90 | return |
91 | } |
92 | } |
93 | |
94 | if (this.req.method === 'POST' || this.req.method === 'PUT') { |
95 | if (/^multipart\/form-data/.test(this.req.headers['content-type'])) { |
96 | var data = {} |
97 | var erred |
98 | var busboy = new Busboy({headers: this.req.headers}) |
99 | var filesCb = multicb({pluck: 1}) |
100 | busboy.on('finish', filesCb()) |
101 | filesCb(function (err) { |
102 | gotData(err, data) |
103 | }) |
104 | function addField(name, value) { |
105 | if (!(name in data)) data[name] = value |
106 | else if (Array.isArray(data[name])) data[name].push(value) |
107 | else data[name] = [data[name], value] |
108 | } |
109 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { |
110 | var cb = filesCb() |
111 | var size = 0 |
112 | pull( |
113 | toPull(file), |
114 | pull.map(function (data) { |
115 | size += data.length |
116 | return data |
117 | }), |
118 | self.app.addBlob(!!data.private, function (err, link) { |
119 | if (err) return cb(err) |
120 | if (size === 0 && !filename) return cb() |
121 | link.name = filename |
122 | link.type = mimetype |
123 | addField(fieldname, link) |
124 | cb() |
125 | }) |
126 | ) |
127 | }) |
128 | busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { |
129 | addField(fieldname, val) |
130 | }) |
131 | this.req.pipe(busboy) |
132 | } else { |
133 | pull( |
134 | toPull(this.req), |
135 | pull.collect(function (err, bufs) { |
136 | var data |
137 | if (!err) try { |
138 | data = qs.parse(Buffer.concat(bufs).toString('ascii')) |
139 | } catch(e) { |
140 | err = e |
141 | } |
142 | gotData(err, data) |
143 | }) |
144 | ) |
145 | } |
146 | } else { |
147 | gotData(null, {}) |
148 | } |
149 | |
150 | function gotData(err, data) { |
151 | self.data = data |
152 | if (err) next(err) |
153 | else if (data.action === 'publish') self.publishJSON(next) |
154 | else if (data.action === 'contact') self.publishContact(next) |
155 | else if (data.action === 'want-blobs') self.wantBlobs(next) |
156 | else if (data.action === 'poll-position') self.publishPollPosition(next) |
157 | else if (data.action_vote) self.publishVote(next) |
158 | else if (data.action_attend) self.publishAttend(next) |
159 | else next() |
160 | } |
161 | |
162 | function next(err, publishedMsg) { |
163 | if (err) { |
164 | self.res.writeHead(400, {'Content-Type': 'text/plain'}) |
165 | self.res.end(err.stack) |
166 | } else if (publishedMsg) { |
167 | if (self.data.redirect_to_published_msg) { |
168 | self.redirect(self.app.render.toUrl(publishedMsg.key)) |
169 | } else { |
170 | self.publishedMsg = publishedMsg |
171 | self.handle() |
172 | } |
173 | } else { |
174 | self.handle() |
175 | } |
176 | } |
177 | } |
178 | |
179 | Serve.prototype.publishJSON = function (cb) { |
180 | var content |
181 | try { |
182 | content = JSON.parse(this.data.content) |
183 | } catch(e) { |
184 | return cb(e) |
185 | } |
186 | this.publish(content, cb) |
187 | } |
188 | |
189 | Serve.prototype.publishVote = function (next) { |
190 | var content = { |
191 | type: 'vote', |
192 | channel: this.data.channel || undefined, |
193 | vote: { |
194 | link: this.data.link, |
195 | value: Number(this.data.vote_value), |
196 | expression: this.data.vote_expression || undefined, |
197 | } |
198 | } |
199 | if (this.data.recps) content.recps = this.data.recps.split(',') |
200 | if (this.app.previewVotes) { |
201 | var json = JSON.stringify(content, 0, 2) |
202 | var q = qs.stringify({text: json, action: 'preview'}) |
203 | var url = this.app.render.toUrl('/compose?' + q) |
204 | this.redirect(url) |
205 | } else { |
206 | this.publish(content, next) |
207 | } |
208 | } |
209 | |
210 | Serve.prototype.publishContact = function (next) { |
211 | var content = { |
212 | type: 'contact', |
213 | contact: this.data.contact, |
214 | } |
215 | if (this.data.follow) content.following = true |
216 | if (this.data.block) content.blocking = true |
217 | if (this.data.unfollow) content.following = false |
218 | if (this.data.unblock) content.blocking = false |
219 | if (this.app.previewContacts) { |
220 | var json = JSON.stringify(content, 0, 2) |
221 | var q = qs.stringify({text: json, action: 'preview'}) |
222 | var url = this.app.render.toUrl('/compose?' + q) |
223 | this.redirect(url) |
224 | } else { |
225 | this.publish(content, next) |
226 | } |
227 | } |
228 | |
229 | Serve.prototype.publishPollPosition = function (cb) { |
230 | var content = { |
231 | type: 'position', |
232 | version: 'v1', |
233 | channel: this.data.channel || undefined, |
234 | root: this.data.poll_root, |
235 | details: { |
236 | type: this.data.poll_type, |
237 | choice: this.data.poll_choice |
238 | } |
239 | } |
240 | if (this.data.recps) content.recps = this.data.recps.split(',') |
241 | var json = JSON.stringify(content, 0, 2) |
242 | var q = qs.stringify({text: json, action: 'preview'}) |
243 | var url = this.app.render.toUrl('/compose?' + q) |
244 | this.redirect(url) |
245 | // this.publish(content, cb) |
246 | } |
247 | |
248 | Serve.prototype.publishAttend = function (cb) { |
249 | var content = { |
250 | type: 'about', |
251 | channel: this.data.channel || undefined, |
252 | about: this.data.link, |
253 | attendee: { |
254 | link: this.app.sbot.id |
255 | } |
256 | } |
257 | if (this.data.recps) content.recps = this.data.recps.split(',') |
258 | this.publish(content, cb) |
259 | } |
260 | |
261 | Serve.prototype.wantBlobs = function (cb) { |
262 | var self = this |
263 | if (!self.data.blob_ids) return cb() |
264 | var ids = self.data.blob_ids.split(',') |
265 | if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(','))) |
266 | var done = multicb({pluck: 1}) |
267 | ids.forEach(function (id) { |
268 | self.app.wantSizeBlob(id, done()) |
269 | }) |
270 | if (self.data.async_want) return cb() |
271 | done(function (err) { |
272 | if (err) return cb(err) |
273 | // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.') |
274 | cb() |
275 | }) |
276 | } |
277 | |
278 | Serve.prototype.publish = function (content, cb) { |
279 | var self = this |
280 | var done = multicb({pluck: 1, spread: true}) |
281 | u.toArray(content && content.mentions).forEach(function (mention) { |
282 | if (mention.link && mention.link[0] === '&' && !isNaN(mention.size)) |
283 | self.app.pushBlob(mention.link, done()) |
284 | }) |
285 | done(function (err) { |
286 | if (err) return cb(err) |
287 | self.app.publish(content, function (err, msg) { |
288 | if (err) return cb(err) |
289 | delete self.data.text |
290 | delete self.data.recps |
291 | return cb(null, msg) |
292 | }) |
293 | }) |
294 | } |
295 | |
296 | Serve.prototype.handle = function () { |
297 | var m = urlIdRegex.exec(this.req.url) |
298 | this.query = m[5] ? qs.parse(m[5]) : {} |
299 | this.useOoo = this.query.ooo != null ? |
300 | Boolean(this.query.ooo) : this.app.useOoo |
301 | switch (m[2]) { |
302 | case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1]) |
303 | case '%': return this.id(m[1], m[3]) |
304 | case '@': return this.userFeed(m[1], m[3]) |
305 | case '%26': m[2] = '&'; m[1] = decodeURIComponent(m[1]) |
306 | case '&': return this.blob(m[1], m[3]) |
307 | default: return this.path(m[4]) |
308 | } |
309 | } |
310 | |
311 | Serve.prototype.respond = function (status, message) { |
312 | this.res.writeHead(status) |
313 | this.res.end(message) |
314 | } |
315 | |
316 | Serve.prototype.respondSink = function (status, headers, cb) { |
317 | var self = this |
318 | if (status || headers) |
319 | self.res.writeHead(status, headers || {'Content-Type': 'text/html'}) |
320 | return toPull(self.res, cb || function (err) { |
321 | if (err) self.app.error(err) |
322 | }) |
323 | } |
324 | |
325 | Serve.prototype.redirect = function (dest) { |
326 | this.res.writeHead(302, { |
327 | Location: dest |
328 | }) |
329 | this.res.end() |
330 | } |
331 | |
332 | Serve.prototype.path = function (url) { |
333 | var m |
334 | url = url.replace(/^\/+/, '/') |
335 | switch (url) { |
336 | case '/': return this.home() |
337 | case '/robots.txt': return this.res.end('User-agent: *') |
338 | } |
339 | if (m = /^\/%23(.*)/.exec(url)) { |
340 | return this.redirect(this.app.render.toUrl('/channel/' |
341 | + decodeURIComponent(m[1]))) |
342 | } |
343 | m = /^([^.]*)(?:\.(.*))?$/.exec(url) |
344 | switch (m[1]) { |
345 | case '/new': return this.new(m[2]) |
346 | case '/public': return this.public(m[2]) |
347 | case '/private': return this.private(m[2]) |
348 | case '/mentions': return this.mentions(m[2]) |
349 | case '/search': return this.search(m[2]) |
350 | case '/advsearch': return this.advsearch(m[2]) |
351 | case '/peers': return this.peers(m[2]) |
352 | case '/status': return this.status(m[2]) |
353 | case '/channels': return this.channels(m[2]) |
354 | case '/friends': return this.friends(m[2]) |
355 | case '/live': return this.live(m[2]) |
356 | case '/compose': return this.compose(m[2]) |
357 | case '/emojis': return this.emojis(m[2]) |
358 | case '/votes': return this.votes(m[2]) |
359 | } |
360 | m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) |
361 | switch (m[1]) { |
362 | case '/channel': return this.channel(m[2]) |
363 | case '/type': return this.type(m[2]) |
364 | case '/links': return this.links(m[2]) |
365 | case '/static': return this.static(m[2]) |
366 | case '/emoji': return this.emoji(m[2]) |
367 | case '/highlight': return this.highlight(m[2]) |
368 | case '/contacts': return this.contacts(m[2]) |
369 | case '/about': return this.about(m[2]) |
370 | case '/git': return this.git(m[2]) |
371 | case '/image': return this.image(m[2]) |
372 | case '/npm': return this.npm(m[2]) |
373 | case '/npm-prebuilds': return this.npmPrebuilds(m[2]) |
374 | case '/npm-readme': return this.npmReadme(m[2]) |
375 | case '/npm-registry': return this.npmRegistry(m[2]) |
376 | case '/markdown': return this.markdown(m[2]) |
377 | case '/zip': return this.zip(m[2]) |
378 | case '/web': return this.web(m[2]) |
379 | } |
380 | return this.respond(404, 'Not found') |
381 | } |
382 | |
383 | Serve.prototype.home = function () { |
384 | pull( |
385 | pull.empty(), |
386 | this.wrapPage('/'), |
387 | this.respondSink(200, { |
388 | 'Content-Type': 'text/html' |
389 | }) |
390 | ) |
391 | } |
392 | |
393 | Serve.prototype.public = function (ext) { |
394 | var q = this.query |
395 | var opts = { |
396 | reverse: !q.forwards, |
397 | sortByTimestamp: q.sort === 'claimed', |
398 | lt: Number(q.lt) || Date.now(), |
399 | gt: Number(q.gt) || -Infinity, |
400 | filter: q.filter, |
401 | } |
402 | |
403 | pull( |
404 | this.app.createLogStream(opts), |
405 | this.renderThreadPaginated(opts, null, q), |
406 | this.wrapMessages(), |
407 | this.wrapPublic(), |
408 | this.wrapPage('public'), |
409 | this.respondSink(200, { |
410 | 'Content-Type': ctype(ext) |
411 | }) |
412 | ) |
413 | } |
414 | |
415 | Serve.prototype.setCookie = function (key, value, options) { |
416 | var header = key + '=' + value |
417 | if (options) for (var k in options) { |
418 | header += '; ' + k + '=' + options[k] |
419 | } |
420 | this.res.setHeader('Set-Cookie', header) |
421 | } |
422 | |
423 | Serve.prototype.new = function (ext) { |
424 | var self = this |
425 | var q = self.query |
426 | var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1] |
427 | var opts = { |
428 | gt: Number(q.gt) || Number(latest) || Date.now(), |
429 | } |
430 | |
431 | if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000}) |
432 | |
433 | var read = self.app.createLogStream(opts) |
434 | self.req.on('closed', function () { |
435 | console.error('closing') |
436 | read(true, function (err) { |
437 | console.log('closed') |
438 | if (err && err !== true) console.error(new Error(err.stack)) |
439 | }) |
440 | }) |
441 | pull.collect(function (err, msgs) { |
442 | if (err) return pull( |
443 | pull.once(u.renderError(err, ext).outerHTML), |
444 | self.wrapPage('peers'), |
445 | self.respondSink(500, {'Content-Type': ctype(ext)}) |
446 | ) |
447 | sort(msgs) |
448 | var maxTS = msgs.reduce(function (max, msg) { |
449 | return Math.max(msg.timestamp, max) |
450 | }, -Infinity) |
451 | pull( |
452 | pull.values(msgs), |
453 | self.renderThread(), |
454 | self.wrapNew({ |
455 | gt: isFinite(maxTS) ? maxTS : Date.now() |
456 | }), |
457 | self.wrapMessages(), |
458 | self.wrapPage('new'), |
459 | self.respondSink(200, { |
460 | 'Content-Type': ctype(ext) |
461 | }) |
462 | ) |
463 | })(read) |
464 | } |
465 | |
466 | Serve.prototype.private = function (ext) { |
467 | var q = this.query |
468 | var opts = { |
469 | reverse: !q.forwards, |
470 | lt: Number(q.lt) || Date.now(), |
471 | gt: Number(q.gt) || -Infinity, |
472 | filter: q.filter, |
473 | } |
474 | |
475 | pull( |
476 | this.app.streamPrivate(opts), |
477 | this.renderThreadPaginated(opts, null, q), |
478 | this.wrapMessages(), |
479 | this.wrapPrivate(opts), |
480 | this.wrapPage('private'), |
481 | this.respondSink(200, { |
482 | 'Content-Type': ctype(ext) |
483 | }) |
484 | ) |
485 | } |
486 | |
487 | Serve.prototype.mentions = function (ext) { |
488 | var self = this |
489 | var q = self.query |
490 | var opts = { |
491 | reverse: !q.forwards, |
492 | sortByTimestamp: q.sort === 'claimed', |
493 | lt: Number(q.lt) || Date.now(), |
494 | gt: Number(q.gt) || -Infinity, |
495 | filter: q.filter, |
496 | } |
497 | |
498 | return pull( |
499 | ph('section', {}, [ |
500 | ph('h3', 'Mentions'), |
501 | pull( |
502 | self.app.streamMentions(opts), |
503 | self.app.unboxMessages(), |
504 | self.renderThreadPaginated(opts, null, q), |
505 | self.wrapMessages() |
506 | ) |
507 | ]), |
508 | self.wrapPage('mentions'), |
509 | self.respondSink(200) |
510 | ) |
511 | } |
512 | |
513 | Serve.prototype.search = function (ext) { |
514 | var searchQ = (this.query.q || '').trim() |
515 | var self = this |
516 | |
517 | if (/^ssb:\/\//.test(searchQ)) { |
518 | var maybeId = searchQ.substr(6) |
519 | if (u.isRef(maybeId)) searchQ = maybeId |
520 | } |
521 | |
522 | if (u.isRef(searchQ) || searchQ[0] === '#') { |
523 | return self.redirect(self.app.render.toUrl(searchQ)) |
524 | } |
525 | |
526 | pull( |
527 | self.app.search(searchQ), |
528 | self.renderThread(), |
529 | self.wrapMessages(), |
530 | self.wrapPage('search · ' + searchQ, searchQ), |
531 | self.respondSink(200, { |
532 | 'Content-Type': ctype(ext), |
533 | }) |
534 | ) |
535 | } |
536 | |
537 | Serve.prototype.advsearch = function (ext) { |
538 | var self = this |
539 | var q = this.query || {} |
540 | |
541 | if (q.source) q.source = u.extractFeedIds(q.source)[0] |
542 | if (q.dest) q.dest = u.extractFeedIds(q.dest)[0] |
543 | var hasQuery = q.text || q.source || q.dest || q.channel |
544 | |
545 | pull( |
546 | cat([ |
547 | ph('section', {}, [ |
548 | ph('form', {action: '', method: 'get'}, [ |
549 | ph('table', [ |
550 | ph('tr', [ |
551 | ph('td', 'text'), |
552 | ph('td', ph('input', {name: 'text', placeholder: 'regex', |
553 | class: 'id-input', |
554 | value: q.text || ''})) |
555 | ]), |
556 | ph('tr', [ |
557 | ph('td', 'author'), |
558 | ph('td', ph('input', {name: 'source', placeholder: '@id', |
559 | class: 'id-input', |
560 | value: q.source || ''})) |
561 | ]), |
562 | ph('tr', [ |
563 | ph('td', 'mentions'), |
564 | ph('td', ph('input', {name: 'dest', placeholder: 'id', |
565 | class: 'id-input', |
566 | value: q.dest || ''})) |
567 | ]), |
568 | ph('tr', [ |
569 | ph('td', 'channel'), |
570 | ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel', |
571 | class: 'id-input', |
572 | value: q.channel || ''}) |
573 | ]) |
574 | ]), |
575 | ph('tr', [ |
576 | ph('td', {colspan: 2}, [ |
577 | ph('input', {type: 'submit', value: 'search'}) |
578 | ]) |
579 | ]), |
580 | ]) |
581 | ]) |
582 | ]), |
583 | hasQuery && pull( |
584 | self.app.advancedSearch(q), |
585 | self.renderThread({ |
586 | feed: q.source, |
587 | }), |
588 | self.wrapMessages() |
589 | ) |
590 | ]), |
591 | self.wrapPage('advanced search'), |
592 | self.respondSink(200, { |
593 | 'Content-Type': ctype(ext), |
594 | }) |
595 | ) |
596 | } |
597 | |
598 | Serve.prototype.live = function (ext) { |
599 | var self = this |
600 | var q = self.query |
601 | var opts = { |
602 | live: true, |
603 | } |
604 | var gt = Number(q.gt) |
605 | if (gt) opts.gt = gt |
606 | else opts.old = false |
607 | |
608 | pull( |
609 | ph('table', {class: 'ssb-msgs'}, pull( |
610 | self.app.sbot.createLogStream(opts), |
611 | self.app.render.renderFeeds({ |
612 | withGt: true, |
613 | filter: q.filter, |
614 | }), |
615 | pull.map(u.toHTML) |
616 | )), |
617 | self.wrapPage('live'), |
618 | self.respondSink(200, { |
619 | 'Content-Type': ctype(ext), |
620 | }) |
621 | ) |
622 | } |
623 | |
624 | Serve.prototype.compose = function (ext) { |
625 | var self = this |
626 | self.composer({ |
627 | channel: '', |
628 | redirectToPublishedMsg: true, |
629 | }, function (err, composer) { |
630 | if (err) return pull( |
631 | pull.once(u.renderError(err).outerHTML), |
632 | self.wrapPage('compose'), |
633 | self.respondSink(500) |
634 | ) |
635 | pull( |
636 | pull.once(u.toHTML(composer)), |
637 | self.wrapPage('compose'), |
638 | self.respondSink(200, { |
639 | 'Content-Type': ctype(ext) |
640 | }) |
641 | ) |
642 | }) |
643 | } |
644 | |
645 | Serve.prototype.votes = function (path) { |
646 | if (path) return pull( |
647 | pull.once(u.renderError(new Error('Not implemented')).outerHTML), |
648 | this.wrapPage('#' + channel), |
649 | this.respondSink(404, {'Content-Type': ctype('html')}) |
650 | ) |
651 | |
652 | var self = this |
653 | var q = self.query |
654 | var opts = { |
655 | reverse: !q.forwards, |
656 | limit: Number(q.limit) || 50, |
657 | } |
658 | var gt = Number(q.gt) |
659 | if (gt) opts.gt = gt |
660 | var lt = Number(q.lt) |
661 | if (lt) opts.lt = lt |
662 | |
663 | self.app.getVoted(opts, function (err, voted) { |
664 | if (err) return pull( |
665 | pull.once(u.renderError(err).outerHTML), |
666 | self.wrapPage('#' + channel), |
667 | self.respondSink(500, {'Content-Type': ctype('html')}) |
668 | ) |
669 | |
670 | pull( |
671 | ph('table', [ |
672 | ph('thead', [ |
673 | ph('tr', [ |
674 | ph('td', {colspan: 2}, self.syncPager({ |
675 | first: voted.firstTimestamp, |
676 | last: voted.lastTimestamp, |
677 | })) |
678 | ]) |
679 | ]), |
680 | ph('tbody', pull( |
681 | pull.values(voted.items), |
682 | paramap(function (item, cb) { |
683 | cb(null, ph('tr', [ |
684 | ph('td', [String(item.value)]), |
685 | ph('td', [ |
686 | self.phIdLink(item.id), |
687 | pull.once(' dug by '), |
688 | self.renderIdsList()(pull.values(item.feeds)) |
689 | ]) |
690 | ])) |
691 | }, 8) |
692 | )), |
693 | ph('tfoot', {}, []), |
694 | ]), |
695 | self.wrapPage('votes'), |
696 | self.respondSink(200, { |
697 | 'Content-Type': ctype('html') |
698 | }) |
699 | ) |
700 | }) |
701 | } |
702 | |
703 | Serve.prototype.syncPager = function (opts) { |
704 | var q = this.query |
705 | var reverse = !q.forwards |
706 | var min = (reverse ? opts.last : opts.first) || Number(q.gt) |
707 | var max = (reverse ? opts.first : opts.last) || Number(q.lt) |
708 | var minDate = new Date(min) |
709 | var maxDate = new Date(max) |
710 | var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined}) |
711 | var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1}) |
712 | var atNewest = reverse ? !q.lt : !max |
713 | var atOldest = reverse ? !min : !q.gt |
714 | if (atNewest && !reverse) qOlder.lt++ |
715 | if (atOldest && reverse) qNewer.gt-- |
716 | return h('div', |
717 | atOldest ? 'oldest' : [ |
718 | h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ', |
719 | h('span', {title: minDate.toString()}, htime(minDate)), ' ', |
720 | ], |
721 | ' - ', |
722 | atNewest ? 'now' : [ |
723 | h('span', {title: maxDate.toString()}, htime(maxDate)), ' ', |
724 | h('a', {href: '?' + qs.stringify(qNewer)}, '>>') |
725 | ] |
726 | ).outerHTML |
727 | } |
728 | |
729 | Serve.prototype.peers = function (ext) { |
730 | var self = this |
731 | if (self.data.action === 'connect') { |
732 | return self.app.sbot.gossip.connect(self.data.address, function (err) { |
733 | if (err) return pull( |
734 | pull.once(u.renderError(err, ext).outerHTML), |
735 | self.wrapPage('peers'), |
736 | self.respondSink(400, {'Content-Type': ctype(ext)}) |
737 | ) |
738 | self.data = {} |
739 | return self.peers(ext) |
740 | }) |
741 | } |
742 | |
743 | pull( |
744 | self.app.streamPeers(), |
745 | paramap(function (peer, cb) { |
746 | var done = multicb({pluck: 1, spread: true}) |
747 | var connectedTime = Date.now() - peer.stateChange |
748 | var addr = peer.host + ':' + peer.port + ':' + peer.key |
749 | done()(null, h('section', |
750 | h('form', {method: 'post', action: ''}, |
751 | peer.client ? '→' : '←', ' ', |
752 | h('code', peer.host, ':', peer.port, ':'), |
753 | self.app.render.idLink(peer.key, done()), ' ', |
754 | peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '', |
755 | peer.state === 'connected' ? 'connected' : [ |
756 | h('input', {name: 'action', type: 'submit', value: 'connect'}), |
757 | h('input', {name: 'address', type: 'hidden', value: addr}) |
758 | ] |
759 | ) |
760 | // h('div', 'source: ', peer.source) |
761 | // JSON.stringify(peer, 0, 2)).outerHTML |
762 | )) |
763 | done(cb) |
764 | }, 8), |
765 | pull.map(u.toHTML), |
766 | self.wrapPeers(), |
767 | self.wrapPage('peers'), |
768 | self.respondSink(200, { |
769 | 'Content-Type': ctype(ext) |
770 | }) |
771 | ) |
772 | } |
773 | |
774 | Serve.prototype.status = function (ext) { |
775 | var self = this |
776 | |
777 | if (!self.app.sbot.status) return pull( |
778 | pull.once('missing sbot status method'), |
779 | this.wrapPage('status'), |
780 | self.respondSink(400) |
781 | ) |
782 | |
783 | pull( |
784 | ph('section', [ |
785 | ph('h3', 'Status'), |
786 | pull( |
787 | u.readNext(function (cb) { |
788 | self.app.sbot.status(function (err, status) { |
789 | cb(err, status && pull.once(status)) |
790 | }) |
791 | }), |
792 | pull.map(function (status) { |
793 | return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML |
794 | }) |
795 | ) |
796 | ]), |
797 | this.wrapPage('status'), |
798 | this.respondSink(200) |
799 | ) |
800 | } |
801 | |
802 | Serve.prototype.channels = function (ext) { |
803 | var self = this |
804 | var id = self.app.sbot.id |
805 | |
806 | function renderMyChannels() { |
807 | return pull( |
808 | self.app.streamMyChannels(id), |
809 | paramap(function (channel, cb) { |
810 | // var subscribed = false |
811 | cb(null, [ |
812 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
813 | ' ' |
814 | ]) |
815 | }, 8), |
816 | pull.map(u.toHTML), |
817 | self.wrapMyChannels() |
818 | ) |
819 | } |
820 | |
821 | function renderNetworkChannels() { |
822 | return pull( |
823 | self.app.streamChannels(), |
824 | paramap(function (channel, cb) { |
825 | // var subscribed = false |
826 | cb(null, [ |
827 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
828 | ' ' |
829 | ]) |
830 | }, 8), |
831 | pull.map(u.toHTML), |
832 | self.wrapChannels() |
833 | ) |
834 | } |
835 | |
836 | pull( |
837 | cat([ |
838 | ph('section', {}, [ |
839 | ph('h3', {}, 'Channels:'), |
840 | renderMyChannels(), |
841 | renderNetworkChannels() |
842 | ]) |
843 | ]), |
844 | this.wrapPage('channels'), |
845 | this.respondSink(200, { |
846 | 'Content-Type': ctype(ext) |
847 | }) |
848 | ) |
849 | } |
850 | |
851 | function renderFriendsList(app) { |
852 | } |
853 | |
854 | Serve.prototype.contacts = function (path) { |
855 | var self = this |
856 | var id = String(path).substr(1) |
857 | var contacts = self.app.contacts.createContactStreams(id) |
858 | var render = self.app.render |
859 | |
860 | pull( |
861 | cat([ |
862 | ph('section', {}, [ |
863 | ph('h3', {}, ['Contacts: ', self.phIdLink(id)]), |
864 | ph('h4', {}, 'Friends'), |
865 | render.friendsList('/contacts/')(contacts.friends), |
866 | ph('h4', {}, 'Follows'), |
867 | render.friendsList('/contacts/')(contacts.follows), |
868 | ph('h4', {}, 'Followers'), |
869 | render.friendsList('/contacts/')(contacts.followers), |
870 | ph('h4', {}, 'Blocks'), |
871 | render.friendsList('/contacts/')(contacts.blocks), |
872 | ph('h4', {}, 'Blocked by'), |
873 | render.friendsList('/contacts/')(contacts.blockers) |
874 | ]) |
875 | ]), |
876 | this.wrapPage('contacts: ' + id), |
877 | this.respondSink(200, { |
878 | 'Content-Type': ctype('html') |
879 | }) |
880 | ) |
881 | } |
882 | |
883 | Serve.prototype.about = function (path) { |
884 | var self = this |
885 | var id = decodeURIComponent(String(path).substr(1)) |
886 | var abouts = self.app.createAboutStreams(id) |
887 | var render = self.app.render |
888 | |
889 | function renderAboutOpImage(link) { |
890 | if (!link) return |
891 | if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link)) |
892 | return ph('img', { |
893 | class: 'ssb-avatar-image', |
894 | src: render.imageUrl(link.link), |
895 | alt: link.link |
896 | + (link.size ? ' (' + render.formatSize(link.size) + ')' : '') |
897 | }) |
898 | } |
899 | |
900 | function renderAboutOpValue(value) { |
901 | if (!value) return |
902 | if (u.isRef(value.link)) return self.phIdLink(value.link) |
903 | if (value.epoch) return new Date(value.epoch).toUTCString() |
904 | return ph('code', {}, JSON.stringify(value)) |
905 | } |
906 | |
907 | function renderAboutOpContent(op) { |
908 | if (op.prop === 'image') |
909 | return renderAboutOpImage(u.toLink(op.value)) |
910 | if (op.prop === 'description') |
911 | return h('div', {innerHTML: render.markdown(op.value)}).outerHTML |
912 | if (op.prop === 'title') |
913 | return h('strong', op.value).outerHTML |
914 | if (op.prop === 'name') |
915 | return h('u', op.value).outerHTML |
916 | return renderAboutOpValue(op.value) |
917 | } |
918 | |
919 | function renderAboutOp(op) { |
920 | return ph('tr', {}, [ |
921 | ph('td', self.phIdLink(op.author)), |
922 | ph('td', |
923 | ph('a', {href: render.toUrl(op.id)}, |
924 | htime(new Date(op.timestamp)))), |
925 | ph('td', op.prop), |
926 | ph('td', renderAboutOpContent(op)) |
927 | ]) |
928 | } |
929 | |
930 | pull( |
931 | cat([ |
932 | ph('section', {}, [ |
933 | ph('h3', {}, ['About: ', self.phIdLink(id)]), |
934 | ph('table', {}, |
935 | pull(abouts.scalars, pull.map(renderAboutOp)) |
936 | ), |
937 | pull( |
938 | abouts.sets, |
939 | pull.map(function (op) { |
940 | return h('pre', JSON.stringify(op, 0, 2)) |
941 | }), |
942 | pull.map(u.toHTML) |
943 | ) |
944 | ]) |
945 | ]), |
946 | this.wrapPage('about: ' + id), |
947 | this.respondSink(200, { |
948 | 'Content-Type': ctype('html') |
949 | }) |
950 | ) |
951 | } |
952 | |
953 | Serve.prototype.type = function (path) { |
954 | var q = this.query |
955 | var type = decodeURIComponent(path.substr(1)) |
956 | var opts = { |
957 | reverse: !q.forwards, |
958 | lt: Number(q.lt) || Date.now(), |
959 | gt: Number(q.gt) || -Infinity, |
960 | type: type, |
961 | filter: q.filter, |
962 | } |
963 | |
964 | pull( |
965 | this.app.sbot.messagesByType(opts), |
966 | this.renderThreadPaginated(opts, null, q), |
967 | this.wrapMessages(), |
968 | this.wrapType(type), |
969 | this.wrapPage('type: ' + type), |
970 | this.respondSink(200, { |
971 | 'Content-Type': ctype('html') |
972 | }) |
973 | ) |
974 | } |
975 | |
976 | Serve.prototype.links = function (path) { |
977 | var q = this.query |
978 | var dest = path.substr(1) |
979 | var opts = { |
980 | dest: dest, |
981 | reverse: true, |
982 | values: true, |
983 | } |
984 | if (q.rel) opts.rel = q.rel |
985 | |
986 | pull( |
987 | this.app.sbot.links(opts), |
988 | this.renderThread(), |
989 | this.wrapMessages(), |
990 | this.wrapLinks(dest), |
991 | this.wrapPage('links: ' + dest), |
992 | this.respondSink(200, { |
993 | 'Content-Type': ctype('html') |
994 | }) |
995 | ) |
996 | } |
997 | |
998 | Serve.prototype.rawId = function (id) { |
999 | var self = this |
1000 | |
1001 | self.getMsgDecryptedMaybeOoo(id, function (err, msg) { |
1002 | if (err) return pull( |
1003 | pull.once(u.renderError(err).outerHTML), |
1004 | self.respondSink(400, {'Content-Type': ctype('html')}) |
1005 | ) |
1006 | return pull( |
1007 | pull.once(msg), |
1008 | self.renderRawMsgPage(id), |
1009 | self.respondSink(200, { |
1010 | 'Content-Type': ctype('html'), |
1011 | }) |
1012 | ) |
1013 | }) |
1014 | } |
1015 | |
1016 | Serve.prototype.channel = function (path) { |
1017 | var channel = decodeURIComponent(String(path).substr(1)) |
1018 | var q = this.query |
1019 | var gt = Number(q.gt) || -Infinity |
1020 | var lt = Number(q.lt) || Date.now() |
1021 | var opts = { |
1022 | reverse: !q.forwards, |
1023 | lt: lt, |
1024 | gt: gt, |
1025 | channel: channel, |
1026 | filter: q.filter, |
1027 | } |
1028 | |
1029 | pull( |
1030 | this.app.streamChannel(opts), |
1031 | this.renderThreadPaginated(opts, null, q), |
1032 | this.wrapMessages(), |
1033 | this.wrapChannel(channel), |
1034 | this.wrapPage('#' + channel), |
1035 | this.respondSink(200, { |
1036 | 'Content-Type': ctype('html') |
1037 | }) |
1038 | ) |
1039 | } |
1040 | |
1041 | function threadHeads(msgs, rootId) { |
1042 | return sort.heads(msgs.filter(function (msg) { |
1043 | var c = msg.value && msg.value.content |
1044 | return (c && c.root === rootId) |
1045 | || msg.key === rootId |
1046 | })) |
1047 | } |
1048 | |
1049 | Serve.prototype.streamThreadWithComposer = function (opts) { |
1050 | var self = this |
1051 | var id = opts.root |
1052 | return ph('table', {class: 'ssb-msgs'}, u.readNext(next)) |
1053 | function next(cb) { |
1054 | self.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) { |
1055 | if (err && err.name === 'NotFoundError') err = null, rootMsg = { |
1056 | key: id, value: {content: false}} |
1057 | if (err) return cb(new Error(err.stack)) |
1058 | if (!rootMsg) { |
1059 | console.log('id', id, 'opts', opts) |
1060 | } |
1061 | var rootContent = rootMsg && rootMsg.value && rootMsg.value.content |
1062 | var recps = rootContent && rootContent.recps |
1063 | || (rootMsg.value.private |
1064 | ? [rootMsg.value.author, self.app.sbot.id].filter(uniques()) |
1065 | : undefined) |
1066 | var threadRootId = rootContent && rootContent.root || id |
1067 | var channel = opts.channel |
1068 | |
1069 | pull( |
1070 | cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]), |
1071 | pull.unique('key'), |
1072 | self.app.unboxMessages(), |
1073 | pull.through(function (msg) { |
1074 | var c = msg && msg.value.content |
1075 | if (!channel && c.channel) channel = c.channel |
1076 | }), |
1077 | pull.collect(function (err, links) { |
1078 | if (err) return gotLinks(err) |
1079 | if (!self.useOoo) return gotLinks(null, links) |
1080 | self.app.expandOoo({msgs: links, dest: id}, gotLinks) |
1081 | }) |
1082 | ) |
1083 | function gotLinks(err, links) { |
1084 | if (err) return cb(new Error(err.stack)) |
1085 | cb(null, pull( |
1086 | pull.values(sort(links)), |
1087 | self.renderThread({ |
1088 | msgId: id, |
1089 | }), |
1090 | self.wrapMessages(), |
1091 | self.wrapThread({ |
1092 | recps: recps, |
1093 | root: threadRootId, |
1094 | post: id, |
1095 | branches: threadHeads(links, threadRootId), |
1096 | postBranches: threadRootId !== id && threadHeads(links, id), |
1097 | placeholder: opts.placeholder, |
1098 | channel: channel, |
1099 | }) |
1100 | )) |
1101 | } |
1102 | }) |
1103 | } |
1104 | } |
1105 | |
1106 | Serve.prototype.id = function (id, path) { |
1107 | var self = this |
1108 | if (self.query.raw != null) return self.rawId(id) |
1109 | pull( |
1110 | self.streamThreadWithComposer({root: id}), |
1111 | self.wrapPage(id), |
1112 | self.respondSink(200) |
1113 | ) |
1114 | } |
1115 | |
1116 | Serve.prototype.userFeed = function (id, path) { |
1117 | var self = this |
1118 | var q = self.query |
1119 | var opts = { |
1120 | id: id, |
1121 | reverse: !q.forwards, |
1122 | lt: Number(q.lt) || Date.now(), |
1123 | gt: Number(q.gt) || -Infinity, |
1124 | feed: id, |
1125 | filter: q.filter, |
1126 | } |
1127 | var isScrolled = q.lt || q.gt |
1128 | |
1129 | self.app.getAbout(id, function (err, about) { |
1130 | if (err) self.app.error(err) |
1131 | pull( |
1132 | self.app.sbot.createUserStream(opts), |
1133 | self.renderThreadPaginated(opts, id, q), |
1134 | self.wrapMessages(), |
1135 | self.wrapUserFeed(isScrolled, id), |
1136 | self.wrapPage(about.name || id), |
1137 | self.respondSink(200) |
1138 | ) |
1139 | }) |
1140 | } |
1141 | |
1142 | Serve.prototype.file = function (file) { |
1143 | var self = this |
1144 | fs.stat(file, function (err, stat) { |
1145 | if (err && err.code === 'ENOENT') return self.respond(404, 'Not found') |
1146 | if (err) return self.respond(500, err.stack || err) |
1147 | if (!stat.isFile()) return self.respond(403, 'May only load files') |
1148 | if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified') |
1149 | self.res.writeHead(200, { |
1150 | 'Content-Type': ctype(file), |
1151 | 'Content-Length': stat.size, |
1152 | 'Last-Modified': stat.mtime.toGMTString() |
1153 | }) |
1154 | fs.createReadStream(file).pipe(self.res) |
1155 | }) |
1156 | } |
1157 | |
1158 | Serve.prototype.static = function (file) { |
1159 | this.file(path.join(__dirname, '../static', file)) |
1160 | } |
1161 | |
1162 | Serve.prototype.emoji = function (emoji) { |
1163 | serveEmoji(this.req, this.res, emoji) |
1164 | } |
1165 | |
1166 | Serve.prototype.highlight = function (dirs) { |
1167 | this.file(path.join(hlCssDir, dirs)) |
1168 | } |
1169 | |
1170 | Serve.prototype.blob = function (id, path) { |
1171 | var self = this |
1172 | var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+') |
1173 | var etag = id + (path || '') + (unbox || '') |
1174 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
1175 | var key |
1176 | if (path) { |
1177 | try { path = decodeURIComponent(path) } catch(e) {} |
1178 | if (path[0] === '#') { |
1179 | unbox = path.substr(1) |
1180 | } else { |
1181 | return self.respond(400, 'Bad blob request') |
1182 | } |
1183 | } |
1184 | if (unbox) { |
1185 | try { |
1186 | key = new Buffer(unbox, 'base64') |
1187 | } catch(err) { |
1188 | return self.respond(400, err.message) |
1189 | } |
1190 | if (key.length !== 32) { |
1191 | return self.respond(400, 'Bad blob key') |
1192 | } |
1193 | } |
1194 | self.app.wantSizeBlob(id, function (err, size) { |
1195 | if (err) { |
1196 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
1197 | else return self.respond(500, err.message || err) |
1198 | } |
1199 | pull( |
1200 | self.app.getBlob(id, key), |
1201 | pull.map(Buffer), |
1202 | ident(gotType), |
1203 | self.respondSink() |
1204 | ) |
1205 | function gotType(type) { |
1206 | type = type && mime.lookup(type) |
1207 | if (type) self.res.setHeader('Content-Type', type) |
1208 | // don't serve size for encrypted blob, because it refers to the size of |
1209 | // the ciphertext |
1210 | if (typeof size === 'number' && !key) |
1211 | self.res.setHeader('Content-Length', size) |
1212 | if (self.query.name) self.res.setHeader('Content-Disposition', |
1213 | 'inline; filename='+encodeDispositionFilename(self.query.name)) |
1214 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
1215 | self.res.setHeader('etag', etag) |
1216 | self.res.writeHead(200) |
1217 | } |
1218 | }) |
1219 | } |
1220 | |
1221 | Serve.prototype.image = function (path) { |
1222 | var self = this |
1223 | var id, key |
1224 | var m = urlIdRegex.exec(path) |
1225 | if (m && m[2] === '&') id = m[1], path = m[3] |
1226 | var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+') |
1227 | var etag = 'image-' + id + (path || '') + (unbox || '') |
1228 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
1229 | if (path) { |
1230 | try { path = decodeURIComponent(path) } catch(e) {} |
1231 | if (path[0] === '#') { |
1232 | unbox = path.substr(1) |
1233 | } else { |
1234 | return self.respond(400, 'Bad blob request') |
1235 | } |
1236 | } |
1237 | if (unbox) { |
1238 | try { |
1239 | key = new Buffer(unbox, 'base64') |
1240 | } catch(err) { |
1241 | return self.respond(400, err.message) |
1242 | } |
1243 | if (key.length !== 32) { |
1244 | return self.respond(400, 'Bad blob key') |
1245 | } |
1246 | } |
1247 | self.app.wantSizeBlob(id, function (err, size) { |
1248 | if (err) { |
1249 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
1250 | else return self.respond(500, err.message || err) |
1251 | } |
1252 | |
1253 | var done = multicb({pluck: 1, spread: true}) |
1254 | var heresTheData = done() |
1255 | var heresTheType = done().bind(self, null) |
1256 | |
1257 | pull( |
1258 | self.app.getBlob(id, key), |
1259 | pull.map(Buffer), |
1260 | ident(heresTheType), |
1261 | pull.collect(onFullBuffer) |
1262 | ) |
1263 | |
1264 | function onFullBuffer (err, buffer) { |
1265 | if (err) return heresTheData(err) |
1266 | buffer = Buffer.concat(buffer) |
1267 | |
1268 | try { |
1269 | jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) { |
1270 | if (!err) buffer = rotatedBuffer |
1271 | |
1272 | heresTheData(null, buffer) |
1273 | pull( |
1274 | pull.once(buffer), |
1275 | self.respondSink() |
1276 | ) |
1277 | }) |
1278 | } catch (err) { |
1279 | console.trace(err) |
1280 | self.respond(500, err.message || err) |
1281 | } |
1282 | } |
1283 | |
1284 | done(function (err, data, type) { |
1285 | if (err) { |
1286 | console.trace(err) |
1287 | self.respond(500, err.message || err) |
1288 | } |
1289 | type = type && mime.lookup(type) |
1290 | if (type) self.res.setHeader('Content-Type', type) |
1291 | self.res.setHeader('Content-Length', data.length) |
1292 | if (self.query.name) self.res.setHeader('Content-Disposition', |
1293 | 'inline; filename='+encodeDispositionFilename(self.query.name)) |
1294 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
1295 | self.res.setHeader('etag', etag) |
1296 | self.res.writeHead(200) |
1297 | }) |
1298 | }) |
1299 | } |
1300 | |
1301 | Serve.prototype.ifModified = function (lastMod) { |
1302 | var ifModSince = this.req.headers['if-modified-since'] |
1303 | if (!ifModSince) return false |
1304 | var d = new Date(ifModSince) |
1305 | return d && Math.floor(d/1000) >= Math.floor(lastMod/1000) |
1306 | } |
1307 | |
1308 | Serve.prototype.wrapMessages = function () { |
1309 | return u.hyperwrap(function (content, cb) { |
1310 | cb(null, h('table.ssb-msgs', content)) |
1311 | }) |
1312 | } |
1313 | |
1314 | Serve.prototype.renderThread = function (opts) { |
1315 | return pull( |
1316 | this.app.render.renderFeeds({ |
1317 | raw: false, |
1318 | full: this.query.full != null, |
1319 | feed: opts && opts.feed, |
1320 | msgId: opts && opts.msgId, |
1321 | filter: this.query.filter, |
1322 | limit: Number(this.query.limit), |
1323 | serve: this, |
1324 | }), |
1325 | pull.map(u.toHTML) |
1326 | ) |
1327 | } |
1328 | |
1329 | Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { |
1330 | var self = this |
1331 | function linkA(opts, name) { |
1332 | var q1 = u.mergeOpts(q, opts) |
1333 | return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit) |
1334 | } |
1335 | function links(opts) { |
1336 | var limit = opts.limit || q.limit || 10 |
1337 | return h('tr', h('td.paginate', {colspan: 3}, |
1338 | opts.forwards ? '↑ newer ' : '↓ older ', |
1339 | linkA(u.mergeOpts(opts, {limit: 1})), ' ', |
1340 | linkA(u.mergeOpts(opts, {limit: 10})), ' ', |
1341 | linkA(u.mergeOpts(opts, {limit: 100})) |
1342 | )) |
1343 | } |
1344 | |
1345 | return pull( |
1346 | self.app.filterMessages({ |
1347 | feed: opts && opts.feed, |
1348 | msgId: opts && opts.msgId, |
1349 | filter: this.query.filter, |
1350 | limit: Number(this.query.limit) || 12, |
1351 | }), |
1352 | paginate( |
1353 | function onFirst(msg, cb) { |
1354 | var num = feedId ? msg.value.sequence : |
1355 | opts.sortByTimestamp ? msg.value.timestamp : |
1356 | msg.timestamp || msg.ts |
1357 | if (q.forwards) { |
1358 | cb(null, links({ |
1359 | lt: num, |
1360 | gt: null, |
1361 | forwards: null, |
1362 | filter: opts.filter, |
1363 | })) |
1364 | } else { |
1365 | cb(null, links({ |
1366 | lt: null, |
1367 | gt: num, |
1368 | forwards: 1, |
1369 | filter: opts.filter, |
1370 | })) |
1371 | } |
1372 | }, |
1373 | this.app.render.renderFeeds({ |
1374 | raw: false, |
1375 | full: this.query.full != null, |
1376 | feed: opts && opts.feed, |
1377 | msgId: opts && opts.msgId, |
1378 | filter: this.query.filter, |
1379 | limit: Number(this.query.limit) || 12, |
1380 | }), |
1381 | function onLast(msg, cb) { |
1382 | var num = feedId ? msg.value.sequence : |
1383 | opts.sortByTimestamp ? msg.value.timestamp : |
1384 | msg.timestamp || msg.ts |
1385 | if (q.forwards) { |
1386 | cb(null, links({ |
1387 | lt: null, |
1388 | gt: num, |
1389 | forwards: 1, |
1390 | filter: opts.filter, |
1391 | })) |
1392 | } else { |
1393 | cb(null, links({ |
1394 | lt: num, |
1395 | gt: null, |
1396 | forwards: null, |
1397 | filter: opts.filter, |
1398 | })) |
1399 | } |
1400 | }, |
1401 | function onEmpty(cb) { |
1402 | if (q.forwards) { |
1403 | cb(null, links({ |
1404 | gt: null, |
1405 | lt: opts.gt + 1, |
1406 | forwards: null, |
1407 | filter: opts.filter, |
1408 | })) |
1409 | } else { |
1410 | cb(null, links({ |
1411 | gt: opts.lt - 1, |
1412 | lt: null, |
1413 | forwards: 1, |
1414 | filter: opts.filter, |
1415 | })) |
1416 | } |
1417 | } |
1418 | ), |
1419 | pull.map(u.toHTML) |
1420 | ) |
1421 | } |
1422 | |
1423 | Serve.prototype.renderRawMsgPage = function (id) { |
1424 | var showMarkdownSource = (this.query.raw === 'md') |
1425 | var raw = !showMarkdownSource |
1426 | return pull( |
1427 | this.app.render.renderFeeds({ |
1428 | raw: raw, |
1429 | msgId: id, |
1430 | filter: this.query.filter, |
1431 | markdownSource: showMarkdownSource |
1432 | }), |
1433 | pull.map(u.toHTML), |
1434 | this.wrapMessages(), |
1435 | this.wrapPage(id) |
1436 | ) |
1437 | } |
1438 | |
1439 | function catchHTMLError() { |
1440 | return function (read) { |
1441 | var ended |
1442 | return function (abort, cb) { |
1443 | if (ended) return cb(ended) |
1444 | read(abort, function (end, data) { |
1445 | if (!end || end === true) return cb(end, data) |
1446 | ended = true |
1447 | cb(null, u.renderError(end).outerHTML) |
1448 | }) |
1449 | } |
1450 | } |
1451 | } |
1452 | |
1453 | function catchTextError() { |
1454 | return function (read) { |
1455 | var ended |
1456 | return function (abort, cb) { |
1457 | if (ended) return cb(ended) |
1458 | read(abort, function (end, data) { |
1459 | if (!end || end === true) return cb(end, data) |
1460 | ended = true |
1461 | cb(null, end.stack + '\n') |
1462 | }) |
1463 | } |
1464 | } |
1465 | } |
1466 | |
1467 | function styles() { |
1468 | return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8') |
1469 | } |
1470 | |
1471 | Serve.prototype.appendFooter = function () { |
1472 | var self = this |
1473 | return function (read) { |
1474 | return cat([read, u.readNext(function (cb) { |
1475 | var ms = new Date() - self.startDate |
1476 | cb(null, pull.once(h('footer', |
1477 | h('a', {href: pkg.homepage}, pkg.name), ' · ', |
1478 | ms/1000 + 's' |
1479 | ).outerHTML)) |
1480 | })]) |
1481 | } |
1482 | } |
1483 | |
1484 | Serve.prototype.wrapPage = function (title, searchQ) { |
1485 | var self = this |
1486 | var render = self.app.render |
1487 | return pull( |
1488 | catchHTMLError(), |
1489 | self.appendFooter(), |
1490 | u.hyperwrap(function (content, cb) { |
1491 | var done = multicb({pluck: 1, spread: true}) |
1492 | done()(null, h('html', h('head', |
1493 | h('meta', {charset: 'utf-8'}), |
1494 | h('title', title), |
1495 | h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}), |
1496 | h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}), |
1497 | h('style', styles()), |
1498 | h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')}) |
1499 | ), |
1500 | h('body', |
1501 | h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'}, |
1502 | h('a', {href: render.toUrl('/new')}, 'new') , ' ', |
1503 | h('a', {href: render.toUrl('/public')}, 'public'), ' ', |
1504 | h('a', {href: render.toUrl('/private')}, 'private') , ' ', |
1505 | h('a', {href: render.toUrl('/mentions')}, 'mentions') , ' ', |
1506 | h('a', {href: render.toUrl('/peers')}, 'peers') , ' ', |
1507 | self.app.sbot.status ? |
1508 | [h('a', {href: render.toUrl('/status')}, 'status'), ' '] : '', |
1509 | h('a', {href: render.toUrl('/channels')}, 'channels') , ' ', |
1510 | h('a', {href: render.toUrl('/friends')}, 'friends'), ' ', |
1511 | h('a', {href: render.toUrl('/advsearch')}, 'search'), ' ', |
1512 | h('a', {href: render.toUrl('/live')}, 'live'), ' ', |
1513 | h('a', {href: render.toUrl('/compose')}, 'compose'), ' ', |
1514 | h('a', {href: render.toUrl('/votes')}, 'votes'), ' ', |
1515 | h('a', {href: render.toUrl('/emojis')}, 'emojis'), ' ', |
1516 | render.idLink(self.app.sbot.id, done()), ' ', |
1517 | h('input.search-input', {name: 'q', value: searchQ, |
1518 | placeholder: 'search'}) |
1519 | // h('a', {href: '/convos'}, 'convos'), ' ', |
1520 | // h('a', {href: '/friends'}, 'friends'), ' ', |
1521 | // h('a', {href: '/git'}, 'git') |
1522 | )), |
1523 | self.publishedMsg ? h('div', |
1524 | 'published ', |
1525 | self.app.render.msgLink(self.publishedMsg, done()) |
1526 | ) : '', |
1527 | // self.note, |
1528 | content |
1529 | ))) |
1530 | done(cb) |
1531 | }) |
1532 | ) |
1533 | } |
1534 | |
1535 | Serve.prototype.phIdLink = function (id) { |
1536 | return pull( |
1537 | pull.once(id), |
1538 | this.renderIdsList() |
1539 | ) |
1540 | } |
1541 | |
1542 | Serve.prototype.phIdAvatar = function (id) { |
1543 | var self = this |
1544 | return u.readNext(function (cb) { |
1545 | var el = self.app.render.avatarImage(id, function (err) { |
1546 | if (err) return cb(err) |
1547 | cb(null, pull.once(u.toHTML(el))) |
1548 | }) |
1549 | }) |
1550 | } |
1551 | |
1552 | Serve.prototype.friends = function (path) { |
1553 | var self = this |
1554 | pull( |
1555 | self.app.sbot.friends.createFriendStream({hops: 1}), |
1556 | self.renderIdsList(), |
1557 | u.hyperwrap(function (items, cb) { |
1558 | cb(null, [ |
1559 | h('section', |
1560 | h('h3', 'Friends') |
1561 | ), |
1562 | h('section', items) |
1563 | ]) |
1564 | }), |
1565 | this.wrapPage('friends'), |
1566 | this.respondSink(200, { |
1567 | 'Content-Type': ctype('html') |
1568 | }) |
1569 | ) |
1570 | } |
1571 | |
1572 | Serve.prototype.renderIdsList = function () { |
1573 | var self = this |
1574 | return pull( |
1575 | paramap(function (id, cb) { |
1576 | self.app.render.getNameLink(id, cb) |
1577 | }, 8), |
1578 | pull.map(function (el) { |
1579 | return [el, ' '] |
1580 | }), |
1581 | pull.map(u.toHTML) |
1582 | ) |
1583 | } |
1584 | |
1585 | Serve.prototype.aboutDescription = function (id) { |
1586 | var self = this |
1587 | return u.readNext(function (cb) { |
1588 | self.app.getAbout(id, function (err, about) { |
1589 | if (err) return cb(err) |
1590 | if (!about.description) return cb(null, pull.empty()) |
1591 | cb(null, ph('div', self.app.render.markdown(about.description))) |
1592 | }) |
1593 | }) |
1594 | } |
1595 | |
1596 | Serve.prototype.followInfo = function (id, myId) { |
1597 | var self = this |
1598 | return u.readNext(function (cb) { |
1599 | var done = multicb({pluck: 1, spread: true}) |
1600 | self.app.getContact(myId, id, done()) |
1601 | self.app.getContact(id, myId, done()) |
1602 | done(function (err, contactToThem, contactFromThem) { |
1603 | if (err) return cb(err) |
1604 | cb(null, ph('form', {action: '', method: 'post'}, [ |
1605 | contactFromThem ? contactToThem ? 'friend ' : 'follows you ' : |
1606 | contactFromThem === false ? 'blocks you ' : '', |
1607 | ph('input', {type: 'hidden', name: 'action', value: 'contact'}), |
1608 | ph('input', {type: 'hidden', name: 'contact', value: id}), |
1609 | ph('input', {type: 'submit', |
1610 | name: contactToThem ? 'unfollow' : 'follow', |
1611 | value: contactToThem ? 'unfollow' : 'follow'}), ' ', |
1612 | ph('input', {type: 'submit', |
1613 | name: contactToThem === false ? 'unblock' : 'block', |
1614 | value: contactToThem === false ? 'unblock' : 'block'}) |
1615 | ])) |
1616 | }) |
1617 | }) |
1618 | } |
1619 | |
1620 | Serve.prototype.friendInfo = function (id, myId) { |
1621 | var first = false |
1622 | return pull( |
1623 | this.app.contacts.createFollowedFollowersStream(myId, id), |
1624 | this.app.render.friendsList(), |
1625 | pull.map(function (html) { |
1626 | if (!first) { |
1627 | first = true |
1628 | return 'followed by your friends: ' + html |
1629 | } |
1630 | return html |
1631 | }) |
1632 | ) |
1633 | } |
1634 | |
1635 | Serve.prototype.wrapUserFeed = function (isScrolled, id) { |
1636 | var self = this |
1637 | var myId = self.app.sbot.id |
1638 | var render = self.app.render |
1639 | return function (thread) { |
1640 | return cat([ |
1641 | ph('section', {class: 'ssb-feed'}, ph('table', [ |
1642 | isScrolled ? '' : ph('tr', [ |
1643 | ph('td', self.phIdAvatar(id)), |
1644 | ph('td', {class: 'feed-about'}, [ |
1645 | ph('h3', {class: 'feed-name'}, |
1646 | ph('strong', self.phIdLink(id))), |
1647 | ph('code', ph('small', id)), |
1648 | self.aboutDescription(id) |
1649 | ]) |
1650 | ]), |
1651 | isScrolled ? '' : ph('tr', [ |
1652 | ph('td'), |
1653 | ph('td', pull( |
1654 | self.app.getAddresses(id), |
1655 | pull.map(function (address) { |
1656 | return ph('div', [ |
1657 | ph('code', address) |
1658 | ]) |
1659 | }) |
1660 | )) |
1661 | ]), |
1662 | ph('tr', [ |
1663 | ph('td'), |
1664 | ph('td', [ |
1665 | ph('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ', |
1666 | ph('a', {href: render.toUrl('/about/' + id)}, 'about') |
1667 | ]) |
1668 | ]), |
1669 | ph('tr', [ |
1670 | ph('td'), |
1671 | ph('td', |
1672 | ph('form', {action: render.toUrl('/advsearch'), method: 'get'}, [ |
1673 | ph('input', {type: 'hidden', name: 'source', value: id}), |
1674 | ph('input', {type: 'text', name: 'text', placeholder: 'text'}), |
1675 | ph('input', {type: 'submit', value: 'search'}) |
1676 | ]) |
1677 | ) |
1678 | ]), |
1679 | isScrolled || id === myId ? '' : [ |
1680 | ph('tr', [ |
1681 | ph('td'), |
1682 | ph('td', {class: 'follow-info'}, self.followInfo(id, myId)) |
1683 | ]), |
1684 | ph('tr', [ |
1685 | ph('td'), |
1686 | ph('td', self.friendInfo(id, myId)) |
1687 | ]) |
1688 | ] |
1689 | ])), |
1690 | thread |
1691 | ]) |
1692 | } |
1693 | } |
1694 | |
1695 | Serve.prototype.git = function (url) { |
1696 | var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url) |
1697 | switch (m[1]) { |
1698 | case 'commit': return this.gitCommit(m[2]) |
1699 | case 'tag': return this.gitTag(m[2]) |
1700 | case 'tree': return this.gitTree(m[2]) |
1701 | case 'blob': return this.gitBlob(m[2]) |
1702 | case 'raw': return this.gitRaw(m[2]) |
1703 | case 'diff': return this.gitDiff(m[2]) |
1704 | case 'line-comment': return this.gitLineComment(m[2]) |
1705 | default: return this.respond(404, 'Not found') |
1706 | } |
1707 | } |
1708 | |
1709 | Serve.prototype.gitRaw = function (rev) { |
1710 | var self = this |
1711 | if (!/[0-9a-f]{24}/.test(rev)) { |
1712 | return pull( |
1713 | pull.once('\'' + rev + '\' is not a git object id'), |
1714 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
1715 | ) |
1716 | } |
1717 | if (!u.isRef(self.query.msg)) return pull( |
1718 | ph('div.error', 'missing message id'), |
1719 | self.wrapPage('git tree ' + rev), |
1720 | self.respondSink(400) |
1721 | ) |
1722 | |
1723 | self.app.git.openObject({ |
1724 | obj: rev, |
1725 | msg: self.query.msg, |
1726 | }, function (err, obj) { |
1727 | if (err && err.name === 'BlobNotFoundError') |
1728 | return self.askWantBlobs(err.links) |
1729 | if (err) return pull( |
1730 | pull.once(err.stack), |
1731 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
1732 | ) |
1733 | pull( |
1734 | self.app.git.readObject(obj), |
1735 | catchTextError(), |
1736 | ident(function (type) { |
1737 | type = type && mime.lookup(type) |
1738 | if (type) self.res.setHeader('Content-Type', type) |
1739 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
1740 | self.res.setHeader('etag', rev) |
1741 | self.res.writeHead(200) |
1742 | }), |
1743 | self.respondSink() |
1744 | ) |
1745 | }) |
1746 | } |
1747 | |
1748 | Serve.prototype.gitAuthorLink = function (author) { |
1749 | if (author.feed) { |
1750 | var myName = this.app.getNameSync(author.feed) |
1751 | var sigil = author.name === author.localpart ? '@' : '' |
1752 | return ph('a', { |
1753 | href: this.app.render.toUrl(author.feed), |
1754 | title: author.localpart + (myName ? ' (' + myName + ')' : '') |
1755 | }, u.escapeHTML(sigil + author.name)) |
1756 | } else { |
1757 | return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)}, |
1758 | u.escapeHTML(author.name)) |
1759 | } |
1760 | } |
1761 | |
1762 | Serve.prototype.gitCommit = function (rev) { |
1763 | var self = this |
1764 | if (!/[0-9a-f]{24}/.test(rev)) { |
1765 | return pull( |
1766 | ph('div.error', 'rev is not a git object id'), |
1767 | self.wrapPage('git'), |
1768 | self.respondSink(400) |
1769 | ) |
1770 | } |
1771 | if (!u.isRef(self.query.msg)) return pull( |
1772 | ph('div.error', 'missing message id'), |
1773 | self.wrapPage('git commit ' + rev), |
1774 | self.respondSink(400) |
1775 | ) |
1776 | |
1777 | if (self.query.search) { |
1778 | return self.app.git.getObjectMsg({ |
1779 | obj: rev, |
1780 | headMsgId: self.query.msg, |
1781 | }, function (err, msg) { |
1782 | if (err && err.name === 'BlobNotFoundError') |
1783 | return self.askWantBlobs(err.links) |
1784 | if (err) return pull( |
1785 | pull.once(u.renderError(err).outerHTML), |
1786 | self.wrapPage('git commit ' + rev), |
1787 | self.respondSink(400) |
1788 | ) |
1789 | var path = '/git/commit/' + rev |
1790 | + '?msg=' + encodeURIComponent(msg.key) |
1791 | return self.redirect(self.app.render.toUrl(path)) |
1792 | }) |
1793 | } |
1794 | |
1795 | self.app.git.openObject({ |
1796 | obj: rev, |
1797 | msg: self.query.msg, |
1798 | }, function (err, obj) { |
1799 | if (err && err.name === 'BlobNotFoundError') |
1800 | return self.askWantBlobs(err.links) |
1801 | if (err) return pull( |
1802 | pull.once(u.renderError(err).outerHTML), |
1803 | self.wrapPage('git commit ' + rev), |
1804 | self.respondSink(400) |
1805 | ) |
1806 | var msgDate = new Date(obj.msg.value.timestamp) |
1807 | self.app.git.getCommit(obj, function (err, commit) { |
1808 | var missingBlobs |
1809 | if (err && err.name === 'BlobNotFoundError') |
1810 | missingBlobs = err.links, err = null |
1811 | if (err) return pull( |
1812 | pull.once(u.renderError(err).outerHTML), |
1813 | self.wrapPage('git commit ' + rev), |
1814 | self.respondSink(400) |
1815 | ) |
1816 | pull( |
1817 | ph('section', [ |
1818 | ph('h3', ph('a', {href: ''}, rev)), |
1819 | ph('div', [ |
1820 | self.phIdLink(obj.msg.value.author), ' pushed ', |
1821 | ph('a', { |
1822 | href: self.app.render.toUrl(obj.msg.key), |
1823 | title: msgDate.toLocaleString(), |
1824 | }, htime(msgDate)) |
1825 | ]), |
1826 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
1827 | ph('div', [ |
1828 | self.gitAuthorLink(commit.committer), |
1829 | ' committed ', |
1830 | ph('span', {title: commit.committer.date.toLocaleString()}, |
1831 | htime(commit.committer.date)), |
1832 | ' in ', commit.committer.tz |
1833 | ]), |
1834 | commit.author ? ph('div', [ |
1835 | self.gitAuthorLink(commit.author), |
1836 | ' authored ', |
1837 | ph('span', {title: commit.author.date.toLocaleString()}, |
1838 | htime(commit.author.date)), |
1839 | ' in ', commit.author.tz |
1840 | ]) : '', |
1841 | commit.parents.length ? ph('div', ['parents: ', pull( |
1842 | pull.values(commit.parents), |
1843 | self.gitObjectLinks(obj.msg.key, 'commit') |
1844 | )]) : '', |
1845 | commit.tree ? ph('div', ['tree: ', pull( |
1846 | pull.once(commit.tree), |
1847 | self.gitObjectLinks(obj.msg.key, 'tree') |
1848 | )]) : '', |
1849 | h('blockquote', |
1850 | self.app.render.gitCommitBody(commit.body)).outerHTML, |
1851 | ph('h4', 'files'), |
1852 | ph('table', pull( |
1853 | self.app.git.readCommitChanges(commit), |
1854 | pull.map(function (file) { |
1855 | var msg = file.msg || obj.msg |
1856 | return ph('tr', [ |
1857 | ph('td', ph('code', u.escapeHTML(file.name))), |
1858 | ph('td', file.deleted ? 'deleted' |
1859 | : file.created ? |
1860 | ph('a', {href: |
1861 | self.app.render.toUrl('/git/blob/' |
1862 | + (file.hash[1] || file.hash[0]) |
1863 | + '?msg=' + encodeURIComponent(msg.key)) |
1864 | + '&commit=' + rev |
1865 | + '&path=' + encodeURIComponent(file.name) |
1866 | }, 'created') |
1867 | : file.hash ? |
1868 | ph('a', {href: |
1869 | self.app.render.toUrl('/git/diff/' |
1870 | + file.hash[0] + '..' + file.hash[1] |
1871 | + '?msg=' + encodeURIComponent(msg.key)) |
1872 | + '&commit=' + rev |
1873 | + '&path=' + encodeURIComponent(file.name) |
1874 | }, 'changed') |
1875 | : file.mode ? 'mode changed' |
1876 | : JSON.stringify(file)) |
1877 | ]) |
1878 | }), |
1879 | Catch(function (err) { |
1880 | if (err && err.name === 'ObjectNotFoundError') return |
1881 | if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links) |
1882 | return false |
1883 | }) |
1884 | )) |
1885 | ] |
1886 | ]), |
1887 | self.wrapPage('git commit ' + rev), |
1888 | self.respondSink(missingBlobs ? 409 : 200) |
1889 | ) |
1890 | }) |
1891 | }) |
1892 | } |
1893 | |
1894 | Serve.prototype.gitTag = function (rev) { |
1895 | var self = this |
1896 | if (!/[0-9a-f]{24}/.test(rev)) { |
1897 | return pull( |
1898 | ph('div.error', 'rev is not a git object id'), |
1899 | self.wrapPage('git'), |
1900 | self.respondSink(400) |
1901 | ) |
1902 | } |
1903 | if (!u.isRef(self.query.msg)) return pull( |
1904 | ph('div.error', 'missing message id'), |
1905 | self.wrapPage('git tag ' + rev), |
1906 | self.respondSink(400) |
1907 | ) |
1908 | |
1909 | self.app.git.openObject({ |
1910 | obj: rev, |
1911 | msg: self.query.msg, |
1912 | }, function (err, obj) { |
1913 | if (err && err.name === 'BlobNotFoundError') |
1914 | return self.askWantBlobs(err.links) |
1915 | if (err) return pull( |
1916 | pull.once(u.renderError(err).outerHTML), |
1917 | self.wrapPage('git tag ' + rev), |
1918 | self.respondSink(400) |
1919 | ) |
1920 | var msgDate = new Date(obj.msg.value.timestamp) |
1921 | self.app.git.getTag(obj, function (err, tag) { |
1922 | var missingBlobs |
1923 | if (err && err.name === 'BlobNotFoundError') |
1924 | missingBlobs = err.links, err = null |
1925 | if (err) return pull( |
1926 | pull.once(u.renderError(err).outerHTML), |
1927 | self.wrapPage('git tag ' + rev), |
1928 | self.respondSink(400) |
1929 | ) |
1930 | pull( |
1931 | ph('section', [ |
1932 | ph('h3', ph('a', {href: ''}, rev)), |
1933 | ph('div', [ |
1934 | self.phIdLink(obj.msg.value.author), ' pushed ', |
1935 | ph('a', { |
1936 | href: self.app.render.toUrl(obj.msg.key), |
1937 | title: msgDate.toLocaleString(), |
1938 | }, htime(msgDate)) |
1939 | ]), |
1940 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
1941 | ph('div', [ |
1942 | self.gitAuthorLink(tag.tagger), |
1943 | ' tagged ', |
1944 | ph('span', {title: tag.tagger.date.toLocaleString()}, |
1945 | htime(tag.tagger.date)), |
1946 | ' in ', tag.tagger.tz |
1947 | ]), |
1948 | tag.type, ' ', |
1949 | pull( |
1950 | pull.once(tag.object), |
1951 | self.gitObjectLinks(obj.msg.key, tag.type) |
1952 | ), ' ', |
1953 | ph('code', u.escapeHTML(tag.tag)), |
1954 | h('pre', self.app.render.linkify(tag.body)).outerHTML, |
1955 | ] |
1956 | ]), |
1957 | self.wrapPage('git tag ' + rev), |
1958 | self.respondSink(missingBlobs ? 409 : 200) |
1959 | ) |
1960 | }) |
1961 | }) |
1962 | } |
1963 | |
1964 | Serve.prototype.gitTree = function (rev) { |
1965 | var self = this |
1966 | if (!/[0-9a-f]{24}/.test(rev)) { |
1967 | return pull( |
1968 | ph('div.error', 'rev is not a git object id'), |
1969 | self.wrapPage('git'), |
1970 | self.respondSink(400) |
1971 | ) |
1972 | } |
1973 | if (!u.isRef(self.query.msg)) return pull( |
1974 | ph('div.error', 'missing message id'), |
1975 | self.wrapPage('git tree ' + rev), |
1976 | self.respondSink(400) |
1977 | ) |
1978 | |
1979 | self.app.git.openObject({ |
1980 | obj: rev, |
1981 | msg: self.query.msg, |
1982 | }, function (err, obj) { |
1983 | var missingBlobs |
1984 | if (err && err.name === 'BlobNotFoundError') |
1985 | missingBlobs = err.links, err = null |
1986 | if (err) return pull( |
1987 | pull.once(u.renderError(err).outerHTML), |
1988 | self.wrapPage('git tree ' + rev), |
1989 | self.respondSink(400) |
1990 | ) |
1991 | var msgDate = new Date(obj.msg.value.timestamp) |
1992 | pull( |
1993 | ph('section', [ |
1994 | ph('h3', ph('a', {href: ''}, rev)), |
1995 | ph('div', [ |
1996 | self.phIdLink(obj.msg.value.author), ' ', |
1997 | ph('a', { |
1998 | href: self.app.render.toUrl(obj.msg.key), |
1999 | title: msgDate.toLocaleString(), |
2000 | }, htime(msgDate)) |
2001 | ]), |
2002 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [ |
2003 | pull( |
2004 | self.app.git.readTreeFull(obj), |
2005 | pull.map(function (item) { |
2006 | if (!item.msg) return ph('tr', [ |
2007 | ph('td', |
2008 | u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')), |
2009 | ph('td', u.escapeHTML(item.hash)), |
2010 | ph('td', 'missing') |
2011 | ]) |
2012 | var ext = item.name.replace(/.*\./, '') |
2013 | var path = '/git/' + item.type + '/' + item.hash |
2014 | + '?msg=' + encodeURIComponent(item.msg.key) |
2015 | + (ext ? '&ext=' + ext : '') |
2016 | var fileDate = new Date(item.msg.value.timestamp) |
2017 | return ph('tr', [ |
2018 | ph('td', |
2019 | ph('a', {href: self.app.render.toUrl(path)}, |
2020 | u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))), |
2021 | ph('td', |
2022 | self.phIdLink(item.msg.value.author)), |
2023 | ph('td', |
2024 | ph('a', { |
2025 | href: self.app.render.toUrl(item.msg.key), |
2026 | title: fileDate.toLocaleString(), |
2027 | }, htime(fileDate)) |
2028 | ), |
2029 | ]) |
2030 | }), |
2031 | Catch(function (err) { |
2032 | if (err && err.name === 'ObjectNotFoundError') return |
2033 | if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links) |
2034 | return false |
2035 | }) |
2036 | ) |
2037 | ]), |
2038 | ]), |
2039 | self.wrapPage('git tree ' + rev), |
2040 | self.respondSink(missingBlobs ? 409 : 200) |
2041 | ) |
2042 | }) |
2043 | } |
2044 | |
2045 | Serve.prototype.gitBlob = function (rev) { |
2046 | var self = this |
2047 | if (!/[0-9a-f]{24}/.test(rev)) { |
2048 | return pull( |
2049 | ph('div.error', 'rev is not a git object id'), |
2050 | self.wrapPage('git'), |
2051 | self.respondSink(400) |
2052 | ) |
2053 | } |
2054 | if (!u.isRef(self.query.msg)) return pull( |
2055 | ph('div.error', 'missing message id'), |
2056 | self.wrapPage('git object ' + rev), |
2057 | self.respondSink(400) |
2058 | ) |
2059 | |
2060 | self.getMsgDecryptedMaybeOoo(self.query.msg, function (err, msg) { |
2061 | if (err) return pull( |
2062 | pull.once(u.renderError(err).outerHTML), |
2063 | self.wrapPage('git object ' + rev), |
2064 | self.respondSink(400) |
2065 | ) |
2066 | var msgDate = new Date(msg.value.timestamp) |
2067 | self.app.git.openObject({ |
2068 | obj: rev, |
2069 | msg: msg.key, |
2070 | }, function (err, obj) { |
2071 | var missingBlobs |
2072 | if (err && err.name === 'BlobNotFoundError') |
2073 | missingBlobs = err.links, err = null |
2074 | if (err) return pull( |
2075 | pull.once(u.renderError(err).outerHTML), |
2076 | self.wrapPage('git object ' + rev), |
2077 | self.respondSink(400) |
2078 | ) |
2079 | pull( |
2080 | ph('section', [ |
2081 | ph('h3', ph('a', {href: ''}, rev)), |
2082 | ph('div', [ |
2083 | self.phIdLink(msg.value.author), ' ', |
2084 | ph('a', { |
2085 | href: self.app.render.toUrl(msg.key), |
2086 | title: msgDate.toLocaleString(), |
2087 | }, htime(msgDate)) |
2088 | ]), |
2089 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull( |
2090 | self.app.git.readObject(obj), |
2091 | self.wrapBinary({ |
2092 | obj: obj, |
2093 | rawUrl: self.app.render.toUrl('/git/raw/' + rev |
2094 | + '?msg=' + encodeURIComponent(msg.key)), |
2095 | ext: self.query.ext |
2096 | }) |
2097 | ), |
2098 | ]), |
2099 | self.wrapPage('git blob ' + rev), |
2100 | self.respondSink(200) |
2101 | ) |
2102 | }) |
2103 | }) |
2104 | } |
2105 | |
2106 | Serve.prototype.gitDiff = function (revs) { |
2107 | var self = this |
2108 | var parts = revs.split('..') |
2109 | if (parts.length !== 2) return pull( |
2110 | ph('div.error', 'revs should be <rev1>..<rev2>'), |
2111 | self.wrapPage('git diff'), |
2112 | self.respondSink(400) |
2113 | ) |
2114 | var rev1 = parts[0] |
2115 | var rev2 = parts[1] |
2116 | if (!/[0-9a-f]{24}/.test(rev1)) return pull( |
2117 | ph('div.error', 'rev 1 is not a git object id'), |
2118 | self.wrapPage('git diff'), |
2119 | self.respondSink(400) |
2120 | ) |
2121 | if (!/[0-9a-f]{24}/.test(rev2)) return pull( |
2122 | ph('div.error', 'rev 2 is not a git object id'), |
2123 | self.wrapPage('git diff'), |
2124 | self.respondSink(400) |
2125 | ) |
2126 | |
2127 | if (!u.isRef(self.query.msg)) return pull( |
2128 | ph('div.error', 'missing message id'), |
2129 | self.wrapPage('git diff'), |
2130 | self.respondSink(400) |
2131 | ) |
2132 | |
2133 | var done = multicb({pluck: 1, spread: true}) |
2134 | // the msg qs param should point to the message for rev2 object. the msg for |
2135 | // rev1 object we will have to look up. |
2136 | self.app.git.getObjectMsg({ |
2137 | obj: rev1, |
2138 | headMsgId: self.query.msg, |
2139 | type: 'blob', |
2140 | }, done()) |
2141 | self.getMsgDecryptedMaybeOoo(self.query.msg, done()) |
2142 | done(function (err, msg1, msg2) { |
2143 | if (err && err.name === 'BlobNotFoundError') |
2144 | return self.askWantBlobs(err.links) |
2145 | if (err) return pull( |
2146 | pull.once(u.renderError(err).outerHTML), |
2147 | self.wrapPage('git diff ' + revs), |
2148 | self.respondSink(400) |
2149 | ) |
2150 | var msg1Date = new Date(msg1.value.timestamp) |
2151 | var msg2Date = new Date(msg2.value.timestamp) |
2152 | var revsShort = rev1.substr(0, 8) + '..' + rev2.substr(0, 8) |
2153 | pull( |
2154 | ph('section', [ |
2155 | ph('h3', ph('a', {href: ''}, revsShort)), |
2156 | ph('div', [ |
2157 | ph('a', { |
2158 | href: self.app.render.toUrl('/git/blob/' + rev1 + '?msg=' + encodeURIComponent(msg1.key)) |
2159 | }, rev1), ' ', |
2160 | self.phIdLink(msg1.value.author), ' ', |
2161 | ph('a', { |
2162 | href: self.app.render.toUrl(msg1.key), |
2163 | title: msg1Date.toLocaleString(), |
2164 | }, htime(msg1Date)) |
2165 | ]), |
2166 | ph('div', [ |
2167 | ph('a', { |
2168 | href: self.app.render.toUrl('/git/blob/' + rev2 + '?msg=' + encodeURIComponent(msg2.key)) |
2169 | }, rev2), ' ', |
2170 | self.phIdLink(msg2.value.author), ' ', |
2171 | ph('a', { |
2172 | href: self.app.render.toUrl(msg2.key), |
2173 | title: msg2Date.toLocaleString(), |
2174 | }, htime(msg2Date)) |
2175 | ]), |
2176 | u.readNext(function (cb) { |
2177 | var done = multicb({pluck: 1, spread: true}) |
2178 | self.app.git.openObject({ |
2179 | obj: rev1, |
2180 | msg: msg1.key, |
2181 | }, done()) |
2182 | self.app.git.openObject({ |
2183 | obj: rev2, |
2184 | msg: msg2.key, |
2185 | }, done()) |
2186 | /* |
2187 | self.app.git.guessCommitAndPath({ |
2188 | obj: rev2, |
2189 | msg: msg2.key, |
2190 | }, done()) |
2191 | */ |
2192 | done(function (err, obj1, obj2/*, info2*/) { |
2193 | if (err && err.name === 'BlobNotFoundError') |
2194 | return cb(null, self.askWantBlobsForm(err.links)) |
2195 | if (err) return cb(err) |
2196 | |
2197 | var done = multicb({pluck: 1, spread: true}) |
2198 | pull.collect(done())(self.app.git.readObject(obj1)) |
2199 | pull.collect(done())(self.app.git.readObject(obj2)) |
2200 | self.app.getLineComments({obj: obj2, hash: rev2}, done()) |
2201 | done(function (err, bufs1, bufs2, lineComments) { |
2202 | if (err) return cb(err) |
2203 | var str1 = Buffer.concat(bufs1, obj1.length).toString('utf8') |
2204 | var str2 = Buffer.concat(bufs2, obj2.length).toString('utf8') |
2205 | var diff = Diff.structuredPatch('', '', str1, str2) |
2206 | cb(null, self.gitDiffTable(diff, lineComments, { |
2207 | obj: obj2, |
2208 | hash: rev2, |
2209 | commit: self.query.commit, // info2.commit, |
2210 | path: self.query.path, // info2.path, |
2211 | })) |
2212 | }) |
2213 | }) |
2214 | }) |
2215 | ]), |
2216 | self.wrapPage('git diff'), |
2217 | self.respondSink(200) |
2218 | ) |
2219 | }) |
2220 | } |
2221 | |
2222 | Serve.prototype.gitDiffTable = function (diff, lineComments, lineCommentInfo) { |
2223 | var updateMsg = lineCommentInfo.obj.msg |
2224 | var self = this |
2225 | return pull( |
2226 | ph('table', [ |
2227 | pull( |
2228 | pull.values(diff.hunks), |
2229 | pull.map(function (hunk) { |
2230 | var oldLine = hunk.oldStart |
2231 | var newLine = hunk.newStart |
2232 | return [ |
2233 | ph('tr', [ |
2234 | ph('td', {colspan: 3}), |
2235 | ph('td', ph('pre', |
2236 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
2237 | '+' + newLine + ',' + hunk.newLines + ' @@')) |
2238 | ]), |
2239 | pull( |
2240 | pull.values(hunk.lines), |
2241 | pull.map(function (line) { |
2242 | var s = line[0] |
2243 | if (s == '\\') return |
2244 | var html = self.app.render.highlight(line) |
2245 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
2246 | var hash = lineCommentInfo.hash |
2247 | var newLineNum = lineNums[lineNums.length-1] |
2248 | var id = hash + '-' + (newLineNum || (lineNums[0] + '-')) |
2249 | var idEnc = encodeURIComponent(id) |
2250 | var allowComment = s !== '-' |
2251 | && self.query.commit && self.query.path |
2252 | return [ |
2253 | ph('tr', { |
2254 | class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
2255 | }, [ |
2256 | lineNums.map(function (num, i) { |
2257 | return ph('td', [ |
2258 | ph('a', { |
2259 | name: i === 0 ? idEnc : undefined, |
2260 | href: '#' + idEnc |
2261 | }, String(num)) |
2262 | ]) |
2263 | }), |
2264 | ph('td', |
2265 | allowComment ? ph('a', { |
2266 | href: '?msg=' + |
2267 | encodeURIComponent(self.query.msg) |
2268 | + '&comment=' + idEnc |
2269 | + '&commit=' + encodeURIComponent(self.query.commit) |
2270 | + '&path=' + encodeURIComponent(self.query.path) |
2271 | + '#' + idEnc |
2272 | }, '…') : '' |
2273 | ), |
2274 | ph('td', ph('pre', u.escapeHTML(html))) |
2275 | ]), |
2276 | (lineComments[newLineNum] ? |
2277 | ph('tr', |
2278 | ph('td', {colspan: 4}, |
2279 | self.renderLineCommentThread(lineComments[newLineNum], id) |
2280 | ) |
2281 | ) |
2282 | : newLineNum && lineCommentInfo && self.query.comment === id ? |
2283 | ph('tr', |
2284 | ph('td', {colspan: 4}, |
2285 | self.renderLineCommentForm({ |
2286 | id: id, |
2287 | line: newLineNum, |
2288 | updateId: updateMsg.key, |
2289 | blobId: hash, |
2290 | repoId: updateMsg.value.content.repo, |
2291 | commitId: lineCommentInfo.commit, |
2292 | filePath: lineCommentInfo.path, |
2293 | }) |
2294 | ) |
2295 | ) |
2296 | : '') |
2297 | ] |
2298 | }) |
2299 | ) |
2300 | ] |
2301 | }) |
2302 | ) |
2303 | ]) |
2304 | ) |
2305 | } |
2306 | |
2307 | Serve.prototype.renderLineCommentThread = function (lineComment, id) { |
2308 | return this.streamThreadWithComposer({ |
2309 | root: lineComment.msg.key, |
2310 | id: id, |
2311 | placeholder: 'reply to line comment thread' |
2312 | }) |
2313 | } |
2314 | |
2315 | Serve.prototype.renderLineCommentForm = function (opts) { |
2316 | return [ |
2317 | this.phComposer({ |
2318 | placeholder: 'comment on this line', |
2319 | id: opts.id, |
2320 | lineComment: opts |
2321 | }) |
2322 | ] |
2323 | } |
2324 | |
2325 | // return a composer, pull-hyperscript style |
2326 | Serve.prototype.phComposer = function (opts) { |
2327 | var self = this |
2328 | return u.readNext(function (cb) { |
2329 | self.composer(opts, function (err, composer) { |
2330 | if (err) return cb(err) |
2331 | cb(null, pull.once(composer.outerHTML)) |
2332 | }) |
2333 | }) |
2334 | } |
2335 | |
2336 | Serve.prototype.gitLineComment = function (path) { |
2337 | var self = this |
2338 | var id |
2339 | try { |
2340 | id = decodeURIComponent(String(path)) |
2341 | if (id[0] === '%') { |
2342 | return self.getMsgDecryptedMaybeOoo(id, gotMsg) |
2343 | } else { |
2344 | msg = JSON.parse(id) |
2345 | } |
2346 | } catch(e) { |
2347 | return gotMsg(e) |
2348 | } |
2349 | gotMsg(null, msg) |
2350 | function gotMsg(err, msg) { |
2351 | if (err) return pull( |
2352 | pull.once(u.renderError(err).outerHTML), |
2353 | self.respondSink(400, {'Content-Type': ctype('html')}) |
2354 | ) |
2355 | var c = msg && msg.value && msg.value.content |
2356 | if (!c) return pull( |
2357 | pull.once('Missing message ' + id), |
2358 | self.respondSink(500, {'Content-Type': ctype('html')}) |
2359 | ) |
2360 | self.app.git.diffFile({ |
2361 | msg: c.updateId, |
2362 | commit: c.commitId, |
2363 | path: c.filePath, |
2364 | }, function (err, file) { |
2365 | if (err && err.name === 'BlobNotFoundError') |
2366 | return self.askWantBlobs(err.links) |
2367 | if (err) return pull( |
2368 | pull.once(err.stack), |
2369 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
2370 | ) |
2371 | var path |
2372 | if (file.created) { |
2373 | path = '/git/blob/' + file.hash[1] |
2374 | + '?msg=' + encodeURIComponent(c.updateId) |
2375 | + '&commit=' + c.commitId |
2376 | + '&path=' + encodeURIComponent(c.filePath) |
2377 | + '#' + file.hash[1] + '-' + c.line |
2378 | } else { |
2379 | path = '/git/diff/' + file.hash[0] + '..' + file.hash[1] |
2380 | + '?msg=' + encodeURIComponent(c.updateId) |
2381 | + '&commit=' + c.commitId |
2382 | + '&path=' + encodeURIComponent(c.filePath) |
2383 | + '#' + file.hash[1] + '-' + c.line |
2384 | } |
2385 | var url = self.app.render.toUrl(path) |
2386 | /* |
2387 | return pull( |
2388 | ph('a', {href: url}, path), |
2389 | self.wrapPage(id), |
2390 | self.respondSink(200) |
2391 | ) |
2392 | */ |
2393 | self.redirect(url) |
2394 | }) |
2395 | } |
2396 | } |
2397 | |
2398 | Serve.prototype.gitObjectLinks = function (headMsgId, type) { |
2399 | var self = this |
2400 | return paramap(function (id, cb) { |
2401 | self.app.git.getObjectMsg({ |
2402 | obj: id, |
2403 | headMsgId: headMsgId, |
2404 | type: type, |
2405 | }, function (err, msg) { |
2406 | if (err && err.name === 'BlobNotFoundError') |
2407 | return cb(null, self.askWantBlobsForm(err.links)) |
2408 | if (err && err.name === 'ObjectNotFoundError') |
2409 | return cb(null, [ |
2410 | ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)']) |
2411 | if (err) return cb(err) |
2412 | var path = '/git/' + type + '/' + id |
2413 | + '?msg=' + encodeURIComponent(msg.key) |
2414 | cb(null, [ph('code', ph('a', { |
2415 | href: self.app.render.toUrl(path) |
2416 | }, u.escapeHTML(id.substr(0, 8)))), ' ']) |
2417 | }) |
2418 | }, 8) |
2419 | } |
2420 | |
2421 | Serve.prototype.npm = function (url) { |
2422 | var self = this |
2423 | var parts = url.split('/') |
2424 | var author = parts[1] && parts[1][0] === '@' |
2425 | ? u.unescapeId(parts.splice(1, 1)[0]) : null |
2426 | var name = parts[1] |
2427 | var version = parts[2] |
2428 | var distTag = parts[3] |
2429 | var prefix = 'npm:' + |
2430 | (name ? name + ':' + |
2431 | (version ? version + ':' + |
2432 | (distTag ? distTag + ':' : '') : '') : '') |
2433 | |
2434 | var render = self.app.render |
2435 | var base = '/npm/' + (author ? u.escapeId(author) + '/' : '') |
2436 | var pathWithoutAuthor = '/npm' + |
2437 | (name ? '/' + name + |
2438 | (version ? '/' + version + |
2439 | (distTag ? '/' + distTag : '') : '') : '') |
2440 | return pull( |
2441 | ph('section', {}, [ |
2442 | ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ', |
2443 | author ? [ |
2444 | self.phIdLink(author), ' ', |
2445 | ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '×')), |
2446 | ' : ' |
2447 | ] : '', |
2448 | name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '', |
2449 | version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '', |
2450 | distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : '' |
2451 | ]), |
2452 | ph('table', [ |
2453 | ph('thead', ph('tr', [ |
2454 | ph('th', 'publisher'), |
2455 | ph('th', 'package'), |
2456 | ph('th', 'version'), |
2457 | ph('th', 'tag'), |
2458 | ph('th', 'size'), |
2459 | ph('th', 'tarball'), |
2460 | ph('th', 'readme') |
2461 | ])), |
2462 | ph('tbody', pull( |
2463 | self.app.blobMentions({ |
2464 | name: {$prefix: prefix}, |
2465 | author: author, |
2466 | }), |
2467 | distTag && !version && pull.filter(function (link) { |
2468 | return link.name.split(':')[3] === distTag |
2469 | }), |
2470 | paramap(function (link, cb) { |
2471 | self.app.render.npmPackageMention(link, { |
2472 | withAuthor: true, |
2473 | author: author, |
2474 | name: name, |
2475 | version: version, |
2476 | distTag: distTag, |
2477 | }, cb) |
2478 | }, 4), |
2479 | pull.map(u.toHTML) |
2480 | )) |
2481 | ]) |
2482 | ]), |
2483 | self.wrapPage(prefix), |
2484 | self.respondSink(200) |
2485 | ) |
2486 | } |
2487 | |
2488 | Serve.prototype.npmPrebuilds = function (url) { |
2489 | var self = this |
2490 | var parts = url.split('/') |
2491 | var author = parts[1] && parts[1][0] === '@' |
2492 | ? u.unescapeId(parts.splice(1, 1)[0]) : null |
2493 | var name = parts[1] |
2494 | var version = parts[2] |
2495 | var prefix = 'prebuild:' + |
2496 | (name ? name + '-' + |
2497 | (version ? version + '-' : '') : '') |
2498 | |
2499 | var render = self.app.render |
2500 | var base = '/npm-prebuilds/' + (author ? u.escapeId(author) + '/' : '') |
2501 | return pull( |
2502 | ph('section', {}, [ |
2503 | ph('h3', [ph('a', {href: render.toUrl('/npm-prebuilds/')}, 'npm prebuilds'), ' : ', |
2504 | name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '', |
2505 | version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version)] : '', |
2506 | ]), |
2507 | ph('table', [ |
2508 | ph('thead', ph('tr', [ |
2509 | ph('th', 'publisher'), |
2510 | ph('th', 'name'), |
2511 | ph('th', 'version'), |
2512 | ph('th', 'runtime'), |
2513 | ph('th', 'abi'), |
2514 | ph('th', 'platform+libc'), |
2515 | ph('th', 'arch'), |
2516 | ph('th', 'size'), |
2517 | ph('th', 'tarball') |
2518 | ])), |
2519 | ph('tbody', pull( |
2520 | self.app.blobMentions({ |
2521 | name: {$prefix: prefix}, |
2522 | author: author, |
2523 | }), |
2524 | paramap(function (link, cb) { |
2525 | self.app.render.npmPrebuildMention(link, { |
2526 | withAuthor: true, |
2527 | author: author, |
2528 | name: name, |
2529 | version: version, |
2530 | }, cb) |
2531 | }, 4), |
2532 | pull.map(u.toHTML) |
2533 | )) |
2534 | ]) |
2535 | ]), |
2536 | self.wrapPage(prefix), |
2537 | self.respondSink(200) |
2538 | ) |
2539 | } |
2540 | |
2541 | Serve.prototype.npmReadme = function (url) { |
2542 | var self = this |
2543 | var id = decodeURIComponent(url.substr(1)) |
2544 | return pull( |
2545 | ph('section', {}, [ |
2546 | ph('h3', [ |
2547 | 'npm readme for ', |
2548 | ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…') |
2549 | ]), |
2550 | ph('blockquote', u.readNext(function (cb) { |
2551 | self.app.getNpmReadme(id, function (err, readme, isMarkdown) { |
2552 | if (err) return cb(null, ph('div', u.renderError(err).outerHTML)) |
2553 | cb(null, isMarkdown |
2554 | ? ph('div', self.app.render.markdown(readme)) |
2555 | : ph('pre', readme)) |
2556 | }) |
2557 | })) |
2558 | ]), |
2559 | self.wrapPage('npm readme'), |
2560 | self.respondSink(200) |
2561 | ) |
2562 | } |
2563 | |
2564 | Serve.prototype.npmRegistry = function (url) { |
2565 | var self = this |
2566 | self.req.url = url |
2567 | self.app.serveSsbNpmRegistry(self.req, self.res) |
2568 | } |
2569 | |
2570 | Serve.prototype.markdown = function (url) { |
2571 | var self = this |
2572 | var id = decodeURIComponent(url.substr(1)) |
2573 | var blobs = self.app.sbot.blobs |
2574 | return pull( |
2575 | ph('section', {}, [ |
2576 | ph('h3', [ |
2577 | ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…') |
2578 | ]), |
2579 | u.readNext(function (cb) { |
2580 | blobs.size(id, function (err, size) { |
2581 | if (size == null) return cb(null, self.askWantBlobsForm([id])) |
2582 | pull(blobs.get(id), pull.collect(function (err, chunks) { |
2583 | if (err) return cb(null, ph('div', u.renderError(err).outerHTML)) |
2584 | var text = Buffer.concat(chunks).toString() |
2585 | cb(null, ph('blockquote', self.app.render.markdown(text))) |
2586 | })) |
2587 | }) |
2588 | }) |
2589 | ]), |
2590 | self.wrapPage('markdown'), |
2591 | self.respondSink(200) |
2592 | ) |
2593 | } |
2594 | |
2595 | Serve.prototype.zip = function (url) { |
2596 | var self = this |
2597 | var parts = url.split('/').slice(1) |
2598 | var id = decodeURIComponent(parts.shift()) |
2599 | var filename = parts.join('/') |
2600 | var blobs = self.app.sbot.blobs |
2601 | var etag = id + filename |
2602 | var index = filename === '' || /\/$/.test(filename) |
2603 | var indexFilename = index && (filename + 'index.html') |
2604 | if (filename === '/' || /\/\/$/.test(filename)) { |
2605 | // force directory listing if path ends in // |
2606 | filename = filename.replace(/\/$/, '') |
2607 | indexFilename = false |
2608 | } |
2609 | var files = index && [] |
2610 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
2611 | blobs.size(id, function (err, size) { |
2612 | if (size == null) return askWantBlobsForm([id]) |
2613 | if (err) { |
2614 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
2615 | else return self.respond(500, err.message || err) |
2616 | } |
2617 | var unzip = require('unzip') |
2618 | var parseUnzip = unzip.Parse() |
2619 | var gotEntry = false |
2620 | parseUnzip.on('entry', function (entry) { |
2621 | if (index) { |
2622 | if (!gotEntry) { |
2623 | if (entry.path === indexFilename) { |
2624 | gotEntry = true |
2625 | return serveFile(entry) |
2626 | } else if (entry.path.substr(0, filename.length) === filename) { |
2627 | files.push({path: entry.path, type: entry.type, props: entry.props}) |
2628 | } |
2629 | } |
2630 | } else { |
2631 | if (!gotEntry && entry.path === filename) { |
2632 | gotEntry = true |
2633 | // if (false && entry.type === 'Directory') return serveDirectory(entry) |
2634 | return serveFile(entry) |
2635 | } |
2636 | } |
2637 | entry.autodrain() |
2638 | }) |
2639 | parseUnzip.on('close', function () { |
2640 | if (gotEntry) return |
2641 | if (!index) return self.respond(404, 'Entry not found') |
2642 | pull( |
2643 | ph('section', {}, [ |
2644 | ph('h3', [ |
2645 | ph('a', {href: self.app.render.toUrl('/links/' + id)}, id.substr(0, 8) + '…'), |
2646 | ' ', |
2647 | ph('a', {href: self.app.render.toUrl('/zip/' + encodeURIComponent(id) + '/' + filename)}, filename || '/'), |
2648 | ]), |
2649 | pull( |
2650 | pull.values(files), |
2651 | pull.map(function (file) { |
2652 | var path = '/zip/' + encodeURIComponent(id) + '/' + file.path |
2653 | return ph('li', [ |
2654 | ph('a', {href: self.app.render.toUrl(path)}, file.path) |
2655 | ]) |
2656 | }) |
2657 | ) |
2658 | ]), |
2659 | self.wrapPage(id + filename), |
2660 | self.respondSink(200) |
2661 | ) |
2662 | gotEntry = true // so that the handler on error event does not run |
2663 | }) |
2664 | parseUnzip.on('error', function (err) { |
2665 | if (!gotEntry) return self.respond(400, err.message) |
2666 | }) |
2667 | var size |
2668 | function serveFile(entry) { |
2669 | size = entry.size |
2670 | pull( |
2671 | toPull.source(entry), |
2672 | ident(gotType), |
2673 | self.respondSink() |
2674 | ) |
2675 | } |
2676 | pull( |
2677 | self.app.getBlob(id), |
2678 | toPull(parseUnzip) |
2679 | ) |
2680 | function gotType(type) { |
2681 | type = type && mime.lookup(type) |
2682 | if (type) self.res.setHeader('Content-Type', type) |
2683 | if (size) self.res.setHeader('Content-Length', size) |
2684 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
2685 | self.res.setHeader('etag', etag) |
2686 | self.res.writeHead(200) |
2687 | } |
2688 | }) |
2689 | } |
2690 | |
2691 | Serve.prototype.web = function (url) { |
2692 | var self = this |
2693 | var id = decodeURIComponent(url.substr(1)) |
2694 | |
2695 | var components = url.split('/') |
2696 | if (components[0] === '') components.shift() |
2697 | components[0] = decodeURIComponent(components[0]) |
2698 | |
2699 | var type = mime.lookup(components[components.length - 1]) |
2700 | var headers = {} |
2701 | if (type) headers['Content-Type'] = type |
2702 | webresolve(this.app.sbot, components, function (err, res) { |
2703 | if (err) { |
2704 | return pull( |
2705 | pull.once(err.toString()), |
2706 | self.respondSink(404) |
2707 | ) |
2708 | } |
2709 | headers['Content-Length'] = res.length |
2710 | return pull( |
2711 | pull.once(res), |
2712 | self.respondSink(200, headers) |
2713 | ) |
2714 | }) |
2715 | } |
2716 | |
2717 | // wrap a binary source and render it or turn into an embed |
2718 | Serve.prototype.wrapBinary = function (opts) { |
2719 | var self = this |
2720 | var ext = opts.ext |
2721 | var hash = opts.obj.hash |
2722 | return function (read) { |
2723 | var readRendered, type |
2724 | read = ident(function (_ext) { |
2725 | if (_ext) ext = _ext |
2726 | type = ext && mime.lookup(ext) || 'text/plain' |
2727 | })(read) |
2728 | return function (abort, cb) { |
2729 | if (readRendered) return readRendered(abort, cb) |
2730 | if (abort) return read(abort, cb) |
2731 | if (!type) read(null, function (end, buf) { |
2732 | if (end) return cb(end) |
2733 | if (!type) return cb(new Error('unable to get type')) |
2734 | readRendered = pickSource(type, cat([pull.once(buf), read])) |
2735 | readRendered(null, cb) |
2736 | }) |
2737 | } |
2738 | } |
2739 | function pickSource(type, read) { |
2740 | if (/^image\//.test(type)) { |
2741 | read(true, function (err) { |
2742 | if (err && err !== true) console.trace(err) |
2743 | }) |
2744 | return ph('img', { |
2745 | src: opts.rawUrl |
2746 | }) |
2747 | } |
2748 | if (type === 'text/markdown') { |
2749 | // TODO: rewrite links to files/images to be correct |
2750 | return ph('blockquote', u.readNext(function (cb) { |
2751 | pull.collect(function (err, bufs) { |
2752 | if (err) return cb(pull.error(err)) |
2753 | var text = Buffer.concat(bufs).toString('utf8') |
2754 | return cb(null, pull.once(self.app.render.markdown(text))) |
2755 | })(read) |
2756 | })) |
2757 | } |
2758 | var i = 1 |
2759 | var updateMsg = opts.obj.msg |
2760 | var commitId = self.query.commit |
2761 | var filePath = self.query.path |
2762 | var lineComments = opts.lineComments || {} |
2763 | return u.readNext(function (cb) { |
2764 | if (commitId && filePath) { |
2765 | self.app.getLineComments({ |
2766 | obj: opts.obj, |
2767 | hash: hash, |
2768 | }, gotLineComments) |
2769 | } else { |
2770 | gotLineComments(null, {}) |
2771 | } |
2772 | function gotLineComments(err, lineComments) { |
2773 | if (err) return cb(err) |
2774 | cb(null, ph('table', |
2775 | pull( |
2776 | read, |
2777 | utf8(), |
2778 | split(), |
2779 | pull.map(function (line) { |
2780 | var lineNum = i++ |
2781 | var id = hash + '-' + lineNum |
2782 | var idEnc = encodeURIComponent(id) |
2783 | var allowComment = self.query.commit && self.query.path |
2784 | return [ |
2785 | ph('tr', [ |
2786 | ph('td', |
2787 | allowComment ? ph('a', { |
2788 | href: '?msg=' + encodeURIComponent(self.query.msg) |
2789 | + '&commit=' + encodeURIComponent(self.query.commit) |
2790 | + '&path=' + encodeURIComponent(self.query.path) |
2791 | + '&comment=' + idEnc |
2792 | + '#' + idEnc |
2793 | }, '…') : '' |
2794 | ), |
2795 | ph('td', ph('a', { |
2796 | name: id, |
2797 | href: '#' + idEnc |
2798 | }, String(lineNum))), |
2799 | ph('td', ph('pre', self.app.render.highlight(line, ext))) |
2800 | ]), |
2801 | lineComments[lineNum] ? ph('tr', |
2802 | ph('td', {colspan: 4}, |
2803 | self.renderLineCommentThread(lineComments[lineNum], id) |
2804 | ) |
2805 | ) : '', |
2806 | self.query.comment === id ? ph('tr', |
2807 | ph('td', {colspan: 4}, |
2808 | self.renderLineCommentForm({ |
2809 | id: id, |
2810 | line: lineNum, |
2811 | updateId: updateMsg.key, |
2812 | repoId: updateMsg.value.content.repo, |
2813 | commitId: commitId, |
2814 | blobId: hash, |
2815 | filePath: filePath, |
2816 | }) |
2817 | ) |
2818 | ) : '' |
2819 | ] |
2820 | }) |
2821 | ) |
2822 | )) |
2823 | } |
2824 | }) |
2825 | } |
2826 | } |
2827 | |
2828 | Serve.prototype.wrapPublic = function (opts) { |
2829 | var self = this |
2830 | return u.hyperwrap(function (thread, cb) { |
2831 | self.composer({ |
2832 | channel: '', |
2833 | }, function (err, composer) { |
2834 | if (err) return cb(err) |
2835 | cb(null, [ |
2836 | composer, |
2837 | thread |
2838 | ]) |
2839 | }) |
2840 | }) |
2841 | } |
2842 | |
2843 | Serve.prototype.askWantBlobsForm = function (links) { |
2844 | var self = this |
2845 | return ph('form', {action: '', method: 'post'}, [ |
2846 | ph('section', [ |
2847 | ph('h3', 'Missing blobs'), |
2848 | ph('p', 'The application needs these blobs to continue:'), |
2849 | ph('table', links.map(u.toLink).map(function (link) { |
2850 | if (!u.isRef(link.link)) return |
2851 | return ph('tr', [ |
2852 | ph('td', ph('code', link.link)), |
2853 | !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '', |
2854 | ]) |
2855 | })), |
2856 | ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), |
2857 | ph('input', {type: 'hidden', name: 'blob_ids', |
2858 | value: links.map(u.linkDest).join(',')}), |
2859 | ph('p', ph('input', {type: 'submit', value: 'Want Blobs'})) |
2860 | ]) |
2861 | ]) |
2862 | } |
2863 | |
2864 | Serve.prototype.askWantBlobs = function (links) { |
2865 | var self = this |
2866 | pull( |
2867 | self.askWantBlobsForm(links), |
2868 | self.wrapPage('missing blobs'), |
2869 | self.respondSink(409) |
2870 | ) |
2871 | } |
2872 | |
2873 | Serve.prototype.wrapPrivate = function (opts) { |
2874 | var self = this |
2875 | return u.hyperwrap(function (thread, cb) { |
2876 | self.composer({ |
2877 | placeholder: 'private message', |
2878 | private: true, |
2879 | }, function (err, composer) { |
2880 | if (err) return cb(err) |
2881 | cb(null, [ |
2882 | composer, |
2883 | thread |
2884 | ]) |
2885 | }) |
2886 | }) |
2887 | } |
2888 | |
2889 | Serve.prototype.wrapThread = function (opts) { |
2890 | var self = this |
2891 | return u.hyperwrap(function (thread, cb) { |
2892 | self.app.render.prepareLinks(opts.recps, function (err, recps) { |
2893 | if (err) return cb(er) |
2894 | self.composer({ |
2895 | placeholder: opts.placeholder |
2896 | || (recps ? 'private reply' : 'reply'), |
2897 | id: 'reply', |
2898 | root: opts.root, |
2899 | post: opts.post, |
2900 | channel: opts.channel || '', |
2901 | branches: opts.branches, |
2902 | postBranches: opts.postBranches, |
2903 | recps: recps, |
2904 | private: opts.recps != null, |
2905 | }, function (err, composer) { |
2906 | if (err) return cb(err) |
2907 | cb(null, [ |
2908 | thread, |
2909 | composer |
2910 | ]) |
2911 | }) |
2912 | }) |
2913 | }) |
2914 | } |
2915 | |
2916 | Serve.prototype.wrapNew = function (opts) { |
2917 | var self = this |
2918 | return u.hyperwrap(function (thread, cb) { |
2919 | self.composer({ |
2920 | channel: '', |
2921 | }, function (err, composer) { |
2922 | if (err) return cb(err) |
2923 | cb(null, [ |
2924 | composer, |
2925 | h('table.ssb-msgs', |
2926 | thread, |
2927 | h('tr', h('td.paginate.msg-left', {colspan: 3}, |
2928 | h('form', {method: 'get', action: ''}, |
2929 | h('input', {type: 'hidden', name: 'gt', value: opts.gt}), |
2930 | h('input', {type: 'hidden', name: 'catchup', value: '1'}), |
2931 | h('input', {type: 'submit', value: 'catchup'}) |
2932 | ) |
2933 | )) |
2934 | ) |
2935 | ]) |
2936 | }) |
2937 | }) |
2938 | } |
2939 | |
2940 | Serve.prototype.wrapChannel = function (channel) { |
2941 | var self = this |
2942 | return u.hyperwrap(function (thread, cb) { |
2943 | self.composer({ |
2944 | placeholder: 'public message in #' + channel, |
2945 | channel: channel, |
2946 | }, function (err, composer) { |
2947 | if (err) return cb(err) |
2948 | cb(null, [ |
2949 | h('section', |
2950 | h('h3.feed-name', |
2951 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel) |
2952 | ) |
2953 | ), |
2954 | composer, |
2955 | thread |
2956 | ]) |
2957 | }) |
2958 | }) |
2959 | } |
2960 | |
2961 | Serve.prototype.wrapType = function (type) { |
2962 | var self = this |
2963 | return u.hyperwrap(function (thread, cb) { |
2964 | cb(null, [ |
2965 | h('section', |
2966 | h('h3.feed-name', |
2967 | h('a', {href: self.app.render.toUrl('/type/' + type)}, |
2968 | h('code', type), 's')) |
2969 | ), |
2970 | thread |
2971 | ]) |
2972 | }) |
2973 | } |
2974 | |
2975 | Serve.prototype.wrapLinks = function (dest) { |
2976 | var self = this |
2977 | return u.hyperwrap(function (thread, cb) { |
2978 | cb(null, [ |
2979 | h('section', |
2980 | h('h3.feed-name', 'links: ', |
2981 | h('a', {href: self.app.render.toUrl('/links/' + dest)}, |
2982 | h('code', dest))) |
2983 | ), |
2984 | thread |
2985 | ]) |
2986 | }) |
2987 | } |
2988 | |
2989 | Serve.prototype.wrapPeers = function (opts) { |
2990 | var self = this |
2991 | return u.hyperwrap(function (peers, cb) { |
2992 | cb(null, [ |
2993 | h('section', |
2994 | h('h3', 'Peers') |
2995 | ), |
2996 | peers |
2997 | ]) |
2998 | }) |
2999 | } |
3000 | |
3001 | Serve.prototype.wrapChannels = function (opts) { |
3002 | var self = this |
3003 | return u.hyperwrap(function (channels, cb) { |
3004 | cb(null, [ |
3005 | h('section', |
3006 | h('h4', 'Network') |
3007 | ), |
3008 | h('section', |
3009 | channels |
3010 | ) |
3011 | ]) |
3012 | }) |
3013 | } |
3014 | |
3015 | Serve.prototype.wrapMyChannels = function (opts) { |
3016 | var self = this |
3017 | return u.hyperwrap(function (channels, cb) { |
3018 | cb(null, [ |
3019 | h('section', |
3020 | h('h4', 'Subscribed') |
3021 | ), |
3022 | h('section', |
3023 | channels |
3024 | ) |
3025 | ]) |
3026 | }) |
3027 | } |
3028 | |
3029 | Serve.prototype.composer = function (opts, cb) { |
3030 | var self = this |
3031 | opts = opts || {} |
3032 | var data = self.data |
3033 | var myId = self.app.sbot.id |
3034 | |
3035 | if (opts.id && data.composer_id && opts.id !== data.composer_id) { |
3036 | // don't share data between multiple composers |
3037 | data = {} |
3038 | } |
3039 | |
3040 | if (!data.text && self.query.text) data.text = self.query.text |
3041 | if (!data.action && self.query.action) data.action = self.query.action |
3042 | |
3043 | var blobs = u.tryDecodeJSON(data.blobs) || {} |
3044 | if (data.upload && typeof data.upload === 'object') { |
3045 | blobs[data.upload.link] = { |
3046 | type: data.upload.type, |
3047 | size: data.upload.size, |
3048 | key: data.upload.key, |
3049 | } |
3050 | } |
3051 | if (data.blob_type && blobs[data.blob_link]) { |
3052 | blobs[data.blob_link].type = data.blob_type |
3053 | } |
3054 | var channel = data.channel != null ? data.channel : opts.channel |
3055 | |
3056 | var formNames = {} |
3057 | var mentionIds = u.toArray(data.mention_id) |
3058 | var mentionNames = u.toArray(data.mention_name) |
3059 | for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) { |
3060 | formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0] |
3061 | } |
3062 | |
3063 | var formEmojiNames = {} |
3064 | var emojiIds = u.toArray(data.emoji_id) |
3065 | var emojiNames = u.toArray(data.emoji_name) |
3066 | for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) { |
3067 | var upload = data['emoji_upload_' + i] |
3068 | formEmojiNames[emojiNames[i]] = |
3069 | (upload && upload.link) || u.extractBlobIds(emojiIds[i])[0] |
3070 | if (upload) blobs[upload.link] = { |
3071 | type: upload.type, |
3072 | size: upload.size, |
3073 | } |
3074 | } |
3075 | |
3076 | if (data.upload) { |
3077 | var href = data.upload.link |
3078 | + (data.upload.key ? '?unbox=' + data.upload.key + '.boxs': '') |
3079 | // TODO: be able to change the content-type |
3080 | var isImage = /^image\//.test(data.upload.type) |
3081 | data.text = (data.text ? data.text + '\n' : '') |
3082 | + (isImage ? '!' : '') |
3083 | + '[' + data.upload.name + '](' + href + ')' |
3084 | } |
3085 | |
3086 | // get bare feed names |
3087 | var unknownMentionNames = {} |
3088 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
3089 | var unknownMentions = mentions |
3090 | .filter(function (mention) { |
3091 | return mention.link === '@' |
3092 | }) |
3093 | .map(function (mention) { |
3094 | return mention.name |
3095 | }) |
3096 | .filter(uniques()) |
3097 | .map(function (name) { |
3098 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
3099 | return {name: name, id: id} |
3100 | }) |
3101 | |
3102 | var emoji = mentions |
3103 | .filter(function (mention) { return mention.emoji }) |
3104 | .map(function (mention) { return mention.name }) |
3105 | .filter(uniques()) |
3106 | .map(function (name) { |
3107 | // 1. check emoji-image mapping for this message |
3108 | var id = formEmojiNames[name] |
3109 | if (id) return {name: name, id: id} |
3110 | // 2. TODO: check user's preferred emoji-image mapping |
3111 | // 3. check builtin emoji |
3112 | var link = self.getBuiltinEmojiLink(name) |
3113 | if (link) { |
3114 | return {name: name, id: link.link} |
3115 | blobs[id] = {type: link.type, size: link.size} |
3116 | } |
3117 | // 4. check recently seen emoji |
3118 | id = self.app.getReverseEmojiNameSync(name) |
3119 | return {name: name, id: id} |
3120 | }) |
3121 | |
3122 | // strip content other than names and feed ids from the recps field |
3123 | if (data.recps) { |
3124 | data.recps = recpsToFeedIds(data.recps) |
3125 | } |
3126 | |
3127 | var done = multicb({pluck: 1, spread: true}) |
3128 | done()(null, h('section.composer', |
3129 | h('form', {method: 'post', action: opts.id ? '#' + opts.id : '', |
3130 | enctype: 'multipart/form-data'}, |
3131 | h('input', {type: 'hidden', name: 'blobs', |
3132 | value: JSON.stringify(blobs)}), |
3133 | h('input', {type: 'hidden', name: 'composer_id', value: opts.id}), |
3134 | opts.recps ? self.app.render.privateLine(opts.recps, done()) : |
3135 | opts.private ? h('div', h('input.recps-input', {name: 'recps', |
3136 | value: data.recps || '', placeholder: 'recipient ids'})) : '', |
3137 | channel != null ? |
3138 | h('div', '#', h('input', {name: 'channel', placeholder: 'channel', |
3139 | value: channel})) : '', |
3140 | opts.root !== opts.post ? h('div', |
3141 | h('label', {for: 'fork_thread'}, |
3142 | h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}), |
3143 | ' fork thread' |
3144 | ) |
3145 | ) : '', |
3146 | h('textarea', { |
3147 | id: opts.id, |
3148 | name: 'text', |
3149 | rows: Math.max(4, u.rows(data.text)), |
3150 | cols: 70, |
3151 | placeholder: opts.placeholder || 'public message', |
3152 | }, data.text || ''), |
3153 | unknownMentions.length > 0 ? [ |
3154 | h('div', h('em', 'names:')), |
3155 | h('ul.mentions', unknownMentions.map(function (mention) { |
3156 | return h('li', |
3157 | h('code', '@' + mention.name), ': ', |
3158 | h('input', {name: 'mention_name', type: 'hidden', |
3159 | value: mention.name}), |
3160 | h('input.id-input', {name: 'mention_id', size: 60, |
3161 | value: mention.id, placeholder: '@id'})) |
3162 | })) |
3163 | ] : '', |
3164 | emoji.length > 0 ? [ |
3165 | h('div', h('em', 'emoji:')), |
3166 | h('ul.mentions', emoji.map(function (link, i) { |
3167 | return h('li', |
3168 | h('code', link.name), ': ', |
3169 | h('input', {name: 'emoji_name', type: 'hidden', |
3170 | value: link.name}), |
3171 | h('input.id-input', {name: 'emoji_id', size: 60, |
3172 | value: link.id, placeholder: '&id'}), ' ', |
3173 | h('input', {type: 'file', name: 'emoji_upload_' + i})) |
3174 | })) |
3175 | ] : '', |
3176 | h('table.ssb-msgs', |
3177 | h('tr.msg-row', |
3178 | h('td.msg-left', {colspan: 2}, |
3179 | opts.private ? |
3180 | h('input', {type: 'hidden', name: 'private', value: '1'}) : '', |
3181 | h('input', {type: 'file', name: 'upload'}) |
3182 | ), |
3183 | h('td.msg-right', |
3184 | h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ', |
3185 | h('input', {type: 'submit', name: 'action', value: 'preview'}) |
3186 | ) |
3187 | ) |
3188 | ), |
3189 | data.action === 'preview' ? preview(false, done()) : |
3190 | data.action === 'raw' ? preview(true, done()) : '' |
3191 | ) |
3192 | )) |
3193 | done(cb) |
3194 | |
3195 | function recpsToFeedIds (recps) { |
3196 | var res = data.recps.split(',') |
3197 | .map(function (str) { |
3198 | str = str.trim() |
3199 | var ids = u.extractFeedIds(str).filter(uniques()) |
3200 | if (ids.length >= 1) { |
3201 | return ids[0] |
3202 | } else { |
3203 | ids = u.extractFeedIds(self.app.getReverseNameSync(str)) |
3204 | if (ids.length >= 1) { |
3205 | return ids[0] |
3206 | } else { |
3207 | return null |
3208 | } |
3209 | } |
3210 | }) |
3211 | .filter(Boolean) |
3212 | return res.join(', ') |
3213 | } |
3214 | |
3215 | function prepareContent(cb) { |
3216 | var done = multicb({pluck: 1}) |
3217 | content = { |
3218 | type: 'post', |
3219 | text: String(data.text).replace(/\r\n/g, '\n'), |
3220 | } |
3221 | if (opts.lineComment) { |
3222 | content.type = 'line-comment' |
3223 | content.updateId = opts.lineComment.updateId |
3224 | content.repo = opts.lineComment.repoId |
3225 | content.commitId = opts.lineComment.commitId |
3226 | content.filePath = opts.lineComment.filePath |
3227 | content.blobId = opts.lineComment.blobId |
3228 | content.line = opts.lineComment.line |
3229 | } |
3230 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
3231 | .filter(function (mention) { |
3232 | if (mention.emoji) { |
3233 | mention.link = formEmojiNames[mention.name] |
3234 | if (!mention.link) { |
3235 | var link = self.getBuiltinEmojiLink(mention.name) |
3236 | if (link) { |
3237 | mention.link = link.link |
3238 | mention.size = link.size |
3239 | mention.type = link.type |
3240 | } else { |
3241 | mention.link = self.app.getReverseEmojiNameSync(mention.name) |
3242 | if (!mention.link) return false |
3243 | } |
3244 | } |
3245 | } |
3246 | var blob = blobs[mention.link] |
3247 | if (blob) { |
3248 | if (!isNaN(blob.size)) |
3249 | mention.size = blob.size |
3250 | if (blob.type && blob.type !== 'application/octet-stream') |
3251 | mention.type = blob.type |
3252 | } else if (mention.link === '@') { |
3253 | // bare feed name |
3254 | var name = mention.name |
3255 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
3256 | if (id) mention.link = id |
3257 | else return false |
3258 | } |
3259 | if (mention.link && mention.link[0] === '&' && mention.size == null) { |
3260 | var linkCb = done() |
3261 | self.app.sbot.blobs.size(mention.link, function (err, size) { |
3262 | if (!err && size != null) mention.size = size |
3263 | linkCb() |
3264 | }) |
3265 | } |
3266 | return true |
3267 | }) |
3268 | if (mentions.length) content.mentions = mentions |
3269 | if (data.recps != null) { |
3270 | if (opts.recps) return cb(new Error('got recps in opts and data')) |
3271 | content.recps = [myId] |
3272 | u.extractFeedIds(data.recps).forEach(function (recp) { |
3273 | if (content.recps.indexOf(recp) === -1) content.recps.push(recp) |
3274 | }) |
3275 | } else { |
3276 | if (opts.recps) content.recps = opts.recps |
3277 | } |
3278 | if (data.fork_thread) { |
3279 | content.root = opts.post || undefined |
3280 | content.branch = u.fromArray(opts.postBranches) || undefined |
3281 | } else { |
3282 | content.root = opts.root || undefined |
3283 | content.branch = u.fromArray(opts.branches) || undefined |
3284 | } |
3285 | if (channel) content.channel = data.channel |
3286 | |
3287 | done(function (err) { |
3288 | cb(err, content) |
3289 | }) |
3290 | } |
3291 | |
3292 | function preview(raw, cb) { |
3293 | var msgContainer = h('table.ssb-msgs') |
3294 | var contentInput = h('input', {type: 'hidden', name: 'content'}) |
3295 | var warningsContainer = h('div') |
3296 | var sizeEl = h('span') |
3297 | |
3298 | var content |
3299 | try { content = JSON.parse(data.text) } |
3300 | catch (err) {} |
3301 | if (content) gotContent(null, content) |
3302 | else prepareContent(gotContent) |
3303 | |
3304 | function gotContent(err, content) { |
3305 | if (err) return cb(err) |
3306 | contentInput.value = JSON.stringify(content) |
3307 | var msg = { |
3308 | value: { |
3309 | author: myId, |
3310 | timestamp: Date.now(), |
3311 | content: content |
3312 | } |
3313 | } |
3314 | if (content.recps) msg.value.private = true |
3315 | |
3316 | var warnings = [] |
3317 | u.toLinkArray(content.mentions).forEach(function (link) { |
3318 | if (link.emoji && link.size >= 10e3) { |
3319 | warnings.push(h('li', |
3320 | 'emoji ', h('q', link.name), |
3321 | ' (', h('code', String(link.link).substr(0, 8) + '…'), ')' |
3322 | + ' is >10KB')) |
3323 | } else if (link.link[0] === '&' && link.size >= 10e6 && link.type) { |
3324 | // if link.type is set, we probably just uploaded this blob |
3325 | warnings.push(h('li', |
3326 | 'attachment ', |
3327 | h('code', String(link.link).substr(0, 8) + '…'), |
3328 | ' is >10MB')) |
3329 | } |
3330 | }) |
3331 | |
3332 | var draftMsg = { |
3333 | key: '%0000000000000000000000000000000000000000000=.sha256', |
3334 | value: { |
3335 | previous: '%0000000000000000000000000000000000000000000=.sha256', |
3336 | author: '@0000000000000000000000000000000000000000000=.ed25519', |
3337 | sequence: 1000, |
3338 | timestamp: 1000000000000, |
3339 | hash: 'sha256', |
3340 | content: content |
3341 | } |
3342 | } |
3343 | var estSize = JSON.stringify(draftMsg, null, 2).length |
3344 | sizeEl.innerHTML = self.app.render.formatSize(estSize) |
3345 | if (estSize > 8192) warnings.push(h('li', 'message is too long')) |
3346 | |
3347 | if (warnings.length) { |
3348 | warningsContainer.appendChild(h('div', h('em', 'warning:'))) |
3349 | warningsContainer.appendChild(h('ul.mentions', warnings)) |
3350 | } |
3351 | |
3352 | pull( |
3353 | pull.once(msg), |
3354 | self.app.unboxMessages(), |
3355 | self.app.render.renderFeeds({ |
3356 | raw: raw, |
3357 | filter: self.query.filter, |
3358 | }), |
3359 | pull.drain(function (el) { |
3360 | msgContainer.appendChild(h('tbody', el)) |
3361 | }, cb) |
3362 | ) |
3363 | } |
3364 | |
3365 | return [ |
3366 | contentInput, |
3367 | opts.redirectToPublishedMsg ? h('input', {type: 'hidden', |
3368 | name: 'redirect_to_published_msg', value: '1'}) : '', |
3369 | warningsContainer, |
3370 | h('div', h('em', 'draft:'), ' ', sizeEl), |
3371 | msgContainer, |
3372 | h('div.composer-actions', |
3373 | h('input', {type: 'submit', name: 'action', value: 'publish'}) |
3374 | ) |
3375 | ] |
3376 | } |
3377 | |
3378 | } |
3379 | |
3380 | function hashBuf(buf) { |
3381 | var hash = crypto.createHash('sha256') |
3382 | hash.update(buf) |
3383 | return '&' + hash.digest('base64') + '.sha256' |
3384 | } |
3385 | |
3386 | Serve.prototype.getBuiltinEmojiLink = function (name) { |
3387 | if (!(name in emojis)) return |
3388 | var file = path.join(emojiDir, name + '.png') |
3389 | var fileBuf = fs.readFileSync(file) |
3390 | var id = hashBuf(fileBuf) |
3391 | // seed the builtin emoji |
3392 | pull(pull.once(fileBuf), this.app.sbot.blobs.add(id, function (err) { |
3393 | if (err) console.error('error adding builtin emoji as blob', err) |
3394 | })) |
3395 | return { |
3396 | link: id, |
3397 | type: 'image/png', |
3398 | size: fileBuf.length, |
3399 | } |
3400 | } |
3401 | |
3402 | Serve.prototype.getMsgDecryptedMaybeOoo = function (key, cb) { |
3403 | if (this.useOoo) this.app.getMsgDecryptedOoo(key, cb) |
3404 | else this.app.getMsgDecrypted(key, cb) |
3405 | } |
3406 | |
3407 | Serve.prototype.emojis = function (path) { |
3408 | var self = this |
3409 | var seen = {} |
3410 | pull( |
3411 | ph('section', [ |
3412 | ph('h3', 'Emojis'), |
3413 | ph('ul', {class: 'mentions'}, pull( |
3414 | self.app.streamEmojis(), |
3415 | pull.map(function (emoji) { |
3416 | if (!seen[emoji.name]) { |
3417 | // cache the first use, so that our uses take precedence over other feeds' |
3418 | self.app.reverseEmojiNameCache.set(emoji.name, emoji.link) |
3419 | seen[emoji.name] = true |
3420 | } |
3421 | return ph('li', [ |
3422 | ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)}, |
3423 | ph('img', { |
3424 | class: 'ssb-emoji', |
3425 | src: self.app.render.imageUrl(emoji.link), |
3426 | size: 32, |
3427 | }) |
3428 | ), ' ', |
3429 | u.escapeHTML(emoji.name) |
3430 | ]) |
3431 | }) |
3432 | )) |
3433 | ]), |
3434 | this.wrapPage('emojis'), |
3435 | this.respondSink(200) |
3436 | ) |
3437 | } |
3438 |
Built with git-ssb-web