aboutsummaryrefslogtreecommitdiff
path: root/lib/serve.js
diff options
context:
space:
mode:
authorcel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2020-03-19 17:41:46 -0400
committercel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2020-03-19 17:43:55 -0400
commitf50d7806676b8d789e3cb387d1f52269749e960c (patch)
treef4202d6b77d7f959514e81c45715a7fc71a5f708 /lib/serve.js
parent637e98b75e71e3ba16f2de5c98e2dea8f6ef62a8 (diff)
downloadpatchfoo-f50d7806676b8d789e3cb387d1f52269749e960c.tar.gz
patchfoo-f50d7806676b8d789e3cb387d1f52269749e960c.zip
Add pages to create, view, and edit Gatherings
Diffstat (limited to 'lib/serve.js')
-rw-r--r--lib/serve.js506
1 files changed, 506 insertions, 0 deletions
diff --git a/lib/serve.js b/lib/serve.js
index 29a6350..3b278b7 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -169,6 +169,7 @@ Serve.prototype.go = function () {
function gotData(err, data) {
self.data = data
if (err) next(err)
+ else if (data.action === 'publish' && data.about_new) self.publishNewAbout(next)
else if (data.action === 'publish') self.publishJSON(next)
else if (data.action === 'contact') self.publishContact(next)
else if (data.action === 'want-blobs') self.wantBlobs(next)
@@ -223,6 +224,29 @@ Serve.prototype.publishJSON = function (cb) {
this.publish(content, cb)
}
+Serve.prototype.publishNewAbout = function (cb) {
+ var self = this
+ var aboutContent
+ try {
+ aboutContent = JSON.parse(this.data.content)
+ } catch(e) {
+ return cb(e)
+ }
+ if (typeof aboutContent !== 'object' || aboutContent === null) {
+ return cb(new TypeError('content must be object'))
+ }
+ var newContent = {
+ type: this.data.about_new,
+ recps: aboutContent.recps
+ }
+ self.app.publish(newContent, function (err, msg) {
+ if (err) return cb(err)
+ if (!msg) return cb(new Error('aborted'))
+ aboutContent.about = msg.key
+ self.publish(aboutContent, cb)
+ })
+}
+
Serve.prototype.publishVote = function (next) {
var content = {
type: 'vote',
@@ -469,6 +493,7 @@ Serve.prototype.path = function (url) {
case '/emojis': return this.emojis(m[2])
case '/votes': return this.votes(m[2])
case '/about-self': return this.aboutSelf(m[2])
+ case '/new-gathering': return this.newGathering(m[2])
}
m = /^(\/?[^\/]*)(\/.*)?$/.exec(url)
switch (m[1]) {
@@ -490,6 +515,8 @@ Serve.prototype.path = function (url) {
case '/markdown': return this.markdown(m[2])
case '/edit-diff': return this.editDiff(m[2])
case '/about-diff': return this.aboutDiff(m[2])
+ case '/gathering': return this.gathering(m[2])
+ case '/edit-gathering': return this.editGathering(m[2])
case '/shard': return this.shard(m[2])
case '/zip': return this.zip(m[2])
case '/web': return this.web(m[2])
@@ -1231,6 +1258,466 @@ Serve.prototype.aboutSelf = function (ext) {
})
}
+Serve.prototype.gathering = function (url) {
+ var self = this
+ var id
+ try {
+ id = decodeURIComponent(url.substr(1))
+ } catch(err) {
+ return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('Gathering ' + id),
+ self.respondSink(400)
+ )
+ }
+
+ var q = self.query
+ var data = self.data || {}
+ var selfId = self.app.sbot.id
+ var render = self.app.render
+
+ function hashLink(id) {
+ if (!id) return ''
+ if (typeof id !== 'string') return u.escapeHTML(JSON.stringify(id))
+ return h('a', {style: 'font: monospace', href: render.toUrl(id)},
+ id.substr(0, 8) + '…').outerHTML + ' '
+ }
+
+ self.app.getAbout(id, function (err, about) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.respondSink(400)
+ )
+
+ var sources = about._sources || {}
+ var start = about.startDateTime
+ var img = about.imageLink
+ pull(
+ ph('section', [
+ ph('h2', ['Gathering ', hashLink(id)]),
+ ph('div', ph('a', {href: render.toUrl('/edit-gathering/' + encodeURIComponent(id))}, 'Edit')),
+ ph('table', [
+ img ? ph('tr', [
+ ph('th', 'Image'),
+ ph('td', sources.image.map(hashLink)),
+ ph('td', u.isRef(img.link) ? ph('img', {
+ class: 'ssb-avatar-image',
+ src: render.imageUrl(img.link),
+ alt: img.link
+ + (img.size ? ' (' + render.formatSize(img.size) + ')' : '')
+ }) : ph('code', JSON.stringify(img))),
+ ]) : '',
+ about.title ? ph('tr', [
+ ph('th', 'Title'),
+ ph('td', sources.title.map(hashLink)),
+ ph('td', ph('h1', {style: 'margin: 0'}, u.escapeHTML(about.title)))
+ ]) : '',
+ about.location ? ph('tr', [
+ ph('th', 'Location'),
+ ph('td', sources.location.map(hashLink)),
+ ph('td', u.escapeHTML(about.location))
+ ]) : '',
+ start ? ph('tr', [
+ ph('th', 'Start time'),
+ ph('td', sources.startDateTime.map(hashLink)),
+ ph('td', [
+ ph('code', u.escapeHTML(new Date(start.epoch).toISOString().replace(/ .*/, ''))),
+ start.tz ? [
+ ' ',
+ ph('code', u.escapeHTML(start.tz))
+ ] : ''
+ ])
+ ]) : '',
+ about.description ? ph('tr', [
+ ph('th', 'Description'),
+ ph('td', sources.description.map(hashLink)),
+ ph('td', render.markdown(about.description))
+ ]) : '',
+ /*
+ about.branch ? ph('tr', [
+ ph('th', 'Latest updates'),
+ ph('th'),
+ ph('td', about.branch.map(hashLink))
+ ]) : '',
+ */
+ about.attendee ? about.attendee.map(function (link, i, rows) {
+ return ph('tr', [
+ i === 0 ? ph('th', {rowspan: rows.length}, 'Attendees') : '',
+ ph('td', hashLink(link.source)),
+ ph('td', self.phIdLink(link.link))
+ ])
+ }) : '',
+ ])
+ ]),
+ self.wrapPage('Gathering ' + id),
+ self.respondSink(200)
+ )
+ })
+}
+
+var ignoreDateTimeProps = {
+ silent: true, // internal to spacetime module
+}
+
+function isDateTimeEqual(a, b) {
+ if (a === b) return true
+ if ((a && !b) || (!b && a)) return false
+ for (var k in a) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false
+ for (var k in b) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false
+ return true
+}
+
+Serve.prototype.editGathering = function (url) {
+ var self = this
+ var id
+ try {
+ id = decodeURIComponent(url.substr(1))
+ } catch(err) {
+ return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('Edit Gathering ' + id),
+ self.respondSink(400)
+ )
+ }
+
+ var q = self.query
+ var data = self.data || {}
+ var selfId = self.app.sbot.id
+ var render = self.app.render
+
+ self.app.getAbout(id, function (err, about) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.respondSink(400)
+ )
+
+ var aboutImageLink = about.imageLink || {}
+ var aboutStartDateTime = about.startDateTime || {}
+ var title = data.title != null ?
+ data.title === '' ? null : data.title :
+ about.title || null
+ var location = data.location != null ?
+ data.location === '' ? null : data.location :
+ about.location || null
+ var image = data.image_upload != null ? {
+ link: data.image_upload.link,
+ type: data.image_upload.type,
+ size: data.image_upload.size
+ } : data.remove_image ? null :
+ data.image_id && data.image_id !== aboutImageLink.link ? {
+ link: data.image_id,
+ type: data.image_type,
+ size: data.image_size
+ } : aboutImageLink
+ var description = data.description != null ?
+ data.description === '' ? null : data.description :
+ about.description || null
+
+ // use undefined instead of null to reset these values,
+ // since they are set as a whole object
+ var startDateTimeEpoch = data.startDateTimeStr != null ?
+ data.startDateTimeStr === '' ? null :
+ new Date(data.startDateTimeStr).getTime() :
+ aboutStartDateTime.epoch
+ var startDateTime = startDateTimeEpoch === null ? null
+ : typeof startDateTimeEpoch === 'number' && !isNaN(startDateTimeEpoch) ? {
+ epoch: startDateTimeEpoch,
+ tz: data.startTZ != null ?
+ data.startTZ === '' ? undefined : data.startTZ :
+ aboutStartDateTime.tz,
+ _weekStart: data.startWeekStart != null ?
+ data.startWeekStart === '' ? undefined : Number(data.startWeekStart) :
+ aboutStartDateTime._weekStart
+ } : about.startDateTime || null
+
+ var attendeeIds = u.toLinkArray(about.attendee).map(u.linkDest)
+ var aboutSelfAttending = attendeeIds.indexOf(selfId) !== -1
+ var selfAttending = data.attending != null ? Boolean(data.attending) :
+ aboutSelfAttending
+ var mentionAttendees = data.mention_attendees === '' ? true : Boolean(data.mention_attendees)
+ var attendeeMentions = mentionAttendees ? attendeeIds.filter(function (id) {
+ return id !== selfId
+ }) : []
+ var additionalMentions = data.mentions ?
+ u.extractRefs(data.mentions).filter(uniques()) : []
+ var mentions = attendeeMentions.concat(additionalMentions)
+
+ var content
+ if (data.preview || data.preview_raw) {
+ content = {
+ type: 'about',
+ about: id
+ }
+ if (about.recps) content.recps = about.recps
+ if (about.branch) content.branch = about.branch
+ if (title != about.title) content.title = title
+ if (location != about.location) content.location = location
+ if (image === null) {
+ if (about.image) content.image = {link: about.image, remove: true}
+ } else if (image.link != about.image) content.image = image
+ if (description != about.description) {
+ content.description = description
+ var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false})
+ // don't mention ids already mentioned in the thread
+ textMentions.forEach(function (link) {
+ if (mentions.indexOf(link.link) === -1) {
+ mentions.push(link)
+ }
+ })
+ }
+ if (!isDateTimeEqual(startDateTime, about.startDateTime)) content.startDateTime = startDateTime
+ if (mentions.length) content.mentions = mentions
+ if (selfAttending != aboutSelfAttending) content.attendee = selfAttending ? {
+ link: selfId
+ } : {
+ link: selfId,
+ remove: true
+ }
+ }
+
+ var startDateTimeStr = ''
+ if (startDateTime) try {
+ startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '')
+ } catch(e) {}
+ var startTz = startDateTime && startDateTime.tz || null
+ var startWeekStart = startDateTime && startDateTime._weekStart || null
+
+ pull(
+ ph('section', [
+ ph('h2', ['Edit Gathering ', self.phIdLink(id)]),
+ ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
+ ph('div', [
+ ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)})
+ ]),
+ ph('div', [
+ ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)})
+ ]),
+ ph('div', [
+ ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr),
+ style: 'font: monospace', size: 30, placeholder: 'Start date time'}),
+ ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '',
+ style: 'font: monospace', size: 20, placeholder: 'TZ'}),
+ ph('input', {name: 'startWeekStart',
+ value: startWeekStart == null ? '' : u.toString(startWeekStart),
+ style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'})
+ ]),
+ ph('table', ph('tr', [
+ ph('td', [
+ data.remove_image ? ph('input', {type: 'hidden', name: 'remove_image', value: u.escapeHTML(data.remove_image)}) : '',
+ image && image.link ? ph('a', {href: render.toUrl(image.link)}, [
+ ph('img', {
+ class: 'ssb-avatar-image',
+ src: render.imageUrl(image.link),
+ alt: image.link || 'fallback avatar',
+ title: image.link || 'fallback avatar'
+ })
+ ]) : ''
+ ]),
+ ph('td', [
+ image && image.link ? ph('div', [
+ ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ',
+ ph('small', ph('code', u.escapeHTML(image.link))),
+ ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
+ ]) : '',
+ image && image.size ? [
+ ph('code', render.formatSize(image.size)),
+ ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
+ ] : '',
+ image && image.type ? [
+ ph('input', {type: 'hidden', name: 'image_type', value: image.type})
+ ] : '',
+ ph('div', [
+ ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
+ ])
+ ])
+ ])),
+ ph('div', ph('textarea', {
+ name: 'description', placeholder: 'Description',
+ cols: 70,
+ rows: Math.max(5, u.rows(description))
+ }, u.escapeHTML(description))),
+ ph('div', ph('select', {name: 'attending'}, [
+ ph('option', {value: '1', selected: !selfAttending ? 'selected' : undefined}, 'You are not attending'),
+ ph('option', {value: '1', selected: selfAttending ? 'selected' : undefined}, 'You are attending'),
+ ])),
+ ph('div', ph('label', {for: 'mention_attendees'}, [
+ ph('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: '1', checked: mentionAttendees ? 'checked' : undefined}),
+ ' mention attendees'
+ ])),
+ ph('div', additionalMentions.length > 0 || data.add_mentions ? ph('textarea', {
+ name: 'mentions', placeholder: 'Additional mentions',
+ cols: 70, style: 'font: monospace',
+ rows: Math.max(3, additionalMentions.length + 2),
+ }, u.escapeHTML(additionalMentions.join('\n'))
+ ) : ph('label', {for: 'additional_mentions'}, [
+ ph('input', {id: 'additional_mentions', type: 'checkbox', name: 'add_mentions', value: '1'}),
+ ' additional mentions'
+ ])
+ ),
+ self.phMsgActions(content)
+ ]),
+ content ? self.phPreview(content, {raw: data.preview_raw}) : ''
+ ]),
+ self.wrapPage('Edit Gathering ' + id),
+ self.respondSink(200)
+ )
+ })
+}
+
+Serve.prototype.newGathering = function () {
+ var self = this
+ var q = self.query
+ var data = self.data || {}
+ var selfId = self.app.sbot.id
+ var render = self.app.render
+
+ var title = data.title ? u.toString(data.title) : null
+ var description = data.description ? u.toString(data.description) : null
+ var location = data.location ? u.toString(data.location) : null
+ var image = data.image_upload != null ? {
+ link: data.image_upload.link,
+ type: data.image_upload.type,
+ size: data.image_upload.size
+ } : data.remove_image ? null : data.image_id ? {
+ link: data.image_id,
+ type: data.image_type,
+ size: data.image_size
+ } : null
+ var private = Boolean(data.private)
+ var recps = data.recps ? u.extractRefs(data.recps) : private ? [selfId] : null
+ // default recps to self id but allow removing it
+
+ var startTs = data.startDateTimeStr ?
+ new Date(data.startDateTimeStr).getTime() : null
+ var startDateTime = typeof startTs === 'number' && !isNaN(startTs) ? {
+ epoch: startTs,
+ tz: data.startTZ ? u.toString(data.startTZ) : undefined,
+ _weekStart: data.startWeekStart ? Number(data.startWeekStart) : undefined
+ } : null
+
+ var selfAttending = Boolean(data.attending)
+ var additionalMentions = data.mentions ? u.extractRefs(data.mentions) : []
+ var mentions = additionalMentions.slice(0)
+
+ var content
+ if (data.preview || data.preview_raw) {
+ content = {
+ type: 'about'
+ }
+ if (private && recps) content.recps = recps
+ if (title) content.title = title
+ if (location) content.location = location
+ if (image) content.image = image
+ if (description) {
+ content.description = description
+ var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false})
+ textMentions.forEach(function (link) {
+ if (mentions.indexOf(link.link) === -1) {
+ mentions.push(link)
+ }
+ })
+ }
+ if (startDateTime) content.startDateTime = startDateTime
+ if (mentions.length) content.mentions = mentions
+ if (selfAttending) content.attendee = {
+ link: selfId
+ }
+ }
+
+ var startDateTimeStr = ''
+ if (startDateTime) try {
+ startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '')
+ } catch(e) {}
+ var startTz = startDateTime && startDateTime.tz || null
+ var startWeekStart = startDateTime && startDateTime._weekStart || null
+
+ pull(
+ ph('section', [
+ ph('h2', ['New Gathering']),
+ ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [
+ ph('div', [
+ ph('select', {name: 'private'}, [
+ ph('option', {value: '', selected: !private ? 'selected' : undefined}, 'Public'),
+ ph('option', {value: '1', selected: private ? 'selected' : undefined}, 'Private')
+ ])
+ ]),
+ private ? ph('div', ph('textarea', {
+ name: 'recps', placeholder: 'Recipients',
+ title: 'Recipient IDs for private gathering',
+ cols: 70, style: 'font: monospace',
+ rows: Math.max(3, recps.length + 2),
+ }, u.escapeHTML(recps.join('\n')) + '\n')) : recps ? ph('input', {
+ type: 'hidden', name: 'recps', value: recps.join('\n')
+ }) : null,
+ ph('div', [
+ ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)})
+ ]),
+ ph('div', [
+ ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)})
+ ]),
+ ph('div', [
+ ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr),
+ style: 'font: monospace', size: 30, placeholder: 'Start date time'}),
+ ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '',
+ style: 'font: monospace', size: 20, placeholder: 'TZ'}),
+ ph('input', {name: 'startWeekStart',
+ value: startWeekStart == null ? '' : u.toString(startWeekStart),
+ style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'})
+ ]),
+ ph('table', ph('tr', [
+ ph('td', [
+ image && image.link ? ph('a', {href: render.toUrl(image.link)}, [
+ ph('img', {
+ class: 'ssb-avatar-image',
+ src: render.imageUrl(image.link),
+ alt: image.link || 'fallback avatar',
+ title: image.link || 'fallback avatar'
+ })
+ ]) : ''
+ ]),
+ ph('td', [
+ image && image.link ? ph('div', [
+ ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ',
+ ph('small', ph('code', u.escapeHTML(image.link))),
+ ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ',
+ ]) : '',
+ image && image.size ? [
+ ph('code', render.formatSize(image.size)),
+ ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ',
+ ] : '',
+ image && image.type ? [
+ ph('input', {type: 'hidden', name: 'image_type', value: image.type})
+ ] : '',
+ ph('div', [
+ ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'})
+ ])
+ ])
+ ])),
+ ph('div', ph('textarea', {
+ name: 'description', placeholder: 'Description',
+ cols: 70,
+ rows: Math.max(5, u.rows(description))
+ }, u.escapeHTML(description))),
+ ph('div', ph('label', {for: 'attending'}, [
+ ph('input', {id: 'attending', type: 'checkbox', name: 'attending',
+ value: '1', checked: selfAttending ? 'checked' : undefined}),
+ ' attending'
+ ])),
+ ph('div', ph('textarea', {
+ name: 'mentions', placeholder: 'Additional mentions',
+ title: 'SSB feed/blob/message IDs to mention besides those in the description',
+ cols: 70, style: 'font: monospace',
+ rows: Math.max(3, additionalMentions.length + 2),
+ }, u.escapeHTML(additionalMentions.join('\n')))),
+ self.phMsgActions(content)
+ ]),
+ content ? self.phPreview(content, {raw: data.preview_raw, aboutNew: 'gathering'}) : ''
+ ]),
+ self.wrapPage('New Gathering'),
+ self.respondSink(200)
+ )
+}
+
Serve.prototype.block = function (path) {
var self = this
var data = self.data
@@ -4243,6 +4730,24 @@ Serve.prototype.phPreview = function (content, opts) {
var estSize = u.estimateMessageSize(content)
if (estSize > 8192) warnings.push(ph('li', 'message is too long'))
+ var aboutNewType = opts.aboutNew, aboutNewMsg
+ if (aboutNewType) {
+ aboutNewMsg = {
+ value: {
+ author: this.app.sbot.id,
+ timestamp: Date.now(),
+ content: {
+ type: aboutNewType
+ }
+ }
+ }
+ // this duplicates functionality in publishNewAbout, for display purposes
+ if (content.recps) {
+ aboutNewMsg.value.content.recps = content.recps
+ aboutNewMsg.value.private = true
+ }
+ }
+
return ph('form', {action: '', method: 'post'}, [
ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}),
warnings.length ? [
@@ -4264,6 +4769,7 @@ Serve.prototype.phPreview = function (content, opts) {
pull.map(u.toHTML)
)),
ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}),
+ aboutNewType ? ph('input', {type: 'hidden', name: 'about_new', value: u.escapeHTML(aboutNewType)}) : null,
ph('div', {class: 'composer-actions'}, [
ph('input', {type: 'submit', name: 'action', value: 'publish'})
])