Files: 8667de56c81425100b274655b183def02e92725c / lib / serve.js
166479 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 u = require('./util') |
10 | var cat = require('pull-cat') |
11 | var h = require('hyperscript') |
12 | var paginate = require('pull-paginate') |
13 | var ssbMentions = require('ssb-mentions') |
14 | var multicb = require('multicb') |
15 | var pkg = require('../package') |
16 | var Busboy = require('busboy') |
17 | var mime = require('mime-types') |
18 | var ident = require('pull-identify-filetype') |
19 | var htime = require('human-time') |
20 | var ph = require('pull-hyperscript') |
21 | var jpeg = require('jpeg-autorotate') |
22 | var Catch = require('pull-catch') |
23 | var Diff = require('diff') |
24 | var split = require('pull-split') |
25 | var utf8 = require('pull-utf8-decoder') |
26 | var webresolve = require('ssb-web-resolver') |
27 | var Url = require('url') |
28 | |
29 | module.exports = Serve |
30 | |
31 | var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles') |
32 | |
33 | var urlIdRegex = /^(?:\/+(([%&@]|%25|%26)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/ |
34 | |
35 | function ctype(name) { |
36 | switch (name && /[^.\/]*$/.exec(name)[0] || 'html') { |
37 | case 'html': return 'text/html' |
38 | case 'txt': return 'text/plain' |
39 | case 'js': return 'text/javascript' |
40 | case 'css': return 'text/css' |
41 | case 'png': return 'image/png' |
42 | case 'json': return 'application/json' |
43 | case 'ico': return 'image/x-icon' |
44 | } |
45 | } |
46 | |
47 | function encodeDispositionFilename(fname) { |
48 | fname = String(fname).replace(/\/g/, '\\\\').replace(/"/, '\\\"') |
49 | return '"' + encodeURIComponent(fname) + '"' |
50 | } |
51 | |
52 | function uniques() { |
53 | var set = {} |
54 | return function (item) { |
55 | if (set[item]) return false |
56 | return set[item] = true |
57 | } |
58 | } |
59 | |
60 | function reduceLink(link) { |
61 | return link |
62 | && typeof link.link === 'string' |
63 | && typeof link.name === 'undefined' |
64 | && Object.keys(link).length === 1 |
65 | ? link.link : link |
66 | } |
67 | |
68 | function Serve(app, req, res) { |
69 | this.app = app |
70 | this.req = req |
71 | this.res = res |
72 | this.startDate = new Date() |
73 | var hostname = req.headers.host || app.hostname |
74 | this.baseUrl = 'http://' + hostname + (app.opts.base || '/') |
75 | } |
76 | |
77 | Serve.prototype.go = function () { |
78 | console.log(this.req.method, this.req.url) |
79 | var self = this |
80 | |
81 | this.res.setTimeout(0) |
82 | var conf = self.app.config.patchfoo || {} |
83 | this.conf = conf |
84 | |
85 | var authtok = conf.auth || null |
86 | if (authtok) { |
87 | var auth = this.req.headers['authorization'] |
88 | var tok = null |
89 | //console.log('Authorization: ',auth) |
90 | |
91 | if (auth) { |
92 | var a = auth.split(' ') |
93 | if (a[0] == 'Basic') { |
94 | tok = Buffer.from(a[1],'base64').toString('ascii') |
95 | } |
96 | } |
97 | if (tok != authtok) { |
98 | self.res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Patchfoo"'}) |
99 | self.res.end('Not authorized') |
100 | return |
101 | } |
102 | } |
103 | var allowAddresses = conf.allowAddresses |
104 | if (allowAddresses) { |
105 | var ip = this.req.socket.remoteAddress |
106 | if (allowAddresses.indexOf(ip) === -1) { |
107 | this.res.writeHead(403) |
108 | return this.res.end('Not authorized') |
109 | } |
110 | } |
111 | |
112 | if (!this.app.isAllowedHostHeader(this.req.headers.host)) { |
113 | console.error('Host header not allowed: "' + this.req.headers.host + '"') |
114 | this.res.writeHead(403) |
115 | return this.res.end('Forbidden') |
116 | } |
117 | |
118 | this.replyMentionFeeds = conf.replyMentionFeeds == null ? true : |
119 | Boolean(conf.replyMentionFeeds) |
120 | |
121 | if (this.req.method === 'POST' || this.req.method === 'PUT') { |
122 | var referer = this.req.headers.referer |
123 | var refererPath = this.app.getRefererPath(referer) |
124 | if (!refererPath) { |
125 | if (!referer) console.error('Missing referer') |
126 | else console.error('Referer not allowed: "' + referer + '"') |
127 | this.res.writeHead(403) |
128 | return this.res.end('Forbidden') |
129 | } |
130 | if (this.isUnsafePath(refererPath)) { |
131 | console.error('Unsafe referer path not allowed: "' + refererPath + '"') |
132 | this.res.writeHead(403) |
133 | return this.res.end('Forbidden') |
134 | } |
135 | if (/^multipart\/form-data/.test(this.req.headers['content-type'])) { |
136 | var data = {} |
137 | var erred |
138 | var busboy = new Busboy({headers: this.req.headers}) |
139 | var filesCb = multicb({pluck: 1}) |
140 | busboy.on('finish', filesCb()) |
141 | filesCb(function (err) { |
142 | gotData(err, data) |
143 | }) |
144 | function addField(name, value) { |
145 | if (typeof value === 'string') value = value.replace(/\r\n/g, '\n') |
146 | if (!(name in data)) data[name] = value |
147 | else if (Array.isArray(data[name])) data[name].push(value) |
148 | else data[name] = [data[name], value] |
149 | } |
150 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { |
151 | var cb = filesCb() |
152 | var size = 0 |
153 | pull( |
154 | toPull(file), |
155 | pull.map(function (data) { |
156 | size += data.length |
157 | return data |
158 | }), |
159 | self.app.addBlob(!!data.private, function (err, link) { |
160 | if (err) return cb(err) |
161 | if (size === 0 && !filename) return cb() |
162 | link.name = filename |
163 | link.type = mimetype |
164 | addField(fieldname, link) |
165 | cb() |
166 | }) |
167 | ) |
168 | }) |
169 | busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { |
170 | addField(fieldname, val) |
171 | }) |
172 | this.req.pipe(busboy) |
173 | } else { |
174 | pull( |
175 | toPull(this.req), |
176 | pull.collect(function (err, bufs) { |
177 | var data |
178 | if (!err) try { |
179 | var str = Buffer.concat(bufs).toString('utf8') |
180 | str = str.replace(/%0D%0A/ig, '\n') |
181 | data = qs.parse(str) |
182 | } catch(e) { |
183 | err = e |
184 | } |
185 | gotData(err, data) |
186 | }) |
187 | ) |
188 | } |
189 | } else { |
190 | gotData(null, {}) |
191 | } |
192 | |
193 | function gotData(err, data) { |
194 | self.data = data |
195 | if (err) next(err) |
196 | else if (data.action === 'publish' && data.about_new_content) self.publishNewAbout(next) |
197 | else if (data.action === 'publish') self.publishJSON(next) |
198 | else if (data.action === 'contact') self.publishContact(next) |
199 | else if (data.action === 'want-blobs') self.wantBlobs(next) |
200 | else if (data.action === 'poll-position') self.publishPollPosition(next) |
201 | else if (data.action_vote) self.publishVote(next) |
202 | else if (data.action_attend) self.publishAttend(next) |
203 | else next() |
204 | } |
205 | |
206 | function next(err, publishedMsg) { |
207 | if (err) { |
208 | if (err.redirectUrl) return self.redirect(err.redirectUrl) |
209 | self.res.writeHead(400, {'Content-Type': 'text/plain'}) |
210 | self.res.end(err.stack) |
211 | } else if (publishedMsg) { |
212 | if (self.data.redirect_to_published_msg) { |
213 | self.redirect(self.app.render.toUrl(publishedMsg.key)) |
214 | } else { |
215 | var u = Url.parse(self.req.url) |
216 | var q = u.query || (u.query = {}) |
217 | q.published = publishedMsg.key |
218 | self.redirect(Url.format(u)) |
219 | } |
220 | } else { |
221 | self.handle() |
222 | } |
223 | } |
224 | } |
225 | |
226 | Serve.prototype.isUnsafePath = function (path) { |
227 | return typeof path !== 'string' |
228 | || /^&|^%26/.test(path) |
229 | || path.indexOf('../') !== -1 |
230 | || path.startsWith('/web/') |
231 | || path.startsWith('/npm-readme/') |
232 | } |
233 | |
234 | Serve.prototype.saveDraft = function (content, cb) { |
235 | var self = this |
236 | var data = self.data |
237 | var form = {} |
238 | for (var k in data) { |
239 | if (k === 'url' || k === 'draft_id' || k === 'content' |
240 | || k === 'draft_id' || k === 'save_draft') continue |
241 | form[k] = data[k] |
242 | } |
243 | self.app.saveDraft(data.draft_id, data.url, form, content, function (err, id) { |
244 | if (err) return cb(err) |
245 | cb(null, id || data.draft_id) |
246 | }) |
247 | } |
248 | |
249 | Serve.prototype.publishJSON = function (cb) { |
250 | var content |
251 | try { |
252 | content = JSON.parse(this.data.content) |
253 | } catch(e) { |
254 | return cb(e) |
255 | } |
256 | this.publish(content, cb) |
257 | } |
258 | |
259 | Serve.prototype.publishNewAbout = function (cb) { |
260 | var self = this |
261 | var aboutContent, aboutNewContent |
262 | try { |
263 | aboutContent = JSON.parse(this.data.content) |
264 | aboutNewContent = JSON.parse(this.data.about_new_content) |
265 | } catch(e) { |
266 | return cb(e) |
267 | } |
268 | if (typeof aboutContent !== 'object' || aboutContent === null) { |
269 | return cb(new TypeError('content must be object')) |
270 | } |
271 | aboutNewContent.recps = aboutContent.recps |
272 | self.app.publish(aboutNewContent, function (err, msg) { |
273 | if (err) return cb(err) |
274 | if (!msg) return cb(new Error('aborted')) |
275 | aboutContent.about = msg.key |
276 | self.publish(aboutContent, cb) |
277 | }) |
278 | } |
279 | |
280 | Serve.prototype.publishVote = function (next) { |
281 | var content = { |
282 | type: 'vote', |
283 | channel: this.data.channel || undefined, |
284 | root: this.data.root || undefined, |
285 | branch: this.data.branches ? this.data.branches.split(',') : undefined, |
286 | vote: { |
287 | link: this.data.link, |
288 | value: Number(this.data.vote_value), |
289 | expression: this.data.vote_expression || undefined, |
290 | } |
291 | } |
292 | if (this.data.recps) content.recps = this.data.recps.split(',') |
293 | if (this.app.previewVotes) { |
294 | var json = JSON.stringify(content, 0, 2) |
295 | var q = qs.stringify({text: json, action: 'preview'}) |
296 | var url = this.app.render.toUrl('/compose?' + q) |
297 | this.redirect(url) |
298 | } else { |
299 | this.publish(content, next) |
300 | } |
301 | } |
302 | |
303 | Serve.prototype.requestReplicate = function (id, replicateIt, next) { |
304 | var self = this |
305 | var replicate = self.app.sbot.replicate |
306 | var request = replicate && replicate.request |
307 | if (!request) return this.respond(500, 'Missing replicate.request method') |
308 | request(id, replicateIt, function (err) { |
309 | if (err) return pull( |
310 | pull.once(u.renderError(err, ext).outerHTML), |
311 | self.wrapPage('replicate request'), |
312 | self.respondSink(400) |
313 | ) |
314 | self.requestedReplicate = replicateIt |
315 | next() |
316 | }) |
317 | } |
318 | |
319 | Serve.prototype.publishContact = function (next) { |
320 | if (this.data.replicate) return this.requestReplicate(this.data.contact, true, next) |
321 | if (this.data.unreplicate) return this.requestReplicate(this.data.contact, false, next) |
322 | if (this.data.block1) return this.redirect(this.app.render.toUrl('/block/' + this.data.contact)) |
323 | var content = { |
324 | type: 'contact', |
325 | contact: this.data.contact, |
326 | } |
327 | if (this.data.follow) content.following = true |
328 | if (this.data.block) content.blocking = true |
329 | if (this.data.unfollow) content.following = false |
330 | if (this.data.unblock) content.blocking = false |
331 | if (this.data.mute) content.blocking = true |
332 | if (this.data.unmute) content.blocking = false |
333 | if (this.data.mute || this.data.unmute) content.recps = [this.app.sbot.id] |
334 | if (this.app.previewContacts) { |
335 | var json = JSON.stringify(content, 0, 2) |
336 | var q = qs.stringify({text: json, action: 'preview'}) |
337 | var url = this.app.render.toUrl('/compose?' + q) |
338 | this.redirect(url) |
339 | } else { |
340 | this.publish(content, next) |
341 | } |
342 | } |
343 | |
344 | Serve.prototype.publishPollPosition = function (cb) { |
345 | var content = { |
346 | type: 'position', |
347 | version: 'v1', |
348 | channel: this.data.channel || undefined, |
349 | root: this.data.poll_root, |
350 | branch: this.data.branches || [], |
351 | reason: this.data.poll_reason || undefined, |
352 | details: { |
353 | type: this.data.poll_type |
354 | } |
355 | } |
356 | if (this.data.poll_choice != null) { |
357 | content.details.choice = Number(this.data.poll_choice) |
358 | } else { |
359 | content.details.choices = u.toArray(this.data.poll_choices).map(Number) |
360 | } |
361 | if (this.data.recps) content.recps = this.data.recps.split(',') |
362 | var json = JSON.stringify(content, 0, 2) |
363 | var q = qs.stringify({text: json, action: 'preview'}) |
364 | var url = this.app.render.toUrl('/compose?' + q) |
365 | this.redirect(url) |
366 | // this.publish(content, cb) |
367 | } |
368 | |
369 | Serve.prototype.publishAttend = function (cb) { |
370 | var content = { |
371 | type: 'about', |
372 | channel: this.data.channel || undefined, |
373 | about: this.data.link, |
374 | attendee: { |
375 | link: this.app.sbot.id |
376 | } |
377 | } |
378 | if (this.data.recps) content.recps = this.data.recps.split(',') |
379 | this.publish(content, cb) |
380 | } |
381 | |
382 | function removeQuery(ref) { |
383 | return ref.replace(/\?.*/, '') |
384 | } |
385 | |
386 | Serve.prototype.wantBlobs = function (cb) { |
387 | var self = this |
388 | if (!self.data.blob_ids) return cb() |
389 | var ids = self.data.blob_ids.split(',') |
390 | .map(removeQuery) |
391 | if (!ids.every(u.isRef)) return cb(new Error('bad blob ids ' + ids.join(','))) |
392 | var done = multicb({pluck: 1}) |
393 | ids.forEach(function (id) { |
394 | self.app.wantSizeBlob(id, done()) |
395 | }) |
396 | if (self.data.async_want) return cb() |
397 | done(function (err) { |
398 | if (err) return cb(err) |
399 | // self.note = h('div', 'wanted blobs: ' + ids.join(', ') + '.') |
400 | cb() |
401 | }) |
402 | } |
403 | |
404 | Serve.prototype.publish = function (content, cb) { |
405 | var self = this |
406 | var done = multicb({pluck: 1, spread: true}) |
407 | u.toArray(content && content.mentions).forEach(function (mention) { |
408 | if (mention.link && mention.link[0] === '&' && !isNaN(mention.size)) |
409 | self.app.pushBlob(mention.link, done()) |
410 | }) |
411 | done(function (err) { |
412 | if (err) return cb(err) |
413 | self.publishMayRedirect(content, function (err, msg) { |
414 | if (err) return cb(err) |
415 | delete self.data.text |
416 | delete self.data.recps |
417 | return cb(null, msg) |
418 | }) |
419 | }) |
420 | } |
421 | |
422 | Serve.prototype.publishMayRedirect = function (content, cb) { |
423 | var publishguard = this.app.sbot.publishguard |
424 | if (Array.isArray(content.recps)) { |
425 | var recps = content.recps.map(u.linkDest) |
426 | if (publishguard && publishguard.privatePublishGetUrl) { |
427 | return publishguard.privatePublishGetUrl({ |
428 | content: content, |
429 | recps: recps, |
430 | redirectBase: this.baseUrl |
431 | }, onPublishGetUrl) |
432 | } else { |
433 | this.app.privatePublish(content, recps, cb) |
434 | } |
435 | } else { |
436 | if (publishguard && publishguard.publishGetUrl) { |
437 | publishguard.publishGetUrl({ |
438 | content: content, |
439 | redirectBase: this.baseUrl |
440 | }, onPublishGetUrl) |
441 | } else { |
442 | this.app.sbot.publish(content, cb) |
443 | } |
444 | } |
445 | function onPublishGetUrl(err, url) { |
446 | if (err) return cb(err) |
447 | cb({redirectUrl: url}) |
448 | } |
449 | } |
450 | |
451 | Serve.prototype.handle = function () { |
452 | var m = urlIdRegex.exec(this.req.url) |
453 | this.query = m[5] ? qs.parse(m[5]) : {} |
454 | this.useOoo = this.query.ooo != null ? |
455 | Boolean(this.query.ooo) : this.app.useOoo |
456 | if (this.query.printView != null) { |
457 | this.printView = true |
458 | this.noNav = true |
459 | this.noFooter = true |
460 | this.noComposer = true |
461 | this.noActions = true |
462 | this.noAvatar = true |
463 | this.noMsgTime = true |
464 | this.msgDate = true |
465 | } |
466 | if (this.query.noThread != null) { |
467 | this.noThread = true |
468 | } |
469 | |
470 | switch (m[2]) { |
471 | case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1]) |
472 | case '%': return this.id(m[1], m[3]) |
473 | case '@': return this.userFeed(m[1], m[3]) |
474 | case '%26': m[2] = '&'; m[1] = decodeURIComponent(m[1]) |
475 | case '&': return this.blob(m[1], m[3]) |
476 | default: return this.path(m[4]) |
477 | } |
478 | } |
479 | |
480 | Serve.prototype.respond = function (status, message) { |
481 | this.res.writeHead(status) |
482 | this.res.end(message) |
483 | } |
484 | |
485 | Serve.prototype.respondSink = function (status, headers, cb) { |
486 | var self = this |
487 | if (status || headers) |
488 | self.res.writeHead(status, headers || {'Content-Type': 'text/html'}) |
489 | return toPull(self.res, cb || function (err) { |
490 | if (err) self.app.error(err) |
491 | }) |
492 | } |
493 | |
494 | Serve.prototype.redirect = function (dest) { |
495 | this.res.writeHead(302, { |
496 | Location: dest |
497 | }) |
498 | this.res.end() |
499 | } |
500 | |
501 | Serve.prototype.path = function (url) { |
502 | var m |
503 | url = url.replace(/^\/+/, '/') |
504 | switch (url) { |
505 | case '/': return this.home() |
506 | case '/robots.txt': return this.res.end('User-agent: *') |
507 | } |
508 | if (m = /^\/%23(.*)/.exec(url)) { |
509 | return this.redirect(this.app.render.toUrl('/channel/' |
510 | + decodeURIComponent(m[1]))) |
511 | } |
512 | m = /^([^.]*)(?:\.(.*))?$/.exec(url) |
513 | switch (m[1]) { |
514 | case '/new': return this.new(m[2]) |
515 | case '/public': return this.public(m[2]) |
516 | case '/threads': return this.threads(m[2]) |
517 | case '/private': return this.private(m[2]) |
518 | case '/mentions': return this.mentions(m[2]) |
519 | case '/search': return this.search(m[2]) |
520 | case '/advsearch': return this.advsearch(m[2]) |
521 | case '/peers': return this.peers(m[2]) |
522 | case '/status': return this.status(m[2]) |
523 | case '/channels': return this.channels(m[2]) |
524 | case '/tags': return this.tags(m[2]) |
525 | case '/friends': return this.friends(m[2]) |
526 | case '/live': return this.live(m[2]) |
527 | case '/compose': return this.compose(m[2]) |
528 | case '/emojis': return this.emojis(m[2]) |
529 | case '/votes': return this.votes(m[2]) |
530 | case '/about-self': return this.aboutSelf(m[2]) |
531 | case '/new-gathering': return this.newGathering(m[2]) |
532 | } |
533 | m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) |
534 | switch (m[1]) { |
535 | case '/channel': return this.channel(m[2]) |
536 | case '/type': return this.type(m[2]) |
537 | case '/links': return this.links(m[2]) |
538 | case '/static': return this.static(m[2]) |
539 | case '/highlight': return this.highlight(m[2]) |
540 | case '/contacts': return this.contacts(m[2]) |
541 | case '/about': return this.about(m[2]) |
542 | case '/pub': return this.pub(m[2]) |
543 | case '/git': return this.git(m[2]) |
544 | case '/image': return this.image(m[2]) |
545 | case '/npm': return this.npm(m[2]) |
546 | case '/npm-prebuilds': return this.npmPrebuilds(m[2]) |
547 | case '/npm-readme': return this.npmReadme(m[2]) |
548 | case '/npm-registry': return this.npmRegistry(m[2]) |
549 | case '/markdown': return this.markdown(m[2]) |
550 | case '/edit-diff': return this.editDiff(m[2]) |
551 | case '/about-diff': return this.aboutDiff(m[2]) |
552 | case '/gathering': return this.gathering(m[2]) |
553 | case '/edit-gathering': return this.editGathering(m[2]) |
554 | case '/shard': return this.shard(m[2]) |
555 | case '/zip': return this.zip(m[2]) |
556 | case '/web': return this.web(m[2]) |
557 | case '/block': return this.block(m[2]) |
558 | case '/script': return this.script(m[2]) |
559 | case '/drafts': return this.drafts(m[2]) |
560 | } |
561 | return this.respond(404, 'Not found') |
562 | } |
563 | |
564 | Serve.prototype.home = function () { |
565 | pull( |
566 | pull.empty(), |
567 | this.wrapPage('/'), |
568 | this.respondSink(200, { |
569 | 'Content-Type': 'text/html' |
570 | }) |
571 | ) |
572 | } |
573 | |
574 | Serve.prototype.public = function (ext) { |
575 | var q = this.query |
576 | var opts = { |
577 | reverse: !q.forwards, |
578 | sortByTimestamp: q.sort === 'claimed', |
579 | lt: Number(q.lt) || Date.now(), |
580 | gt: Number(q.gt) || -Infinity, |
581 | filter: q.filter, |
582 | } |
583 | |
584 | pull( |
585 | this.app.createLogStream(opts), |
586 | this.renderThreadPaginated(opts, null, q), |
587 | this.wrapMessages(), |
588 | this.wrapPublic(), |
589 | this.wrapPage('public'), |
590 | this.respondSink(200, { |
591 | 'Content-Type': ctype(ext) |
592 | }) |
593 | ) |
594 | } |
595 | |
596 | Serve.prototype.threads = function (ext) { |
597 | var q = this.query |
598 | var opts = { |
599 | type: 'post', |
600 | reverse: !q.forwards, |
601 | sortByTimestamp: q.sort === 'claimed', |
602 | lt: Number(q.lt) || Date.now(), |
603 | gt: Number(q.gt) || -Infinity, |
604 | filter: q.filter, |
605 | } |
606 | |
607 | pull( |
608 | this.app.sbotMessagesByType(opts), |
609 | pull.filter(msg => { |
610 | return !msg.value.content.root |
611 | }), |
612 | this.renderThreadPaginated(opts, null, q), |
613 | this.wrapMessages(), |
614 | this.wrapPublic(), |
615 | this.wrapPage('threads'), |
616 | this.respondSink(200, { |
617 | 'Content-Type': ctype(ext) |
618 | }) |
619 | ) |
620 | } |
621 | |
622 | Serve.prototype.setCookie = function (key, value, options) { |
623 | var header = key + '=' + value |
624 | if (options) for (var k in options) { |
625 | header += '; ' + k + '=' + options[k] |
626 | } |
627 | this.res.setHeader('Set-Cookie', header) |
628 | } |
629 | |
630 | Serve.prototype.new = function (ext) { |
631 | var self = this |
632 | var q = self.query |
633 | var latest = (/latest=([^;]*)/.exec(self.req.headers.cookie) || [])[1] |
634 | var limit = Number(q.limit || self.conf.newLimit || 500) |
635 | var now = Date.now() |
636 | var opts = { |
637 | gt: Number(q.gt) || Number(latest) || now, |
638 | lte: now, |
639 | limit: limit |
640 | } |
641 | |
642 | if (q.catchup) self.setCookie('latest', opts.gt, {'Max-Age': 86400000}) |
643 | |
644 | var read = self.app.createLogStream(opts) |
645 | self.req.on('closed', function () { |
646 | console.error('closing') |
647 | read(true, function (err) { |
648 | console.log('closed') |
649 | if (err && err !== true) console.error(new Error(err.stack)) |
650 | }) |
651 | }) |
652 | pull.collect(function (err, msgs) { |
653 | if (err) return pull( |
654 | pull.once(u.renderError(err, ext).outerHTML), |
655 | self.wrapPage('peers'), |
656 | self.respondSink(500, {'Content-Type': ctype(ext)}) |
657 | ) |
658 | sort(msgs) |
659 | var maxTS = msgs.reduce(function (max, msg) { |
660 | return Math.max(msg.timestamp, max) |
661 | }, -Infinity) |
662 | pull( |
663 | pull.values(msgs), |
664 | self.renderThread(), |
665 | self.wrapNew({ |
666 | reachedLimit: msgs.length === limit && limit, |
667 | gt: isFinite(maxTS) ? maxTS : Date.now() |
668 | }), |
669 | self.wrapMessages(), |
670 | self.wrapPage('new'), |
671 | self.respondSink(200, { |
672 | 'Content-Type': ctype(ext) |
673 | }) |
674 | ) |
675 | })(read) |
676 | } |
677 | |
678 | Serve.prototype.private = function (ext) { |
679 | var q = this.query |
680 | var opts = { |
681 | sortByTimestamp: q.sort === 'claimed', |
682 | reverse: !q.forwards, |
683 | lt: Number(q.lt) || Date.now(), |
684 | gt: Number(q.gt) || -Infinity, |
685 | filter: q.filter, |
686 | } |
687 | |
688 | pull( |
689 | this.app.streamPrivate(opts), |
690 | this.renderThreadPaginated(opts, null, q), |
691 | this.wrapMessages(), |
692 | this.wrapPrivate(opts), |
693 | this.wrapPage('private'), |
694 | this.respondSink(200, { |
695 | 'Content-Type': ctype(ext) |
696 | }) |
697 | ) |
698 | } |
699 | |
700 | Serve.prototype.mentions = function (ext) { |
701 | var self = this |
702 | var q = self.query |
703 | var opts = { |
704 | reverse: !q.forwards, |
705 | sortByTimestamp: q.sort === 'claimed', |
706 | lt: Number(q.lt) || Date.now(), |
707 | gt: Number(q.gt) || -Infinity, |
708 | filter: q.filter, |
709 | id: q.id |
710 | } |
711 | |
712 | return pull( |
713 | ph('section', {}, [ |
714 | ph('h3', 'Mentions'), |
715 | pull( |
716 | self.app.streamMentions(opts), |
717 | self.app.unboxMessages(), |
718 | self.renderThreadPaginated(opts, null, q), |
719 | self.wrapMessages() |
720 | ) |
721 | ]), |
722 | self.wrapPage('mentions'), |
723 | self.respondSink(200) |
724 | ) |
725 | } |
726 | |
727 | Serve.prototype.search = function (ext) { |
728 | var searchQ = (this.query.q || '').trim() |
729 | var self = this |
730 | |
731 | if (/^ssb:\/\//.test(searchQ)) { |
732 | var maybeId = searchQ.substr(6) |
733 | if (u.isRef(maybeId)) searchQ = maybeId |
734 | } |
735 | |
736 | if (u.isRef(searchQ) || searchQ[0] === '#') { |
737 | return self.redirect(self.app.render.toUrl(searchQ)) |
738 | } |
739 | |
740 | pull( |
741 | self.app.search(searchQ), |
742 | self.renderThread(), |
743 | self.wrapMessages(), |
744 | self.wrapPage('search - ' + searchQ, searchQ), |
745 | self.respondSink(200, { |
746 | 'Content-Type': ctype(ext), |
747 | }) |
748 | ) |
749 | } |
750 | |
751 | Serve.prototype.advsearch = function (ext) { |
752 | var self = this |
753 | var q = this.query || {} |
754 | |
755 | if (q.source) q.source = u.extractFeedIds(q.source)[0] |
756 | if (q.dest) q.dest = u.extractFeedIds(q.dest)[0] |
757 | var hasQuery = q.text || q.source || q.dest || q.channel |
758 | |
759 | pull( |
760 | cat([ |
761 | ph('section', {}, [ |
762 | ph('form', {action: '', method: 'get'}, [ |
763 | ph('table', [ |
764 | ph('tr', [ |
765 | ph('td', 'text'), |
766 | ph('td', ph('input', {name: 'text', placeholder: 'regex', |
767 | class: 'id-input', |
768 | value: u.escapeHTML(q.text)})) |
769 | ]), |
770 | ph('tr', [ |
771 | ph('td', 'author'), |
772 | ph('td', ph('input', {name: 'source', placeholder: '@id', |
773 | class: 'id-input', |
774 | value: u.escapeHTML(q.source)})) |
775 | ]), |
776 | ph('tr', [ |
777 | ph('td', 'mentions'), |
778 | ph('td', ph('input', {name: 'dest', placeholder: 'id', |
779 | class: 'id-input', |
780 | value: u.escapeHTML(q.dest)})) |
781 | ]), |
782 | ph('tr', [ |
783 | ph('td', 'channel'), |
784 | ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel', |
785 | class: 'id-input', |
786 | value: u.escapeHTML(q.channel)}) |
787 | ]) |
788 | ]), |
789 | ph('tr', [ |
790 | ph('td', {colspan: 2}, [ |
791 | ph('input', {type: 'submit', value: 'search'}) |
792 | ]) |
793 | ]), |
794 | ]) |
795 | ]) |
796 | ]), |
797 | hasQuery && pull( |
798 | self.app.advancedSearch(q), |
799 | self.renderThread({ |
800 | feed: q.source, |
801 | }), |
802 | self.wrapMessages() |
803 | ) |
804 | ]), |
805 | self.wrapPage('advanced search'), |
806 | self.respondSink(200, { |
807 | 'Content-Type': ctype(ext), |
808 | }) |
809 | ) |
810 | } |
811 | |
812 | Serve.prototype.live = function (ext) { |
813 | var self = this |
814 | var q = self.query |
815 | var opts = { |
816 | live: true, |
817 | } |
818 | var gt = Number(q.gt) |
819 | if (gt) opts.gt = gt |
820 | else opts.old = false |
821 | |
822 | pull( |
823 | ph('table', {class: 'ssb-msgs'}, pull( |
824 | self.app.sbot.createLogStream(opts), |
825 | self.app.render.renderFeeds({ |
826 | serve: self, |
827 | withGt: true, |
828 | filter: q.filter, |
829 | }), |
830 | pull.map(u.toHTML) |
831 | )), |
832 | self.wrapPage('live'), |
833 | self.respondSink(200, { |
834 | 'Content-Type': ctype(ext), |
835 | }) |
836 | ) |
837 | } |
838 | |
839 | Serve.prototype.compose = function (ext) { |
840 | var self = this |
841 | self.composer({ |
842 | channel: '', |
843 | redirectToPublishedMsg: true, |
844 | }, function (err, composer) { |
845 | if (err) return pull( |
846 | pull.once(u.renderError(err).outerHTML), |
847 | self.wrapPage('compose'), |
848 | self.respondSink(500) |
849 | ) |
850 | pull( |
851 | pull.once(u.toHTML(composer)), |
852 | self.wrapPage('compose'), |
853 | self.respondSink(200, { |
854 | 'Content-Type': ctype(ext) |
855 | }) |
856 | ) |
857 | }) |
858 | } |
859 | |
860 | Serve.prototype.votes = function (path) { |
861 | if (path) return pull( |
862 | pull.once(u.renderError(new Error('Not implemented')).outerHTML), |
863 | this.wrapPage('#' + channel), |
864 | this.respondSink(404, {'Content-Type': ctype('html')}) |
865 | ) |
866 | |
867 | var self = this |
868 | var q = self.query |
869 | var opts = { |
870 | reverse: !q.forwards, |
871 | limit: Number(q.limit) || 50, |
872 | } |
873 | var gt = Number(q.gt) |
874 | if (gt) opts.gt = gt |
875 | var lt = Number(q.lt) |
876 | if (lt) opts.lt = lt |
877 | |
878 | self.app.getVoted(opts, function (err, voted) { |
879 | if (err) return pull( |
880 | pull.once(u.renderError(err).outerHTML), |
881 | self.wrapPage('#' + channel), |
882 | self.respondSink(500, {'Content-Type': ctype('html')}) |
883 | ) |
884 | |
885 | pull( |
886 | ph('table', [ |
887 | ph('thead', [ |
888 | ph('tr', [ |
889 | ph('td', {colspan: 2}, self.syncPager({ |
890 | first: voted.firstTimestamp, |
891 | last: voted.lastTimestamp, |
892 | })) |
893 | ]) |
894 | ]), |
895 | ph('tbody', pull( |
896 | pull.values(voted.items), |
897 | paramap(function (item, cb) { |
898 | cb(null, ph('tr', [ |
899 | ph('td', [String(item.value)]), |
900 | ph('td', [ |
901 | self.phIdLink(item.id), |
902 | pull.once(' dug by '), |
903 | self.renderIdsList()(pull.values(item.feeds)) |
904 | ]) |
905 | ])) |
906 | }, 8) |
907 | )), |
908 | ph('tfoot', {}, []), |
909 | ]), |
910 | self.wrapPage('votes'), |
911 | self.respondSink(200, { |
912 | 'Content-Type': ctype('html') |
913 | }) |
914 | ) |
915 | }) |
916 | } |
917 | |
918 | Serve.prototype.syncPager = function (opts) { |
919 | var q = this.query |
920 | var reverse = !q.forwards |
921 | var min = (reverse ? opts.last : opts.first) || Number(q.gt) |
922 | var max = (reverse ? opts.first : opts.last) || Number(q.lt) |
923 | var minDate = new Date(min) |
924 | var maxDate = new Date(max) |
925 | var qOlder = u.mergeOpts(q, {lt: min, gt: undefined, forwards: undefined}) |
926 | var qNewer = u.mergeOpts(q, {gt: max, lt: undefined, forwards: 1}) |
927 | var atNewest = reverse ? !q.lt : !max |
928 | var atOldest = reverse ? !min : !q.gt |
929 | if (atNewest && !reverse) qOlder.lt++ |
930 | if (atOldest && reverse) qNewer.gt-- |
931 | return h('div', |
932 | atOldest ? 'oldest' : [ |
933 | h('a', {href: '?' + qs.stringify(qOlder)}, '<<'), ' ', |
934 | h('span', {title: minDate.toString()}, htime(minDate)), ' ', |
935 | ], |
936 | ' - ', |
937 | atNewest ? 'now' : [ |
938 | h('span', {title: maxDate.toString()}, htime(maxDate)), ' ', |
939 | h('a', {href: '?' + qs.stringify(qNewer)}, '>>') |
940 | ] |
941 | ).outerHTML |
942 | } |
943 | |
944 | Serve.prototype.peers = function (ext) { |
945 | var self = this |
946 | if (self.data.action === 'connect') { |
947 | return self.app.sbot.gossip.connect(self.data.address, function (err) { |
948 | if (err) return pull( |
949 | pull.once(u.renderError(err, ext).outerHTML), |
950 | self.wrapPage('peers'), |
951 | self.respondSink(400, {'Content-Type': ctype(ext)}) |
952 | ) |
953 | self.data = {} |
954 | return self.peers(ext) |
955 | }) |
956 | } |
957 | |
958 | pull( |
959 | self.app.streamPeers(), |
960 | paramap(function (peer, cb) { |
961 | var done = multicb({pluck: 1, spread: true}) |
962 | var connectedTime = Date.now() - peer.stateChange |
963 | var addr = peer.host + ':' + peer.port + ':' + peer.key |
964 | done()(null, h('section', |
965 | h('form', {method: 'post', action: ''}, |
966 | peer.client ? '→' : '←', ' ', |
967 | h('code', peer.host, ':', peer.port, ':'), |
968 | self.app.render.idLink(peer.key, done()), ' ', |
969 | peer.stateChange ? [htime(new Date(peer.stateChange)), ' '] : '', |
970 | peer.state === 'connected' ? 'connected' : [ |
971 | h('input', {name: 'action', type: 'submit', value: 'connect'}), |
972 | h('input', {name: 'address', type: 'hidden', value: addr}) |
973 | ] |
974 | ) |
975 | // h('div', 'source: ', peer.source) |
976 | // JSON.stringify(peer, 0, 2)).outerHTML |
977 | )) |
978 | done(cb) |
979 | }, 8), |
980 | pull.map(u.toHTML), |
981 | self.wrapPeers(), |
982 | self.wrapPage('peers'), |
983 | self.respondSink(200, { |
984 | 'Content-Type': ctype(ext) |
985 | }) |
986 | ) |
987 | } |
988 | |
989 | Serve.prototype.status = function (ext) { |
990 | var self = this |
991 | |
992 | if (!self.app.sbot.status) return pull( |
993 | pull.once('missing sbot status method'), |
994 | this.wrapPage('status'), |
995 | self.respondSink(400) |
996 | ) |
997 | |
998 | pull( |
999 | ph('section', [ |
1000 | ph('h3', 'Status'), |
1001 | pull( |
1002 | u.readNext(function (cb) { |
1003 | self.app.sbotStatus(function (err, status) { |
1004 | cb(err, status && pull.once(status)) |
1005 | }) |
1006 | }), |
1007 | pull.map(function (status) { |
1008 | return h('pre', self.app.render.linkify(JSON.stringify(status, 0, 2))).outerHTML |
1009 | }) |
1010 | ) |
1011 | ]), |
1012 | this.wrapPage('status'), |
1013 | this.respondSink(200) |
1014 | ) |
1015 | } |
1016 | |
1017 | Serve.prototype.channels = function (ext) { |
1018 | var self = this |
1019 | var id = self.app.sbot.id |
1020 | |
1021 | function renderMyChannels() { |
1022 | return pull( |
1023 | self.app.streamMyChannels(id), |
1024 | paramap(function (channel, cb) { |
1025 | // var subscribed = false |
1026 | cb(null, [ |
1027 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
1028 | ' ' |
1029 | ]) |
1030 | }, 8), |
1031 | pull.map(u.toHTML), |
1032 | self.wrapMyChannels() |
1033 | ) |
1034 | } |
1035 | |
1036 | function renderNetworkChannels() { |
1037 | return pull( |
1038 | self.app.streamChannels(), |
1039 | paramap(function (channel, cb) { |
1040 | // var subscribed = false |
1041 | cb(null, [ |
1042 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), |
1043 | ' ' |
1044 | ]) |
1045 | }, 8), |
1046 | pull.map(u.toHTML), |
1047 | self.wrapChannels() |
1048 | ) |
1049 | } |
1050 | |
1051 | pull( |
1052 | cat([ |
1053 | ph('section', {}, [ |
1054 | ph('h3', {}, 'Channels:'), |
1055 | renderMyChannels(), |
1056 | renderNetworkChannels() |
1057 | ]) |
1058 | ]), |
1059 | this.wrapPage('channels'), |
1060 | this.respondSink(200, { |
1061 | 'Content-Type': ctype(ext) |
1062 | }) |
1063 | ) |
1064 | } |
1065 | |
1066 | Serve.prototype.tags = function (path) { |
1067 | var self = this |
1068 | var seen = {} |
1069 | pull( |
1070 | ph('section', [ |
1071 | ph('h3', 'Tags'), |
1072 | pull( |
1073 | self.app.streamTags(), |
1074 | pull.map(function (msg) { |
1075 | return [self.phIdLink(msg.key), ' '] |
1076 | }) |
1077 | ) |
1078 | ]), |
1079 | this.wrapPage('tags'), |
1080 | this.respondSink(200) |
1081 | ) |
1082 | } |
1083 | |
1084 | Serve.prototype.contacts = function (path) { |
1085 | var self = this |
1086 | var id = String(path).substr(1) |
1087 | var contacts = self.app.contacts.createContactStreams({ |
1088 | id: id, |
1089 | msgIds: true, |
1090 | enemies: true |
1091 | }) |
1092 | var render = self.app.render |
1093 | |
1094 | pull( |
1095 | cat([ |
1096 | ph('section', {}, [ |
1097 | ph('h3', {}, ['Contacts: ', self.phIdLink(id)]), |
1098 | ph('h4', {}, 'Friends'), |
1099 | render.friendsList('/contacts/')(contacts.friends), |
1100 | ph('h4', {}, 'Follows'), |
1101 | render.friendsList('/contacts/')(contacts.follows), |
1102 | ph('h4', {}, 'Followers'), |
1103 | render.friendsList('/contacts/')(contacts.followers), |
1104 | ph('h4', {}, 'Blocks'), |
1105 | render.friendsList('/contacts/')(contacts.blocks), |
1106 | ph('h4', {}, 'Blocked by'), |
1107 | render.friendsList('/contacts/')(contacts.blockers), |
1108 | contacts.enemies ? [ |
1109 | ph('h4', {}, 'Enemies'), |
1110 | render.friendsList('/contacts/')(contacts.enemies), |
1111 | ] : '' |
1112 | ]) |
1113 | ]), |
1114 | this.wrapPage('contacts: ' + id), |
1115 | this.respondSink(200, { |
1116 | 'Content-Type': ctype('html') |
1117 | }) |
1118 | ) |
1119 | } |
1120 | |
1121 | Serve.prototype.about = function (path) { |
1122 | var self = this |
1123 | var id = decodeURIComponent(String(path).substr(1)) |
1124 | var abouts = self.app.createAboutStreams(id) |
1125 | var render = self.app.render |
1126 | |
1127 | function renderAboutOpImage(link) { |
1128 | if (!link) return |
1129 | if (!u.isRef(link.link)) return ph('code', {}, JSON.stringify(link)) |
1130 | return ph('img', { |
1131 | class: 'ssb-avatar-image', |
1132 | src: render.imageUrl(link.link), |
1133 | alt: link.link |
1134 | + (link.size ? ' (' + render.formatSize(link.size) + ')' : '') |
1135 | }) |
1136 | } |
1137 | |
1138 | function renderAboutOpValue(value) { |
1139 | if (typeof value === 'object' && value !== null) { |
1140 | if (u.isRef(value.link)) return self.phIdLink(value.link) |
1141 | if (value.epoch) return new Date(value.epoch).toUTCString() |
1142 | } |
1143 | return ph('code', {}, JSON.stringify(value)) |
1144 | } |
1145 | |
1146 | function renderAboutOpContent(op) { |
1147 | if (op.prop === 'image') |
1148 | return renderAboutOpImage(u.toLink(op.value)) |
1149 | if (op.prop === 'description') |
1150 | return h('div', {innerHTML: render.markdown(op.value)}).outerHTML |
1151 | if (op.prop === 'title') |
1152 | return h('strong', op.value).outerHTML |
1153 | if (op.prop === 'name') |
1154 | return h('u', op.value).outerHTML |
1155 | return renderAboutOpValue(op.value) |
1156 | } |
1157 | |
1158 | function renderAboutOp(op) { |
1159 | return ph('tr', {}, [ |
1160 | ph('td', self.phIdLink(op.author)), |
1161 | ph('td', |
1162 | ph('a', {href: render.toUrl(op.id)}, |
1163 | htime(new Date(op.timestamp)))), |
1164 | ph('td', op.prop), |
1165 | ph('td', renderAboutOpContent(op)) |
1166 | ]) |
1167 | } |
1168 | |
1169 | pull( |
1170 | cat([ |
1171 | ph('section', {}, [ |
1172 | ph('h3', {}, ['About: ', self.phIdLink(id)]), |
1173 | ph('table', {}, |
1174 | pull(abouts.scalars, pull.map(renderAboutOp)) |
1175 | ), |
1176 | pull( |
1177 | abouts.sets, |
1178 | pull.map(function (op) { |
1179 | return h('pre', JSON.stringify(op, 0, 2)) |
1180 | }), |
1181 | pull.map(u.toHTML) |
1182 | ) |
1183 | ]) |
1184 | ]), |
1185 | this.wrapPage('about: ' + id), |
1186 | this.respondSink(200, { |
1187 | 'Content-Type': ctype('html') |
1188 | }) |
1189 | ) |
1190 | } |
1191 | |
1192 | Serve.prototype.aboutSelf = function (ext) { |
1193 | var self = this |
1194 | var id = self.app.sbot.id |
1195 | var render = self.app.render |
1196 | |
1197 | self.app.getAbout(id, function gotAbout(err, about) { |
1198 | if (err) return cb(err) |
1199 | |
1200 | var data = self.data |
1201 | var aboutName = about.name ? String(about.name).replace(/^@/, '') : '' |
1202 | var aboutImageLink = about.imageLink || {} |
1203 | var name = data.name != null ? |
1204 | data.name === '' ? null : data.name : |
1205 | aboutName || null |
1206 | var image = data.image_upload != null ? { |
1207 | link: data.image_upload.link, |
1208 | type: data.image_upload.type, |
1209 | size: data.image_upload.size |
1210 | } : data.image_id && data.image_id !== aboutImageLink.link ? { |
1211 | link: data.image_id, |
1212 | type: data.image_type, |
1213 | size: data.image_size |
1214 | } : aboutImageLink |
1215 | var imageId = image.link || '&Zq5m3UOWlFfyUoenOL75ukghOdjmv2yxHREkBNrorWM=.sha256' |
1216 | var description = data.description != null ? |
1217 | data.description === '' ? null : data.description : |
1218 | about.description || null |
1219 | var publicWebHosting = data.publicWebHosting != null ? |
1220 | data.publicWebHosting === 'false' ? false : |
1221 | data.publicWebHosting === 'null' ? null : !!data.publicWebHosting : |
1222 | about.publicWebHosting |
1223 | |
1224 | var content |
1225 | if (data.preview || data.preview_raw) { |
1226 | content = { |
1227 | type: 'about', |
1228 | about: id |
1229 | } |
1230 | if (name != aboutName) content.name = name |
1231 | if (image.link != about.image) content.image = image |
1232 | if (description != about.description) content.description = description |
1233 | if (publicWebHosting != about.publicWebHosting) content.publicWebHosting = publicWebHosting |
1234 | } |
1235 | |
1236 | pull( |
1237 | ph('section', {}, [ |
1238 | ph('h4', 'Your public profile'), |
1239 | ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ |
1240 | ph('div', [ |
1241 | '@', ph('input', {id: 'name', name: 'name', placeholder: 'name', value: name}) |
1242 | ]), |
1243 | ph('table', ph('tr', [ |
1244 | ph('td', [ |
1245 | ph('a', {href: render.toUrl(imageId)}, [ |
1246 | ph('img', { |
1247 | class: 'ssb-avatar-image', |
1248 | src: render.imageUrl(imageId), |
1249 | alt: image.link || 'fallback avatar', |
1250 | title: image.link || 'fallback avatar' |
1251 | }) |
1252 | ]) |
1253 | ]), |
1254 | ph('td', [ |
1255 | image.link ? ph('div', [ |
1256 | ph('small', ph('code', u.escapeHTML(image.link))), |
1257 | ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ', |
1258 | ]) : '', |
1259 | image.size ? [ |
1260 | ph('code', render.formatSize(image.size)), |
1261 | ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ', |
1262 | ] : '', |
1263 | image.type ? [ |
1264 | ph('input', {type: 'hidden', name: 'image_type', value: image.type}) |
1265 | ] : '', |
1266 | ph('div', [ |
1267 | ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'}) |
1268 | ]) |
1269 | ]) |
1270 | ])), |
1271 | ph('textarea', { |
1272 | id: 'description', name: 'description', placeholder: 'description', |
1273 | rows: Math.max(4, u.rows(description)) |
1274 | }, u.escapeHTML(description)), |
1275 | ph('div', { |
1276 | title: 'Allow your messages to be hosted on public viewer websites' |
1277 | }, [ |
1278 | ph('label', {for: 'publicWebHosting'}, 'Public web hosting: '), |
1279 | ph('select', {name: 'publicWebHosting', id: 'publicWebHosting'}, [ |
1280 | ph('option', {value: 'true', selected: publicWebHosting}, 'yes'), |
1281 | ph('option', {value: 'false', selected: publicWebHosting === false}, 'no'), |
1282 | ph('option', {value: 'null', selected: publicWebHosting == null}, '…'), |
1283 | ]) |
1284 | ]), |
1285 | self.phMsgActions(content), |
1286 | ]), |
1287 | content ? self.phPreview(content, {raw: data.preview_raw}) : '' |
1288 | ]), |
1289 | self.wrapPage('about self: ' + id), |
1290 | self.respondSink(200, { |
1291 | 'Content-Type': ctype('html') |
1292 | }) |
1293 | ) |
1294 | }) |
1295 | } |
1296 | |
1297 | Serve.prototype.gathering = function (url) { |
1298 | var self = this |
1299 | var id |
1300 | try { |
1301 | id = decodeURIComponent(url.substr(1)) |
1302 | } catch(err) { |
1303 | return pull( |
1304 | pull.once(u.renderError(err).outerHTML), |
1305 | self.wrapPage('Gathering ' + id), |
1306 | self.respondSink(400) |
1307 | ) |
1308 | } |
1309 | |
1310 | var q = self.query |
1311 | var data = self.data || {} |
1312 | var selfId = self.app.sbot.id |
1313 | var render = self.app.render |
1314 | |
1315 | function hashLink(id) { |
1316 | if (!id) return '' |
1317 | if (typeof id !== 'string') return u.escapeHTML(JSON.stringify(id)) |
1318 | return h('a', {style: 'font: monospace', href: render.toUrl(id)}, |
1319 | id.substr(0, 8) + '…').outerHTML + ' ' |
1320 | } |
1321 | |
1322 | self.app.getAbout(id, function (err, about) { |
1323 | if (err) return pull( |
1324 | pull.once(u.renderError(err).outerHTML), |
1325 | self.respondSink(400) |
1326 | ) |
1327 | |
1328 | var sources = about._sources || {} |
1329 | var start = about.startDateTime |
1330 | var img = about.imageLink |
1331 | var startDate = start && new Date(start.epoch) |
1332 | pull( |
1333 | ph('section', [ |
1334 | ph('h2', ['Gathering ', hashLink(id)]), |
1335 | ph('div', ph('a', {href: render.toUrl('/edit-gathering/' + encodeURIComponent(id))}, 'Edit')), |
1336 | ph('table', [ |
1337 | img ? ph('tr', [ |
1338 | ph('th', 'Image'), |
1339 | ph('td', sources.image.map(hashLink)), |
1340 | ph('td', u.isRef(img.link) ? ph('img', { |
1341 | class: 'ssb-avatar-image', |
1342 | src: render.imageUrl(img.link), |
1343 | alt: img.link |
1344 | + (img.size ? ' (' + render.formatSize(img.size) + ')' : '') |
1345 | }) : ph('code', JSON.stringify(img))), |
1346 | ]) : '', |
1347 | about.title ? ph('tr', [ |
1348 | ph('th', 'Title'), |
1349 | ph('td', sources.title.map(hashLink)), |
1350 | ph('td', ph('h1', {style: 'margin: 0'}, u.escapeHTML(about.title))) |
1351 | ]) : '', |
1352 | about.location ? ph('tr', [ |
1353 | ph('th', 'Location'), |
1354 | ph('td', sources.location.map(hashLink)), |
1355 | ph('td', u.escapeHTML(about.location)) |
1356 | ]) : '', |
1357 | start ? ph('tr', [ |
1358 | ph('th', 'Start time'), |
1359 | ph('td', sources.startDateTime.map(hashLink)), |
1360 | ph('td', [ |
1361 | ph('code', u.escapeHTML(startDate.toISOString().replace(/ .*/, ''))), |
1362 | start.tz ? [ |
1363 | '<br>', |
1364 | ph('code', u.escapeHTML(start.tz)) |
1365 | ] : '', |
1366 | ph('div', u.escapeHTML(startDate.toString())) |
1367 | ]) |
1368 | ]) : '', |
1369 | about.description ? ph('tr', [ |
1370 | ph('th', 'Description'), |
1371 | ph('td', sources.description.map(hashLink)), |
1372 | ph('td', render.markdown(about.description)) |
1373 | ]) : '', |
1374 | /* |
1375 | about.branch ? ph('tr', [ |
1376 | ph('th', 'Latest updates'), |
1377 | ph('th'), |
1378 | ph('td', about.branch.map(hashLink)) |
1379 | ]) : '', |
1380 | */ |
1381 | about.attendee ? about.attendee.map(function (link, i, rows) { |
1382 | return ph('tr', [ |
1383 | i === 0 ? ph('th', {rowspan: rows.length}, 'Attendees') : '', |
1384 | ph('td', hashLink(link.source)), |
1385 | ph('td', self.phIdLink(link.link)) |
1386 | ]) |
1387 | }) : '', |
1388 | ]) |
1389 | ]), |
1390 | self.wrapPage('Gathering ' + id), |
1391 | self.respondSink(200) |
1392 | ) |
1393 | }) |
1394 | } |
1395 | |
1396 | var ignoreDateTimeProps = { |
1397 | silent: true, // internal to spacetime module |
1398 | } |
1399 | |
1400 | function isDateTimeEqual(a, b) { |
1401 | if (a === b) return true |
1402 | if ((a && !b) || (!b && a)) return false |
1403 | for (var k in a) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false |
1404 | for (var k in b) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false |
1405 | return true |
1406 | } |
1407 | |
1408 | Serve.prototype.editGathering = function (url) { |
1409 | var self = this |
1410 | var id |
1411 | try { |
1412 | id = decodeURIComponent(url.substr(1)) |
1413 | } catch(err) { |
1414 | return pull( |
1415 | pull.once(u.renderError(err).outerHTML), |
1416 | self.wrapPage('Edit Gathering ' + id), |
1417 | self.respondSink(400) |
1418 | ) |
1419 | } |
1420 | |
1421 | var q = self.query |
1422 | var data = self.data || {} |
1423 | var selfId = self.app.sbot.id |
1424 | var render = self.app.render |
1425 | |
1426 | self.app.getAbout(id, function (err, about) { |
1427 | if (err) return pull( |
1428 | pull.once(u.renderError(err).outerHTML), |
1429 | self.respondSink(400) |
1430 | ) |
1431 | |
1432 | var aboutImageLink = about.imageLink || {} |
1433 | var aboutStartDateTime = about.startDateTime || {} |
1434 | var title = data.title != null ? |
1435 | data.title === '' ? null : data.title : |
1436 | about.title || null |
1437 | var location = data.location != null ? |
1438 | data.location === '' ? null : data.location : |
1439 | about.location || null |
1440 | var image = data.image_upload != null ? { |
1441 | link: data.image_upload.link, |
1442 | type: data.image_upload.type, |
1443 | size: data.image_upload.size |
1444 | } : data.remove_image ? null : |
1445 | data.image_id && data.image_id !== aboutImageLink.link ? { |
1446 | link: data.image_id, |
1447 | type: data.image_type, |
1448 | size: data.image_size |
1449 | } : aboutImageLink |
1450 | var description = data.description != null ? |
1451 | data.description === '' ? null : data.description : |
1452 | about.description || null |
1453 | |
1454 | // use undefined instead of null to reset these values, |
1455 | // since they are set as a whole object |
1456 | var startDateTimeEpoch = data.startDateTimeStr != null ? |
1457 | data.startDateTimeStr === '' ? null : |
1458 | new Date(data.startDateTimeStr).getTime() : |
1459 | aboutStartDateTime.epoch |
1460 | var startDateTime = startDateTimeEpoch === null ? null |
1461 | : typeof startDateTimeEpoch === 'number' && !isNaN(startDateTimeEpoch) ? { |
1462 | epoch: startDateTimeEpoch, |
1463 | tz: data.startTZ != null ? |
1464 | data.startTZ === '' ? undefined : data.startTZ : |
1465 | aboutStartDateTime.tz, |
1466 | _weekStart: data.startWeekStart != null ? |
1467 | data.startWeekStart === '' ? undefined : Number(data.startWeekStart) : |
1468 | aboutStartDateTime._weekStart |
1469 | } : about.startDateTime || null |
1470 | |
1471 | var attendeeIds = u.toLinkArray(about.attendee).map(u.linkDest) |
1472 | var aboutSelfAttending = attendeeIds.indexOf(selfId) !== -1 |
1473 | var selfAttending = data.attending != null ? Boolean(data.attending) : |
1474 | aboutSelfAttending |
1475 | var mentionAttendees = data.mention_attendees === '' ? true : Boolean(data.mention_attendees) |
1476 | var attendeeMentions = mentionAttendees ? attendeeIds.filter(function (id) { |
1477 | return id !== selfId |
1478 | }) : [] |
1479 | var previousMentions = u.toLinkArray(about.mentions).map(u.linkDest) |
1480 | var additionalMentions = data.mentions ? |
1481 | u.extractRefs(data.mentions).filter(uniques()) : [] |
1482 | var mentions = attendeeMentions.concat(additionalMentions) |
1483 | |
1484 | var content |
1485 | if (data.preview || data.preview_raw) { |
1486 | content = { |
1487 | type: 'about', |
1488 | about: id |
1489 | } |
1490 | if (about.recps) content.recps = about.recps |
1491 | if (about.branch) content.branch = about.branch |
1492 | if (title != about.title) content.title = title |
1493 | if (location != about.location) content.location = location |
1494 | if (image === null) { |
1495 | if (about.image) content.image = {link: about.image, remove: true} |
1496 | } else if (image.link != about.image) content.image = image |
1497 | if (description != about.description) { |
1498 | content.description = description |
1499 | var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false}) |
1500 | // don't mention ids already mentioned in the thread |
1501 | textMentions.forEach(function (link) { |
1502 | if (mentions.indexOf(link.link) === -1 |
1503 | && previousMentions.indexOf(link.link) === -1) { |
1504 | mentions.push(reduceLink(link)) |
1505 | } |
1506 | }) |
1507 | } |
1508 | if (!isDateTimeEqual(startDateTime, about.startDateTime)) content.startDateTime = startDateTime |
1509 | if (mentions.length) content.mentions = mentions |
1510 | if (selfAttending != aboutSelfAttending) content.attendee = selfAttending ? { |
1511 | link: selfId |
1512 | } : { |
1513 | link: selfId, |
1514 | remove: true |
1515 | } |
1516 | } |
1517 | |
1518 | var startDateTimeStr = '' |
1519 | if (startDateTime) try { |
1520 | startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '') |
1521 | } catch(e) {} |
1522 | var startTz = startDateTime && startDateTime.tz || null |
1523 | var startWeekStart = startDateTime && startDateTime._weekStart || null |
1524 | |
1525 | pull( |
1526 | ph('section', [ |
1527 | ph('h2', ['Edit Gathering ', self.phIdLink(id)]), |
1528 | ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ |
1529 | ph('div', [ |
1530 | ph('input', {name: 'title', size: 64, placeholder: 'Title', value: u.escapeHTML(title)}) |
1531 | ]), |
1532 | ph('div', [ |
1533 | ph('input', {name: 'location', size: 64, placeholder: 'Location', value: u.escapeHTML(location)}) |
1534 | ]), |
1535 | ph('div', [ |
1536 | ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr), |
1537 | style: 'font: monospace', size: 30, placeholder: 'Start date time'}), |
1538 | ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '', |
1539 | style: 'font: monospace', size: 20, placeholder: 'TZ'}), |
1540 | ph('input', {name: 'startWeekStart', |
1541 | value: startWeekStart == null ? '' : u.toString(startWeekStart), |
1542 | style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'}) |
1543 | ]), |
1544 | ph('table', ph('tr', [ |
1545 | ph('td', [ |
1546 | data.remove_image ? ph('input', {type: 'hidden', name: 'remove_image', value: u.escapeHTML(data.remove_image)}) : '', |
1547 | image && image.link ? ph('a', {href: render.toUrl(image.link)}, [ |
1548 | ph('img', { |
1549 | class: 'ssb-avatar-image', |
1550 | src: render.imageUrl(image.link), |
1551 | alt: image.link || 'fallback avatar', |
1552 | title: image.link || 'fallback avatar' |
1553 | }) |
1554 | ]) : '' |
1555 | ]), |
1556 | ph('td', [ |
1557 | image && image.link ? ph('div', [ |
1558 | ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ', |
1559 | ph('small', ph('code', u.escapeHTML(image.link))), |
1560 | ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ', |
1561 | ]) : '', |
1562 | image && image.size ? [ |
1563 | ph('code', render.formatSize(image.size)), |
1564 | ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ', |
1565 | ] : '', |
1566 | image && image.type ? [ |
1567 | ph('input', {type: 'hidden', name: 'image_type', value: image.type}) |
1568 | ] : '', |
1569 | ph('div', [ |
1570 | ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'}) |
1571 | ]) |
1572 | ]) |
1573 | ])), |
1574 | ph('div', ph('textarea', { |
1575 | name: 'description', placeholder: 'Description', |
1576 | cols: 64, |
1577 | rows: Math.max(5, u.rows(description)) |
1578 | }, u.escapeHTML(description))), |
1579 | ph('div', ph('select', {name: 'attending'}, [ |
1580 | ph('option', {value: '1', selected: !selfAttending ? 'selected' : undefined}, 'You are not attending'), |
1581 | ph('option', {value: '1', selected: selfAttending ? 'selected' : undefined}, 'You are attending'), |
1582 | ])), |
1583 | ph('div', ph('label', {for: 'mention_attendees'}, [ |
1584 | ph('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: '1', checked: mentionAttendees ? 'checked' : undefined}), |
1585 | ' mention attendees' |
1586 | ])), |
1587 | ph('div', additionalMentions.length > 0 || data.add_mentions ? ph('textarea', { |
1588 | name: 'mentions', placeholder: 'Additional mentions', |
1589 | cols: 64, style: 'font: monospace', |
1590 | rows: Math.max(3, additionalMentions.length + 2), |
1591 | }, u.escapeHTML(additionalMentions.join('\n')) |
1592 | ) : ph('label', {for: 'additional_mentions'}, [ |
1593 | ph('input', {id: 'additional_mentions', type: 'checkbox', name: 'add_mentions', value: '1'}), |
1594 | ' additional mentions' |
1595 | ]) |
1596 | ), |
1597 | self.phMsgActions(content) |
1598 | ]), |
1599 | content ? self.phPreview(content, {raw: data.preview_raw}) : '' |
1600 | ]), |
1601 | self.wrapPage('Edit Gathering ' + id), |
1602 | self.respondSink(200) |
1603 | ) |
1604 | }) |
1605 | } |
1606 | |
1607 | Serve.prototype.newGathering = function () { |
1608 | var self = this |
1609 | var q = self.query |
1610 | var data = self.data || {} |
1611 | var selfId = self.app.sbot.id |
1612 | var render = self.app.render |
1613 | |
1614 | var title = data.title ? u.toString(data.title) : null |
1615 | var description = data.description ? u.toString(data.description) : null |
1616 | var location = data.location ? u.toString(data.location) : null |
1617 | var image = data.image_upload != null ? { |
1618 | link: data.image_upload.link, |
1619 | type: data.image_upload.type, |
1620 | size: data.image_upload.size |
1621 | } : data.remove_image ? null : data.image_id ? { |
1622 | link: data.image_id, |
1623 | type: data.image_type, |
1624 | size: data.image_size |
1625 | } : null |
1626 | var private = Boolean(data.private) |
1627 | var recps = data.recps ? u.extractRefs(data.recps) : private ? [selfId] : null |
1628 | // default recps to self id but allow removing it |
1629 | |
1630 | var startTs = data.startDateTimeStr ? |
1631 | new Date(data.startDateTimeStr).getTime() : null |
1632 | var startDateTime = typeof startTs === 'number' && !isNaN(startTs) ? { |
1633 | epoch: startTs, |
1634 | tz: data.startTZ ? u.toString(data.startTZ) : undefined, |
1635 | _weekStart: data.startWeekStart ? Number(data.startWeekStart) : undefined |
1636 | } : null |
1637 | |
1638 | var selfAttending = Boolean(data.attending) |
1639 | var initialMentions = data.mentions ? u.extractRefs(data.mentions) : [] |
1640 | |
1641 | var content |
1642 | if (data.preview || data.preview_raw) { |
1643 | content = { |
1644 | type: 'about' |
1645 | } |
1646 | if (private && recps) content.recps = recps |
1647 | if (title) content.title = title |
1648 | if (location) content.location = location |
1649 | if (image) content.image = image |
1650 | if (description) { |
1651 | content.description = description |
1652 | var mentions = ssbMentions(description, {bareFeedNames: false, emoji: false}) |
1653 | .map(reduceLink) |
1654 | if (mentions.length) content.mentions = mentions |
1655 | } |
1656 | if (startDateTime) content.startDateTime = startDateTime |
1657 | if (selfAttending) content.attendee = { |
1658 | link: selfId |
1659 | } |
1660 | } |
1661 | |
1662 | var aboutNewContent = { |
1663 | type: 'gathering', |
1664 | mentions: initialMentions.length ? initialMentions : undefined |
1665 | } |
1666 | |
1667 | var startDateTimeStr = '' |
1668 | if (startDateTime) try { |
1669 | startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '') |
1670 | } catch(e) {} |
1671 | var startTz = startDateTime && startDateTime.tz || null |
1672 | var startWeekStart = startDateTime && startDateTime._weekStart || null |
1673 | |
1674 | pull( |
1675 | ph('section', [ |
1676 | ph('h2', ['New Gathering']), |
1677 | ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ |
1678 | ph('div', [ |
1679 | ph('select', {name: 'private'}, [ |
1680 | ph('option', {value: '', selected: !private ? 'selected' : undefined}, 'Public'), |
1681 | ph('option', {value: '1', selected: private ? 'selected' : undefined}, 'Private') |
1682 | ]) |
1683 | ]), |
1684 | private ? ph('div', ph('textarea', { |
1685 | name: 'recps', placeholder: 'Recipients', |
1686 | title: 'Recipient IDs for private gathering', |
1687 | cols: 64, style: 'font: monospace', |
1688 | rows: Math.max(3, recps.length + 2), |
1689 | }, u.escapeHTML(recps.join('\n')) + '\n')) : recps ? ph('input', { |
1690 | type: 'hidden', name: 'recps', value: recps.join('\n') |
1691 | }) : null, |
1692 | ph('div', [ |
1693 | ph('input', {name: 'title', size: 64, placeholder: 'Title', value: u.escapeHTML(title)}) |
1694 | ]), |
1695 | ph('div', [ |
1696 | ph('input', {name: 'location', size: 64, placeholder: 'Location', value: u.escapeHTML(location)}) |
1697 | ]), |
1698 | ph('div', [ |
1699 | ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr), |
1700 | style: 'font: monospace', size: 30, placeholder: 'Start date time'}), |
1701 | ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '', |
1702 | style: 'font: monospace', size: 20, placeholder: 'TZ'}), |
1703 | ph('input', {name: 'startWeekStart', |
1704 | value: startWeekStart == null ? '' : u.toString(startWeekStart), |
1705 | style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'}) |
1706 | ]), |
1707 | ph('table', ph('tr', [ |
1708 | ph('td', [ |
1709 | image && image.link ? ph('a', {href: render.toUrl(image.link)}, [ |
1710 | ph('img', { |
1711 | class: 'ssb-avatar-image', |
1712 | src: render.imageUrl(image.link), |
1713 | alt: image.link || 'fallback avatar', |
1714 | title: image.link || 'fallback avatar' |
1715 | }) |
1716 | ]) : '' |
1717 | ]), |
1718 | ph('td', [ |
1719 | image && image.link ? ph('div', [ |
1720 | ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ', |
1721 | ph('small', ph('code', u.escapeHTML(image.link))), |
1722 | ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ', |
1723 | ]) : '', |
1724 | image && image.size ? [ |
1725 | ph('code', render.formatSize(image.size)), |
1726 | ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ', |
1727 | ] : '', |
1728 | image && image.type ? [ |
1729 | ph('input', {type: 'hidden', name: 'image_type', value: image.type}) |
1730 | ] : '', |
1731 | ph('div', [ |
1732 | ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'}) |
1733 | ]) |
1734 | ]) |
1735 | ])), |
1736 | ph('div', ph('textarea', { |
1737 | name: 'description', placeholder: 'Description', |
1738 | cols: 64, |
1739 | rows: Math.max(5, u.rows(description)) |
1740 | }, u.escapeHTML(description))), |
1741 | ph('div', ph('label', {for: 'attending'}, [ |
1742 | ph('input', {id: 'attending', type: 'checkbox', name: 'attending', |
1743 | value: '1', checked: selfAttending ? 'checked' : undefined}), |
1744 | ' attending' |
1745 | ])), |
1746 | ph('div', ph('textarea', { |
1747 | name: 'mentions', placeholder: 'Initial mentions', |
1748 | title: 'SSB feed/blob/message IDs to mention in the gathering root message', |
1749 | cols: 53, style: 'font: monospace', |
1750 | rows: Math.max(3, initialMentions.length + 1), |
1751 | }, u.escapeHTML(initialMentions.join('\n')+'\n'))), |
1752 | self.phMsgActions(content) |
1753 | ]), |
1754 | content ? self.phPreview(content, { |
1755 | raw: data.preview_raw, |
1756 | aboutNewContent: aboutNewContent |
1757 | }) : '' |
1758 | ]), |
1759 | self.wrapPage('New Gathering'), |
1760 | self.respondSink(200) |
1761 | ) |
1762 | } |
1763 | |
1764 | Serve.prototype.block = function (path) { |
1765 | var self = this |
1766 | var data = self.data |
1767 | var id = String(path).substr(1) |
1768 | try { id = decodeURIComponent(id) } |
1769 | catch(e) {} |
1770 | |
1771 | var content |
1772 | if (data.preview || data.preview_raw) { |
1773 | content = { |
1774 | type: 'contact', |
1775 | contact: id, |
1776 | blocking: true |
1777 | } |
1778 | var reason = typeof data.reason === 'string' ? data.reason : null |
1779 | if (reason) content.reason = reason |
1780 | } |
1781 | |
1782 | function renderDraftLink(draftId) { |
1783 | return pull.values([ |
1784 | ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURI(draftId)), |
1785 | title: 'draft link'}, u.escapeHTML(draftId)), |
1786 | ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ', |
1787 | ]) |
1788 | } |
1789 | |
1790 | pull( |
1791 | ph('section', [ |
1792 | ph('h2', ['Block ', self.phIdLink(id)]), |
1793 | ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ |
1794 | 'Reason: ', ph('input', {name: 'reason', value: reason || '', |
1795 | className: 'wide', |
1796 | placeholder: 'spam, abuse, etc.'}), |
1797 | self.phMsgActions(content), |
1798 | ]), |
1799 | content ? self.phPreview(content, {raw: data.preview_raw}) : '' |
1800 | ]), |
1801 | self.wrapPage('Block ' + id), |
1802 | self.respondSink(200) |
1803 | ) |
1804 | } |
1805 | |
1806 | Serve.prototype.type = function (path) { |
1807 | var q = this.query |
1808 | var type = decodeURIComponent(path.substr(1)) |
1809 | var opts = { |
1810 | sortByTimestamp: q.sort === 'claimed', |
1811 | reverse: !q.forwards, |
1812 | lt: Number(q.lt) || Date.now(), |
1813 | gt: Number(q.gt) || -Infinity, |
1814 | type: type, |
1815 | filter: q.filter, |
1816 | } |
1817 | |
1818 | pull( |
1819 | this.app.sbotMessagesByType(opts), |
1820 | this.renderThreadPaginated(opts, null, q), |
1821 | this.wrapMessages(), |
1822 | this.wrapType(type), |
1823 | this.wrapPage('type: ' + type), |
1824 | this.respondSink(200, { |
1825 | 'Content-Type': ctype('html') |
1826 | }) |
1827 | ) |
1828 | } |
1829 | |
1830 | Serve.prototype.links = function (path) { |
1831 | var q = this.query |
1832 | var dest = path.substr(1) |
1833 | var sbot = this.app.sbot |
1834 | |
1835 | pull( |
1836 | q.rel || q.author || !sbot.backlinks ? (q.type ? |
1837 | pull.error(new Error('Unable to satisfy query')) : |
1838 | sbot.links({ |
1839 | source: q.author || undefined, |
1840 | dest: dest, |
1841 | reverse: true, |
1842 | values: true, |
1843 | rel: q.rel |
1844 | })) : sbot.backlinks.read({ |
1845 | reverse: true, |
1846 | query: [ |
1847 | {$filter: { |
1848 | dest: dest, |
1849 | value: q.type ? { |
1850 | content: { |
1851 | type: q.type |
1852 | } |
1853 | } : {} |
1854 | }} |
1855 | ] |
1856 | }), |
1857 | this.renderThread(), |
1858 | this.wrapMessages(), |
1859 | this.wrapLinks(dest), |
1860 | this.wrapPage('links: ' + dest), |
1861 | this.respondSink(200, { |
1862 | 'Content-Type': ctype('html') |
1863 | }) |
1864 | ) |
1865 | } |
1866 | |
1867 | Serve.prototype.rawId = function (id) { |
1868 | var self = this |
1869 | |
1870 | self.getMsgDecryptedMaybeOoo(id, function (err, msg) { |
1871 | if (err) return pull( |
1872 | pull.once(u.renderError(err).outerHTML), |
1873 | self.respondSink(400, {'Content-Type': ctype('html')}) |
1874 | ) |
1875 | return pull( |
1876 | pull.once(msg), |
1877 | self.renderRawMsgPage(id), |
1878 | self.respondSink(200, { |
1879 | 'Content-Type': ctype('html'), |
1880 | }) |
1881 | ) |
1882 | }) |
1883 | } |
1884 | |
1885 | Serve.prototype.channel = function (path) { |
1886 | var channel = decodeURIComponent(String(path).substr(1)) |
1887 | var q = this.query |
1888 | var gt = Number(q.gt) || -Infinity |
1889 | var lt = Number(q.lt) || Date.now() |
1890 | var opts = { |
1891 | sortByTimestamp: q.sort === 'claimed', |
1892 | reverse: !q.forwards, |
1893 | lt: lt, |
1894 | gt: gt, |
1895 | channel: channel, |
1896 | filter: q.filter, |
1897 | } |
1898 | |
1899 | pull( |
1900 | this.app.streamChannel(opts), |
1901 | this.renderThreadPaginated(opts, null, q), |
1902 | this.wrapMessages(), |
1903 | this.wrapChannel(channel), |
1904 | this.wrapPage('#' + channel), |
1905 | this.respondSink(200, { |
1906 | 'Content-Type': ctype('html') |
1907 | }) |
1908 | ) |
1909 | } |
1910 | |
1911 | function threadHeads(msgs, rootId, opts) { |
1912 | var includeVotes = opts && opts.includeVotes |
1913 | return sort.heads(msgs.filter(function (msg) { |
1914 | var c = msg.value && msg.value.content |
1915 | return (c && ( |
1916 | c.type === 'web-root' ? c.site === rootId : |
1917 | c.type === 'talenet-idea-comment_reply' ? c.ideaKey === rootId : |
1918 | c.type === 'vote' ? includeVotes : |
1919 | c.root === rootId)) |
1920 | || msg.key === rootId |
1921 | })) |
1922 | } |
1923 | |
1924 | Serve.prototype.streamThreadWithComposer = function (opts) { |
1925 | var self = this |
1926 | var id = opts.root |
1927 | var threadHeadsOpts = {includeVotes: self.app.voteBranches} |
1928 | return ph('table', {class: 'ssb-msgs'}, u.readNext(next)) |
1929 | function next(cb) { |
1930 | self.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) { |
1931 | if (err && err.name === 'NotFoundError') err = null, rootMsg = { |
1932 | key: id, value: {content: false}} |
1933 | if (err) return cb(new Error(err.stack || err)) |
1934 | if (!rootMsg) { |
1935 | console.log('id', id, 'opts', opts) |
1936 | } |
1937 | var rootContent = rootMsg && rootMsg.value && rootMsg.value.content |
1938 | var recps = rootContent && rootContent.recps |
1939 | || ((rootMsg.value.private || typeof rootMsg.value.content === 'string') |
1940 | ? [rootMsg.value.author, self.app.sbot.id].filter(uniques()) |
1941 | : undefined) |
1942 | var threadRootId = rootContent && ( |
1943 | rootContent.type === 'web-root' ? rootContent.site : rootContent.root |
1944 | ) || id |
1945 | var channel = opts.channel |
1946 | |
1947 | pull( |
1948 | self.noThread ? pull.once(rootMsg) : self.app.getThread(rootMsg), |
1949 | pull.unique('key'), |
1950 | self.app.unboxMessages(), |
1951 | pull.through(function (msg) { |
1952 | var c = msg && msg.value.content |
1953 | if (!channel && c && c.channel) channel = c.channel |
1954 | }), |
1955 | pull.collect(function (err, links) { |
1956 | if (err) return gotLinks(err) |
1957 | if (!self.useOoo) return gotLinks(null, links) |
1958 | self.app.expandOoo({msgs: links, dest: id}, gotLinks) |
1959 | }) |
1960 | ) |
1961 | function gotLinks(err, links) { |
1962 | if (err) return cb(new Error(err.stack)) |
1963 | var branches = threadHeads(links, threadRootId, threadHeadsOpts) |
1964 | cb(null, pull( |
1965 | pull.values(sort(links)), |
1966 | self.app.voteBranches && pull.map(function (link) { |
1967 | var o = {} |
1968 | for (var k in link) o[k] = link[k] |
1969 | o.threadBranches = branches |
1970 | o.threadRoot = threadRootId |
1971 | return o |
1972 | }), |
1973 | self.renderThread({ |
1974 | msgId: id, |
1975 | branches: branches, |
1976 | links: links, |
1977 | }), |
1978 | self.wrapMessages(), |
1979 | self.wrapThread({ |
1980 | recps: recps, |
1981 | root: threadRootId, |
1982 | post: id, |
1983 | branches: branches, |
1984 | links: links, |
1985 | postBranches: threadRootId !== id && threadHeads(links, id, threadHeadsOpts), |
1986 | placeholder: opts.placeholder, |
1987 | channel: channel, |
1988 | }) |
1989 | )) |
1990 | } |
1991 | }) |
1992 | } |
1993 | } |
1994 | |
1995 | Serve.prototype.streamMsg = function (id) { |
1996 | var self = this |
1997 | return pull( |
1998 | self.app.pullGetMsg(id), |
1999 | self.renderThread({ |
2000 | msgId: id, |
2001 | single: self.query.single != null |
2002 | }), |
2003 | self.wrapMessages() |
2004 | ) |
2005 | } |
2006 | |
2007 | Serve.prototype.id = function (id, path) { |
2008 | var self = this |
2009 | if (self.query.raw != null) return self.rawId(id) |
2010 | pull( |
2011 | self.query.single != null |
2012 | ? self.streamMsg(id) |
2013 | : self.streamThreadWithComposer({root: id}), |
2014 | self.wrapPage(id), |
2015 | self.respondSink(200) |
2016 | ) |
2017 | } |
2018 | |
2019 | Serve.prototype.userFeed = function (id, path) { |
2020 | var self = this |
2021 | var q = self.query |
2022 | var opts = { |
2023 | id: id, |
2024 | reverse: !q.forwards, |
2025 | lt: Number(q.lt) || Date.now(), |
2026 | gt: Number(q.gt) || -Infinity, |
2027 | feed: id, |
2028 | filter: q.filter, |
2029 | } |
2030 | var isScrolled = q.lt || q.gt |
2031 | |
2032 | self.app.getAbout(id, function (err, about) { |
2033 | if (err) self.app.error(err) |
2034 | pull( |
2035 | self.app.sbotCreateUserStream(opts), |
2036 | self.renderThreadPaginated(opts, id, q), |
2037 | self.wrapMessages(), |
2038 | self.wrapUserFeed(isScrolled, id), |
2039 | self.wrapPage(about && about.name || id), |
2040 | self.respondSink(200) |
2041 | ) |
2042 | }) |
2043 | } |
2044 | |
2045 | Serve.prototype.file = function (file) { |
2046 | var self = this |
2047 | fs.stat(file, function (err, stat) { |
2048 | if (err && err.code === 'ENOENT') return self.respond(404, 'Not found') |
2049 | if (err) return self.respond(500, err.stack || err) |
2050 | if (!stat.isFile()) return self.respond(403, 'May only load files') |
2051 | if (self.ifModified(stat.mtime)) return self.respond(304, 'Not modified') |
2052 | self.res.writeHead(200, { |
2053 | 'Content-Type': ctype(file), |
2054 | 'Content-Length': stat.size, |
2055 | 'Last-Modified': stat.mtime.toGMTString() |
2056 | }) |
2057 | fs.createReadStream(file).pipe(self.res) |
2058 | }) |
2059 | } |
2060 | |
2061 | Serve.prototype.static = function (file) { |
2062 | this.file(path.join(__dirname, '../static', file)) |
2063 | } |
2064 | |
2065 | Serve.prototype.highlight = function (dirs) { |
2066 | this.file(path.join(hlCssDir, dirs)) |
2067 | } |
2068 | |
2069 | Serve.prototype.blob = function (id, path) { |
2070 | var self = this |
2071 | var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+') |
2072 | var etag = '"' + id + (path || '') + (unbox || '') + '"' |
2073 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
2074 | var key |
2075 | if (path) { |
2076 | try { path = decodeURIComponent(path) } catch(e) {} |
2077 | if (path[0] === '#') { |
2078 | unbox = path.substr(1) |
2079 | } else { |
2080 | return self.respond(400, 'Bad blob request') |
2081 | } |
2082 | } |
2083 | if (unbox) { |
2084 | try { |
2085 | key = Buffer.from(unbox, 'base64') |
2086 | } catch(err) { |
2087 | return self.respond(400, err.message) |
2088 | } |
2089 | if (key.length !== 32) { |
2090 | return self.respond(400, 'Bad blob key') |
2091 | } |
2092 | } |
2093 | self.app.wantSizeBlob(id, function (err, size) { |
2094 | if (err) { |
2095 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
2096 | else return self.respond(500, err.message || err) |
2097 | } |
2098 | self.res.setHeader('Accept-Ranges', 'bytes') |
2099 | var range = self.req.headers.range |
2100 | if (range) { |
2101 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
2102 | // TODO: support multiple ranges |
2103 | var m = /^bytes=([0-9]*)-([0-9]*)$/.exec(range) |
2104 | if (!m) return self.respond(416, 'Unable to parse range') |
2105 | var start = m[1] |
2106 | var last = m[2] |
2107 | if (start === '') { |
2108 | start = size - last |
2109 | last = size - 1 |
2110 | } else if (last === '') { |
2111 | start = Number(start) |
2112 | last = size - 1 |
2113 | } else { |
2114 | start = Number(start) |
2115 | last = Number(last) |
2116 | } |
2117 | if (start > size || last >= size) return res.writeHead(416, 'Range not satisfiable') |
2118 | var end = last + 1 |
2119 | var length = end - start |
2120 | var wroteHeaders = false |
2121 | pull( |
2122 | // TODO: figure out how to use readBlobSlice for private blob range request |
2123 | key ? pull( |
2124 | self.app.getBlob(id, key), |
2125 | u.pullSlice(start, end) |
2126 | ) : self.app.readBlobSlice({ |
2127 | link: id, |
2128 | size: size |
2129 | }, { |
2130 | start: start, |
2131 | end: end, |
2132 | }), |
2133 | pull.through(function (buf) { |
2134 | if (wroteHeaders) return |
2135 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
2136 | self.res.setHeader('ETag', etag) |
2137 | self.res.setHeader('Content-Length', length) |
2138 | self.res.setHeader('Content-Range', 'bytes ' + start + '-' + last + '/' + size) |
2139 | self.res.writeHead(206) |
2140 | wroteHeaders = true |
2141 | }), |
2142 | pull.map(Buffer.from), |
2143 | self.respondSink() |
2144 | ) |
2145 | |
2146 | } else { |
2147 | pull( |
2148 | self.app.getBlob(id, key), |
2149 | pull.map(Buffer.from), |
2150 | ident(gotType), |
2151 | self.respondSink() |
2152 | ) |
2153 | function gotType(type) { |
2154 | type = type && mime.lookup(type) |
2155 | if (type) self.res.setHeader('Content-Type', type) |
2156 | // don't serve size for encrypted blob, because it refers to the size of |
2157 | // the ciphertext |
2158 | if (typeof size === 'number' && !key) |
2159 | self.res.setHeader('Content-Length', size) |
2160 | if (self.query.filename) self.res.setHeader('Content-Disposition', |
2161 | 'inline; filename='+encodeDispositionFilename(self.query.filename)) |
2162 | if (self.query.gzip) |
2163 | self.res.setHeader('Content-Encoding', 'gzip') |
2164 | if (self.query.contentType) |
2165 | self.res.setHeader('Content-Type', self.query.contentType) |
2166 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
2167 | self.res.setHeader('etag', etag) |
2168 | self.res.writeHead(200) |
2169 | } |
2170 | } |
2171 | }) |
2172 | } |
2173 | |
2174 | Serve.prototype.image = function (path) { |
2175 | var self = this |
2176 | var id, key |
2177 | var m = urlIdRegex.exec(path) |
2178 | if (m && m[2] === '&') id = m[1], path = m[3] |
2179 | var unbox = typeof this.query.unbox === 'string' && this.query.unbox.replace(/\s/g, '+') |
2180 | var etag = '"image-' + id + (path || '') + (unbox || '') + '"' |
2181 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
2182 | if (path) { |
2183 | try { path = decodeURIComponent(path) } catch(e) {} |
2184 | if (path[0] === '#') { |
2185 | unbox = path.substr(1) |
2186 | } else { |
2187 | return self.respond(400, 'Bad blob request') |
2188 | } |
2189 | } |
2190 | if (unbox) { |
2191 | try { |
2192 | key = Buffer.from(unbox, 'base64') |
2193 | } catch(err) { |
2194 | return self.respond(400, err.message) |
2195 | } |
2196 | if (key.length !== 32) { |
2197 | return self.respond(400, 'Bad blob key') |
2198 | } |
2199 | } |
2200 | self.app.wantSizeBlob(id, function (err, size) { |
2201 | if (err) { |
2202 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
2203 | else return self.respond(500, err.message || err) |
2204 | } |
2205 | |
2206 | var done = multicb({pluck: 1, spread: true}) |
2207 | var heresTheData = done() |
2208 | var heresTheType = done().bind(self, null) |
2209 | |
2210 | pull( |
2211 | self.app.getBlob(id, key), |
2212 | pull.map(Buffer.from), |
2213 | ident(heresTheType), |
2214 | pull.collect(onFullBuffer) |
2215 | ) |
2216 | |
2217 | function onFullBuffer (err, buffer) { |
2218 | if (err) return heresTheData(err) |
2219 | buffer = Buffer.concat(buffer) |
2220 | |
2221 | try { |
2222 | jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) { |
2223 | if (!err) buffer = rotatedBuffer |
2224 | |
2225 | heresTheData(null, buffer) |
2226 | pull( |
2227 | pull.once(buffer), |
2228 | self.respondSink() |
2229 | ) |
2230 | }) |
2231 | } catch (err) { |
2232 | console.trace(err) |
2233 | self.respond(500, err.message || err) |
2234 | } |
2235 | } |
2236 | |
2237 | done(function (err, data, type) { |
2238 | if (err) { |
2239 | console.trace(err) |
2240 | self.respond(500, err.message || err) |
2241 | return |
2242 | } |
2243 | type = type && mime.lookup(type) |
2244 | if (type) self.res.setHeader('Content-Type', type) |
2245 | self.res.setHeader('Content-Length', data.length) |
2246 | if (self.query.filename) self.res.setHeader('Content-Disposition', |
2247 | 'inline; filename='+encodeDispositionFilename(self.query.filename)) |
2248 | if (self.query.gzip) |
2249 | self.res.setHeader('Content-Encoding', 'gzip') |
2250 | if (self.query.contentType) |
2251 | self.res.setHeader('Content-Type', self.query.contentType) |
2252 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
2253 | self.res.setHeader('ETag', etag) |
2254 | self.res.writeHead(200) |
2255 | }) |
2256 | }) |
2257 | } |
2258 | |
2259 | Serve.prototype.ifModified = function (lastMod) { |
2260 | var ifModSince = this.req.headers['if-modified-since'] |
2261 | if (!ifModSince) return false |
2262 | var d = new Date(ifModSince) |
2263 | return d && Math.floor(d/1000) >= Math.floor(lastMod/1000) |
2264 | } |
2265 | |
2266 | Serve.prototype.wrapMessages = function () { |
2267 | var self = this |
2268 | return u.hyperwrap(function (content, cb) { |
2269 | cb(null, h('table.ssb-msgs', content)) |
2270 | }) |
2271 | } |
2272 | |
2273 | Serve.prototype.renderThread = function (opts) { |
2274 | return pull( |
2275 | this.app.render.renderFeeds({ |
2276 | raw: false, |
2277 | full: this.query.full != null, |
2278 | feed: opts && opts.feed, |
2279 | msgId: opts && opts.msgId, |
2280 | filter: this.query.filter, |
2281 | limit: Number(this.query.limit), |
2282 | serve: this, |
2283 | links: opts && opts.links, |
2284 | single: opts && opts.single, |
2285 | branches: opts && opts.branches, |
2286 | }), |
2287 | pull.map(u.toHTML) |
2288 | ) |
2289 | } |
2290 | |
2291 | Serve.prototype.renderThreadPaginated = function (opts, feedId, q) { |
2292 | var self = this |
2293 | function linkA(opts, name) { |
2294 | var q1 = u.mergeOpts(q, opts) |
2295 | return h('a', {href: '?' + qs.stringify(q1)}, name || q1.limit) |
2296 | } |
2297 | function links(opts) { |
2298 | var limit = opts.limit || q.limit || 10 |
2299 | return h('tr', h('td.paginate', {colspan: 3}, |
2300 | opts.forwards ? '↑ newer ' : '↓ older ', |
2301 | linkA(u.mergeOpts(opts, {limit: 1})), ' ', |
2302 | linkA(u.mergeOpts(opts, {limit: 10})), ' ', |
2303 | linkA(u.mergeOpts(opts, {limit: 100})) |
2304 | )) |
2305 | } |
2306 | |
2307 | return pull( |
2308 | self.app.filterMessages({ |
2309 | feed: opts && opts.feed, |
2310 | msgId: opts && opts.msgId, |
2311 | filter: this.query.filter, |
2312 | limit: Number(this.query.limit) || 12, |
2313 | }), |
2314 | paginate( |
2315 | function onFirst(msg, cb) { |
2316 | var num = feedId ? msg.value.sequence : |
2317 | opts.sortByTimestamp ? msg.value.timestamp : |
2318 | msg.timestamp || msg.ts |
2319 | if (q.forwards) { |
2320 | cb(null, links({ |
2321 | lt: num, |
2322 | gt: null, |
2323 | forwards: null, |
2324 | filter: opts.filter, |
2325 | })) |
2326 | } else { |
2327 | cb(null, links({ |
2328 | lt: null, |
2329 | gt: num, |
2330 | forwards: 1, |
2331 | filter: opts.filter, |
2332 | })) |
2333 | } |
2334 | }, |
2335 | this.app.render.renderFeeds({ |
2336 | raw: false, |
2337 | full: this.query.full != null, |
2338 | feed: opts && opts.feed, |
2339 | msgId: opts && opts.msgId, |
2340 | filter: this.query.filter, |
2341 | serve: this, |
2342 | limit: Number(this.query.limit) || 12, |
2343 | }), |
2344 | function onLast(msg, cb) { |
2345 | var num = feedId ? msg.value.sequence : |
2346 | opts.sortByTimestamp ? msg.value.timestamp : |
2347 | msg.timestamp || msg.ts |
2348 | if (q.forwards) { |
2349 | cb(null, links({ |
2350 | lt: null, |
2351 | gt: num, |
2352 | forwards: 1, |
2353 | filter: opts.filter, |
2354 | })) |
2355 | } else { |
2356 | cb(null, links({ |
2357 | lt: num, |
2358 | gt: null, |
2359 | forwards: null, |
2360 | filter: opts.filter, |
2361 | })) |
2362 | } |
2363 | }, |
2364 | function onEmpty(cb) { |
2365 | if (q.forwards) { |
2366 | cb(null, links({ |
2367 | gt: null, |
2368 | lt: opts.gt + 1, |
2369 | forwards: null, |
2370 | filter: opts.filter, |
2371 | })) |
2372 | } else { |
2373 | cb(null, links({ |
2374 | gt: opts.lt - 1, |
2375 | lt: null, |
2376 | forwards: 1, |
2377 | filter: opts.filter, |
2378 | })) |
2379 | } |
2380 | } |
2381 | ), |
2382 | pull.map(u.toHTML) |
2383 | ) |
2384 | } |
2385 | |
2386 | Serve.prototype.renderRawMsgPage = function (id) { |
2387 | var showMarkdownSource = (this.query.raw === 'md') |
2388 | var raw = !showMarkdownSource |
2389 | return pull( |
2390 | this.app.render.renderFeeds({ |
2391 | raw: raw, |
2392 | msgId: id, |
2393 | filter: this.query.filter, |
2394 | serve: this, |
2395 | markdownSource: showMarkdownSource |
2396 | }), |
2397 | pull.map(u.toHTML), |
2398 | this.wrapMessages(), |
2399 | this.wrapPage(id) |
2400 | ) |
2401 | } |
2402 | |
2403 | function catchHTMLError() { |
2404 | return function (read) { |
2405 | var ended |
2406 | return function (abort, cb) { |
2407 | if (ended) return cb(ended) |
2408 | read(abort, function (end, data) { |
2409 | if (!end || end === true) { |
2410 | try { return cb(end, data) } |
2411 | catch(e) { return console.trace(e) } |
2412 | } |
2413 | ended = true |
2414 | cb(null, u.renderError(end).outerHTML) |
2415 | }) |
2416 | } |
2417 | } |
2418 | } |
2419 | |
2420 | function catchTextError() { |
2421 | return function (read) { |
2422 | var ended |
2423 | return function (abort, cb) { |
2424 | if (ended) return cb(ended) |
2425 | read(abort, function (end, data) { |
2426 | if (!end || end === true) return cb(end, data) |
2427 | ended = true |
2428 | cb(null, end.stack + '\n') |
2429 | }) |
2430 | } |
2431 | } |
2432 | } |
2433 | |
2434 | function styles() { |
2435 | return fs.readFileSync(path.join(__dirname, '../static/styles.css'), 'utf8') |
2436 | } |
2437 | |
2438 | Serve.prototype.appendFooter = function () { |
2439 | var self = this |
2440 | return function (read) { |
2441 | if (self.noFooter) return read |
2442 | return cat([read, u.readNext(function (cb) { |
2443 | var ms = new Date() - self.startDate |
2444 | cb(null, pull.once(h('footer', |
2445 | h('a', {href: pkg.homepage}, pkg.name), ' - ', |
2446 | ms/1000 + 's' |
2447 | ).outerHTML)) |
2448 | })]) |
2449 | } |
2450 | } |
2451 | |
2452 | Serve.prototype.wrapPage = function (title, searchQ) { |
2453 | var self = this |
2454 | var render = self.app.render |
2455 | return pull( |
2456 | catchHTMLError(), |
2457 | self.appendFooter(), |
2458 | u.hyperwrap(function (content, cb) { |
2459 | var done = multicb({pluck: 1, spread: true}) |
2460 | done()(null, h('html', h('head', |
2461 | h('meta', {'http-equiv': 'Content-Type', content: 'text/html; charset=utf-8'}), |
2462 | h('title', title), |
2463 | h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}), |
2464 | h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}), |
2465 | h('style', styles()), |
2466 | h('link', {rel: 'stylesheet', href: render.toUrl('/highlight/foundation.css')}) |
2467 | ), |
2468 | h('body' + (self.printView ? '.print-view' : ''), |
2469 | self.noNav ? '' : |
2470 | h('nav.nav-bar', h('form', {action: render.toUrl('/search'), method: 'get'}, |
2471 | self.app.navLinks.map(function (link, i) { |
2472 | return [i == 0 ? '' : ' ', |
2473 | link === 'self' ? render.idLink(self.app.sbot.id, done()) : |
2474 | link === 'searchbox' ? h('input.search-input', |
2475 | {name: 'q', value: searchQ, placeholder: 'search'}) : |
2476 | link === 'search' ? h('a', {href: render.toUrl('/advsearch')}, 'search') : |
2477 | typeof link === 'string' ? h('a', {href: render.toUrl('/' + link)}, link) : |
2478 | link ? h('a', {href: render.toUrl(link.url)}, link.name) : '' |
2479 | ] |
2480 | }) |
2481 | )), |
2482 | self.query.published ? h('div', |
2483 | 'published ', |
2484 | render.msgIdLink(self.query.published, done()) |
2485 | ) : '', |
2486 | // self.note, |
2487 | content |
2488 | ))) |
2489 | done(cb) |
2490 | }) |
2491 | ) |
2492 | } |
2493 | |
2494 | Serve.prototype.phIdLink = function (id, opts) { |
2495 | return pull( |
2496 | pull.once(id), |
2497 | this.renderIdsList(opts) |
2498 | ) |
2499 | } |
2500 | |
2501 | Serve.prototype.phIdAvatar = function (id) { |
2502 | var self = this |
2503 | return u.readNext(function (cb) { |
2504 | var el = self.app.render.avatarImage(id, function (err) { |
2505 | if (err) return cb(err) |
2506 | cb(null, pull.once(u.toHTML(el))) |
2507 | }) |
2508 | }) |
2509 | } |
2510 | |
2511 | Serve.prototype.friends = function (path) { |
2512 | var self = this |
2513 | var friends = self.app.sbot.friends |
2514 | if (!friends) return pull( |
2515 | pull.once('missing ssb-friends plugin'), |
2516 | this.wrapPage('friends'), |
2517 | self.respondSink(400) |
2518 | ) |
2519 | if (!friends.createFriendStream) return pull( |
2520 | pull.once('missing friends.createFriendStream method'), |
2521 | this.wrapPage('friends'), |
2522 | self.respondSink(400) |
2523 | ) |
2524 | |
2525 | pull( |
2526 | friends.createFriendStream({hops: 1}), |
2527 | self.renderIdsList(), |
2528 | u.hyperwrap(function (items, cb) { |
2529 | cb(null, [ |
2530 | h('section', |
2531 | h('h3', 'Friends') |
2532 | ), |
2533 | h('section', items) |
2534 | ]) |
2535 | }), |
2536 | this.wrapPage('friends'), |
2537 | this.respondSink(200, { |
2538 | 'Content-Type': ctype('html') |
2539 | }) |
2540 | ) |
2541 | } |
2542 | |
2543 | Serve.prototype.renderIdsList = function (opts) { |
2544 | var self = this |
2545 | return pull( |
2546 | paramap(function (id, cb) { |
2547 | self.app.render.getNameLink(id, opts, cb) |
2548 | }, 8), |
2549 | pull.map(function (el) { |
2550 | return [el, ' '] |
2551 | }), |
2552 | pull.map(u.toHTML) |
2553 | ) |
2554 | } |
2555 | |
2556 | Serve.prototype.aboutDescription = function (id) { |
2557 | var self = this |
2558 | return u.readNext(function (cb) { |
2559 | self.app.getAbout(id, function (err, about) { |
2560 | if (err) return cb(err) |
2561 | if (!about.description) return cb(null, pull.empty()) |
2562 | cb(null, ph('div', self.app.render.markdown(about.description))) |
2563 | }) |
2564 | }) |
2565 | } |
2566 | |
2567 | Serve.prototype.followInfo = function (id, myId) { |
2568 | var self = this |
2569 | return u.readNext(function (cb) { |
2570 | var done = multicb({pluck: 1, spread: true}) |
2571 | self.app.getContact(myId, id, done()) |
2572 | self.app.getContact(id, myId, done()) |
2573 | self.app.isMuted(id, done()) |
2574 | done(function (err, contactToThem, contactFromThem, isMuted) { |
2575 | if (err) return cb(err) |
2576 | cb(null, ph('form', {action: '', method: 'post'}, [ |
2577 | contactFromThem ? contactToThem ? 'friend ' : 'follows you ' : |
2578 | contactFromThem === false ? 'blocks you ' : '', |
2579 | ph('input', {type: 'hidden', name: 'action', value: 'contact'}), |
2580 | ph('input', {type: 'hidden', name: 'contact', value: id}), |
2581 | ph('input', {type: 'submit', |
2582 | name: contactToThem ? 'unfollow' : 'follow', |
2583 | value: contactToThem ? 'unfollow' : 'follow'}), ' ', |
2584 | contactToThem === false |
2585 | ? ph('input', {type: 'submit', name: 'unblock', value: 'unblock'}) |
2586 | : ph('input', {type: 'submit', name: 'block1', value: 'block…'}), ' ', |
2587 | ph('input', {type: 'submit', |
2588 | name: isMuted ? 'unmute' : 'mute', |
2589 | value: isMuted ? 'unmute' : 'mute', |
2590 | title: isMuted ? 'unmute (private unblock)' : 'mute (private block)'}), ' ', |
2591 | ph('input', {type: 'submit', |
2592 | name: self.requestedReplicate ? 'unreplicate' : 'replicate', |
2593 | value: self.requestedReplicate ? 'unreplicate' : 'replicate', |
2594 | title: self.requestedReplicate |
2595 | ? 'Temporarily cancel replicating this feed' |
2596 | : 'Temporarily replicate this feed'}) |
2597 | ])) |
2598 | }) |
2599 | }) |
2600 | } |
2601 | |
2602 | Serve.prototype.friendInfo = function (id, myId) { |
2603 | var first = false |
2604 | return pull( |
2605 | this.app.contacts.createFollowedFollowersStream(myId, id), |
2606 | this.app.render.friendsList(), |
2607 | pull.map(function (html) { |
2608 | if (!first) { |
2609 | first = true |
2610 | return 'followed by your friends: ' + html |
2611 | } |
2612 | return html |
2613 | }) |
2614 | ) |
2615 | } |
2616 | |
2617 | Serve.prototype.wrapUserFeed = function (isScrolled, id) { |
2618 | var self = this |
2619 | var myId = self.app.sbot.id |
2620 | var render = self.app.render |
2621 | return function (thread) { |
2622 | return cat([ |
2623 | ph('section', {class: 'ssb-feed'}, ph('table', [ |
2624 | isScrolled ? '' : ph('tr', [ |
2625 | ph('td', self.phIdAvatar(id)), |
2626 | ph('td', {class: 'feed-about'}, [ |
2627 | ph('h3', {class: 'feed-name'}, |
2628 | ph('strong', self.phIdLink(id))), |
2629 | ph('code', ph('small', id)), |
2630 | self.aboutDescription(id) |
2631 | ]) |
2632 | ]), |
2633 | ph('tr', [ |
2634 | ph('td'), |
2635 | ph('td', [ |
2636 | ph('a', {href: render.toUrl('/contacts/' + id)}, 'contacts'), ' ', |
2637 | ph('a', {href: render.toUrl('/about/' + id)}, 'about'), |
2638 | id === myId ? [' ', |
2639 | ph('a', {href: render.toUrl('/about-self')}, 'about-self')] : '', |
2640 | !isScrolled ? u.readNext(function (cb) { |
2641 | self.app.isPub(id, function (err, isPub) { |
2642 | if (err) return cb(err) |
2643 | if (!isPub) return cb(null, pull.empty()) |
2644 | cb(null, ph('span', [' ', ph('a', {href: render.toUrl('/pub/' + id)}, 'pub')])) |
2645 | }) |
2646 | }) : '' |
2647 | ]) |
2648 | ]), |
2649 | ph('tr', [ |
2650 | ph('td'), |
2651 | ph('td', |
2652 | ph('form', {action: render.toUrl('/advsearch'), method: 'get'}, [ |
2653 | ph('input', {type: 'hidden', name: 'source', value: id}), |
2654 | ph('input', {type: 'text', name: 'text', placeholder: 'text'}), |
2655 | ph('input', {type: 'submit', value: 'search'}) |
2656 | ]) |
2657 | ) |
2658 | ]), |
2659 | isScrolled || id === myId ? '' : [ |
2660 | ph('tr', [ |
2661 | ph('td'), |
2662 | ph('td', {class: 'follow-info'}, self.followInfo(id, myId)) |
2663 | /* |
2664 | ]), |
2665 | ph('tr', [ |
2666 | ph('td'), |
2667 | ph('td', self.friendInfo(id, myId)) |
2668 | */ |
2669 | ]) |
2670 | ] |
2671 | ])), |
2672 | thread |
2673 | ]) |
2674 | } |
2675 | } |
2676 | |
2677 | Serve.prototype.git = function (url) { |
2678 | var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url) |
2679 | switch (m[1]) { |
2680 | case 'object': return this.gitObject(m[2]) |
2681 | case 'commit': return this.gitCommit(m[2]) |
2682 | case 'tag': return this.gitTag(m[2]) |
2683 | case 'tree': return this.gitTree(m[2]) |
2684 | case 'blob': return this.gitBlob(m[2]) |
2685 | case 'raw': return this.gitRaw(m[2]) |
2686 | case 'diff': return this.gitDiff(m[2]) |
2687 | case 'signature': return this.gitSignature(m[2]) |
2688 | case 'line-comment': return this.gitLineComment(m[2]) |
2689 | default: return this.respond(404, 'Not found') |
2690 | } |
2691 | } |
2692 | |
2693 | Serve.prototype.gitRaw = function (rev) { |
2694 | var self = this |
2695 | if (!/[0-9a-f]{24}/.test(rev)) { |
2696 | return pull( |
2697 | pull.once('\'' + rev + '\' is not a git object id'), |
2698 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
2699 | ) |
2700 | } |
2701 | if (!u.isRef(self.query.msg)) return pull( |
2702 | ph('div.error', 'missing message id'), |
2703 | self.wrapPage('git object ' + rev), |
2704 | self.respondSink(400) |
2705 | ) |
2706 | |
2707 | self.app.git.openObject({ |
2708 | obj: rev, |
2709 | msg: self.query.msg, |
2710 | }, function (err, obj) { |
2711 | if (err && err.name === 'BlobNotFoundError') |
2712 | return self.askWantBlobs(err.links) |
2713 | if (err) return pull( |
2714 | pull.once(err.stack), |
2715 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
2716 | ) |
2717 | pull( |
2718 | self.app.git.readObject(obj), |
2719 | catchTextError(), |
2720 | ident(function (type) { |
2721 | type = type && mime.lookup(type) |
2722 | if (type) self.res.setHeader('Content-Type', type) |
2723 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
2724 | self.res.setHeader('ETag', rev) |
2725 | self.res.writeHead(200) |
2726 | }), |
2727 | self.respondSink() |
2728 | ) |
2729 | }) |
2730 | } |
2731 | |
2732 | Serve.prototype.gitAuthorLink = function (author) { |
2733 | if (author.feed) { |
2734 | var myName = this.app.getNameSync(author.feed) |
2735 | var sigil = author.name === author.localpart ? '@' : '' |
2736 | return ph('a', { |
2737 | href: this.app.render.toUrl(author.feed), |
2738 | title: author.localpart + (myName ? ' (' + myName + ')' : '') |
2739 | }, u.escapeHTML(sigil + author.name)) |
2740 | } else { |
2741 | return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)}, |
2742 | u.escapeHTML(author.name)) |
2743 | } |
2744 | } |
2745 | |
2746 | Serve.prototype.gitObject = function (rev) { |
2747 | var self = this |
2748 | if (!/[0-9a-f]{24}/.test(rev)) { |
2749 | return pull( |
2750 | ph('div.error', 'rev is not a git object id'), |
2751 | self.wrapPage('git'), |
2752 | self.respondSink(400) |
2753 | ) |
2754 | } |
2755 | if (!u.isRef(self.query.msg)) return pull( |
2756 | ph('div.error', 'missing message id'), |
2757 | self.wrapPage('git object ' + rev), |
2758 | self.respondSink(400) |
2759 | ) |
2760 | |
2761 | if (self.query.search) { |
2762 | return self.app.git.getObjectMsg({ |
2763 | obj: rev, |
2764 | headMsgId: self.query.msg, |
2765 | }, function (err, msg) { |
2766 | if (err && err.name === 'BlobNotFoundError') |
2767 | return self.askWantBlobs(err.links) |
2768 | if (err) return pull( |
2769 | pull.once(u.renderError(err).outerHTML), |
2770 | self.wrapPage('git object ' + rev), |
2771 | self.respondSink(400) |
2772 | ) |
2773 | var path = '/git/object/' + rev |
2774 | + '?msg=' + encodeURIComponent(msg.key) |
2775 | return self.redirect(self.app.render.toUrl(path)) |
2776 | }) |
2777 | } |
2778 | |
2779 | self.app.git.openObject({ |
2780 | obj: rev, |
2781 | msg: self.query.msg, |
2782 | }, function (err, obj) { |
2783 | if (err && err.name === 'BlobNotFoundError') |
2784 | return self.askWantBlobs(err.links) |
2785 | if (err) return pull( |
2786 | pull.once(u.renderError(err).outerHTML), |
2787 | self.wrapPage('git object ' + rev), |
2788 | self.respondSink(400) |
2789 | ) |
2790 | self.app.git.statObject(obj, function (err, stat) { |
2791 | if (err) return pull( |
2792 | pull.once(u.renderError(err).outerHTML), |
2793 | self.wrapPage('git object ' + rev), |
2794 | self.respondSink(400) |
2795 | ) |
2796 | var path = '/git/' + stat.type + '/' + rev |
2797 | + '?msg=' + encodeURIComponent(self.query.msg) |
2798 | return self.redirect(self.app.render.toUrl(path)) |
2799 | }) |
2800 | }) |
2801 | } |
2802 | |
2803 | Serve.prototype.gitSignature = function (id) { |
2804 | var self = this |
2805 | if (!/[0-9a-f]{24}/.test(id)) { |
2806 | return pull( |
2807 | ph('div.error', 'not a git object id'), |
2808 | self.wrapPage('git'), |
2809 | self.respondSink(400) |
2810 | ) |
2811 | } |
2812 | if (!u.isRef(self.query.msg)) return pull( |
2813 | ph('div.error', 'missing message id'), |
2814 | self.wrapPage('git signature for ' + id), |
2815 | self.respondSink(400) |
2816 | ) |
2817 | |
2818 | self.app.git.openObject({ |
2819 | obj: id, |
2820 | msg: self.query.msg, |
2821 | type: self.query.type, |
2822 | }, function (err, obj) { |
2823 | if (err) return handleError(err) |
2824 | var msgDate = new Date(obj.msg.value.timestamp) |
2825 | self.app.verifyGitObjectSignature(obj, function (err, verification) { |
2826 | if (err) return handleError(err) |
2827 | var objPath = '/git/object/' + id + '?msg=' + encodeURIComponent(obj.msg.key) |
2828 | pull( |
2829 | ph('section', [ |
2830 | ph('h3', [ |
2831 | ph('a', {href: self.app.render.toUrl(objPath)}, id), ': ', |
2832 | ph('a', {href: ''}, 'signature') |
2833 | ]), |
2834 | ph('div', [ |
2835 | self.phIdLink(obj.msg.value.author), ' pushed ', |
2836 | ph('a', { |
2837 | href: self.app.render.toUrl(obj.msg.key), |
2838 | title: msgDate.toLocaleString(), |
2839 | }, htime(msgDate)) |
2840 | ]), |
2841 | ph('pre', u.escapeHTML(verification.output)) |
2842 | /* |
2843 | verification.goodsig ? 'good' : 'bad', |
2844 | ph('pre', u.escapeHTML(verification.status)) |
2845 | */ |
2846 | ]), |
2847 | self.wrapPage('git signature for ' + id), |
2848 | self.respondSink(200) |
2849 | ) |
2850 | }) |
2851 | }) |
2852 | |
2853 | function handleError(err) { |
2854 | if (err && err.name === 'BlobNotFoundError') |
2855 | return self.askWantBlobs(err.links) |
2856 | if (err) return pull( |
2857 | pull.once(u.renderError(err).outerHTML), |
2858 | self.wrapPage('git signature for ' + id), |
2859 | self.respondSink(400) |
2860 | ) |
2861 | } |
2862 | } |
2863 | |
2864 | Serve.prototype.gitCommit = function (rev) { |
2865 | var self = this |
2866 | if (!/[0-9a-f]{24}/.test(rev)) { |
2867 | return pull( |
2868 | ph('div.error', 'rev is not a git object id'), |
2869 | self.wrapPage('git'), |
2870 | self.respondSink(400) |
2871 | ) |
2872 | } |
2873 | if (!u.isRef(self.query.msg)) return pull( |
2874 | ph('div.error', 'missing message id'), |
2875 | self.wrapPage('git commit ' + rev), |
2876 | self.respondSink(400) |
2877 | ) |
2878 | |
2879 | if (self.query.search) { |
2880 | return self.app.git.getObjectMsg({ |
2881 | obj: rev, |
2882 | headMsgId: self.query.msg, |
2883 | }, function (err, msg) { |
2884 | if (err && err.name === 'BlobNotFoundError') |
2885 | return self.askWantBlobs(err.links) |
2886 | if (err) return pull( |
2887 | pull.once(u.renderError(err).outerHTML), |
2888 | self.wrapPage('git commit ' + rev), |
2889 | self.respondSink(400) |
2890 | ) |
2891 | var path = '/git/commit/' + rev |
2892 | + '?msg=' + encodeURIComponent(msg.key) |
2893 | return self.redirect(self.app.render.toUrl(path)) |
2894 | }) |
2895 | } |
2896 | |
2897 | self.app.git.openObject({ |
2898 | obj: rev, |
2899 | msg: self.query.msg, |
2900 | type: 'commit', |
2901 | }, function (err, obj) { |
2902 | if (err && err.name === 'BlobNotFoundError') |
2903 | return self.askWantBlobs(err.links) |
2904 | if (err) return pull( |
2905 | pull.once(u.renderError(err).outerHTML), |
2906 | self.wrapPage('git commit ' + rev), |
2907 | self.respondSink(400) |
2908 | ) |
2909 | var msgDate = new Date(obj.msg.value.timestamp) |
2910 | self.app.git.getCommit(obj, function (err, commit) { |
2911 | var missingBlobs |
2912 | if (err && err.name === 'BlobNotFoundError') |
2913 | missingBlobs = err.links, err = null |
2914 | if (err) return pull( |
2915 | pull.once(u.renderError(err).outerHTML), |
2916 | self.wrapPage('git commit ' + rev), |
2917 | self.respondSink(400) |
2918 | ) |
2919 | pull( |
2920 | ph('section', [ |
2921 | ph('h3', ph('a', {href: ''}, rev)), |
2922 | ph('div', [ |
2923 | self.phIdLink(obj.msg.value.author), ' pushed ', |
2924 | ph('a', { |
2925 | href: self.app.render.toUrl(obj.msg.key), |
2926 | title: msgDate.toLocaleString(), |
2927 | }, htime(msgDate)) |
2928 | ]), |
2929 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
2930 | ph('div', [ |
2931 | self.gitAuthorLink(commit.committer), |
2932 | ' committed ', |
2933 | ph('span', {title: commit.committer.date.toLocaleString()}, |
2934 | htime(commit.committer.date)), |
2935 | ' in ', commit.committer.tz |
2936 | ]), |
2937 | commit.author ? ph('div', [ |
2938 | self.gitAuthorLink(commit.author), |
2939 | ' authored ', |
2940 | ph('span', {title: commit.author.date.toLocaleString()}, |
2941 | htime(commit.author.date)), |
2942 | ' in ', commit.author.tz |
2943 | ]) : '', |
2944 | commit.parents.length ? ph('div', ['parents: ', pull( |
2945 | pull.values(commit.parents), |
2946 | self.gitObjectLinks(obj.msg.key, 'commit') |
2947 | )]) : '', |
2948 | commit.tree ? ph('div', ['tree: ', pull( |
2949 | pull.once(commit.tree), |
2950 | self.gitObjectLinks(obj.msg.key, 'tree') |
2951 | )]) : '', |
2952 | commit.gpgsig ? ph('div', [ |
2953 | ph('a', {href: self.app.render.toUrl( |
2954 | '/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg) |
2955 | )}, 'signature'), |
2956 | commit.signatureVersion ? [' from ', ph('code', u.escapeHTML(commit.signatureVersion))] : '' |
2957 | ]) : '', |
2958 | h('blockquote', |
2959 | self.app.render.gitCommitBody(commit.body)).outerHTML, |
2960 | ph('h4', 'files'), |
2961 | ph('table', pull( |
2962 | self.app.git.readCommitChanges(commit), |
2963 | pull.map(function (file) { |
2964 | var msg = file.msg || obj.msg |
2965 | return ph('tr', [ |
2966 | ph('td', ph('code', u.escapeHTML(file.name))), |
2967 | ph('td', file.deleted ? 'deleted' |
2968 | : file.created ? |
2969 | ph('a', {href: |
2970 | self.app.render.toUrl('/git/blob/' |
2971 | + (file.hash[1] || file.hash[0]) |
2972 | + '?msg=' + encodeURIComponent(msg.key)) |
2973 | + '&commit=' + rev |
2974 | + '&path=' + encodeURIComponent(file.name) |
2975 | + '&search=1' |
2976 | }, 'created') |
2977 | : file.hash ? |
2978 | ph('a', {href: |
2979 | self.app.render.toUrl('/git/diff/' |
2980 | + file.hash[0] + '..' + file.hash[1] |
2981 | + '?msg=' + encodeURIComponent(msg.key)) |
2982 | + '&commit=' + rev |
2983 | + '&path=' + encodeURIComponent(file.name) |
2984 | + '&search=1' |
2985 | }, 'changed') |
2986 | : file.mode ? 'mode changed' |
2987 | : JSON.stringify(file)) |
2988 | ]) |
2989 | }), |
2990 | Catch(function (err) { |
2991 | if (err && err.name === 'ObjectNotFoundError') return |
2992 | if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links) |
2993 | return false |
2994 | }) |
2995 | )) |
2996 | ] |
2997 | ]), |
2998 | self.wrapPage('git commit ' + rev), |
2999 | self.respondSink(missingBlobs ? 409 : 200) |
3000 | ) |
3001 | }) |
3002 | }) |
3003 | } |
3004 | |
3005 | Serve.prototype.gitTag = function (rev) { |
3006 | var self = this |
3007 | if (!/[0-9a-f]{24}/.test(rev)) { |
3008 | return pull( |
3009 | ph('div.error', 'rev is not a git object id'), |
3010 | self.wrapPage('git'), |
3011 | self.respondSink(400) |
3012 | ) |
3013 | } |
3014 | if (!u.isRef(self.query.msg)) return pull( |
3015 | ph('div.error', 'missing message id'), |
3016 | self.wrapPage('git tag ' + rev), |
3017 | self.respondSink(400) |
3018 | ) |
3019 | |
3020 | if (self.query.search) { |
3021 | return self.app.git.getObjectMsg({ |
3022 | obj: rev, |
3023 | headMsgId: self.query.msg, |
3024 | }, function (err, msg) { |
3025 | if (err && err.name === 'BlobNotFoundError') |
3026 | return self.askWantBlobs(err.links) |
3027 | if (err) return pull( |
3028 | pull.once(u.renderError(err).outerHTML), |
3029 | self.wrapPage('git tag ' + rev), |
3030 | self.respondSink(400) |
3031 | ) |
3032 | var path = '/git/tag/' + rev |
3033 | + '?msg=' + encodeURIComponent(msg.key) |
3034 | return self.redirect(self.app.render.toUrl(path)) |
3035 | }) |
3036 | } |
3037 | |
3038 | self.app.git.openObject({ |
3039 | obj: rev, |
3040 | msg: self.query.msg, |
3041 | type: 'tag', |
3042 | }, function (err, obj) { |
3043 | if (err && err.name === 'BlobNotFoundError') |
3044 | return self.askWantBlobs(err.links) |
3045 | if (err) return pull( |
3046 | pull.once(u.renderError(err).outerHTML), |
3047 | self.wrapPage('git tag ' + rev), |
3048 | self.respondSink(400) |
3049 | ) |
3050 | |
3051 | var msgDate = new Date(obj.msg.value.timestamp) |
3052 | self.app.git.getTag(obj, function (err, tag) { |
3053 | if (err && err.message === 'expected type \'tag\' but found \'commit\'') { |
3054 | var path = '/git/commit/' + rev |
3055 | + '?msg=' + encodeURIComponent(self.query.msg) |
3056 | return self.redirect(self.app.render.toUrl(path)) |
3057 | } |
3058 | var missingBlobs |
3059 | if (err && err.name === 'BlobNotFoundError') |
3060 | missingBlobs = err.links, err = null |
3061 | if (err) return pull( |
3062 | pull.once(u.renderError(err).outerHTML), |
3063 | self.wrapPage('git tag ' + rev), |
3064 | self.respondSink(400) |
3065 | ) |
3066 | pull( |
3067 | ph('section', [ |
3068 | ph('h3', ph('a', {href: ''}, rev)), |
3069 | ph('div', [ |
3070 | self.phIdLink(obj.msg.value.author), ' pushed ', |
3071 | ph('a', { |
3072 | href: self.app.render.toUrl(obj.msg.key), |
3073 | title: msgDate.toLocaleString(), |
3074 | }, htime(msgDate)) |
3075 | ]), |
3076 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ |
3077 | ph('div', [ |
3078 | self.gitAuthorLink(tag.tagger), |
3079 | ' tagged ', |
3080 | ph('span', {title: tag.tagger.date.toLocaleString()}, |
3081 | htime(tag.tagger.date)), |
3082 | ' in ', tag.tagger.tz |
3083 | ]), |
3084 | tag.type, ' ', |
3085 | pull( |
3086 | pull.once(tag.object), |
3087 | self.gitObjectLinks(obj.msg.key, tag.type) |
3088 | ), ' ', |
3089 | ph('code', u.escapeHTML(tag.tag)), |
3090 | tag.gpgsig ? ph('div', [ |
3091 | ph('a', {href: self.app.render.toUrl( |
3092 | '/git/signature/' + rev + '?msg=' + encodeURIComponent(self.query.msg) |
3093 | )}, 'signature'), |
3094 | tag.signatureVersion ? [' from ', ph('code', u.escapeHTML(tag.signatureVersion))] : '' |
3095 | ]) : '', |
3096 | h('pre', self.app.render.linkify(tag.body)).outerHTML, |
3097 | ] |
3098 | ]), |
3099 | self.wrapPage('git tag ' + rev), |
3100 | self.respondSink(missingBlobs ? 409 : 200) |
3101 | ) |
3102 | }) |
3103 | }) |
3104 | } |
3105 | |
3106 | Serve.prototype.gitTree = function (rev) { |
3107 | var self = this |
3108 | if (!/[0-9a-f]{24}/.test(rev)) { |
3109 | return pull( |
3110 | ph('div.error', 'rev is not a git object id'), |
3111 | self.wrapPage('git'), |
3112 | self.respondSink(400) |
3113 | ) |
3114 | } |
3115 | if (!u.isRef(self.query.msg)) return pull( |
3116 | ph('div.error', 'missing message id'), |
3117 | self.wrapPage('git tree ' + rev), |
3118 | self.respondSink(400) |
3119 | ) |
3120 | |
3121 | self.app.git.openObject({ |
3122 | obj: rev, |
3123 | msg: self.query.msg, |
3124 | }, function (err, obj) { |
3125 | var missingBlobs |
3126 | if (err && err.name === 'BlobNotFoundError') |
3127 | missingBlobs = err.links, err = null |
3128 | if (err) return pull( |
3129 | pull.once(u.renderError(err).outerHTML), |
3130 | self.wrapPage('git tree ' + rev), |
3131 | self.respondSink(400) |
3132 | ) |
3133 | var msgDate = new Date(obj.msg.value.timestamp) |
3134 | pull( |
3135 | ph('section', [ |
3136 | ph('h3', ph('a', {href: ''}, rev)), |
3137 | ph('div', [ |
3138 | self.phIdLink(obj.msg.value.author), ' ', |
3139 | ph('a', { |
3140 | href: self.app.render.toUrl(obj.msg.key), |
3141 | title: msgDate.toLocaleString(), |
3142 | }, htime(msgDate)) |
3143 | ]), |
3144 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [ |
3145 | pull( |
3146 | self.app.git.readTreeFull(obj), |
3147 | pull.map(function (item) { |
3148 | if (!item.msg) return ph('tr', [ |
3149 | ph('td', |
3150 | u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')), |
3151 | ph('td', u.escapeHTML(item.hash)), |
3152 | ph('td', 'missing') |
3153 | ]) |
3154 | var ext = item.name.replace(/.*\./, '') |
3155 | var path = '/git/' + item.type + '/' + item.hash |
3156 | + '?msg=' + encodeURIComponent(item.msg.key) |
3157 | + (ext ? '&ext=' + ext : '') |
3158 | var fileDate = new Date(item.msg.value.timestamp) |
3159 | return ph('tr', [ |
3160 | ph('td', |
3161 | ph('a', {href: self.app.render.toUrl(path)}, |
3162 | u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : ''))), |
3163 | ph('td', |
3164 | self.phIdLink(item.msg.value.author)), |
3165 | ph('td', |
3166 | ph('a', { |
3167 | href: self.app.render.toUrl(item.msg.key), |
3168 | title: fileDate.toLocaleString(), |
3169 | }, htime(fileDate)) |
3170 | ), |
3171 | ]) |
3172 | }), |
3173 | Catch(function (err) { |
3174 | if (err && err.name === 'ObjectNotFoundError') return |
3175 | if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links) |
3176 | return false |
3177 | }) |
3178 | ) |
3179 | ]), |
3180 | ]), |
3181 | self.wrapPage('git tree ' + rev), |
3182 | self.respondSink(missingBlobs ? 409 : 200) |
3183 | ) |
3184 | }) |
3185 | } |
3186 | |
3187 | Serve.prototype.gitBlob = function (rev) { |
3188 | var self = this |
3189 | if (!/[0-9a-f]{24}/.test(rev)) { |
3190 | return pull( |
3191 | ph('div.error', 'rev is not a git object id'), |
3192 | self.wrapPage('git'), |
3193 | self.respondSink(400) |
3194 | ) |
3195 | } |
3196 | if (!u.isRef(self.query.msg)) return pull( |
3197 | ph('div.error', 'missing message id'), |
3198 | self.wrapPage('git object ' + rev), |
3199 | self.respondSink(400) |
3200 | ) |
3201 | |
3202 | if (self.query.search) { |
3203 | return self.app.git.getObjectMsg({ |
3204 | obj: rev, |
3205 | headMsgId: self.query.msg, |
3206 | }, function (err, msg) { |
3207 | if (err && err.name === 'BlobNotFoundError') |
3208 | return self.askWantBlobs(err.links) |
3209 | if (err) return pull( |
3210 | pull.once(u.renderError(err).outerHTML), |
3211 | self.wrapPage('git blob ' + rev), |
3212 | self.respondSink(400) |
3213 | ) |
3214 | var path = '/git/blob/' + rev |
3215 | + '?msg=' + encodeURIComponent(msg.key) |
3216 | return self.redirect(self.app.render.toUrl(path)) |
3217 | }) |
3218 | } |
3219 | |
3220 | self.getMsgDecryptedMaybeOoo(self.query.msg, function (err, msg) { |
3221 | if (err) return pull( |
3222 | pull.once(u.renderError(err).outerHTML), |
3223 | self.wrapPage('git object ' + rev), |
3224 | self.respondSink(400) |
3225 | ) |
3226 | var msgDate = new Date(msg.value.timestamp) |
3227 | self.app.git.openObject({ |
3228 | obj: rev, |
3229 | msg: msg.key, |
3230 | }, function (err, obj) { |
3231 | var missingBlobs |
3232 | if (err && err.name === 'BlobNotFoundError') |
3233 | missingBlobs = err.links, err = null |
3234 | if (err) return pull( |
3235 | pull.once(u.renderError(err).outerHTML), |
3236 | self.wrapPage('git object ' + rev), |
3237 | self.respondSink(400) |
3238 | ) |
3239 | pull( |
3240 | ph('section', [ |
3241 | ph('h3', ph('a', {href: ''}, rev)), |
3242 | ph('div', [ |
3243 | self.phIdLink(msg.value.author), ' ', |
3244 | ph('a', { |
3245 | href: self.app.render.toUrl(msg.key), |
3246 | title: msgDate.toLocaleString(), |
3247 | }, htime(msgDate)) |
3248 | ]), |
3249 | missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull( |
3250 | self.app.git.readObject(obj), |
3251 | self.wrapBinary({ |
3252 | obj: obj, |
3253 | rawUrl: self.app.render.toUrl('/git/raw/' + rev |
3254 | + '?msg=' + encodeURIComponent(msg.key)), |
3255 | ext: self.query.ext |
3256 | }) |
3257 | ), |
3258 | ]), |
3259 | self.wrapPage('git blob ' + rev), |
3260 | self.respondSink(200) |
3261 | ) |
3262 | }) |
3263 | }) |
3264 | } |
3265 | |
3266 | Serve.prototype.gitDiff = function (revs) { |
3267 | var self = this |
3268 | var parts = revs.split('..') |
3269 | if (parts.length !== 2) return pull( |
3270 | ph('div.error', 'revs should be <rev1>..<rev2>'), |
3271 | self.wrapPage('git diff'), |
3272 | self.respondSink(400) |
3273 | ) |
3274 | var rev1 = parts[0] |
3275 | var rev2 = parts[1] |
3276 | if (!/[0-9a-f]{24}/.test(rev1)) return pull( |
3277 | ph('div.error', 'rev 1 is not a git object id'), |
3278 | self.wrapPage('git diff'), |
3279 | self.respondSink(400) |
3280 | ) |
3281 | if (!/[0-9a-f]{24}/.test(rev2)) return pull( |
3282 | ph('div.error', 'rev 2 is not a git object id'), |
3283 | self.wrapPage('git diff'), |
3284 | self.respondSink(400) |
3285 | ) |
3286 | |
3287 | if (!u.isRef(self.query.msg)) return pull( |
3288 | ph('div.error', 'missing message id'), |
3289 | self.wrapPage('git diff'), |
3290 | self.respondSink(400) |
3291 | ) |
3292 | |
3293 | var done = multicb({pluck: 1, spread: true}) |
3294 | // the msg qs param should point to the message for rev2 object. the msg for |
3295 | // rev1 object we will have to look up. |
3296 | self.app.git.getObjectMsg({ |
3297 | obj: rev1, |
3298 | headMsgId: self.query.msg, |
3299 | type: 'blob', |
3300 | }, done()) |
3301 | self.getMsgDecryptedMaybeOoo(self.query.msg, done()) |
3302 | done(function (err, msg1, msg2) { |
3303 | if (err && err.name === 'BlobNotFoundError') |
3304 | return self.askWantBlobs(err.links) |
3305 | if (err) return pull( |
3306 | pull.once(u.renderError(err).outerHTML), |
3307 | self.wrapPage('git diff ' + revs), |
3308 | self.respondSink(400) |
3309 | ) |
3310 | var msg1Date = new Date(msg1.value.timestamp) |
3311 | var msg2Date = new Date(msg2.value.timestamp) |
3312 | var revsShort = rev1.substr(0, 8) + '..' + rev2.substr(0, 8) |
3313 | var path = self.query.path && String(self.query.path) |
3314 | var ext = path && path.replace(/^[^.\/]*/, '') |
3315 | var blob1Url = '/git/blob/' + rev1 + |
3316 | '?msg=' + encodeURIComponent(msg1.key) + |
3317 | (ext ? '&ext=' + encodeURIComponent(ext) : '') |
3318 | var blob2Url = '/git/blob/' + rev2 + |
3319 | '?msg=' + encodeURIComponent(msg2.key) + |
3320 | (ext ? '&ext=' + encodeURIComponent(ext) : '') |
3321 | pull( |
3322 | ph('section', [ |
3323 | ph('h3', ph('a', {href: ''}, revsShort)), |
3324 | ph('div', [ |
3325 | ph('a', { |
3326 | href: self.app.render.toUrl(blob1Url) |
3327 | }, rev1), ' ', |
3328 | self.phIdLink(msg1.value.author), ' ', |
3329 | ph('a', { |
3330 | href: self.app.render.toUrl(msg1.key), |
3331 | title: msg1Date.toLocaleString(), |
3332 | }, htime(msg1Date)) |
3333 | ]), |
3334 | ph('div', [ |
3335 | ph('a', { |
3336 | href: self.app.render.toUrl(blob2Url) |
3337 | }, rev2), ' ', |
3338 | self.phIdLink(msg2.value.author), ' ', |
3339 | ph('a', { |
3340 | href: self.app.render.toUrl(msg2.key), |
3341 | title: msg2Date.toLocaleString(), |
3342 | }, htime(msg2Date)) |
3343 | ]), |
3344 | u.readNext(function (cb) { |
3345 | var done = multicb({pluck: 1, spread: true}) |
3346 | self.app.git.openObject({ |
3347 | obj: rev1, |
3348 | msg: msg1.key, |
3349 | }, done()) |
3350 | self.app.git.openObject({ |
3351 | obj: rev2, |
3352 | msg: msg2.key, |
3353 | }, done()) |
3354 | /* |
3355 | self.app.git.guessCommitAndPath({ |
3356 | obj: rev2, |
3357 | msg: msg2.key, |
3358 | }, done()) |
3359 | */ |
3360 | done(function (err, obj1, obj2/*, info2*/) { |
3361 | if (err && err.name === 'BlobNotFoundError') |
3362 | return cb(null, self.askWantBlobsForm(err.links)) |
3363 | if (err) return cb(err) |
3364 | |
3365 | var done = multicb({pluck: 1, spread: true}) |
3366 | pull.collect(done())(self.app.git.readObject(obj1)) |
3367 | pull.collect(done())(self.app.git.readObject(obj2)) |
3368 | self.app.getLineComments({obj: obj2, hash: rev2}, done()) |
3369 | done(function (err, bufs1, bufs2, lineComments) { |
3370 | if (err) return cb(err) |
3371 | var str1 = Buffer.concat(bufs1, obj1.length).toString('utf8') |
3372 | var str2 = Buffer.concat(bufs2, obj2.length).toString('utf8') |
3373 | var diff = Diff.structuredPatch('', '', str1, str2) |
3374 | cb(null, self.gitDiffTable(diff, lineComments, { |
3375 | obj: obj2, |
3376 | hash: rev2, |
3377 | commit: self.query.commit, // info2.commit, |
3378 | path: self.query.path, // info2.path, |
3379 | })) |
3380 | }) |
3381 | }) |
3382 | }) |
3383 | ]), |
3384 | self.wrapPage('git diff'), |
3385 | self.respondSink(200) |
3386 | ) |
3387 | }) |
3388 | } |
3389 | |
3390 | Serve.prototype.gitDiffTable = function (diff, lineComments, lineCommentInfo) { |
3391 | var updateMsg = lineCommentInfo.obj.msg |
3392 | var self = this |
3393 | return pull( |
3394 | ph('table', [ |
3395 | pull( |
3396 | pull.values(diff.hunks), |
3397 | pull.map(function (hunk) { |
3398 | var oldLine = hunk.oldStart |
3399 | var newLine = hunk.newStart |
3400 | return [ |
3401 | ph('tr', [ |
3402 | ph('td', {colspan: 3}), |
3403 | ph('td', ph('pre', |
3404 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
3405 | '+' + newLine + ',' + hunk.newLines + ' @@')) |
3406 | ]), |
3407 | pull( |
3408 | pull.values(hunk.lines), |
3409 | pull.map(function (line) { |
3410 | var s = line[0] |
3411 | if (s == '\\') return |
3412 | var html = self.app.render.highlight(line) |
3413 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
3414 | var hash = lineCommentInfo.hash |
3415 | var newLineNum = lineNums[lineNums.length-1] |
3416 | var id = hash + '-' + (newLineNum || (lineNums[0] + '-')) |
3417 | var idEnc = encodeURIComponent(id) |
3418 | var allowComment = s !== '-' |
3419 | && self.query.commit && self.query.path |
3420 | return [ |
3421 | ph('tr', { |
3422 | class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
3423 | }, [ |
3424 | lineNums.map(function (num, i) { |
3425 | return ph('td', [ |
3426 | ph('a', { |
3427 | name: i === 0 ? idEnc : undefined, |
3428 | href: '#' + idEnc |
3429 | }, String(num)) |
3430 | ]) |
3431 | }), |
3432 | ph('td', |
3433 | allowComment ? ph('a', { |
3434 | href: '?msg=' + |
3435 | encodeURIComponent(self.query.msg) |
3436 | + '&comment=' + idEnc |
3437 | + '&commit=' + encodeURIComponent(self.query.commit) |
3438 | + '&path=' + encodeURIComponent(self.query.path) |
3439 | + '#' + idEnc |
3440 | }, '…') : '' |
3441 | ), |
3442 | ph('td', ph('pre', html)) |
3443 | ]), |
3444 | (lineComments[newLineNum] ? |
3445 | ph('tr', |
3446 | ph('td', {colspan: 4}, |
3447 | self.renderLineCommentThread(lineComments[newLineNum], id) |
3448 | ) |
3449 | ) |
3450 | : newLineNum && lineCommentInfo && self.query.comment === id ? |
3451 | ph('tr', |
3452 | ph('td', {colspan: 4}, |
3453 | self.renderLineCommentForm({ |
3454 | id: id, |
3455 | line: newLineNum, |
3456 | updateId: updateMsg.key, |
3457 | blobId: hash, |
3458 | repoId: updateMsg.value.content.repo, |
3459 | commitId: lineCommentInfo.commit, |
3460 | filePath: lineCommentInfo.path, |
3461 | }) |
3462 | ) |
3463 | ) |
3464 | : '') |
3465 | ] |
3466 | }) |
3467 | ) |
3468 | ] |
3469 | }) |
3470 | ) |
3471 | ]) |
3472 | ) |
3473 | } |
3474 | |
3475 | Serve.prototype.renderLineCommentThread = function (lineComment, id) { |
3476 | return this.streamThreadWithComposer({ |
3477 | root: lineComment.msg.key, |
3478 | id: id, |
3479 | placeholder: 'reply to line comment thread' |
3480 | }) |
3481 | } |
3482 | |
3483 | Serve.prototype.renderLineCommentForm = function (opts) { |
3484 | return [ |
3485 | this.phComposer({ |
3486 | placeholder: 'comment on this line', |
3487 | id: opts.id, |
3488 | lineComment: opts |
3489 | }) |
3490 | ] |
3491 | } |
3492 | |
3493 | // return a composer, pull-hyperscript style |
3494 | Serve.prototype.phComposer = function (opts) { |
3495 | var self = this |
3496 | return u.readNext(function (cb) { |
3497 | self.composer(opts, function (err, composer) { |
3498 | if (err) return cb(err) |
3499 | cb(null, pull.once(composer.outerHTML)) |
3500 | }) |
3501 | }) |
3502 | } |
3503 | |
3504 | Serve.prototype.gitLineComment = function (path) { |
3505 | var self = this |
3506 | var id |
3507 | try { |
3508 | id = decodeURIComponent(String(path)) |
3509 | if (id[0] === '%') { |
3510 | return self.getMsgDecryptedMaybeOoo(id, gotMsg) |
3511 | } else { |
3512 | msg = JSON.parse(id) |
3513 | } |
3514 | } catch(e) { |
3515 | return gotMsg(e) |
3516 | } |
3517 | gotMsg(null, msg) |
3518 | function gotMsg(err, msg) { |
3519 | if (err) return pull( |
3520 | pull.once(u.renderError(err).outerHTML), |
3521 | self.respondSink(400, {'Content-Type': ctype('html')}) |
3522 | ) |
3523 | var c = msg && msg.value && msg.value.content |
3524 | if (!c) return pull( |
3525 | pull.once('Missing message ' + id), |
3526 | self.respondSink(500, {'Content-Type': ctype('html')}) |
3527 | ) |
3528 | self.app.git.diffFile({ |
3529 | msg: c.updateId, |
3530 | commit: c.commitId, |
3531 | path: c.filePath, |
3532 | }, function (err, file) { |
3533 | if (err && err.name === 'BlobNotFoundError') |
3534 | return self.askWantBlobs(err.links) |
3535 | if (err) return pull( |
3536 | pull.once(err.stack), |
3537 | self.respondSink(400, {'Content-Type': 'text/plain'}) |
3538 | ) |
3539 | var path |
3540 | if (file.created) { |
3541 | path = '/git/blob/' + file.hash[1] |
3542 | + '?msg=' + encodeURIComponent(c.updateId) |
3543 | + '&commit=' + c.commitId |
3544 | + '&path=' + encodeURIComponent(c.filePath) |
3545 | + '#' + file.hash[1] + '-' + c.line |
3546 | } else { |
3547 | path = '/git/diff/' + file.hash[0] + '..' + file.hash[1] |
3548 | + '?msg=' + encodeURIComponent(c.updateId) |
3549 | + '&commit=' + c.commitId |
3550 | + '&path=' + encodeURIComponent(c.filePath) |
3551 | + '#' + file.hash[1] + '-' + c.line |
3552 | } |
3553 | var url = self.app.render.toUrl(path) |
3554 | /* |
3555 | return pull( |
3556 | ph('a', {href: url}, path), |
3557 | self.wrapPage(id), |
3558 | self.respondSink(200) |
3559 | ) |
3560 | */ |
3561 | self.redirect(url) |
3562 | }) |
3563 | } |
3564 | } |
3565 | |
3566 | Serve.prototype.gitObjectLinks = function (headMsgId, type) { |
3567 | var self = this |
3568 | return paramap(function (id, cb) { |
3569 | self.app.git.getObjectMsg({ |
3570 | obj: id, |
3571 | headMsgId: headMsgId, |
3572 | type: type, |
3573 | }, function (err, msg) { |
3574 | if (err && err.name === 'BlobNotFoundError') |
3575 | return cb(null, self.askWantBlobsForm(err.links)) |
3576 | if (err && err.name === 'ObjectNotFoundError') |
3577 | return cb(null, [ |
3578 | ph('code', u.escapeHTML(id.substr(0, 8))), '(missing)']) |
3579 | if (err) return cb(err) |
3580 | var path = '/git/' + type + '/' + id |
3581 | + '?msg=' + encodeURIComponent(msg.key) |
3582 | cb(null, [ph('code', ph('a', { |
3583 | href: self.app.render.toUrl(path) |
3584 | }, u.escapeHTML(id.substr(0, 8)))), ' ']) |
3585 | }) |
3586 | }, 8) |
3587 | } |
3588 | |
3589 | Serve.prototype.npm = function (url) { |
3590 | var self = this |
3591 | var parts = url.split('/') |
3592 | var author = parts[1] && parts[1][0] === '@' |
3593 | ? u.unescapeId(parts.splice(1, 1)[0]) : null |
3594 | var name = parts[1] |
3595 | var version = parts[2] |
3596 | var distTag = parts[3] |
3597 | var prefix = 'npm:' + |
3598 | (name ? name + ':' + |
3599 | (version ? version + ':' + |
3600 | (distTag ? distTag + ':' : '') : '') : '') |
3601 | |
3602 | var render = self.app.render |
3603 | var base = '/npm/' + (author ? u.escapeId(author) + '/' : '') |
3604 | var pathWithoutAuthor = '/npm' + |
3605 | (name ? '/' + name + |
3606 | (version ? '/' + version + |
3607 | (distTag ? '/' + distTag : '') : '') : '') |
3608 | return pull( |
3609 | ph('section', {}, [ |
3610 | ph('h3', [ph('a', {href: render.toUrl('/npm/')}, 'npm'), ' : ', |
3611 | author ? [ |
3612 | self.phIdLink(author), ' ', |
3613 | ph('sub', ph('a', {href: render.toUrl(pathWithoutAuthor)}, '×')), |
3614 | ' : ' |
3615 | ] : '', |
3616 | name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '', |
3617 | version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version), ' : '] : '', |
3618 | distTag ? [ph('a', {href: render.toUrl(base + name + '/' + version + '/' + distTag)}, distTag)] : '' |
3619 | ]), |
3620 | ph('table', [ |
3621 | ph('thead', ph('tr', [ |
3622 | ph('th', 'publisher'), |
3623 | ph('th', 'package'), |
3624 | ph('th', 'version'), |
3625 | ph('th', 'tag'), |
3626 | ph('th', 'size'), |
3627 | ph('th', 'tarball'), |
3628 | ph('th', 'readme') |
3629 | ])), |
3630 | ph('tbody', pull( |
3631 | self.app.blobMentions({ |
3632 | name: {$prefix: prefix}, |
3633 | author: author, |
3634 | }), |
3635 | distTag && !version && pull.filter(function (link) { |
3636 | return link.name.split(':')[3] === distTag |
3637 | }), |
3638 | paramap(function (link, cb) { |
3639 | self.app.render.npmPackageMention(link, { |
3640 | withAuthor: true, |
3641 | author: author, |
3642 | name: name, |
3643 | version: version, |
3644 | distTag: distTag, |
3645 | }, cb) |
3646 | }, 4), |
3647 | pull.map(u.toHTML) |
3648 | )) |
3649 | ]) |
3650 | ]), |
3651 | self.wrapPage(prefix), |
3652 | self.respondSink(200) |
3653 | ) |
3654 | } |
3655 | |
3656 | Serve.prototype.npmPrebuilds = function (url) { |
3657 | var self = this |
3658 | var parts = url.split('/') |
3659 | var author = parts[1] && parts[1][0] === '@' |
3660 | ? u.unescapeId(parts.splice(1, 1)[0]) : null |
3661 | var name = parts[1] |
3662 | var version = parts[2] |
3663 | var prefix = 'prebuild:' + |
3664 | (name ? name + '-' + |
3665 | (version ? version + '-' : '') : '') |
3666 | |
3667 | var render = self.app.render |
3668 | var base = '/npm-prebuilds/' + (author ? u.escapeId(author) + '/' : '') |
3669 | return pull( |
3670 | ph('section', {}, [ |
3671 | ph('h3', [ph('a', {href: render.toUrl('/npm-prebuilds/')}, 'npm prebuilds'), ' : ', |
3672 | name ? [ph('a', {href: render.toUrl(base + name)}, name), ' : '] : '', |
3673 | version ? [ph('a', {href: render.toUrl(base + name + '/' + version)}, version)] : '', |
3674 | ]), |
3675 | ph('table', [ |
3676 | ph('thead', ph('tr', [ |
3677 | ph('th', 'publisher'), |
3678 | ph('th', 'name'), |
3679 | ph('th', 'version'), |
3680 | ph('th', 'runtime'), |
3681 | ph('th', 'abi'), |
3682 | ph('th', 'platform+libc'), |
3683 | ph('th', 'arch'), |
3684 | ph('th', 'size'), |
3685 | ph('th', 'tarball') |
3686 | ])), |
3687 | ph('tbody', pull( |
3688 | self.app.blobMentions({ |
3689 | name: {$prefix: prefix}, |
3690 | author: author, |
3691 | }), |
3692 | paramap(function (link, cb) { |
3693 | self.app.render.npmPrebuildMention(link, { |
3694 | withAuthor: true, |
3695 | author: author, |
3696 | name: name, |
3697 | version: version, |
3698 | }, cb) |
3699 | }, 4), |
3700 | pull.map(u.toHTML) |
3701 | )) |
3702 | ]) |
3703 | ]), |
3704 | self.wrapPage(prefix), |
3705 | self.respondSink(200) |
3706 | ) |
3707 | } |
3708 | |
3709 | Serve.prototype.npmReadme = function (url) { |
3710 | var self = this |
3711 | var id = decodeURIComponent(url.substr(1)) |
3712 | return pull( |
3713 | ph('section', {}, [ |
3714 | ph('h3', [ |
3715 | 'npm readme for ', |
3716 | ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…') |
3717 | ]), |
3718 | ph('blockquote', u.readNext(function (cb) { |
3719 | self.app.getNpmReadme(id, function (err, readme, isMarkdown) { |
3720 | if (err) return cb(null, ph('div', u.renderError(err).outerHTML)) |
3721 | cb(null, isMarkdown |
3722 | ? ph('div', self.app.render.markdown(readme)) |
3723 | : ph('pre', readme)) |
3724 | }) |
3725 | })) |
3726 | ]), |
3727 | self.wrapPage('npm readme'), |
3728 | self.respondSink(200) |
3729 | ) |
3730 | } |
3731 | |
3732 | Serve.prototype.npmRegistry = function (url) { |
3733 | var self = this |
3734 | self.req.url = url |
3735 | self.app.serveSsbNpmRegistry(self.req, self.res) |
3736 | } |
3737 | |
3738 | Serve.prototype.markdown = function (url) { |
3739 | var self = this |
3740 | var id = decodeURIComponent(url.substr(1)) |
3741 | if (typeof self.query.unbox === 'string') id += '?unbox=' + self.query.unbox.replace(/\s/g, '+') |
3742 | return pull( |
3743 | ph('section', {}, [ |
3744 | ph('h3', [ |
3745 | ph('a', {href: '/links/' + id}, id.substr(0, 8) + '…') |
3746 | ]), |
3747 | u.readNext(function (cb) { |
3748 | self.app.getHasBlob(id, function (err, has) { |
3749 | if (err) return cb(err) |
3750 | if (!has) return cb(null, self.askWantBlobsForm([id])) |
3751 | pull(self.app.getBlob(id), pull.collect(function (err, chunks) { |
3752 | if (err) return cb(null, ph('div', u.renderError(err).outerHTML)) |
3753 | var text = Buffer.concat(chunks).toString() |
3754 | cb(null, ph('blockquote', self.app.render.markdown(text))) |
3755 | })) |
3756 | }) |
3757 | }) |
3758 | ]), |
3759 | self.wrapPage('markdown'), |
3760 | self.respondSink(200) |
3761 | ) |
3762 | } |
3763 | |
3764 | Serve.prototype.zip = function (url) { |
3765 | var self = this |
3766 | var parts = url.split('/').slice(1) |
3767 | var id = decodeURIComponent(parts.shift()) |
3768 | var filename = parts.join('/') |
3769 | var blobs = self.app.sbot.blobs |
3770 | var etag = '"' + id + filename + '"' |
3771 | var index = filename === '' || /\/$/.test(filename) |
3772 | var indexFilename = index && (filename + 'index.html') |
3773 | if (filename === '/' || /\/\/$/.test(filename)) { |
3774 | // force directory listing if path ends in // |
3775 | filename = filename.replace(/\/$/, '') |
3776 | indexFilename = false |
3777 | } |
3778 | var files = index && [] |
3779 | if (self.req.headers['if-none-match'] === etag) return self.respond(304) |
3780 | blobs.size(id, function (err, size) { |
3781 | if (size == null) return askWantBlobsForm([id]) |
3782 | if (err) { |
3783 | if (/^invalid/.test(err.message)) return self.respond(400, err.message) |
3784 | else return self.respond(500, err.message || err) |
3785 | } |
3786 | var unzip = require('unzip') |
3787 | var parseUnzip = unzip.Parse() |
3788 | var gotEntry = false |
3789 | parseUnzip.on('entry', function (entry) { |
3790 | if (index) { |
3791 | if (!gotEntry) { |
3792 | if (entry.path === indexFilename) { |
3793 | gotEntry = true |
3794 | return serveFile(entry) |
3795 | } else if (entry.path.substr(0, filename.length) === filename) { |
3796 | files.push({path: entry.path, type: entry.type, props: entry.props}) |
3797 | } |
3798 | } |
3799 | } else { |
3800 | if (!gotEntry && entry.path === filename) { |
3801 | gotEntry = true |
3802 | // if (false && entry.type === 'Directory') return serveDirectory(entry) |
3803 | return serveFile(entry) |
3804 | } |
3805 | } |
3806 | entry.autodrain() |
3807 | }) |
3808 | parseUnzip.on('close', function () { |
3809 | if (gotEntry) return |
3810 | if (!index) return self.respond(404, 'Entry not found') |
3811 | pull( |
3812 | ph('section', {}, [ |
3813 | ph('h3', [ |
3814 | ph('a', {href: self.app.render.toUrl('/links/' + id)}, id.substr(0, 8) + '…'), |
3815 | ' ', |
3816 | ph('a', {href: self.app.render.toUrl('/zip/' + encodeURIComponent(id) + '/' + filename)}, filename || '/'), |
3817 | ]), |
3818 | pull( |
3819 | pull.values(files), |
3820 | pull.map(function (file) { |
3821 | var path = '/zip/' + encodeURIComponent(id) + '/' + file.path |
3822 | return ph('li', [ |
3823 | ph('a', {href: self.app.render.toUrl(path)}, file.path) |
3824 | ]) |
3825 | }) |
3826 | ) |
3827 | ]), |
3828 | self.wrapPage(id + filename), |
3829 | self.respondSink(200) |
3830 | ) |
3831 | gotEntry = true // so that the handler on error event does not run |
3832 | }) |
3833 | parseUnzip.on('error', function (err) { |
3834 | if (!gotEntry) return self.respond(400, err.message) |
3835 | }) |
3836 | var size |
3837 | function serveFile(entry) { |
3838 | size = entry.size |
3839 | pull( |
3840 | toPull.source(entry), |
3841 | ident(gotType), |
3842 | self.respondSink() |
3843 | ) |
3844 | } |
3845 | pull( |
3846 | self.app.getBlob(id), |
3847 | toPull(parseUnzip) |
3848 | ) |
3849 | function gotType(type) { |
3850 | type = type && mime.lookup(type) |
3851 | if (type) self.res.setHeader('Content-Type', type) |
3852 | if (size) self.res.setHeader('Content-Length', size) |
3853 | self.res.setHeader('Cache-Control', 'public, max-age=315360000') |
3854 | self.res.setHeader('ETag', etag) |
3855 | self.res.writeHead(200) |
3856 | } |
3857 | }) |
3858 | } |
3859 | |
3860 | Serve.prototype.web = function (url) { |
3861 | var self = this |
3862 | var id = url.substr(1) |
3863 | try { id = decodeURIComponent(id) } |
3864 | catch(e) {} |
3865 | |
3866 | var components = url.split('/') |
3867 | if (components[0] === '') components.shift() |
3868 | components[0] = decodeURIComponent(components[0]) |
3869 | |
3870 | var type = mime.lookup(components[components.length - 1]) |
3871 | var headers = {} |
3872 | if (type) headers['Content-Type'] = type |
3873 | webresolve(this.app.sbot, components, function (err, res) { |
3874 | if (err) { |
3875 | return pull( |
3876 | pull.once(err.toString()), |
3877 | self.respondSink(404) |
3878 | ) |
3879 | } |
3880 | headers['Content-Length'] = res.length |
3881 | return pull( |
3882 | pull.once(res), |
3883 | self.respondSink(200, headers) |
3884 | ) |
3885 | }) |
3886 | } |
3887 | |
3888 | Serve.prototype.script = function (url) { |
3889 | var self = this |
3890 | var filepath = url.split('?')[0] |
3891 | this.app.getScript(filepath, function (err, fn) { |
3892 | try { |
3893 | if (err) throw err |
3894 | fn(self) |
3895 | } catch(e) { |
3896 | return pull( |
3897 | pull.once(u.renderError(e).outerHTML), |
3898 | self.wrapPage('local: ' + path), |
3899 | self.respondSink(400) |
3900 | ) |
3901 | } |
3902 | }) |
3903 | } |
3904 | |
3905 | // wrap a binary source and render it or turn into an embed |
3906 | Serve.prototype.wrapBinary = function (opts) { |
3907 | var self = this |
3908 | var ext = opts.ext |
3909 | var hash = opts.obj.hash |
3910 | return function (read) { |
3911 | var readRendered, type |
3912 | read = ident(function (_ext) { |
3913 | if (_ext) ext = _ext |
3914 | type = ext && mime.lookup(ext) || 'text/plain' |
3915 | })(read) |
3916 | return function (abort, cb) { |
3917 | if (readRendered) return readRendered(abort, cb) |
3918 | if (abort) return read(abort, cb) |
3919 | if (!type) read(null, function (end, buf) { |
3920 | if (end) return cb(end) |
3921 | if (!type) return cb(new Error('unable to get type')) |
3922 | readRendered = pickSource(type, cat([pull.once(buf), read])) |
3923 | readRendered(null, cb) |
3924 | }) |
3925 | } |
3926 | } |
3927 | function pickSource(type, read) { |
3928 | if (/^image\//.test(type)) { |
3929 | read(true, function (err) { |
3930 | if (err && err !== true) console.trace(err) |
3931 | }) |
3932 | return ph('img', { |
3933 | src: opts.rawUrl |
3934 | }) |
3935 | } |
3936 | if (type === 'text/markdown') { |
3937 | // TODO: rewrite links to files/images to be correct |
3938 | return ph('blockquote', u.readNext(function (cb) { |
3939 | pull.collect(function (err, bufs) { |
3940 | if (err) return cb(pull.error(err)) |
3941 | var text = Buffer.concat(bufs).toString('utf8') |
3942 | return cb(null, pull.once(self.app.render.markdown(text))) |
3943 | })(read) |
3944 | })) |
3945 | } |
3946 | var i = 1 |
3947 | var updateMsg = opts.obj.msg |
3948 | var commitId = self.query.commit |
3949 | var filePath = self.query.path |
3950 | var lineComments = opts.lineComments || {} |
3951 | return u.readNext(function (cb) { |
3952 | if (commitId && filePath) { |
3953 | self.app.getLineComments({ |
3954 | obj: opts.obj, |
3955 | hash: hash, |
3956 | }, gotLineComments) |
3957 | } else { |
3958 | gotLineComments(null, {}) |
3959 | } |
3960 | function gotLineComments(err, lineComments) { |
3961 | if (err) return cb(err) |
3962 | cb(null, ph('table', |
3963 | pull( |
3964 | read, |
3965 | utf8(), |
3966 | split(), |
3967 | pull.map(function (line) { |
3968 | var lineNum = i++ |
3969 | var id = hash + '-' + lineNum |
3970 | var idEnc = encodeURIComponent(id) |
3971 | var allowComment = self.query.commit && self.query.path |
3972 | return [ |
3973 | ph('tr', [ |
3974 | ph('td', |
3975 | allowComment ? ph('a', { |
3976 | href: '?msg=' + encodeURIComponent(self.query.msg) |
3977 | + '&commit=' + encodeURIComponent(self.query.commit) |
3978 | + '&path=' + encodeURIComponent(self.query.path) |
3979 | + '&comment=' + idEnc |
3980 | + '#' + idEnc |
3981 | }, '…') : '' |
3982 | ), |
3983 | ph('td', ph('a', { |
3984 | name: id, |
3985 | href: '#' + idEnc |
3986 | }, String(lineNum))), |
3987 | ph('td', ph('pre', self.app.render.highlight(line, ext))) |
3988 | ]), |
3989 | lineComments[lineNum] ? ph('tr', |
3990 | ph('td', {colspan: 4}, |
3991 | self.renderLineCommentThread(lineComments[lineNum], id) |
3992 | ) |
3993 | ) : '', |
3994 | self.query.comment === id ? ph('tr', |
3995 | ph('td', {colspan: 4}, |
3996 | self.renderLineCommentForm({ |
3997 | id: id, |
3998 | line: lineNum, |
3999 | updateId: updateMsg.key, |
4000 | repoId: updateMsg.value.content.repo, |
4001 | commitId: commitId, |
4002 | blobId: hash, |
4003 | filePath: filePath, |
4004 | }) |
4005 | ) |
4006 | ) : '' |
4007 | ] |
4008 | }) |
4009 | ) |
4010 | )) |
4011 | } |
4012 | }) |
4013 | } |
4014 | } |
4015 | |
4016 | Serve.prototype.wrapPublic = function (opts) { |
4017 | var self = this |
4018 | return u.hyperwrap(function (thread, cb) { |
4019 | self.composer({ |
4020 | channel: '', |
4021 | }, function (err, composer) { |
4022 | if (err) return cb(err) |
4023 | cb(null, [ |
4024 | composer, |
4025 | thread |
4026 | ]) |
4027 | }) |
4028 | }) |
4029 | } |
4030 | |
4031 | function uniqueLink() { |
4032 | var seen = {} |
4033 | return function (link) { |
4034 | if (seen[link.link]) return false |
4035 | return seen[link.link] = true |
4036 | } |
4037 | } |
4038 | |
4039 | Serve.prototype.askWantBlobsForm = function (links) { |
4040 | var self = this |
4041 | return ph('form', {action: '', method: 'post'}, [ |
4042 | ph('section', [ |
4043 | ph('h3', 'Missing blobs'), |
4044 | ph('p', 'The application needs these blobs to continue:'), |
4045 | ph('table', links.map(u.toLink).filter(uniqueLink()).map(function (link) { |
4046 | var id = removeQuery(link.link) |
4047 | if (!u.isRef(id)) return |
4048 | return ph('tr', [ |
4049 | ph('td', ph('code', link.link)), |
4050 | !isNaN(link.size) ? ph('td', self.app.render.formatSize(link.size)) : '', |
4051 | ]) |
4052 | })), |
4053 | ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), |
4054 | ph('input', {type: 'hidden', name: 'blob_ids', |
4055 | value: links.map(u.linkDest).join(',')}), |
4056 | ph('p', ph('input', {type: 'submit', value: 'Want Blobs'})) |
4057 | ]) |
4058 | ]) |
4059 | } |
4060 | |
4061 | Serve.prototype.askWantBlobs = function (links) { |
4062 | var self = this |
4063 | pull( |
4064 | self.askWantBlobsForm(links), |
4065 | self.wrapPage('missing blobs'), |
4066 | self.respondSink(409) |
4067 | ) |
4068 | } |
4069 | |
4070 | Serve.prototype.wrapPrivate = function (opts) { |
4071 | var self = this |
4072 | return u.hyperwrap(function (thread, cb) { |
4073 | self.composer({ |
4074 | placeholder: 'private message', |
4075 | private: true, |
4076 | }, function (err, composer) { |
4077 | if (err) return cb(err) |
4078 | cb(null, [ |
4079 | composer, |
4080 | thread |
4081 | ]) |
4082 | }) |
4083 | }) |
4084 | } |
4085 | |
4086 | Serve.prototype.wrapThread = function (opts) { |
4087 | var self = this |
4088 | return u.hyperwrap(function (thread, cb) { |
4089 | self.app.render.prepareLinks(opts.recps, function (err, recps) { |
4090 | if (err) return cb(er) |
4091 | if (self.noComposer) return cb(null, thread) |
4092 | self.composer({ |
4093 | placeholder: opts.placeholder |
4094 | || (recps ? 'private reply' : 'reply'), |
4095 | id: 'reply', |
4096 | root: opts.root, |
4097 | post: opts.post, |
4098 | channel: opts.channel || '', |
4099 | branches: opts.branches, |
4100 | postBranches: opts.postBranches, |
4101 | recps: recps, |
4102 | links: opts.links, |
4103 | private: opts.recps != null, |
4104 | }, function (err, composer) { |
4105 | if (err) return cb(err) |
4106 | cb(null, [ |
4107 | thread, |
4108 | composer |
4109 | ]) |
4110 | }) |
4111 | }) |
4112 | }) |
4113 | } |
4114 | |
4115 | Serve.prototype.wrapNew = function (opts) { |
4116 | var self = this |
4117 | return u.hyperwrap(function (thread, cb) { |
4118 | self.composer({ |
4119 | channel: '', |
4120 | }, function (err, composer) { |
4121 | if (err) return cb(err) |
4122 | cb(null, [ |
4123 | composer, |
4124 | h('table.ssb-msgs', |
4125 | opts.reachedLimit ? h('tr', h('td.paginate.msg-left', {colspan: 3}, |
4126 | 'Reached limit of ' + opts.reachedLimit + ' messages' |
4127 | )) : '', |
4128 | thread, |
4129 | h('tr', h('td.paginate.msg-left', {colspan: 3}, |
4130 | h('form', {method: 'get', action: ''}, |
4131 | h('input', {type: 'hidden', name: 'gt', value: opts.gt}), |
4132 | self.query.limit ? h('input', {type: 'hidden', name: 'limit', value: self.query.limit}) : '', |
4133 | h('input', {type: 'hidden', name: 'catchup', value: '1'}), |
4134 | h('input', {type: 'submit', value: 'catchup'}) |
4135 | ) |
4136 | )) |
4137 | ) |
4138 | ]) |
4139 | }) |
4140 | }) |
4141 | } |
4142 | |
4143 | Serve.prototype.wrapChannel = function (channel) { |
4144 | var self = this |
4145 | return u.hyperwrap(function (thread, cb) { |
4146 | self.composer({ |
4147 | placeholder: 'public message in #' + channel, |
4148 | channel: channel, |
4149 | }, function (err, composer) { |
4150 | if (err) return cb(err) |
4151 | cb(null, [ |
4152 | h('section', |
4153 | h('h3.feed-name', |
4154 | h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel) |
4155 | ) |
4156 | ), |
4157 | composer, |
4158 | thread |
4159 | ]) |
4160 | }) |
4161 | }) |
4162 | } |
4163 | |
4164 | Serve.prototype.wrapType = function (type) { |
4165 | var self = this |
4166 | return u.hyperwrap(function (thread, cb) { |
4167 | cb(null, [ |
4168 | h('section', |
4169 | h('h3.feed-name', |
4170 | h('a', {href: self.app.render.toUrl('/type/' + type)}, |
4171 | h('code', type), 's')) |
4172 | ), |
4173 | thread |
4174 | ]) |
4175 | }) |
4176 | } |
4177 | |
4178 | Serve.prototype.wrapLinks = function (dest) { |
4179 | var self = this |
4180 | return u.hyperwrap(function (thread, cb) { |
4181 | cb(null, [ |
4182 | h('section', |
4183 | h('h3.feed-name', 'links: ', |
4184 | h('a', {href: self.app.render.toUrl('/links/' + dest)}, |
4185 | h('code', dest))) |
4186 | ), |
4187 | thread |
4188 | ]) |
4189 | }) |
4190 | } |
4191 | |
4192 | Serve.prototype.wrapPeers = function (opts) { |
4193 | var self = this |
4194 | return u.hyperwrap(function (peers, cb) { |
4195 | cb(null, [ |
4196 | h('section', |
4197 | h('h3', 'Peers') |
4198 | ), |
4199 | peers |
4200 | ]) |
4201 | }) |
4202 | } |
4203 | |
4204 | Serve.prototype.wrapChannels = function (opts) { |
4205 | var self = this |
4206 | return u.hyperwrap(function (channels, cb) { |
4207 | cb(null, [ |
4208 | h('section', |
4209 | h('h4', 'Network') |
4210 | ), |
4211 | h('section', |
4212 | channels |
4213 | ) |
4214 | ]) |
4215 | }) |
4216 | } |
4217 | |
4218 | Serve.prototype.wrapMyChannels = function (opts) { |
4219 | var self = this |
4220 | return u.hyperwrap(function (channels, cb) { |
4221 | cb(null, [ |
4222 | h('section', |
4223 | h('h4', 'Subscribed') |
4224 | ), |
4225 | h('section', |
4226 | channels |
4227 | ) |
4228 | ]) |
4229 | }) |
4230 | } |
4231 | |
4232 | var blobPrefixesByType = { |
4233 | audio: 'audio:', |
4234 | video: 'video:', |
4235 | } |
4236 | |
4237 | var blobPrefixesByExt = { |
4238 | mp3: 'audio:', |
4239 | mp4: 'video:', |
4240 | } |
4241 | |
4242 | Serve.prototype.composer = function (opts, cb) { |
4243 | var self = this |
4244 | opts = opts || {} |
4245 | var data = self.data |
4246 | var myId = self.app.sbot.id |
4247 | var links = opts.links || [] |
4248 | |
4249 | if (opts.id && data.composer_id && opts.id !== data.composer_id) { |
4250 | // don't share data between multiple composers |
4251 | data = {} |
4252 | } |
4253 | |
4254 | if (!data.text && self.query.text) data.text = self.query.text |
4255 | if (!data.action && self.query.action) data.action = self.query.action |
4256 | |
4257 | var blobs = u.tryDecodeJSON(data.blobs) || {} |
4258 | var upload = data.upload |
4259 | if (upload && typeof upload === 'object') { |
4260 | data.upload = null |
4261 | |
4262 | var href = upload.link + (upload.key ? '?unbox=' + upload.key + '.boxs': '') |
4263 | var blobType = String(upload.type).split('/')[0] |
4264 | var blobExt = String(upload.name).split('.').pop() |
4265 | var blobPrefix = blobPrefixesByType[blobType] || blobPrefixesByExt[blobExt] || '' |
4266 | var isMedia = blobPrefix || blobType === 'image' |
4267 | var blobName = blobPrefix + upload.name |
4268 | |
4269 | blobs[upload.link] = { |
4270 | type: upload.type, |
4271 | size: upload.size, |
4272 | name: blobName, |
4273 | key: upload.key, |
4274 | } |
4275 | |
4276 | data.text = (data.text ? data.text + '\n' : '') |
4277 | + (isMedia ? '!' : '') |
4278 | + '[' + blobName + '](' + href + ')' |
4279 | } |
4280 | |
4281 | var channel = data.channel != null ? data.channel : opts.channel |
4282 | |
4283 | var formNames = {} |
4284 | var mentionIds = u.toArray(data.mention_id) |
4285 | var mentionNames = u.toArray(data.mention_name) |
4286 | for (var i = 0; i < mentionIds.length && i < mentionNames.length; i++) { |
4287 | formNames[mentionNames[i]] = u.extractFeedIds(mentionIds[i])[0] |
4288 | } |
4289 | |
4290 | var formEmojiNames = {} |
4291 | var emojiIds = u.toArray(data.emoji_id) |
4292 | var emojiNames = u.toArray(data.emoji_name) |
4293 | for (var i = 0; i < emojiIds.length && i < emojiNames.length; i++) { |
4294 | var upload = data['emoji_upload_' + i] |
4295 | formEmojiNames[emojiNames[i]] = |
4296 | (upload && upload.link) || u.extractBlobIds(emojiIds[i])[0] |
4297 | if (upload) blobs[upload.link] = { |
4298 | type: upload.type, |
4299 | size: upload.size, |
4300 | name: upload.name, |
4301 | } |
4302 | } |
4303 | |
4304 | var blobIds = u.toArray(data.blob_id) |
4305 | var blobTypes = u.toArray(data.blob_type) |
4306 | var blobNames = u.toArray(data.blob_name) |
4307 | for (var i = 0; i < blobIds.length && i < blobTypes.length; i++) { |
4308 | var id = blobIds[i] |
4309 | var blob = blobs[id] || (blobs[id] = {}) |
4310 | blob.type = blobTypes[i] |
4311 | blob.nameOverride = data['blob_name_override_' + i] |
4312 | blob.name = blobNames[i] |
4313 | } |
4314 | |
4315 | // get bare feed names |
4316 | var unknownMentionNames = {} |
4317 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
4318 | var unknownMentions = mentions |
4319 | .filter(function (mention) { |
4320 | return mention.link === '@' |
4321 | }) |
4322 | .map(function (mention) { |
4323 | return mention.name |
4324 | }) |
4325 | .filter(uniques()) |
4326 | .map(function (name) { |
4327 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
4328 | return {name: name, id: id} |
4329 | }) |
4330 | |
4331 | var emoji = mentions |
4332 | .filter(function (mention) { return mention.emoji }) |
4333 | .map(function (mention) { return mention.name }) |
4334 | .filter(uniques()) |
4335 | .map(function (name) { |
4336 | // 1. check emoji-image mapping for this message |
4337 | var id = formEmojiNames[name] |
4338 | if (id) return {name: name, id: id} |
4339 | // 2. TODO: check user's preferred emoji-image mapping |
4340 | // 4. check recently seen emoji |
4341 | id = self.app.getReverseEmojiNameSync(name) |
4342 | return {name: name, id: id} |
4343 | }) |
4344 | |
4345 | var blobMentions = mentions |
4346 | .filter(function (mention) { |
4347 | return mention |
4348 | && typeof mention.link === 'string' |
4349 | && mention.link[0] === '&' |
4350 | }) |
4351 | blobMentions.forEach(function (mention) { |
4352 | var blob = blobs[mention.link] |
4353 | if (blob) { |
4354 | mention.type = blob.type |
4355 | if (blob.nameOverride) mention.name = blob.name |
4356 | } |
4357 | }) |
4358 | |
4359 | // strip content other than names and feed ids from the recps field |
4360 | if (data.recps) { |
4361 | data.recps = recpsToFeedIds(data.recps) |
4362 | } |
4363 | |
4364 | var draftLinkContainer |
4365 | function renderDraftLink(draftId) { |
4366 | if (!draftId) return [] |
4367 | var draftHref = self.app.render.toUrl('/drafts/' + encodeURI(draftId)) |
4368 | return [ |
4369 | h('a', {href: draftHref, title: 'draft link'}, u.escapeHTML(draftId)), |
4370 | h('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}) |
4371 | ] |
4372 | } |
4373 | |
4374 | var done = multicb({pluck: 1, spread: true}) |
4375 | done()(null, h('section.composer', |
4376 | h('form', {method: 'post', action: opts.id ? '#' + opts.id : '', |
4377 | enctype: 'multipart/form-data'}, |
4378 | h('input', {type: 'hidden', name: 'blobs', |
4379 | value: JSON.stringify(blobs)}), |
4380 | h('input', {type: 'hidden', name: 'url', value: self.req.url}), |
4381 | h('input', {type: 'hidden', name: 'composer_id', value: opts.id}), |
4382 | opts.recps ? self.app.render.privateLine({recps: opts.recps, |
4383 | isAuthorRecp: true}, done()) : |
4384 | opts.private ? h('div', h('input.wide', {name: 'recps', size: 64, |
4385 | value: data.recps || '', placeholder: 'recipient ids'})) : '', |
4386 | channel != null ? |
4387 | h('div', '#', h('input', {name: 'channel', placeholder: 'channel', |
4388 | value: channel})) : '', |
4389 | opts.root !== opts.post ? h('div', |
4390 | h('label', {for: 'fork_thread'}, |
4391 | h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}), |
4392 | ' fork thread' |
4393 | ) |
4394 | ) : '', |
4395 | closeIssueCheckbox(done()), |
4396 | mentionAttendeesCheckbox(done()), |
4397 | h('div', h('input.wide', {name: 'content_warning', size: 64, |
4398 | value: data.content_warning || '', |
4399 | placeholder: 'Content Warning'})), |
4400 | h('textarea', { |
4401 | id: opts.id, |
4402 | name: 'text', |
4403 | rows: Math.max(4, u.rows(data.text)), |
4404 | cols: 64, |
4405 | placeholder: opts.placeholder || 'public message', |
4406 | }, data.text || ''), |
4407 | unknownMentions.length > 0 ? [ |
4408 | h('div', h('em', 'names:')), |
4409 | h('ul.mentions', unknownMentions.map(function (mention) { |
4410 | return h('li', |
4411 | h('code', '@' + mention.name), ': ', |
4412 | h('input', {name: 'mention_name', type: 'hidden', |
4413 | value: mention.name}), |
4414 | h('input.id-input', {name: 'mention_id', size: 60, |
4415 | value: mention.id, placeholder: '@id'})) |
4416 | })) |
4417 | ] : '', |
4418 | emoji.length > 0 ? [ |
4419 | h('div', h('em', 'emoji:')), |
4420 | h('ul.mentions', emoji.map(function (link, i) { |
4421 | return h('li', |
4422 | h('code', link.name), ': ', |
4423 | h('input', {name: 'emoji_name', type: 'hidden', |
4424 | value: link.name}), |
4425 | h('input.id-input', {name: 'emoji_id', size: 60, |
4426 | value: link.id, placeholder: '&id'}), ' ', |
4427 | h('input', {type: 'file', name: 'emoji_upload_' + i})) |
4428 | })) |
4429 | ] : '', |
4430 | blobMentions.length > 0 ? [ |
4431 | h('div', h('em', 'blobs:')), |
4432 | h('ul.mentions', blobMentions.map(function (link, i) { |
4433 | var link1 = blobs[link.link] || {} |
4434 | var linkHref = self.app.render.toUrl(link.link) |
4435 | return h('li', |
4436 | h('a', {href: linkHref}, h('code', String(link.link).substr(0, 8) + '…')), ' ', |
4437 | h('input', {name: 'blob_id', type: 'hidden', |
4438 | value: link.link}), |
4439 | h('input', {name: 'blob_type', size: 24, |
4440 | value: link.type, placeholder: 'application/octet-stream', |
4441 | title: 'blob mime-type'}), |
4442 | h('input', {name: 'blob_name', size: 24, |
4443 | value: link.name || '', placeholder: 'name', |
4444 | title: 'blob name'}), ' ', |
4445 | h('input', {name: 'blob_name_override_' + i, type: 'checkbox', |
4446 | value: 1, checked: link1.nameOverride ? '1' : undefined, |
4447 | title: 'override name in markdown'})) |
4448 | })) |
4449 | ] : '', |
4450 | h('table.ssb-msgs', |
4451 | h('tr.msg-row', |
4452 | h('td.msg-left', {colspan: 2}, |
4453 | opts.private ? |
4454 | h('input', {type: 'hidden', name: 'private', value: '1'}) : '', |
4455 | h('input', {type: 'file', name: 'upload'}) |
4456 | ), |
4457 | h('td.msg-right', |
4458 | draftLinkContainer = h('span', renderDraftLink(data.draft_id)), ' ', |
4459 | h('label', {for: 'save_draft'}, |
4460 | h('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1', |
4461 | checked: data.save_draft || data.restored_draft ? 'checked' : undefined}), |
4462 | ' save draft '), |
4463 | h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ', |
4464 | h('input', {type: 'submit', name: 'action', value: 'preview'}) |
4465 | ) |
4466 | ) |
4467 | ), |
4468 | data.action === 'preview' ? preview(false, done()) : |
4469 | data.action === 'raw' ? preview(true, done()) : '' |
4470 | ) |
4471 | )) |
4472 | done(cb) |
4473 | |
4474 | function recpsToFeedIds (recps) { |
4475 | var res = data.recps.split(',') |
4476 | .map(function (str) { |
4477 | str = str.trim() |
4478 | var ids = u.extractFeedIds(str).filter(uniques()) |
4479 | if (ids.length >= 1) { |
4480 | return ids[0] |
4481 | } else { |
4482 | ids = u.extractFeedIds(self.app.getReverseNameSync(str)) |
4483 | if (ids.length >= 1) { |
4484 | return ids[0] |
4485 | } else { |
4486 | return null |
4487 | } |
4488 | } |
4489 | }) |
4490 | .filter(Boolean) |
4491 | return res.join(', ') |
4492 | } |
4493 | |
4494 | function prepareContent(cb) { |
4495 | var done = multicb({pluck: 1}) |
4496 | content = { |
4497 | type: 'post', |
4498 | text: String(data.text), |
4499 | } |
4500 | if (opts.lineComment) { |
4501 | content.type = 'line-comment' |
4502 | content.updateId = opts.lineComment.updateId |
4503 | content.repo = opts.lineComment.repoId |
4504 | content.commitId = opts.lineComment.commitId |
4505 | content.filePath = opts.lineComment.filePath |
4506 | content.blobId = opts.lineComment.blobId |
4507 | content.line = opts.lineComment.line |
4508 | } |
4509 | var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true}) |
4510 | .filter(function (mention) { |
4511 | if (typeof mention.name === 'string') { |
4512 | mention.name = mention.name |
4513 | .replace(/'/g, "'") |
4514 | } |
4515 | if (mention.emoji) { |
4516 | mention.link = formEmojiNames[mention.name] |
4517 | if (!mention.link) { |
4518 | mention.link = self.app.getReverseEmojiNameSync(mention.name) |
4519 | if (!mention.link) return false |
4520 | } |
4521 | } |
4522 | var blob = blobs[mention.link] |
4523 | if (blob) { |
4524 | if (!isNaN(blob.size)) |
4525 | mention.size = blob.size |
4526 | if (blob.type && blob.type !== 'application/octet-stream') |
4527 | mention.type = blob.type |
4528 | if (blob.nameOverride) |
4529 | mention.name = blob.name |
4530 | } else if (mention.link === '@') { |
4531 | // bare feed name |
4532 | var name = mention.name |
4533 | var id = formNames[name] || self.app.getReverseNameSync('@' + name) |
4534 | if (id) mention.link = id |
4535 | else return false |
4536 | } |
4537 | if (mention.link && mention.link[0] === '&' && mention.size == null) { |
4538 | var linkCb = done() |
4539 | self.app.sbot.blobs.size(mention.link, function (err, size) { |
4540 | if (!err && size != null) mention.size = size |
4541 | linkCb() |
4542 | }) |
4543 | } |
4544 | return true |
4545 | }) |
4546 | .map(reduceLink) |
4547 | |
4548 | if (mentions.length) content.mentions = mentions |
4549 | if (data.recps != null) { |
4550 | if (opts.recps) return cb(new Error('got recps in opts and data')) |
4551 | content.recps = [myId] |
4552 | u.extractFeedIds(data.recps).forEach(function (recp) { |
4553 | if (content.recps.indexOf(recp) === -1) content.recps.push(recp) |
4554 | }) |
4555 | } else { |
4556 | if (opts.recps) content.recps = opts.recps |
4557 | } |
4558 | if (data.fork_thread) { |
4559 | content.root = opts.post || undefined |
4560 | content.fork = opts.root || undefined |
4561 | content.branch = u.fromArray(opts.postBranches) || undefined |
4562 | } else { |
4563 | content.root = opts.root || undefined |
4564 | content.branch = u.fromArray(opts.branches) || undefined |
4565 | } |
4566 | if (data.close_issue) { |
4567 | content.issue = opts.root || undefined |
4568 | content.project = data.project || undefined |
4569 | content.repo = data.repo || undefined |
4570 | content.issues = [ |
4571 | { |
4572 | link: opts.root, |
4573 | open: false |
4574 | } |
4575 | ] |
4576 | } |
4577 | |
4578 | if (self.replyMentionFeeds && links && content.branch) { |
4579 | var reply = {} |
4580 | var ids = {} |
4581 | u.toArray(content.branch).forEach(function (branch) { |
4582 | ids[branch] = true |
4583 | }) |
4584 | links.forEach(function (link) { |
4585 | if (ids[link.key]) { |
4586 | var author = link.value.author |
4587 | if (author !== myId) reply[link.key] = author |
4588 | } |
4589 | }) |
4590 | if (Object.keys(reply).length > 0) content.reply = reply |
4591 | } |
4592 | |
4593 | if (data.mention_attendees) { |
4594 | var attendeeLinks = u.toArray(String(data.attendees || '').split(',')) |
4595 | .filter(function (id) { |
4596 | return id !== myId |
4597 | }) |
4598 | if (!content.mentions) content.mentions = attendeeLinks |
4599 | else { |
4600 | var alreadyMentioned = {} |
4601 | content.mentions.map(u.linkDest).forEach(function (id) { |
4602 | alreadyMentioned[id] = true |
4603 | }) |
4604 | attendeeLinks.forEach(function (link) { |
4605 | if (!alreadyMentioned[link]) content.mentions.push(link) |
4606 | }) |
4607 | } |
4608 | } |
4609 | if (data.content_warning) content.contentWarning = String(data.content_warning) |
4610 | if (channel) content.channel = data.channel |
4611 | |
4612 | done(function (err) { |
4613 | cb(err, content) |
4614 | }) |
4615 | } |
4616 | |
4617 | function closeIssueCheckbox(cb) { |
4618 | var container = h('div') |
4619 | if (opts.root && opts.root[0] === '%') self.getMsgDecryptedMaybeOoo(opts.root, function (err, rootMsg) { |
4620 | if (err) return console.trace(err), cb(null) |
4621 | var rootC = rootMsg && rootMsg.value.content && rootMsg.value.content |
4622 | if (!rootC) return cb(null) |
4623 | var canCloseIssue = rootC.type === 'issue' |
4624 | var canClosePR = rootC.type === 'pull-request' |
4625 | if (canCloseIssue || canClosePR) container.appendChild(h('label', {for: 'close_issue'}, |
4626 | h('input', {id: 'close_issue', type: 'checkbox', name: 'close_issue', value: 'post', checked: data.close_issue || undefined}), |
4627 | ' close ' + (canClosePR ? 'pull-request' : 'issue'), |
4628 | rootC.project ? h('input', {type: 'hidden', name: 'project', value: rootC.project}) : '', |
4629 | rootC.repo ? h('input', {type: 'hidden', name: 'repo', value: rootC.repo}) : '' |
4630 | )) |
4631 | cb(null) |
4632 | }) |
4633 | else cb(null) |
4634 | return container |
4635 | } |
4636 | |
4637 | function mentionAttendeesCheckbox(cb) { |
4638 | var container = h('div') |
4639 | if (opts.root && opts.root[0] === '%') self.getMsgDecryptedMaybeOoo(opts.root, function (err, rootMsg) { |
4640 | if (err) return console.trace(err), cb(null) |
4641 | var rootC = rootMsg && rootMsg.value.content && rootMsg.value.content |
4642 | if (!rootC) return cb(null) |
4643 | var canMentionAttendees = rootC.type === 'gathering' |
4644 | if (!canMentionAttendees) return cb(null) |
4645 | if (opts.id === opts.root) gotLinks(null, links) |
4646 | else pull( |
4647 | self.app.getLinksBy(opts.root, 'about'), |
4648 | pull.unique('key'), |
4649 | self.app.unboxMessages(), |
4650 | pull.collect(gotLinks) |
4651 | ) |
4652 | function gotLinks(err, links2) { |
4653 | if (err) console.trace(err), links2 = links |
4654 | var attendees = {} |
4655 | links2.forEach(function (link) { |
4656 | var c = link && link.value && link.value.content |
4657 | var attendee = c && c.type === 'about' && c.about === opts.root |
4658 | && u.toLink(c.attendee) |
4659 | if (!attendee) return |
4660 | var author = link.value.author |
4661 | if (attendee.link !== author) return |
4662 | if (attendee.remove) delete attendees[author] |
4663 | else attendees[author] = true |
4664 | }) |
4665 | var attendeeIds = Object.keys(attendees) |
4666 | container.appendChild(h('label', {for: 'mention_attendees'}, |
4667 | h('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: 'post', checked: data.mention_attendees || undefined}), |
4668 | ' mention attendees (' + attendeeIds.length + ')', |
4669 | h('input', {type: 'hidden', name: 'attendees', value: attendeeIds.join(',')}) |
4670 | )) |
4671 | cb(null) |
4672 | } |
4673 | }) |
4674 | else cb(null) |
4675 | return container |
4676 | } |
4677 | |
4678 | function preview(raw, cb) { |
4679 | var msgContainer = h('table.ssb-msgs') |
4680 | var contentInput = h('input', {type: 'hidden', name: 'content'}) |
4681 | var warningsContainer = h('div') |
4682 | var sizeEl = h('span') |
4683 | var blobsSizeEl = h('span') |
4684 | var blobsSizeContainer = h('span') |
4685 | |
4686 | var content |
4687 | try { content = JSON.parse(data.text) } |
4688 | catch (err) {} |
4689 | if (content) gotContent(null, content) |
4690 | else prepareContent(gotContent) |
4691 | |
4692 | function gotContent(err, _content) { |
4693 | if (err) return cb(err) |
4694 | content = _content |
4695 | if (data.save_draft) self.saveDraft(content, saved) |
4696 | else saved() |
4697 | } |
4698 | |
4699 | function saved(err, draftId) { |
4700 | if (err) return cb(err) |
4701 | |
4702 | if (draftId) { |
4703 | draftLinkContainer.childNodes = renderDraftLink(draftId) |
4704 | } |
4705 | |
4706 | contentInput.value = JSON.stringify(content) |
4707 | var msg = { |
4708 | value: { |
4709 | author: myId, |
4710 | timestamp: Date.now(), |
4711 | content: content |
4712 | } |
4713 | } |
4714 | if (content.recps) msg.value.private = true |
4715 | |
4716 | var warnings = [] |
4717 | var blobsSize = 0 |
4718 | u.toLinkArray(content.mentions).forEach(function (link) { |
4719 | var isBlob = link.link[0] === '&' |
4720 | if (isBlob && !isNaN(link.size)) blobsSize += link.size |
4721 | if (link.emoji && link.size >= 10e3) { |
4722 | warnings.push(h('li', |
4723 | 'emoji ', h('q', link.name), |
4724 | ' (', h('code', String(link.link).substr(0, 8) + '…'), ')' |
4725 | + ' is >10KB')) |
4726 | } else if (isBlob && link.size >= 5242880) { |
4727 | warnings.push(h('li', |
4728 | 'linked blob ', |
4729 | h('code', String(link.link).substr(0, 8) + '…'), |
4730 | ' (', h('code', self.app.render.formatSize(link.size)), ')', |
4731 | ' is larger than 5MB')) |
4732 | } |
4733 | }) |
4734 | |
4735 | var estSize = u.estimateMessageSize(content) |
4736 | sizeEl.innerHTML = self.app.render.formatSize(estSize) |
4737 | if (estSize > 8192) warnings.push(h('li', 'message is too long')) |
4738 | |
4739 | if (blobsSize) blobsSizeContainer.appendChild(h('span', |
4740 | ', ', |
4741 | h('em', {title: 'total size of linked blobs'}, 'blobs:'), ' ', |
4742 | self.app.render.formatSize(blobsSize) |
4743 | )) |
4744 | |
4745 | if (warnings.length) { |
4746 | warningsContainer.appendChild(h('div', h('em', 'warning:'))) |
4747 | warningsContainer.appendChild(h('ul.mentions', warnings)) |
4748 | } |
4749 | |
4750 | pull( |
4751 | pull.once(msg), |
4752 | self.app.unboxMessages(), |
4753 | self.app.render.renderFeeds({ |
4754 | raw: raw, |
4755 | dualMarkdown: self.query.dualMd != null ? self.query.dualMd : |
4756 | self.conf.dualMarkdownPreview, |
4757 | serve: self, |
4758 | filter: self.query.filter, |
4759 | }), |
4760 | pull.drain(function (el) { |
4761 | msgContainer.appendChild(h('tbody', el)) |
4762 | }, cb) |
4763 | ) |
4764 | } |
4765 | |
4766 | return [ |
4767 | contentInput, |
4768 | opts.redirectToPublishedMsg ? h('input', {type: 'hidden', |
4769 | name: 'redirect_to_published_msg', value: '1'}) : '', |
4770 | warningsContainer, |
4771 | h('div', h('em', 'draft:'), ' ', sizeEl, blobsSizeContainer), |
4772 | msgContainer, |
4773 | h('div.composer-actions', |
4774 | h('input', {type: 'submit', name: 'action', value: 'publish'}) |
4775 | ) |
4776 | ] |
4777 | } |
4778 | |
4779 | } |
4780 | |
4781 | Serve.prototype.phPreview = function (content, opts) { |
4782 | var self = this |
4783 | var msg = { |
4784 | value: { |
4785 | author: this.app.sbot.id, |
4786 | timestamp: Date.now(), |
4787 | content: content |
4788 | } |
4789 | } |
4790 | opts = opts || {} |
4791 | if (content.recps) msg.value.private = true |
4792 | var warnings = [] |
4793 | var estSize = u.estimateMessageSize(content) |
4794 | if (estSize > 8192) warnings.push(ph('li', 'message is too long')) |
4795 | |
4796 | var aboutNewContent = opts.aboutNewContent, aboutNewMsg |
4797 | if (aboutNewContent) { |
4798 | aboutNewMsg = { |
4799 | value: { |
4800 | author: this.app.sbot.id, |
4801 | timestamp: Date.now(), |
4802 | content: aboutNewContent |
4803 | } |
4804 | } |
4805 | // this duplicates functionality in publishNewAbout, for display purposes |
4806 | if (content.recps) { |
4807 | aboutNewContent.recps = content.recps |
4808 | aboutNewMsg.value.private = true |
4809 | } |
4810 | } |
4811 | |
4812 | return ph('form', {action: '', method: 'post'}, [ |
4813 | ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}), |
4814 | warnings.length ? [ |
4815 | ph('div', ph('em', 'warning:')), |
4816 | ph('ul', {class: 'mentions'}, warnings) |
4817 | ] : '', |
4818 | ph('div', [ |
4819 | ph('em', 'draft:'), ' ', |
4820 | u.escapeHTML(this.app.render.formatSize(estSize)) |
4821 | ]), |
4822 | ph('table', {class: 'ssb-msgs'}, pull( |
4823 | aboutNewMsg |
4824 | ? pull.values([aboutNewMsg, msg]) |
4825 | : pull.once(msg), |
4826 | this.app.unboxMessages(), |
4827 | this.app.render.renderFeeds({ |
4828 | serve: self, |
4829 | raw: opts.raw, |
4830 | filter: this.query.filter, |
4831 | }), |
4832 | pull.map(u.toHTML) |
4833 | )), |
4834 | ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}), |
4835 | aboutNewContent ? ph('input', {type: 'hidden', name: 'about_new_content', value: u.escapeHTML(JSON.stringify(aboutNewContent))}) : null, |
4836 | ph('div', {class: 'composer-actions'}, [ |
4837 | ph('input', {type: 'submit', name: 'action', value: 'publish'}) |
4838 | ]) |
4839 | ]) |
4840 | } |
4841 | |
4842 | Serve.prototype.phMsgActions = function (content) { |
4843 | var self = this |
4844 | var data = self.data |
4845 | |
4846 | function renderDraftLink(draftId) { |
4847 | return pull.values([ |
4848 | ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURI(draftId)), |
4849 | title: 'draft link'}, u.escapeHTML(draftId)), |
4850 | ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ', |
4851 | ]) |
4852 | } |
4853 | |
4854 | return [ |
4855 | ph('input', {type: 'hidden', name: 'url', value: self.req.url}), |
4856 | ph('p', {class: 'msg-right'}, [ |
4857 | data.save_draft && content ? u.readNext(function (cb) { |
4858 | self.saveDraft(content, function (err, draftId) { |
4859 | if (err) return cb(err) |
4860 | cb(null, renderDraftLink(draftId)) |
4861 | }) |
4862 | }) : data.draft_id ? renderDraftLink(data.draft_id) : '', |
4863 | ph('label', {for: 'save_draft'}, [ |
4864 | ph('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1', |
4865 | checked: data.save_draft || data.restored_draft ? 'checked' : undefined}), |
4866 | ' save draft ' |
4867 | ]), |
4868 | ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ', |
4869 | ph('input', {type: 'submit', name: 'preview', value: 'Preview'}), |
4870 | ]) |
4871 | ] |
4872 | } |
4873 | |
4874 | function hashBuf(buf) { |
4875 | var hash = crypto.createHash('sha256') |
4876 | hash.update(buf) |
4877 | return '&' + hash.digest('base64') + '.sha256' |
4878 | } |
4879 | |
4880 | Serve.prototype.getMsgDecryptedMaybeOoo = function (key, cb) { |
4881 | var self = this |
4882 | if (this.useOoo) this.app.getMsgDecryptedOoo(key, next) |
4883 | else this.app.getMsgDecrypted(key, next) |
4884 | function next(err, msg) { |
4885 | if (err) return cb(err) |
4886 | var c = msg && msg.value && msg.value.content |
4887 | if (typeof c === 'string' && self.query.unbox) |
4888 | self.app.unboxMsgWithKey(msg, String(self.query.unbox).replace(/ /g, '+'), cb) |
4889 | else cb(null, msg) |
4890 | } |
4891 | } |
4892 | |
4893 | Serve.prototype.emojis = function (path) { |
4894 | var self = this |
4895 | var seen = {} |
4896 | pull( |
4897 | ph('section', [ |
4898 | ph('h3', 'Emojis'), |
4899 | ph('ul', {class: 'mentions'}, pull( |
4900 | self.app.streamEmojis(), |
4901 | pull.map(function (emoji) { |
4902 | if (!seen[emoji.name]) { |
4903 | // cache the first use, so that our uses take precedence over other feeds' |
4904 | self.app.reverseEmojiNameCache.set(emoji.name, emoji.link) |
4905 | seen[emoji.name] = true |
4906 | } |
4907 | return ph('li', [ |
4908 | ph('a', {href: self.app.render.toUrl('/links/' + emoji.link)}, |
4909 | ph('img', { |
4910 | class: 'ssb-emoji', |
4911 | src: self.app.render.imageUrl(emoji.link), |
4912 | size: 32, |
4913 | }) |
4914 | ), ' ', |
4915 | u.escapeHTML(emoji.name) |
4916 | ]) |
4917 | }) |
4918 | )) |
4919 | ]), |
4920 | this.wrapPage('emojis'), |
4921 | this.respondSink(200) |
4922 | ) |
4923 | } |
4924 | |
4925 | Serve.prototype.editDiff = function (url) { |
4926 | var self = this |
4927 | var id |
4928 | try { |
4929 | id = decodeURIComponent(url.substr(1)) |
4930 | } catch(err) { |
4931 | return pull( |
4932 | pull.once(u.renderError(err).outerHTML), |
4933 | self.wrapPage('diff: ' + id), |
4934 | self.respondSink(400) |
4935 | ) |
4936 | } |
4937 | return pull( |
4938 | ph('section', {}, [ |
4939 | 'diff: ', |
4940 | ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')), |
4941 | u.readNext(function (cb) { |
4942 | self.getMsgDecryptedMaybeOoo(id, function (err, msg) { |
4943 | if (err) return cb(null, pull.once(u.renderError(err).outerHTML)) |
4944 | var c = msg.value.content || {} |
4945 | self.getMsgDecryptedMaybeOoo(c.updated, function (err, oldMsg) { |
4946 | if (err) return cb(null, pull.once(u.renderError(err).outerHTML)) |
4947 | cb(null, self.textEditDiffTable(oldMsg, msg)) |
4948 | }) |
4949 | }) |
4950 | }) |
4951 | ]), |
4952 | self.wrapPage('diff: ' + id), |
4953 | self.respondSink(200) |
4954 | ) |
4955 | } |
4956 | |
4957 | function findMsg(msgs, id) { |
4958 | for (var i = 0; i < msgs.length; i++) { |
4959 | if (msgs[i].key === id) return i |
4960 | } |
4961 | return -1 |
4962 | } |
4963 | |
4964 | Serve.prototype.aboutDiff = function (url) { |
4965 | var self = this |
4966 | var id |
4967 | try { |
4968 | id = decodeURIComponent(url.substr(1)) |
4969 | } catch(err) { |
4970 | return pull( |
4971 | pull.once(u.renderError(err).outerHTML), |
4972 | self.wrapPage('diff: ' + id), |
4973 | self.respondSink(400) |
4974 | ) |
4975 | } |
4976 | return pull( |
4977 | ph('section', {}, [ |
4978 | 'diff: ', |
4979 | ph('a', {href: self.app.render.toUrl(id)}, ph('code', id.substr(0, 8) + '…')), |
4980 | u.readNext(function (cb) { |
4981 | // About messages don't always include branch links. So get the whole thread |
4982 | // and use ssb-sort to find what to consider the previous message(s). |
4983 | self.getMsgDecryptedMaybeOoo(id, function (err, msg) { |
4984 | if (err) return cb(null, pull.once(u.renderError(err).outerHTML)) |
4985 | var c = msg.value.content || {} |
4986 | var rootId = c.about |
4987 | if (!rootId) return gotLinks(new Error('Missing about root')) |
4988 | var msgDate = new Date(msg.value.timestamp) |
4989 | cb(null, ph('div', [ |
4990 | self.phIdLink(msg.value.author), ' ', |
4991 | ph('span', {title: msgDate.toLocaleString()}, htime(msgDate)), |
4992 | ph('div', u.readNext(next.bind(this, rootId, msg))) |
4993 | ])) |
4994 | }) |
4995 | }) |
4996 | ]), |
4997 | self.wrapPage('diff: ' + id), |
4998 | self.respondSink(200) |
4999 | ) |
5000 | |
5001 | function next(rootId, msg, cb) { |
5002 | pull( |
5003 | rootId === msg.value.author && !self.query.fromAny ? |
5004 | self.app.getLinks3(rootId, msg.value.author, 'about') : |
5005 | self.app.getLinksBy(rootId, 'about'), |
5006 | pull.unique('key'), |
5007 | self.app.unboxMessages(), |
5008 | pull.collect(function (err, links) { |
5009 | if (err) return gotLinks(err) |
5010 | if (!self.useOoo) return gotLinks(null, links) |
5011 | self.app.expandOoo({msgs: links, dest: id}, gotLinks) |
5012 | }) |
5013 | ) |
5014 | function gotLinks(err, links) { |
5015 | if (err) return cb(null, pull.once(u.renderError(err).outerHTML)) |
5016 | |
5017 | sort(links) |
5018 | links = links.filter(function (msg) { |
5019 | var c = msg && msg.value && msg.value.content |
5020 | return c && c.type === 'about' && c.about === rootId |
5021 | && typeof c.description === 'string' |
5022 | }) |
5023 | var i = findMsg(links, id) |
5024 | if (i < 0) return cb(null, ph('div', 'Unable to find previous message')) |
5025 | var prevMsg = links[i-1] |
5026 | var nextMsg = links[i+1] |
5027 | var prevHref = prevMsg ? |
5028 | self.app.render.toUrl('/about-diff/' + encodeURIComponent(prevMsg.key)) : null |
5029 | var nextHref = nextMsg ? |
5030 | self.app.render.toUrl('/about-diff/' + encodeURIComponent(nextMsg.key)) : null |
5031 | cb(null, cat([ |
5032 | prevMsg |
5033 | ? pull.values(['prev: ', ph('a', {href: prevHref}, ph('code', |
5034 | prevMsg.key.substr(0, 8) + '…')), ', ']) |
5035 | : pull.empty(), |
5036 | nextMsg |
5037 | ? pull.values(['next: ', ph('a', {href: nextHref}, ph('code', |
5038 | nextMsg.key.substr(0, 8) + '…'))]) |
5039 | : pull.empty(), |
5040 | prevMsg || msg ? self.textEditDiffTable(prevMsg, msg) : pull.empty() |
5041 | ])) |
5042 | } |
5043 | } |
5044 | } |
5045 | |
5046 | Serve.prototype.textEditDiffTable = function (oldMsg, newMsg) { |
5047 | var oldC = oldMsg && oldMsg.value.content || {} |
5048 | var newC = newMsg && newMsg.value.content || {} |
5049 | var oldText = String(oldC.text || oldC.description || '') |
5050 | var newText = String(newC.text || newC.description || '') |
5051 | var diff = Diff.structuredPatch('', '', oldText, newText) |
5052 | var self = this |
5053 | // note: this structure is duplicated in lib/serve.js |
5054 | return pull( |
5055 | ph('table', {class: 'diff-table'}, [ |
5056 | pull( |
5057 | pull.values(diff.hunks), |
5058 | pull.map(function (hunk) { |
5059 | var oldLine = hunk.oldStart |
5060 | var newLine = hunk.newStart |
5061 | return [ |
5062 | ph('tr', [ |
5063 | ph('td', {colspan: 2}), |
5064 | ph('td', {colspan: 2}, ph('pre', |
5065 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
5066 | '+' + newLine + ',' + hunk.newLines + ' @@')) |
5067 | ]), |
5068 | pull( |
5069 | pull.values(hunk.lines), |
5070 | pull.map(function (line) { |
5071 | var s = line[0] |
5072 | if (s == '\\') return |
5073 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
5074 | return [ |
5075 | ph('tr', { |
5076 | class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
5077 | }, [ |
5078 | lineNums.map(function (num, i) { |
5079 | return ph('td', String(num)) |
5080 | }), |
5081 | ph('td', {class: 'diff-sigil'}, ph('code', s)), |
5082 | ph('td', {class: 'diff-line'}, [ |
5083 | u.unwrapP(self.app.render.markdown(line.substr(1), |
5084 | s == '-' ? oldC.mentions : newC.mentions)) |
5085 | ]) |
5086 | ]) |
5087 | ] |
5088 | }) |
5089 | ) |
5090 | ] |
5091 | }) |
5092 | ) |
5093 | ]) |
5094 | ) |
5095 | } |
5096 | |
5097 | Serve.prototype.shard = function (url) { |
5098 | var self = this |
5099 | var id |
5100 | try { |
5101 | id = decodeURIComponent(url.substr(1)) |
5102 | } catch(err) { |
5103 | return onError(err) |
5104 | } |
5105 | function onError(err) { |
5106 | pull( |
5107 | pull.once(u.renderError(err).outerHTML), |
5108 | self.wrapPage('shard: ' + id), |
5109 | self.respondSink(400) |
5110 | ) |
5111 | } |
5112 | self.app.getShard(id, function (err, shard) { |
5113 | if (err) return onError(err) |
5114 | pull( |
5115 | pull.once(shard), |
5116 | self.respondSink(200, {'Content-Type': 'text/plain'}) |
5117 | ) |
5118 | }) |
5119 | } |
5120 | |
5121 | Serve.prototype.pub = function (path) { |
5122 | var self = this |
5123 | var id = String(path).substr(1) |
5124 | try { id = decodeURIComponent(id) } |
5125 | catch(e) {} |
5126 | pull( |
5127 | ph('section', [ |
5128 | ph('h3', ['Pub addresses: ', self.phIdLink(id)]), |
5129 | pull( |
5130 | self.app.getAddresses(id), |
5131 | pull.map(function (address) { |
5132 | return ph('div', [ |
5133 | ph('code', self.app.removeDefaultPort(address)) |
5134 | ]) |
5135 | }) |
5136 | ) |
5137 | ]), |
5138 | self.wrapPage('Addresses: ' + id), |
5139 | self.respondSink(200) |
5140 | ) |
5141 | } |
5142 | |
5143 | function hiddenInput(key, value) { |
5144 | return Array.isArray(value) ? value.map(function (value) { |
5145 | return ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)}) |
5146 | }) : ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)}) |
5147 | } |
5148 | |
5149 | Serve.prototype.drafts = function (filepath) { |
5150 | var self = this |
5151 | var id = filepath && String(filepath).substr(1) |
5152 | if (id) try { id = decodeURIComponent(id) } |
5153 | catch(e) {} |
5154 | var dir |
5155 | if (id) try { |
5156 | var draftFile = path.join(self.app.draftsDir, id) |
5157 | var stat = fs.statSync(draftFile) |
5158 | if (stat.isDirectory()) { |
5159 | dir = id |
5160 | id = null |
5161 | } |
5162 | } catch(e) { |
5163 | return this.respond(404, 'Not found') |
5164 | } |
5165 | |
5166 | if (id) { |
5167 | var idHref = '/drafts' |
5168 | return pull( |
5169 | ph('section', [ |
5170 | ph('h3', [ |
5171 | ph('a', {href: self.app.render.toUrl('/drafts')}, 'Drafts'), |
5172 | id.split(/\/+/).map(function (part, i) { |
5173 | idHref += '/' + part |
5174 | var href = self.app.render.toUrl(idHref) |
5175 | return [ |
5176 | ': ', |
5177 | ph('a', {href: href}, u.escapeHTML(part)) |
5178 | ] |
5179 | }) |
5180 | ]), |
5181 | u.readNext(function (cb) { |
5182 | if (self.data.draft_discard) { |
5183 | return self.app.discardDraft(id, function (err) { |
5184 | if (err) return cb(err) |
5185 | cb(null, ph('div', 'Discarded')) |
5186 | }) |
5187 | } |
5188 | self.app.getDraft(id, function (err, draft) { |
5189 | if (err) return cb(err) |
5190 | var form = draft.form || {} |
5191 | var content = draft.content || {type: 'post', text: ''} |
5192 | var composerUrl = self.app.render.toUrl(draft.url) |
5193 | + (form.composer_id ? '#' + encodeURIComponent(form.composer_id) : '') |
5194 | cb(null, ph('div', [ |
5195 | ph('table', ph('tr', [ |
5196 | ph('td', ph('form', {method: 'post', action: u.escapeHTML(composerUrl)}, [ |
5197 | hiddenInput('draft_id', id), |
5198 | hiddenInput('restored_draft', '1'), |
5199 | Object.keys(form).map(function (key) { |
5200 | if (key === 'draft_id' || key === 'save_draft') return '' |
5201 | return hiddenInput(key, draft.form[key]) |
5202 | }), |
5203 | ph('input', {type: 'submit', name: 'draft_edit', value: 'Edit'}) |
5204 | ])), |
5205 | ph('td', ph('form', {method: 'post', action: ''}, [ |
5206 | ph('input', {type: 'submit', name: 'draft_discard', value: 'Discard', |
5207 | title: 'Discard draft'}) |
5208 | ])) |
5209 | ])), |
5210 | self.phPreview(content, {draftId: id}) |
5211 | ])) |
5212 | }) |
5213 | }) |
5214 | ]), |
5215 | self.wrapPage('draft: ' + id), |
5216 | self.respondSink(200) |
5217 | ) |
5218 | } |
5219 | |
5220 | var render = self.app.render |
5221 | var idHref = '/drafts' |
5222 | return pull( |
5223 | ph('section', [ |
5224 | ph('h3', [ |
5225 | ph('a', {href: render.toUrl('/drafts')}, 'Drafts'), |
5226 | dir ? dir.split(/\/+/).map(function (part, i) { |
5227 | idHref += '/' + part |
5228 | return [ |
5229 | ': ', |
5230 | ph('a', {href: render.toUrl(idHref)}, u.escapeHTML(part)) |
5231 | ] |
5232 | }) : '' |
5233 | ]), |
5234 | ph('ul', pull( |
5235 | self.app.listDrafts(dir), |
5236 | pull.asyncMap(function (draft, cb) { |
5237 | var form = draft.form || {} |
5238 | var msg = { |
5239 | key: '/drafts/' + draft.id, |
5240 | value: { |
5241 | author: self.app.sbot.id, |
5242 | timestamp: Date.now(), |
5243 | content: draft.content || ( |
5244 | draft.isDir ? {type: draft.id + '/'} |
5245 | : {type: 'post'} |
5246 | ) |
5247 | } |
5248 | } |
5249 | cb(null, ph('li', self.app.render.phMsgLink(msg))) |
5250 | }) |
5251 | )) |
5252 | ]), |
5253 | self.wrapPage('drafts'), |
5254 | self.respondSink(200) |
5255 | ) |
5256 | } |
5257 |
Built with git-ssb-web