From 5faad9ea56dcf2d715c9e11e07490f50115d25bb Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Wed, 11 Dec 2019 18:11:09 -0800 Subject: First pass at federation! --- views/layouts/main.handlebars | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'views/layouts') diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index 4f0cfbc..3cde5d5 100755 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -18,13 +18,13 @@ - - + + - + {{#if title}}{{title}} · {{/if}}gathio @@ -73,7 +73,7 @@
- GitHub · Made with by Raphael · Need help? Email us.
+ GitHub · Made with by Raphael · Need help? Email us.
If you like gathio, you might like sweet, my utopian social network.
-- cgit v1.2.3 From 8587b5b41e5781ea6fe4ce130e36ac327548864b Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 4 Jan 2020 13:33:35 -0800 Subject: Email refactoring Tons of refactoring of email. This no longer uses Sendgrid templates and now uses source-controlled handlebars files in the `views/emails/` directory. This means that email messages are now source-controlled and vastly reduces the sendgrid setup process. --- app.js | 2 + config/domain-example.js | 3 +- package-lock.json | 20 +- routes.js | 370 +++++++++++++++------------- views/emails/addeventattendee.handlebars | 7 + views/emails/addeventcomment.handlebars | 7 + views/emails/createevent.handlebars | 19 ++ views/emails/createeventgroup.handlebars | 27 ++ views/emails/deleteevent.handlebars | 4 + views/emails/editevent.handlebars | 8 + views/emails/removeeventattendee.handlebars | 4 + views/emails/unattendevent.handlebars | 8 + views/layouts/email.handlebars | 126 ++++++++++ 13 files changed, 426 insertions(+), 179 deletions(-) create mode 100644 views/emails/addeventattendee.handlebars create mode 100644 views/emails/addeventcomment.handlebars create mode 100644 views/emails/createevent.handlebars create mode 100644 views/emails/createeventgroup.handlebars create mode 100644 views/emails/deleteevent.handlebars create mode 100644 views/emails/editevent.handlebars create mode 100644 views/emails/removeeventattendee.handlebars create mode 100644 views/emails/unattendevent.handlebars create mode 100644 views/layouts/email.handlebars (limited to 'views/layouts') diff --git a/app.js b/app.js index ab4eb56..c6e0647 100755 --- a/app.js +++ b/app.js @@ -19,6 +19,7 @@ const app = express(); hbsInstance = hbs.create({ defaultLayout: 'main', partialsDir: ['views/partials/'], + layoutsDir: 'views/layouts/', helpers: { plural: function(number, text) { var singular = number === 1; @@ -39,6 +40,7 @@ hbsInstance = hbs.create({ }); app.engine('handlebars', hbsInstance.engine); app.set('view engine', 'handlebars'); +app.set('hbsInstance', hbsInstance); // Static files // diff --git a/config/domain-example.js b/config/domain-example.js index 2f22fe7..3b77197 100644 --- a/config/domain-example.js +++ b/config/domain-example.js @@ -2,5 +2,6 @@ module.exports = { // Your domain goes here. If there is a port it should be 'domain:port', but otherwise just 'domain' 'domain' : 'localhost:3000' , 'port': '3000', - 'email': 'contact@example.com' + 'email': 'contact@example.com', + 'sitename': 'gathio' }; diff --git a/package-lock.json b/package-lock.json index e9b5b66..10d0d73 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1535,9 +1535,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "optional": true }, "component-emitter": { @@ -2732,9 +2732,9 @@ } }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -5276,12 +5276,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.3.tgz", + "integrity": "sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==", "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { diff --git a/routes.js b/routes.js index 2475904..34dc1f8 100755 --- a/routes.js +++ b/routes.js @@ -26,6 +26,7 @@ const request = require('request'); const domain = require('./config/domain.js').domain; const contactEmail = require('./config/domain.js').email; +const siteName = require('./config/domain.js').sitename var sanitizeHtml = require('sanitize-html'); // Extra marked renderer (used to render plaintext event description for page metadata) @@ -610,13 +611,23 @@ router.get('/:eventID', (req, res) => { } } } - let eventAttendees = event.attendees.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)); - let spotsRemaining, noMoreSpots; - if (event.maxAttendees) { - spotsRemaining = event.maxAttendees - eventAttendees.length; - if (spotsRemaining <= 0) { - noMoreSpots = true; - } + let eventAttendees = event.attendees.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)) + .map(el => { + if (!el.id) { + el.id = el._id; + } + return el; + }) + .filter((obj, pos, arr) => { + return arr.map(mapObj => mapObj.id).indexOf(obj.id) === pos; + }); + + let spotsRemaining, noMoreSpots; + if (event.maxAttendees) { + spotsRemaining = event.maxAttendees - eventAttendees.length; + if (spotsRemaining <= 0) { + noMoreSpots = true; + } } let metadata = { title: event.name, @@ -887,30 +898,28 @@ router.post('/newevent', async (req, res) => { addToLog("createEvent", "success", "Event " + eventID + "created"); // Send email with edit link if (sendEmails) { - const msg = { - to: req.body.creatorEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-00330b8278ab463e9f88c16566487d97', - dynamic_template_data: { - subject: 'gathio: ' + req.body.eventName, - eventID: eventID, - editToken: editToken - }, - }; - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); + req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', {eventID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${req.body.eventName}`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } res.writeHead(302, { 'Location': '/' + eventID + '?e=' + editToken }); res.end(); }) - .catch((err) => { res.send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);}); + .catch((err) => { res.status(500).send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);}); }); router.post('/importevent', (req, res) => { @@ -957,23 +966,21 @@ router.post('/importevent', (req, res) => { addToLog("createEvent", "success", "Event " + eventID + " created"); // Send email with edit link if (sendEmails) { - const msg = { - to: creatorEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-00330b8278ab463e9f88c16566487d97', - dynamic_template_data: { - subject: 'gathio: ' + req.body.eventName, - eventID: eventID, - editToken: editToken - }, - }; - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); + req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', {eventID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${req.body.eventName}`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } res.writeHead(302, { 'Location': '/' + eventID + '?e=' + editToken @@ -1019,23 +1026,21 @@ router.post('/neweventgroup', (req, res) => { addToLog("createEventGroup", "success", "Event group " + eventGroupID + " created"); // Send email with edit link if (sendEmails) { - const msg = { - to: req.body.creatorEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-4c5ddcb34ac44ec5b2313c6da4e405f3', - dynamic_template_data: { - subject: 'gathio: ' + req.body.eventGroupName, - eventGroupID: eventGroupID, - editToken: editToken - }, - }; - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); + req.app.get('hbsInstance').renderView('./views/emails/createeventgroup.handlebars', {eventGroupID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${req.body.eventGroupName}`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } res.writeHead(302, { 'Location': '/group/' + eventGroupID + '?e=' + editToken @@ -1168,21 +1173,21 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { attendeeEmails = ids; if (!error && attendeeEmails !== ""){ console.log("Sending emails to: " + attendeeEmails); - const msg = { - to: attendeeEmails, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', - dynamic_template_data: { - subject: 'gathio: Event edited', - actionType: 'edited', - eventExists: true, - eventID: req.params.eventID - } - } - sgMail.sendMultiple(msg); + req.app.get('hbsInstance').renderView('./views/emails/editevent.handlebars', {diffText, eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: attendeeEmails, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${event.name} was just edited`, + html, + }; + sgMail.sendMultiple(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } else { console.log("Nothing to send!"); @@ -1294,6 +1299,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { if (event.editToken === submittedEditToken) { // Token matches + let eventImage; if (event.image){ eventImage = event.image; } @@ -1304,21 +1310,21 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { attendeeEmails = ids; if (!error){ console.log("Sending emails to: " + attendeeEmails); - const msg = { - to: attendeeEmails, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', - dynamic_template_data: { - subject: 'gathio: Event "' + event.name + '" deleted', - actionType: 'deleted', - eventExists: false, - eventID: req.params.eventID - } - } - sgMail.sendMultiple(msg); + req.app.get('hbsInstance').renderView('./views/emails/deleteevent.handlebars', {siteName, domain, eventName: event.name, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: attendeeEmails, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${event.name} was deleted`, + html, + }; + sgMail.sendMultiple(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } else { console.log("Nothing to send!"); @@ -1432,22 +1438,23 @@ router.post('/attendevent/:eventID', (req, res) => { addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); if (sendEmails) { if (req.body.attendeeEmail){ - const msg = { - to: req.body.attendeeEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-977612474bba49c48b58e269f04f927c', - dynamic_template_data: { - subject: 'gathio: ' + event.name, - eventID: req.params.eventID - }, - }; - sgMail.send(msg); + req.app.get('hbsInstance').renderView('./views/emails/addeventattendee.handlebars', {eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: req.body.attendeeEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: You're RSVPed to ${event.name}`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } } - res.writeHead(302, { 'Location': '/' + req.params.eventID }); @@ -1464,22 +1471,23 @@ router.post('/unattendevent/:eventID', (req, res) => { ) .then(response => { console.log(response) - addToLog("removeEventAttendee", "success", "Attendee removed from event " + req.params.eventID); + addToLog("unattendEvent", "success", "Attendee removed self from event " + req.params.eventID); if (sendEmails) { if (req.body.attendeeEmail){ - const msg = { - to: req.body.attendeeEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-56c97755d6394c23be212fef934b0f1f', - dynamic_template_data: { - subject: 'gathio: You have been removed from an event', - eventID: req.params.eventID - }, - }; - sgMail.send(msg); + req.app.get('hbsInstance').renderView('./views/emails/unattendevent.handlebars', {eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { const msg = { + to: req.body.attendeeEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: You have been removed from an event`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } } res.writeHead(302, { @@ -1501,20 +1509,22 @@ router.post('/removeattendee/:eventID/:attendeeID', (req, res) => { console.log(response) addToLog("removeEventAttendee", "success", "Attendee removed by admin from event " + req.params.eventID); if (sendEmails) { + // currently this is never called because we don't have the email address if (req.body.attendeeEmail){ - const msg = { - to: req.body.attendeeEmail, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-f8ee9e1e2c8a48e3a329d1630d0d371f', - dynamic_template_data: { - subject: 'gathio: You have been removed from an event', - eventID: req.params.eventID - }, - }; - sgMail.send(msg); + req.app.get('hbsInstance').renderView('./views/emails/removeeventattendee.handlebars', {eventName: req.params.eventName, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { const msg = { + to: req.body.attendeeEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: You have been removed from an event`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } } res.writeHead(302, { @@ -1549,20 +1559,21 @@ router.post('/post/comment/:eventID', (req, res) => { attendeeEmails = ids; if (!error){ console.log("Sending emails to: " + attendeeEmails); - const msg = { - to: attendeeEmails, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-756d078561e047aba307155f02b6686d', - dynamic_template_data: { - subject: 'gathio: New comment in ' + event.name, - commentAuthor: req.body.commentAuthor, - eventID: req.params.eventID - } - } - sgMail.sendMultiple(msg); + req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', {siteName, domain, eventID: req.params.eventID, commentAuthor: req.body.commentAuthor, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: attendeeEmails, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: New comment in ${event.name}`, + html, + }; + sgMail.sendMultiple(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } else { console.log("Nothing to send!"); @@ -1601,20 +1612,21 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { attendeeEmails = ids; if (!error){ console.log("Sending emails to: " + attendeeEmails); - const msg = { - to: attendeeEmails, - from: { - name: 'Gathio', - email: contactEmail, - }, - templateId: 'd-756d078561e047aba307155f02b6686d', - dynamic_template_data: { - subject: 'gathio: New comment in ' + event.name, - commentAuthor: req.body.commentAuthor, - eventID: req.params.eventID - } - } - sgMail.sendMultiple(msg); + req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', {siteName, domain, eventID: req.params.eventID, commentAuthor: req.body.replyAuthor, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: attendeeEmails, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: New comment in ${event.name}`, + html, + }; + sgMail.sendMultiple(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); } else { console.log("Nothing to send!"); @@ -1673,13 +1685,23 @@ router.post('/activitypub/inbox', (req, res) => { }, {}); // get the actor + // TODO if this is a Delete for an Actor this won't work request({ url: signature_header.keyId, headers: { 'Accept': 'application/activity+json', 'Content-Type': 'application/activity+json' }}, function (error, response, actor) { - publicKey = JSON.parse(actor).publicKey.publicKeyPem; + let publicKey = ''; + + try { + if (JSON.parse(actor).publicKey) { + publicKey = JSON.parse(actor).publicKey.publicKeyPem; + } + } + catch(err) { + return res.status(500).send('Actor could not be parsed' + err); + } let comparison_string = signature_header.headers.split(' ').map(header => { if (header === '(request-target)') { @@ -1694,10 +1716,14 @@ router.post('/activitypub/inbox', (req, res) => { verifier.update(comparison_string, 'ascii') const publicKeyBuf = new Buffer(publicKey, 'ascii') const signatureBuf = new Buffer(signature_header.signature, 'base64') - const result = verifier.verify(publicKeyBuf, signatureBuf) - console.log('VALIDATE RESULT:', result) + try { + const result = verifier.verify(publicKeyBuf, signatureBuf) + } + catch(err) { + return res.status(401).send('Signature could not be verified: ' + err); + } if (!result) { - res.status(401).send('Signature could not be verified.'); + return res.status(401).send('Signature could not be verified.'); } else { processInbox(req, res); @@ -1863,9 +1889,13 @@ function processInbox(req, res) { .then(() => { addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); console.log('added attendee', attendeeName) - res.send(200); + return res.sendStatus(200); }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); }); + .catch((err) => { addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); return res.status(500).send('Database error, please try again :('); }); + } + else { + // it's a duplicate and this person is already rsvped so just say OK + return res.status(200).send("Attendee is already registered."); } }); } @@ -1874,6 +1904,7 @@ function processInbox(req, res) { }); } if (req.body && req.body.type === 'Delete') { + // TODO: only do this if it's a delete for a Note // figure out if we have a matching comment by id const deleteObjectId = req.body.object.id; // find all events with comments from the author @@ -1920,7 +1951,7 @@ function processInbox(req, res) { if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) { console.log('create note!!') // figure out what this is in reply to -- it should be addressed specifically to us - let {name, attributedTo, inReplyTo, to, cc} = req.body.object; + let {attributedTo, inReplyTo, to, cc} = req.body.object; // normalize cc into an array if (typeof cc === 'string') { cc = [cc]; @@ -1983,8 +2014,11 @@ function processInbox(req, res) { }); } // end ourevent } // end public message - // if it's not a public message, let them know that we only support public messages right now - else { + // if it's not a public message, AND it's not a vote let them know that we only support public messages right now + else if (req.body.object.name !== 'Yes') { + if (!cc) { + cc = []; + } // figure out which event(s) of ours it was addressing ourEvents = cc.concat(to).filter(el => el.includes(`https://${domain}/`)) .map(el => el.replace(`https://${domain}/`,'')); @@ -2007,7 +2041,7 @@ function processInbox(req, res) { "content": `@${name} Sorry, this service only supports posting public messages to the event page. Try contacting the event organizer directly if you need to have a private conversation.`, "tag":[{"type":"Mention","href":req.body.actor,"name":name}] } - res.send(200); + res.sendStatus(200); sendDirectMessage(jsonObject, req.body.actor, eventID); } ); diff --git a/views/emails/addeventattendee.handlebars b/views/emails/addeventattendee.handlebars new file mode 100644 index 0000000..f49c790 --- /dev/null +++ b/views/emails/addeventattendee.handlebars @@ -0,0 +1,7 @@ +

You just marked yourself as attending an event on {{siteName}}. Thank you! We'll send you another email if there are any updates to the event. Your email will be automatically removed from the database once the event finishes.

+

Follow this link to open the event page any time: https://{{domain}}/{{eventID}}

+

Love,

+

{{siteName}}

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.

diff --git a/views/emails/addeventcomment.handlebars b/views/emails/addeventcomment.handlebars new file mode 100644 index 0000000..8ab7ec1 --- /dev/null +++ b/views/emails/addeventcomment.handlebars @@ -0,0 +1,7 @@ +

{{commentAuthor}} has just posted a comment on an event you're attending on {{siteName}}.

+

Click here to see the comment: https://{{domain}}/{{eventID}}

+

Love,

+

{{siteName}}

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.

diff --git a/views/emails/createevent.handlebars b/views/emails/createevent.handlebars new file mode 100644 index 0000000..030ee58 --- /dev/null +++ b/views/emails/createevent.handlebars @@ -0,0 +1,19 @@ +

Your event has been created!

+

Use this link to share it with people: https://{{domain}}/{{eventID}}

+

Click this button to edit your event. DO NOT SHARE THIS, as anyone with this link can edit your event.

+ + + + + + + +
+ + + + + + +
Edit Your Event
+
diff --git a/views/emails/createeventgroup.handlebars b/views/emails/createeventgroup.handlebars new file mode 100644 index 0000000..3f03345 --- /dev/null +++ b/views/emails/createeventgroup.handlebars @@ -0,0 +1,27 @@ +

You just created a new event group on {{siteName}}! Thanks a bunch - we're delighted to have you.

+

You can edit your event group by clicking the button below, or just following this link: https://{{domain}}/{{eventGroupID}}?e={{editToken}}

+

To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the dark grey box which opens:

+

Event group ID: {{eventGroupID}}

+

Event group secret editing code: {{editToken}}

+ + + + + + +
+ + + + + + +
Edit event group
+
+

To let others know about your event group, send them this link: https://{{domain}}/{{eventGroupID}}

+

And that's it - have a great day!

+

Love,

+

{{siteName}}

+
+

Hold up - I have no idea what this email is about!

+

If you didn't make an event group on {{siteName}}, someone may have accidentally typed your email instead of theirs when they were making an event. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes, and if you're still worried, just click on the edit link above and delete that event group, which removes your email from the system as well.

diff --git a/views/emails/deleteevent.handlebars b/views/emails/deleteevent.handlebars new file mode 100644 index 0000000..5a3670c --- /dev/null +++ b/views/emails/deleteevent.handlebars @@ -0,0 +1,4 @@ +

The {{eventName}} event you're attending on {{siteName}} was just deleted by its creator.

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - that event, and your email, is deleted from the system now.

diff --git a/views/emails/editevent.handlebars b/views/emails/editevent.handlebars new file mode 100644 index 0000000..ddb9885 --- /dev/null +++ b/views/emails/editevent.handlebars @@ -0,0 +1,8 @@ +

An event you're attending on {{siteName}} has just been edited.

+

{{{diffText}}}

+

Click here to see the event: https://{{domain}}/{{eventID}}

+

Love,

+

{{siteName}}

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.

diff --git a/views/emails/removeeventattendee.handlebars b/views/emails/removeeventattendee.handlebars new file mode 100644 index 0000000..66ca858 --- /dev/null +++ b/views/emails/removeeventattendee.handlebars @@ -0,0 +1,4 @@ +

You have been removed from the event {{eventName}} on {{siteName}} by the organizer of the event.

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - you won't receive any more of these emails for this event, and your email has been removed from the database.

diff --git a/views/emails/unattendevent.handlebars b/views/emails/unattendevent.handlebars new file mode 100644 index 0000000..62dac8a --- /dev/null +++ b/views/emails/unattendevent.handlebars @@ -0,0 +1,8 @@ +

You just removed yourself from an event on {{siteName}}. You will no longer receive update emails for this event.

+

If you didn't mean to do this, someone else who knows your email removed you from the event.

+

Follow this link to open the event page any time: https://{{domain}}/{{eventID}}

+

Love,

+

{{siteName}}

+
+

Hold up - I have no idea what this email is about!

+

If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs, then removed it. Don't worry - you won't receive any more emails linked to this event.

diff --git a/views/layouts/email.handlebars b/views/layouts/email.handlebars new file mode 100644 index 0000000..6158ddb --- /dev/null +++ b/views/layouts/email.handlebars @@ -0,0 +1,126 @@ + + + + + +{{ subject }} + + + + + + + + + +
  +
+ + + + + + + + +
+ + + + +
+ {{{ body }}} +
+
+ +
+
 
+ + -- cgit v1.2.3 From ef5aadc56f821c31d324fd3ec29f646a331b4612 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 4 Jan 2020 21:59:57 -0800 Subject: Enhancements to AP and one-click removal This adds AP Delete/Event, AP Delete/Actor, and one-click removal from RSVP lists that can be included in the "you have rsvped" confirmation message. --- models/Event.js | 4 + routes.js | 332 ++++++++++++++++++++++++++++++++---------- views/event.handlebars | 2 +- views/layouts/main.handlebars | 6 +- 4 files changed, 266 insertions(+), 78 deletions(-) (limited to 'views/layouts') diff --git a/models/Event.js b/models/Event.js index 9ab455f..19505ea 100755 --- a/models/Event.js +++ b/models/Event.js @@ -220,6 +220,10 @@ const EventSchema = new mongoose.Schema({ type: String, trim: true }, + activityPubEvent: { + type: String, + trim: true + }, publicKey: { type: String, trim: true diff --git a/routes.js b/routes.js index 34dc1f8..eb17d37 100755 --- a/routes.js +++ b/routes.js @@ -117,12 +117,18 @@ const deleteOldEvents = schedule.scheduleJob('59 23 * * *', function(fireDate){ addToLog("deleteOldEvents", "error", "Image deleted for old event "+event.id); }) } - Event.remove({"_id": event._id}) - .then(response => { - addToLog("deleteOldEvents", "success", "Old event "+event.id+" deleted"); - }).catch((err) => { - addToLog("deleteOldEvents", "error", "Attempt to delete old event "+event.id+" failed with error: " + err); - }); + // broadcast a Delete profile message to all followers so that at least Mastodon servers will delete their local profile information + const guidUpdateObject = crypto.randomBytes(16).toString('hex'); + const jsonUpdateObject = JSON.parse(event.activityPubActor); + // first broadcast AP messages, THEN delete from DB + broadcastDeleteMessage(jsonUpdateObject, event.followers, event.id, function(statuses) { + Event.remove({"_id": event._id}) + .then(response => { + addToLog("deleteOldEvents", "success", "Old event "+event.id+" deleted"); + }).catch((err) => { + addToLog("deleteOldEvents", "error", "Attempt to delete old event "+event.id+" failed with error: " + err); + }); + }); }) }).catch((err) => { addToLog("deleteOldEvents", "error", "Attempt to delete old event "+event.id+" failed with error: " + err); @@ -145,6 +151,33 @@ function createWebfinger(eventID, domain) { }; } +function createActivityPubEvent(name, startUTC, endUTC, timezone) { + const guid = crypto.randomBytes(16).toString('hex'); + let eventObject = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': `https://${domain}/${guid}`, + "name": name, + "type": "Event", + "startTime": moment.tz(startUTC, timezone).format(), + "endTime": moment.tz(endUTC, timezone).format(), + } + return JSON.stringify(eventObject); +} + +function updateActivityPubEvent(oldEvent, name, startUTC, endUTC, timezone) { + // we want to persist the old ID no matter what happens to the Event itself + const id = oldEvent.id; + let eventObject = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': id, + "name": name, + "type": "Event", + "startTime": moment.tz(startUTC, timezone).format(), + "endTime": moment.tz(endUTC, timezone).format(), + } + return JSON.stringify(eventObject); +} + function createActivityPubActor(eventID, domain, pubkey, description, name, location, imageFilename, startUTC, endUTC, timezone) { let actor = { '@context': [ @@ -348,7 +381,6 @@ function broadcastUpdateMessage(apObject, followers, eventID, callback) { if (follower) { const actorJson = JSON.parse(follower.actorJson); const inbox = actorJson.inbox; - console.log('found the inbox for', actorId) const createMessage = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': `https://${domain}/m/${guidUpdate}`, @@ -356,8 +388,6 @@ function broadcastUpdateMessage(apObject, followers, eventID, callback) { 'actor': `https://${domain}/${eventID}`, 'object': apObject }; - console.log('UPDATE') - console.log(JSON.stringify(createMessage)); signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) { if (err) { console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); @@ -377,6 +407,69 @@ function broadcastUpdateMessage(apObject, followers, eventID, callback) { }); } // end followers } + +function broadcastDeleteMessage(apObject, followers, eventID, callback) { + callback = callback || function() {}; + // we need to build an array of promises for each message we're sending, run Promise.all(), and then that will resolve when every message has been sent (or failed) + // per spec, each promise will execute *as it is built*, which is fine, we just need the guarantee that they are all done + let promises = []; + + let guidUpdate = crypto.randomBytes(16).toString('hex'); + console.log('building promises'); + // iterate over followers + for (const follower of followers) { + promises.push(new Promise((resolve, reject) => { + let actorId = follower.actorId; + let myURL = new URL(actorId); + let targetDomain = myURL.hostname; + // get the inbox + Event.findOne({ + id: eventID, + }, function(err, event) { + console.log('found the event for broadcast'); + if (event) { + const follower = event.followers.find(el => el.actorId === actorId); + if (follower) { + const actorJson = JSON.parse(follower.actorJson); + const inbox = actorJson.inbox; + const createMessage = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/m/${guidUpdate}`, + 'type': 'Delete', + 'actor': `https://${domain}/${eventID}`, + 'object': apObject + }; + signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) { + if (err) { + console.log(`Didn't send to ${actorId}, status ${status} with error ${err}`); + reject(`Didn't send to ${actorId}, status ${status} with error ${err}`); + } + else { + console.log('sent to', actorId); + resolve('sent to', actorId); + } + }); + } + else { + console.log(`No follower found with the id ${actorId}`, null, 404); + reject(`No follower found with the id ${actorId}`, null, 404); + } + } + else { + console.log(`No event found with the id ${eventID}`, null, 404); + reject(`No event found with the id ${eventID}`, null, 404); + } + }); + })); + } // end followers + + Promise.all(promises.map(p => p.catch(e => e))).then(statuses => { + console.log('DONE') + console.log(statuses) + callback(statuses); + }); +} + function signAndSend(message, eventID, targetDomain, inbox, callback) { let inboxFragment = inbox.replace('https://'+targetDomain,''); // get the private key @@ -448,6 +541,7 @@ router.get('/', (req, res) => { res.render('home', { domain: domain, email: contactEmail, + siteName: siteName, }); }); @@ -665,6 +759,7 @@ router.get('/:eventID', (req, res) => { eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, metadata: metadata, + siteName: siteName }) } } @@ -890,6 +985,7 @@ router.post('/newevent', async (req, res) => { maxAttendees: req.body.maxAttendees, firstLoad: true, activityPubActor: createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, req.body.startUTC, req.body.endUTC, req.body.timezone), + activityPubEvent: createActivityPubEvent(req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone), publicKey: pair.public, privateKey: pair.private }); @@ -1102,11 +1198,14 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null, eventGroup: isPartOfEventGroup ? eventGroup._id : null, - activityPubActor: updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) + activityPubActor: updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), + activityPubEvent: updateActivityPubEvent(JSON.parse(event.activityPubEvent), req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone), } let diffText = '

This event was just updated with new information.

    '; let displayDate; - // TODO: send an Update Profile message if needed? + if (event.name !== updatedEvent.name) { + diffText += `
  • the event name changed to ${updatedEvent.name}
  • `; + } if (event.location !== updatedEvent.location) { diffText += `
  • the location changed to ${updatedEvent.location}
  • `; } @@ -1150,9 +1249,11 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { } broadcastMessage(jsonObject, event.followers, eventID) // also broadcast an Update profile message to all followers so that at least Mastodon servers will update the local profile information - const guidUpdateObject = crypto.randomBytes(16).toString('hex'); const jsonUpdateObject = JSON.parse(event.activityPubActor); broadcastUpdateMessage(jsonUpdateObject, event.followers, eventID) + // also broadcast an Update/Event for any calendar apps that are consuming our Events + const jsonEventObject = JSON.parse(event.activityPubEvent); + broadcastUpdateMessage(jsonEventObject, event.followers, eventID) // DM to attendees for (const attendee of attendees) { @@ -1304,58 +1405,63 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { eventImage = event.image; } - // Send emails here otherwise they don't exist lol - if (sendEmails) { - Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { - attendeeEmails = ids; - if (!error){ - console.log("Sending emails to: " + attendeeEmails); - req.app.get('hbsInstance').renderView('./views/emails/deleteevent.handlebars', {siteName, domain, eventName: event.name, cache: true, layout: 'email.handlebars'}, function(err, html) { - const msg = { - to: attendeeEmails, - from: { - name: siteName, - email: contactEmail, - }, - subject: `${siteName}: ${event.name} was deleted`, - html, - }; - sgMail.sendMultiple(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); + // broadcast a Delete profile message to all followers so that at least Mastodon servers will delete their local profile information + const guidUpdateObject = crypto.randomBytes(16).toString('hex'); + const jsonUpdateObject = JSON.parse(event.activityPubActor); + // first broadcast AP messages, THEN delete from DB + broadcastDeleteMessage(jsonUpdateObject, event.followers, req.params.eventID, function(statuses) { + Event.deleteOne({id: req.params.eventID}, function(err, raw) { + if (err) { + res.send(err); + addToLog("deleteEvent", "error", "Attempt to delete event " + req.params.eventID + " failed with error: " + err); + } + }) + .then(() => { + // Delete image + if (eventImage){ + fs.unlink(global.appRoot + '/public/events/' + eventImage, (err) => { + if (err) { + res.send(err); + addToLog("deleteEvent", "error", "Attempt to delete event image for event " + req.params.eventID + " failed with error: " + err); + } + // Image removed + addToLog("deleteEvent", "success", "Event " + req.params.eventID + " deleted"); + }) + } + res.writeHead(302, { + 'Location': '/' }); - } - else { - console.log("Nothing to send!"); - } - }); - } - - Event.deleteOne({id: req.params.eventID}, function(err, raw) { - if (err) { - res.send(err); - addToLog("deleteEvent", "error", "Attempt to delete event " + req.params.eventID + " failed with error: " + err); - } - }) - .then(() => { - // Delete image - if (eventImage){ - fs.unlink(global.appRoot + '/public/events/' + eventImage, (err) => { - if (err) { - res.send(err); - addToLog("deleteEvent", "error", "Attempt to delete event image for event " + req.params.eventID + " failed with error: " + err); - } - // Image removed - addToLog("deleteEvent", "success", "Event " + req.params.eventID + " deleted"); - }) - } - res.writeHead(302, { - 'Location': '/' - }); - res.end(); - }) - .catch((err) => { res.send('Sorry! Something went wrong (error deleting): ' + err); addToLog("deleteEvent", "error", "Attempt to delete event " + req.params.eventID + " failed with error: " + err);}); + res.end(); + }) + .catch((err) => { res.send('Sorry! Something went wrong (error deleting): ' + err); addToLog("deleteEvent", "error", "Attempt to delete event " + req.params.eventID + " failed with error: " + err);}); + }); + // send emails here otherwise they don't exist lol + if (sendEmails) { + Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { + attendeeEmails = ids; + if (!error){ + console.log("Sending emails to: " + attendeeEmails); + req.app.get('hbsInstance').renderView('./views/emails/deleteevent.handlebars', {siteName, domain, eventName: event.name, cache: true, layout: 'email.handlebars'}, function(err, html) { + const msg = { + to: attendeeEmails, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: ${event.name} was deleted`, + html, + }; + sgMail.sendMultiple(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); + } + else { + console.log("Nothing to send!"); + } + }); + } } else { // Token doesn't match @@ -1500,6 +1606,44 @@ router.post('/unattendevent/:eventID', (req, res) => { }); }); +router.get('/oneclickunattendevent/:eventID/:attendeeID', (req, res) => { + console.log(req.params.eventID, req.params.attendeeID) + Event.update( + { id: req.params.eventID }, + { $pull: { attendees: { _id: req.params.attendeeID } } } + ) + .then(response => { + console.log(response) + addToLog("oneClickUnattend", "success", "Attendee removed via one click unattend " + req.params.eventID); + if (sendEmails) { + // currently this is never called because we don't have the email address + if (req.body.attendeeEmail){ + req.app.get('hbsInstance').renderView('./views/emails/removeeventattendee.handlebars', {eventName: req.params.eventName, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { const msg = { + to: req.body.attendeeEmail, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: You have been removed from an event`, + html, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + }); + } + } + res.writeHead(302, { + 'Location': '/' + req.params.eventID + }); + res.end(); + }) + .catch((err) => { + res.send('Database error, please try again :('); addToLog("removeEventAttendee", "error", "Attempt to remove attendee by admin from event " + req.params.eventID + " failed with error: " + err); + }); +}); + router.post('/removeattendee/:eventID/:attendeeID', (req, res) => { Event.update( { id: req.params.eventID }, @@ -1554,6 +1698,16 @@ router.post('/post/comment/:eventID', (req, res) => { event.save() .then(() => { addToLog("addEventComment", "success", "Comment added to event " + req.params.eventID); + // broadcast an identical message to all followers, will show in their home timeline + const guidObject = crypto.randomBytes(16).toString('hex'); + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": `https://${domain}/m/${guidObject}`, + "name": `Comment on ${event.name}`, + "type": "Note", + "content": `

    ${req.body.commentAuthor} commented: ${req.body.commentContent}.

    See the full conversation here.

    `, + } + broadcastMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { attendeeEmails = ids; @@ -1607,6 +1761,16 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { event.save() .then(() => { addToLog("addEventReply", "success", "Reply added to comment " + commentID + " in event " + req.params.eventID); + // broadcast an identical message to all followers, will show in their home timeline + const guidObject = crypto.randomBytes(16).toString('hex'); + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": `https://${domain}/m/${guidObject}`, + "name": `Comment on ${event.name}`, + "type": "Note", + "content": `

    ${req.body.replyAuthor} commented: ${req.body.replyContent}

    See the full conversation here.

    `, + } + broadcastMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { attendeeEmails = ids; @@ -1718,16 +1882,11 @@ router.post('/activitypub/inbox', (req, res) => { const signatureBuf = new Buffer(signature_header.signature, 'base64') try { const result = verifier.verify(publicKeyBuf, signatureBuf) + processInbox(req, res); } catch(err) { return res.status(401).send('Signature could not be verified: ' + err); } - if (!result) { - return res.status(401).send('Signature could not be verified.'); - } - else { - processInbox(req, res); - } }); }); @@ -1773,6 +1932,11 @@ function processInbox(req, res) { } else { console.log('sent Accept to', req.body.actor); + // ALSO send an ActivityPub Event activity since this person is "interested" in the event, as indicated by the Follow + const jsonEventObject = JSON.parse(event.activityPubEvent); + // send direct message to user + sendDirectMessage(jsonEventObject, newFollower.actorId, event.id); + // if users can self-RSVP, send a Question to the new follower if (event.usersCanAttend) { const jsonObject = { @@ -1886,9 +2050,20 @@ function processInbox(req, res) { }; event.attendees.push(newAttendee); event.save() - .then(() => { + .then((fullEvent) => { addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); - console.log('added attendee', attendeeName) + // get the new attendee with its hidden id from the full event + let fullAttendee = fullEvent.attendees.find(el => el.id === attributedTo); + // send a "click here to remove yourself" link back to the user as a DM + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "name": `RSVP to ${event.name}`, + "type": "Note", + "content": `@${newAttendee.name} Thanks for RSVPing! You can remove yourself from the RSVP list by clicking here: https://${domain}/oneclickunattendevent/${event.id}/${fullAttendee._id}`, + "tag":[{"type":"Mention","href":newAttendee.id,"name":newAttendee.name}] + } + // send direct message to user + sendDirectMessage(jsonObject, newAttendee.id, event.id); return res.sendStatus(200); }) .catch((err) => { addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); return res.status(500).send('Database error, please try again :('); }); @@ -1933,9 +2108,9 @@ function processInbox(req, res) { } // delete the comment - // find the index of the comment + // find the index of the comment, it should have an activityJson field because from an AP server you can only delete an AP-originated comment (and of course it needs to be yours) let indexOfComment = eventWithComment.comments.findIndex(comment => { - return JSON.parse(comment.activityJson).object.id === req.body.object.id; + return comment.activityJson && JSON.parse(comment.activityJson).object.id === req.body.object.id; }); eventWithComment.comments.splice(indexOfComment, 1); eventWithComment.save() @@ -1949,7 +2124,6 @@ function processInbox(req, res) { } // if we are CC'ed on a public or unlisted Create/Note, then this is a comment to us we should replicate if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) { - console.log('create note!!') // figure out what this is in reply to -- it should be addressed specifically to us let {attributedTo, inReplyTo, to, cc} = req.body.object; // normalize cc into an array @@ -2005,6 +2179,16 @@ function processInbox(req, res) { event.save() .then(() => { addToLog("addEventComment", "success", "Comment added to event " + eventID); + // broadcast an identical message to all followers, will show in their home timeline + const guidObject = crypto.randomBytes(16).toString('hex'); + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": `https://${domain}/m/${guidObject}`, + "name": `Comment on ${event.name}`, + "type": "Note", + "content": newComment.content, + } + broadcastMessage(jsonObject, event.followers, req.params.eventID) console.log('added comment'); res.sendStatus(200); }) diff --git a/views/event.handlebars b/views/event.handlebars index a57f62a..efd700a 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -134,7 +134,7 @@ {{#if eventAttendees}}
      {{#each eventAttendees}} - {{this.name}}{{#if ../editingEnabled}} {{/if}} + {{#if this.email}}{{this.name}}{{else}}{{this.name}}{{/if}}{{#if ../editingEnabled}} {{/if}} {{/each}}
    {{else}} diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index 3cde5d5..d7c3bf2 100755 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -17,17 +17,17 @@ - + - + - {{#if title}}{{title}} · {{/if}}gathio + {{#if title}}{{title}} · {{/if}}{{siteName}} -- cgit v1.2.3