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! --- routes.js | 195 ++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 153 insertions(+), 42 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index ee723e2..c04077b 100755 --- a/routes.js +++ b/routes.js @@ -20,6 +20,11 @@ var moment = require('moment-timezone'); const marked = require('marked'); +const generateRSAKeypair = require('generate-rsa-keypair'); + +const domain = require('./config/domain.js').domain; +const contactEmail = require('./config/domain.js').email; + // Extra marked renderer (used to render plaintext event description for page metadata) // Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ // ? to ? helper @@ -121,10 +126,49 @@ const deleteOldEvents = schedule.scheduleJob('59 23 * * *', function(fireDate){ }); +// ACTIVITYPUB HELPER FUNCTIONS +function createWebfinger(eventID, domain) { + return { + 'subject': `acct:${eventID}@${domain}`, + + 'links': [ + { + 'rel': 'self', + 'type': 'application/activity+json', + 'href': `https://${domain}/${eventID}` + } + ] + }; +} + +function createActivityPubActor(eventID, domain, pubkey) { + return JSON.stringify({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + + 'id': `https://${domain}/u/${eventID}`, + 'type': 'Person', + 'preferredUsereventID': `${eventID}`, + 'inbox': `https://${domain}/api/inbox`, + 'followers': `https://${domain}/u/${eventID}/followers`, + + 'publicKey': { + 'id': `https://${domain}/u/${eventID}#main-key`, + 'owner': `https://${domain}/u/${eventID}`, + 'publicKeyPem': pubkey + } + }); +} + // FRONTEND ROUTES router.get('/', (req, res) => { - res.render('home'); + res.render('home', { + domain: domain, + email: contactEmail, + }); }); router.get('/new', (req, res) => { @@ -174,6 +218,48 @@ router.get('/new/event/public', (req, res) => { }); }) +router.get('/.well-known/webfinger', (req, res) => { + console.log(req.query); + let resource = req.query.resource; + if (!resource || !resource.includes('acct:')) { + return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.'); + } + else { + // "foo@domain" + let activityPubAccount = resource.replace('acct:',''); + // "foo" + let eventID = activityPubAccount.replace(/@.*/,''); + console.log(eventID); + Event.findOne({ + id: eventID + }) + .then((event) => { + if (!event) { + res.status(404); + res.render('404', { url: req.url }); + } + else { + res.json(createWebfinger(eventID, domain)); + } + }) + .catch((err) => { + addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + err); + console.log(err) + res.status(404); + res.render('404', { url: req.url }); + return; + }); + //let db = req.app.get('db'); + //let result = db.prepare('select webfinger from accounts where name = ?').get(name); + //if (result === undefined) { + // return res.status(404).send(`No record found for ${name}.`); + //} + //else { + // res.json(JSON.parse(result.webfinger)); + //} + } +}); + router.get('/:eventID', (req, res) => { Event.findOne({ id: req.params.eventID @@ -256,34 +342,51 @@ router.get('/:eventID', (req, res) => { let metadata = { title: event.name, description: marked(event.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), - image: (eventHasCoverImage ? 'https://gath.io/events/' + event.image : null), - url: 'https://gath.io/' + req.params.eventID + image: (eventHasCoverImage ? `https://${domain}/events/` + event.image : null), + url: `https://${domain}/` + req.params.eventID }; - res.set("X-Robots-Tag", "noindex"); - res.render('event', { - title: event.name, - escapedName: escapedName, - eventData: event, - eventAttendees: eventAttendees, - spotsRemaining: spotsRemaining, - noMoreSpots: noMoreSpots, - eventStartISO: eventStartISO, - eventEndISO: eventEndISO, - parsedLocation: parsedLocation, - parsedStart: parsedStart, - parsedEnd: parsedEnd, - displayDate: displayDate, - fromNow: fromNow, - timezone: event.timezone, - parsedDescription: parsedDescription, - editingEnabled: editingEnabled, - eventHasCoverImage: eventHasCoverImage, - eventHasHost: eventHasHost, - firstLoad: firstLoad, - eventHasConcluded: eventHasConcluded, - eventHasBegun: eventHasBegun, - metadata: metadata, - }) + ///////////////////// + if (req.headers.accept && (req.headers.accept.includes('application/activity+json') || req.headers.accept.includes('application/json') || req.headers.accept.includes('application/json+ld'))) { + res.json(JSON.parse(event.activityPubActor)); + + //let tempActor = JSON.parse(result.actor); + //// Added this followers URI for Pleroma compatibility, see https://github.com/dariusk/rss-to-activitypub/issues/11#issuecomment-471390881 + //// New Actors should have this followers URI but in case of migration from an old version this will add it in on the fly + //if (tempActor.followers === undefined) { + // tempActor.followers = `https://${domain}/u/${username}/followers`; + //} + //res.json(tempActor); + } + ///////////////// + else { + res.set("X-Robots-Tag", "noindex"); + res.render('event', { + domain: domain, + email: contactEmail, + title: event.name, + escapedName: escapedName, + eventData: event, + eventAttendees: eventAttendees, + spotsRemaining: spotsRemaining, + noMoreSpots: noMoreSpots, + eventStartISO: eventStartISO, + eventEndISO: eventEndISO, + parsedLocation: parsedLocation, + parsedStart: parsedStart, + parsedEnd: parsedEnd, + displayDate: displayDate, + fromNow: fromNow, + timezone: event.timezone, + parsedDescription: parsedDescription, + editingEnabled: editingEnabled, + eventHasCoverImage: eventHasCoverImage, + eventHasHost: eventHasHost, + firstLoad: firstLoad, + eventHasConcluded: eventHasConcluded, + eventHasBegun: eventHasBegun, + metadata: metadata, + }) + } } else { res.status(404); @@ -375,11 +478,12 @@ router.get('/group/:eventGroupID', (req, res) => { let metadata = { title: eventGroup.name, description: marked(eventGroup.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), - image: (eventGroupHasCoverImage ? 'https://gath.io/events/' + eventGroup.image : null), - url: 'https://gath.io/' + req.params.eventID + image: (eventGroupHasCoverImage ? `https://${domain}/events/` + eventGroup.image : null), + url: `https://${domain}/` + req.params.eventID }; res.set("X-Robots-Tag", "noindex"); res.render('eventgroup', { + domain: domain, title: eventGroup.name, eventGroupData: eventGroup, escapedName: escapedName, @@ -445,6 +549,10 @@ router.post('/newevent', async (req, res) => { isPartOfEventGroup = true; } } + + // generate RSA keypair for ActivityPub + let pair = generateRSAKeypair(); + const event = new Event({ id: eventID, type: req.body.eventType, @@ -466,7 +574,10 @@ router.post('/newevent', async (req, res) => { showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, - firstLoad: true + firstLoad: true, + activityPubActor: createActivityPubActor(eventID, domain, pair.public), + publicKey: pair.public, + privateKey: pair.private }); event.save() .then((event) => { @@ -477,7 +588,7 @@ router.post('/newevent', async (req, res) => { to: req.body.creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-00330b8278ab463e9f88c16566487d97', dynamic_template_data: { @@ -547,7 +658,7 @@ router.post('/importevent', (req, res) => { to: creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-00330b8278ab463e9f88c16566487d97', dynamic_template_data: { @@ -609,7 +720,7 @@ router.post('/neweventgroup', (req, res) => { to: req.body.creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-4c5ddcb34ac44ec5b2313c6da4e405f3', dynamic_template_data: { @@ -701,7 +812,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', dynamic_template_data: { @@ -781,7 +892,7 @@ router.post('/editeventgroup/:eventGroupID/:editToken', (req, res) => { // to: attendeeEmails, // from: { // name: 'Gathio', - // email: 'notifications@gath.io', + // email: contactEmail, // }, // templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', // dynamic_template_data: { @@ -837,7 +948,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', dynamic_template_data: { @@ -964,7 +1075,7 @@ router.post('/attendevent/:eventID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-977612474bba49c48b58e269f04f927c', dynamic_template_data: { @@ -999,7 +1110,7 @@ router.post('/unattendevent/:eventID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-56c97755d6394c23be212fef934b0f1f', dynamic_template_data: { @@ -1034,7 +1145,7 @@ router.post('/removeattendee/:eventID/:attendeeID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-f8ee9e1e2c8a48e3a329d1630d0d371f', dynamic_template_data: { @@ -1080,7 +1191,7 @@ router.post('/post/comment/:eventID', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-756d078561e047aba307155f02b6686d', dynamic_template_data: { @@ -1131,7 +1242,7 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-756d078561e047aba307155f02b6686d', dynamic_template_data: { -- cgit v1.2.3 From 51b42d13a370a9a79a618742c62de42c6cb666d8 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Wed, 11 Dec 2019 19:03:42 -0800 Subject: Fixing federation bug --- routes.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index c04077b..e442e1a 100755 --- a/routes.js +++ b/routes.js @@ -148,15 +148,15 @@ function createActivityPubActor(eventID, domain, pubkey) { 'https://w3id.org/security/v1' ], - 'id': `https://${domain}/u/${eventID}`, + 'id': `https://${domain}/${eventID}`, 'type': 'Person', - 'preferredUsereventID': `${eventID}`, + 'preferredUsername': `${eventID}`, 'inbox': `https://${domain}/api/inbox`, - 'followers': `https://${domain}/u/${eventID}/followers`, + 'followers': `https://${domain}/${eventID}/followers`, 'publicKey': { - 'id': `https://${domain}/u/${eventID}#main-key`, - 'owner': `https://${domain}/u/${eventID}`, + 'id': `https://${domain}/${eventID}#main-key`, + 'owner': `https://${domain}/${eventID}`, 'publicKeyPem': pubkey } }); -- cgit v1.2.3 From 821017e5337612a37179b586d5506666ab70ab77 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Wed, 11 Dec 2019 22:13:39 -0800 Subject: follow Undo now works --- routes.js | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 11 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index e442e1a..0371d96 100755 --- a/routes.js +++ b/routes.js @@ -21,6 +21,8 @@ var moment = require('moment-timezone'); const marked = require('marked'); const generateRSAKeypair = require('generate-rsa-keypair'); +const crypto = require('crypto'); +const request = require('request'); const domain = require('./config/domain.js').domain; const contactEmail = require('./config/domain.js').email; @@ -151,7 +153,7 @@ function createActivityPubActor(eventID, domain, pubkey) { 'id': `https://${domain}/${eventID}`, 'type': 'Person', 'preferredUsername': `${eventID}`, - 'inbox': `https://${domain}/api/inbox`, + 'inbox': `https://${domain}/activitypub/inbox`, 'followers': `https://${domain}/${eventID}/followers`, 'publicKey': { @@ -162,6 +164,63 @@ function createActivityPubActor(eventID, domain, pubkey) { }); } +function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) { + const guid = crypto.randomBytes(16).toString('hex'); + let message = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/${guid}`, + 'type': 'Accept', + 'actor': `https://${domain}/${eventID}`, + 'object': thebody, + }; + signAndSend(message, eventID, domain, req, res, targetDomain); +} + +function signAndSend(message, eventID, domain, req, res, targetDomain) { + // get the URI of the actor object and append 'inbox' to it + let inbox = message.object.actor+'/inbox'; + let inboxFragment = inbox.replace('https://'+targetDomain,''); + // get the private key + Event.findOne({ + id: eventID + }) + .then((event) => { + if (event) { + const privateKey = event.privateKey; + const signer = crypto.createSign('sha256'); + let d = new Date(); + let stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`; + signer.update(stringToSign); + signer.end(); + const signature = signer.sign(privateKey); + const signature_b64 = signature.toString('base64'); + const header = `keyId="https://${domain}/${eventID}",headers="(request-target) host date",signature="${signature_b64}"`; + request({ + url: inbox, + headers: { + 'Host': targetDomain, + 'Date': d.toUTCString(), + 'Signature': header + }, + method: 'POST', + json: true, + body: message + }, function (error, response){ + if (error) { + console.log('Error:', error, response.body); + } + else { + console.log('Response:', response.body); + } + }); + return res.status(200); + } + else { + return res.status(404).send(`No record found for ${eventID}.`); + } + }); +} + // FRONTEND ROUTES router.get('/', (req, res) => { @@ -345,19 +404,9 @@ router.get('/:eventID', (req, res) => { image: (eventHasCoverImage ? `https://${domain}/events/` + event.image : null), url: `https://${domain}/` + req.params.eventID }; - ///////////////////// if (req.headers.accept && (req.headers.accept.includes('application/activity+json') || req.headers.accept.includes('application/json') || req.headers.accept.includes('application/json+ld'))) { res.json(JSON.parse(event.activityPubActor)); - - //let tempActor = JSON.parse(result.actor); - //// Added this followers URI for Pleroma compatibility, see https://github.com/dariusk/rss-to-activitypub/issues/11#issuecomment-471390881 - //// New Actors should have this followers URI but in case of migration from an old version this will add it in on the fly - //if (tempActor.followers === undefined) { - // tempActor.followers = `https://${domain}/u/${username}/followers`; - //} - //res.json(tempActor); } - ///////////////// else { res.set("X-Robots-Tag", "noindex"); res.render('event', { @@ -403,6 +452,37 @@ router.get('/:eventID', (req, res) => { }); }) +router.get('/:eventID/followers', (req, res) => { + const eventID = req.params.eventID; + Event.findOne({ + id: eventID + }) + .then((event) => { + if (event) { + console.log(event.followers); + const followers = event.followers.map(el => el.account); + console.log(followers) + let followersCollection = { + "type": "OrderedCollection", + "totalItems": followers.length, + "id": `https://${domain}/${eventID}/followers`, + "first": { + "type": "OrderedCollectionPage", + "totalItems": followers.length, + "partOf": `https://${domain}/${eventID}/followers`, + "orderedItems": followers, + "id": `https://${domain}/${eventID}/followers?page=1` + }, + "@context":["https://www.w3.org/ns/activitystreams"] + }; + return res.json(followersCollection); + } + else { + return res.status(400).send('Bad request.'); + } + }) +}) + router.get('/group/:eventGroupID', (req, res) => { EventGroup.findOne({ id: req.params.eventGroupID @@ -1295,6 +1375,76 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => { .catch((err) => { res.send('Sorry! Something went wrong: ' + err); addToLog("deleteComment", "error", "Attempt to delete comment " + req.params.commentID + "from event " + req.params.eventID + " failed with error: " + err);}); }); +router.post('/activitypub/inbox', (req, res) => { + console.log('got a inbox message') + const myURL = new URL(req.body.actor); + let targetDomain = myURL.hostname; + // if a Follow activity hits the inbox + if (typeof req.body.object === 'string' && req.body.type === 'Follow') { + console.log('follow!') + let eventID = req.body.object.replace(`https://${domain}/`,''); + sendAcceptMessage(req.body, eventID, domain, req, res, targetDomain); + // Add the user to the DB of accounts that follow the account + console.log(req.body) + + const newFollower = { + account: req.body.actor, + followId: req.body.id + }; + + Event.findOne({ + id: eventID, + }, function(err,event) { + console.log(event.followers) + // if this account is NOT already in our followers list, add it + if (!event.followers.map(el => el.account).includes(req.body.actor)) { + event.followers.push(newFollower); + console.log(event.followers) + event.save() + .then(() => { + addToLog("addEventFollower", "success", "Follower added to event " + eventID); + console.log('successful follower add') + }) + .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err); + console.log('error', err) + }); + } + }); + } + // if an Undo activity with a Follow object hits the inbox + if (req.body && req.body.type === 'Undo' && req.body.object && req.body.object.type === 'Follow') { + console.log('undo follow!') + console.log(req.body) + // get the record of all followers for this account + let eventID = req.body.object.object.replace(`https://${domain}/`,''); + Event.findOne({ + id: eventID, + }, function(err,event) { + // check to see if the Follow object's id matches the id we have on record + console.log(event.followers) + // is this even someone who follows us + const indexOfFollower = event.followers.findIndex(el => {console.log(el.account, req.body.object.actor); return el.account === req.body.object.actor;}); + console.log(indexOfFollower) + if (indexOfFollower !== -1) { + // does the id we have match the id we are being given + if (event.followers[indexOfFollower].followId === req.body.object.id) { + // we have a match and can trust the Undo! remove this person from the followers list + event.followers.splice(indexOfFollower, 1); + console.log('new', indexOfFollower, event.followers); + event.save() + .then(() => { + addToLog("removeEventFollower", "success", "Follower removed from event " + eventID); + console.log('successful follower removal') + }) + .catch((err) => { res.send('Database error, please try again :('); addToLog("removeEventFollower", "error", "Attempt to remove follower from event " + eventID + " failed with error: " + err); + console.log('error', err) + }); + } + } + }); + } +}); + router.use(function(req, res, next){ res.status(404); res.render('404', { url: req.url }); -- cgit v1.2.3 From b8d8d5fcd29f3c5492491e3482319e0efc838030 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Wed, 11 Dec 2019 23:24:49 -0800 Subject: sending pollls --- routes.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 5 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 0371d96..836a55c 100755 --- a/routes.js +++ b/routes.js @@ -173,12 +173,41 @@ function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) { 'actor': `https://${domain}/${eventID}`, 'object': thebody, }; - signAndSend(message, eventID, domain, req, res, targetDomain); -} - -function signAndSend(message, eventID, domain, req, res, targetDomain) { // get the URI of the actor object and append 'inbox' to it let inbox = message.object.actor+'/inbox'; + signAndSend(message, eventID, domain, req, res, targetDomain, inbox); +} + +function rawMessage(json, eventID, domain, follower) { + const guidCreate = crypto.randomBytes(16).toString('hex'); + const guidNote = crypto.randomBytes(16).toString('hex'); + // let db = req.app.get('db'); + let d = new Date(); + + let rawMessagePayload = json; + + rawMessagePayload.published = d.toISOString(); + rawMessagePayload.attributedTo = `https://${domain}/${eventID}`; + rawMessagePayload.to = [follower]; + rawMessagePayload.id = `https://${domain}/m/${guidNote}`; + rawMessagePayload.content = unescape(rawMessagePayload.content) + + let createMessage = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/m/${guidCreate}`, + 'type': 'Create', + 'actor': `https://${domain}/${eventID}`, + 'to': [follower], + 'object': rawMessagePayload + }; + + //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidCreate, JSON.stringify(createMessage)); + //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidNote, JSON.stringify(rawMessagePayload)); + + return createMessage; +} + +function signAndSend(message, eventID, domain, req, res, targetDomain, inbox) { let inboxFragment = inbox.replace('https://'+targetDomain,''); // get the private key Event.findOne({ @@ -1377,12 +1406,14 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => { router.post('/activitypub/inbox', (req, res) => { console.log('got a inbox message') + console.log(req.body); const myURL = new URL(req.body.actor); let targetDomain = myURL.hostname; // if a Follow activity hits the inbox if (typeof req.body.object === 'string' && req.body.type === 'Follow') { console.log('follow!') let eventID = req.body.object.replace(`https://${domain}/`,''); + // Accept the follow request sendAcceptMessage(req.body, eventID, domain, req, res, targetDomain); // Add the user to the DB of accounts that follow the account console.log(req.body) @@ -1403,7 +1434,28 @@ router.post('/activitypub/inbox', (req, res) => { event.save() .then(() => { addToLog("addEventFollower", "success", "Follower added to event " + eventID); - console.log('successful follower add') + console.log('successful follower add'); + // send a Question to the new follower + let inbox = req.body.actor+'/inbox'; + let myURL = new URL(req.body.actor); + let targetDomain = myURL.hostname; + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "http://polls.example.org/question/1", + "name": `RSVP to ${event.name}`, + "type": "Question", + "content": `Will you attend ${event.name}?`, + "oneOf": [ + {"type":"Note","name": "Yes"}, + {"type":"Note","name": "No"}, + {"type":"Note","name": "Maybe"} + ], + "endTime":event.start.toISOString() + } + let message = rawMessage(jsonObject, eventID, domain, req.body.actor); + console.log('!!!!!!!!! sending') + console.log(message) + signAndSend(message, eventID, domain, req, res, targetDomain, inbox); }) .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err); console.log('error', err) -- cgit v1.2.3 From f1e62ef6fa94c3cfb6afadd0dc865f5c502a6a60 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sun, 15 Dec 2019 13:07:50 -0800 Subject: Big refactor and new features --- routes.js | 635 ++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 537 insertions(+), 98 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 836a55c..6d09ab7 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; +var sanitizeHtml = require('sanitize-html'); // Extra marked renderer (used to render plaintext event description for page metadata) // Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ @@ -143,8 +144,8 @@ function createWebfinger(eventID, domain) { }; } -function createActivityPubActor(eventID, domain, pubkey) { - return JSON.stringify({ +function createActivityPubActor(eventID, domain, pubkey, description, name, location, imageFilename) { + let actor = { '@context': [ 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1' @@ -155,17 +156,32 @@ function createActivityPubActor(eventID, domain, pubkey) { 'preferredUsername': `${eventID}`, 'inbox': `https://${domain}/activitypub/inbox`, 'followers': `https://${domain}/${eventID}/followers`, + 'summary': description, + 'name': name, 'publicKey': { 'id': `https://${domain}/${eventID}#main-key`, 'owner': `https://${domain}/${eventID}`, 'publicKeyPem': pubkey } - }); + }; + if (location) { + actor.summary += ` Location: ${location}.` + } + if (imageFilename) { + actor.icon = { + 'type': 'Image', + 'mediaType': 'image/jpg', + 'url': `https://${domain}/events/${imageFilename}`, + }; + } + return JSON.stringify(actor); } -function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) { +function sendAcceptMessage(thebody, eventID, targetDomain, callback) { + callback = callback || function() {}; const guid = crypto.randomBytes(16).toString('hex'); + const actorId = thebody.actor; let message = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': `https://${domain}/${guid}`, @@ -173,41 +189,119 @@ function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) { 'actor': `https://${domain}/${eventID}`, 'object': thebody, }; - // get the URI of the actor object and append 'inbox' to it - let inbox = message.object.actor+'/inbox'; - signAndSend(message, eventID, domain, req, res, targetDomain, inbox); + // get the inbox + Event.findOne({ + id: eventID, + }, function(err, event) { + if (event) { + const follower = event.followers.find(el => el.actorId === actorId); + if (follower) { + const actorJson = JSON.parse(follower.actorJson); + const inbox = actorJson.inbox; + signAndSend(message, eventID, targetDomain, inbox, callback); + } + } + else { + callback(`Could not find event ${eventID}`, null, 404); + } + }); } -function rawMessage(json, eventID, domain, follower) { +// this sends a message "to:" an individual fediverse user +function sendDirectMessage(apObject, actorId, eventID, callback) { + callback = callback || function() {}; const guidCreate = crypto.randomBytes(16).toString('hex'); - const guidNote = crypto.randomBytes(16).toString('hex'); - // let db = req.app.get('db'); + const guidObject = crypto.randomBytes(16).toString('hex'); let d = new Date(); - let rawMessagePayload = json; - - rawMessagePayload.published = d.toISOString(); - rawMessagePayload.attributedTo = `https://${domain}/${eventID}`; - rawMessagePayload.to = [follower]; - rawMessagePayload.id = `https://${domain}/m/${guidNote}`; - rawMessagePayload.content = unescape(rawMessagePayload.content) + apObject.published = d.toISOString(); + apObject.attributedTo = `https://${domain}/${eventID}`; + apObject.to = actorId; + apObject.id = `https://${domain}/m/${guidObject}`; + apObject.content = unescape(apObject.content) let createMessage = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': `https://${domain}/m/${guidCreate}`, 'type': 'Create', 'actor': `https://${domain}/${eventID}`, - 'to': [follower], - 'object': rawMessagePayload + 'to': [actorId], + 'object': apObject }; - //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidCreate, JSON.stringify(createMessage)); - //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidNote, JSON.stringify(rawMessagePayload)); + let myURL = new URL(actorId); + let targetDomain = myURL.hostname; + // get the inbox + Event.findOne({ + id: eventID, + }, function(err, event) { + if (event) { + const follower = event.followers.find(el => el.actorId === actorId); + if (follower) { + const actorJson = JSON.parse(follower.actorJson); + const inbox = actorJson.inbox; + signAndSend(createMessage, eventID, targetDomain, inbox, callback); + } + else { + callback(`No follower found with the id ${actorId}`, null, 404); + } + } + else { + callback(`No event found with the id ${eventID}`, null, 404); + } + }); +} - return createMessage; +// this function sends something to the timeline of every follower in the followers array +function broadcastMessage(apObject, followers, eventID, callback) { + callback = callback || function() {}; + let guidCreate = crypto.randomBytes(16).toString('hex'); + console.log('broadcasting'); + // iterate over followers + for (const follower of followers) { + 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; + console.log('found the inbox for', actorId) + const createMessage = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/m/${guidCreate}`, + 'type': 'Create', + 'actor': `https://${domain}/${eventID}`, + 'to': [actorId], + 'object': apObject + }; + signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) { + if (err) { + console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); + } + else { + console.log('sent to', actorId); + } + }); + } + else { + callback(`No follower found with the id ${actorId}`, null, 404); + } + } + else { + callback(`No event found with the id ${eventID}`, null, 404); + } + }); + } // end followers } -function signAndSend(message, eventID, domain, req, res, targetDomain, inbox) { +function signAndSend(message, eventID, targetDomain, inbox, callback) { let inboxFragment = inbox.replace('https://'+targetDomain,''); // get the private key Event.findOne({ @@ -237,15 +331,37 @@ function signAndSend(message, eventID, domain, req, res, targetDomain, inbox) { }, function (error, response){ if (error) { console.log('Error:', error, response.body); + callback(error, null, 500); } else { - console.log('Response:', response.body); + console.log('Response:', response.statusCode); + // Add the message to the database + const messageID = message.id; + const newMessage = { + id: message.id, + content: JSON.stringify(message) + }; + Event.findOne({ + id: eventID, + }, function(err,event) { + if (!event) return; + event.activityPubMessages.push(newMessage); + event.save() + .then(() => { + addToLog("addActivityPubMessage", "success", "ActivityPubMessage added to event " + eventID); + console.log('successful ActivityPubMessage add'); + callback(null, message.id, 200); + }) + .catch((err) => { addToLog("addActivityPubMessage", "error", "Attempt to add ActivityPubMessage to event " + eventID + " failed with error: " + err); + console.log('error', err) + callback(err, null, 500); + }); + }) } }); - return res.status(200); } else { - return res.status(404).send(`No record found for ${eventID}.`); + callback(`No record found for ${eventID}.`, null, 404); } }); } @@ -411,7 +527,7 @@ router.get('/:eventID', (req, res) => { console.log("No edit token set"); } else { - if (req.query.e == eventEditToken){ + if (req.query.e === eventEditToken){ editingEnabled = true; } else { @@ -489,7 +605,7 @@ router.get('/:eventID/followers', (req, res) => { .then((event) => { if (event) { console.log(event.followers); - const followers = event.followers.map(el => el.account); + const followers = event.followers.map(el => el.actorId); console.log(followers) let followersCollection = { "type": "OrderedCollection", @@ -556,7 +672,7 @@ router.get('/group/:eventGroupID', (req, res) => { }) let upcomingEventsExist = false; - if (events.some(e => e.eventHasConcluded == false)) { + if (events.some(e => e.eventHasConcluded === false)) { upcomingEventsExist = true; } @@ -576,7 +692,7 @@ router.get('/group/:eventGroupID', (req, res) => { console.log("No edit token set"); } else { - if (req.query.e == eventGroupEditToken){ + if (req.query.e === eventGroupEditToken){ editingEnabled = true; } else { @@ -632,10 +748,13 @@ router.get('/group/:eventGroupID', (req, res) => { router.post('/newevent', async (req, res) => { let eventID = shortid.generate(); + // this is a hack, activitypub does not like "-" in ids so we are essentially going + // to have a 63-character alphabet instead of a 64-character one + eventID = eventID.replace(/-/g,'_'); let editToken = randomstring.generate(); let eventImageFilename = ""; let isPartOfEventGroup = false; - if (req.files && Object.keys(req.files).length != 0) { + if (req.files && Object.keys(req.files).length !== 0) { let eventImageBuffer = req.files.imageUpload.data; Jimp.read(eventImageBuffer, (err, img) => { if (err) addToLog("Jimp", "error", "Attempt to edit image failed with error: " + err); @@ -684,7 +803,7 @@ router.post('/newevent', async (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, firstLoad: true, - activityPubActor: createActivityPubActor(eventID, domain, pair.public), + activityPubActor: createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename), publicKey: pair.public, privateKey: pair.private }); @@ -722,7 +841,7 @@ router.post('/newevent', async (req, res) => { router.post('/importevent', (req, res) => { let eventID = shortid.generate(); let editToken = randomstring.generate(); - if (req.files && Object.keys(req.files).length != 0) { + if (req.files && Object.keys(req.files).length !== 0) { importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); for (var key in importediCalObject) { importedEventData = importediCalObject[key]; @@ -744,7 +863,7 @@ router.post('/importevent', (req, res) => { location: importedEventData.location, start: importedEventData.start, end: importedEventData.end, - timezone: typeof importedEventData.start.tz != 'undefined' ? importedEventData.start.tz : "Etc/UTC", + timezone: typeof importedEventData.start.tz !== 'undefined' ? importedEventData.start.tz : "Etc/UTC", description: importedEventData.description, image: '', creatorEmail: creatorEmail, @@ -798,7 +917,7 @@ router.post('/neweventgroup', (req, res) => { let eventGroupID = shortid.generate(); let editToken = randomstring.generate(); let eventGroupImageFilename = ""; - if (req.files && Object.keys(req.files).length != 0) { + if (req.files && Object.keys(req.files).length !== 0) { let eventImageBuffer = req.files.imageUpload.data; Jimp.read(eventImageBuffer, (err, img) => { if (err) addToLog("Jimp", "error", "Attempt to edit image failed with error: " + err); @@ -864,7 +983,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { // If there is a new image, upload that first let eventID = req.params.eventID; let eventImageFilename = event.image; - if (req.files && Object.keys(req.files).length != 0) { + if (req.files && Object.keys(req.files).length !== 0) { let eventImageBuffer = req.files.imageUpload.data; Jimp.read(eventImageBuffer, (err, img) => { if (err) throw err; @@ -887,7 +1006,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { if (eventGroup) { isPartOfEventGroup = true; } - } + } const updatedEvent = { name: req.body.eventName, location: req.body.eventLocation, @@ -904,6 +1023,28 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null, eventGroup: isPartOfEventGroup ? eventGroup._id : null } + let diffText = '

This event was just updated with new information.

    '; + let displayDate; + // TODO: send an Update Profile message if needed? + if (event.location !== updatedEvent.location) { + diffText += `
  • the location changed to ${updatedEvent.location}
  • `; + } + if (event.start.toISOString() !== updatedEvent.start.toISOString()) { + displayDate = moment.tz(updatedEvent.start, updatedEvent.timezone).format('dddd D MMMM YYYY h:mm a'); + diffText += `
  • the start time changed to ${displayDate}
  • `; + } + if (event.end.toISOString() !== updatedEvent.end.toISOString()) { + displayDate = moment.tz(updatedEvent.end, updatedEvent.timezone).format('dddd D MMMM YYYY h:mm a'); + diffText += `
  • the end time changed to ${displayDate}
  • `; + } + if (event.timezone !== updatedEvent.timezone) { + console.log(typeof event.timezone, JSON.stringify(event.timezone), JSON.stringify(updatedEvent.timezone)) + diffText += `
  • the time zone changed to ${updatedEvent.timezone}
  • `; + } + if (event.description !== updatedEvent.description) { + diffText += `
  • the event description changed
  • `; + } + diffText += `
`; Event.findOneAndUpdate({id: req.params.eventID}, updatedEvent, function(err, raw) { if (err) { addToLog("editEvent", "error", "Attempt to edit event " + req.params.eventID + " failed with error: " + err); @@ -912,10 +1053,39 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { }) .then(() => { addToLog("editEvent", "success", "Event " + req.params.eventID + " edited"); + // send update to ActivityPub subscribers + Event.findOne({id: req.params.eventID}, function(err,event) { + if (!event) return; + let attendees = event.attendees.filter(el => el.id); + if (!err) { + // broadcast an identical message to all followers, will show in home timeline + const guidObject = crypto.randomBytes(16).toString('hex'); + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": `https://${domain}/m/${guidObject}`, + "name": `RSVP to ${event.name}`, + "type": "Note", + "content": `${diffText} See here: https://${domain}/${req.params.eventID}`, + } + broadcastMessage(jsonObject, event.followers, eventID) + // DM to attendees + for (const attendee of attendees) { + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "name": `RSVP to ${event.name}`, + "type": "Note", + "content": `@${attendee.name} ${diffText} See here: https://${domain}/${req.params.eventID}`, + "tag":[{"type":"Mention","href":attendee.id,"name":attendee.name}] + } + // send direct message to user + sendDirectMessage(jsonObject, attendee.id, eventID); + } + } + }) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { attendeeEmails = ids; - if (!error && attendeeEmails != ""){ + if (!error && attendeeEmails !== ""){ console.log("Sending emails to: " + attendeeEmails); const msg = { to: attendeeEmails, @@ -966,7 +1136,7 @@ router.post('/editeventgroup/:eventGroupID/:editToken', (req, res) => { // If there is a new image, upload that first let eventGroupID = req.params.eventGroupID; let eventGroupImageFilename = eventGroup.image; - if (req.files && Object.keys(req.files).length != 0) { + if (req.files && Object.keys(req.files).length !== 0) { let eventImageBuffer = req.files.eventGroupImageUpload.data; Jimp.read(eventImageBuffer, (err, img) => { if (err) throw err; @@ -1174,6 +1344,7 @@ router.post('/attendevent/:eventID', (req, res) => { Event.findOne({ id: req.params.eventID, }, function(err,event) { + if (!event) return; event.attendees.push(newAttendee); event.save() .then(() => { @@ -1287,6 +1458,7 @@ router.post('/post/comment/:eventID', (req, res) => { Event.findOne({ id: req.params.eventID, }, function(err,event) { + if (!event) return; event.comments.push(newComment); event.save() .then(() => { @@ -1337,6 +1509,7 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { Event.findOne({ id: req.params.eventID, }, function(err,event) { + if (!event) return; var parentComment = event.comments.id(commentID); parentComment.replies.push(newReply); event.save() @@ -1405,86 +1578,149 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => { }); router.post('/activitypub/inbox', (req, res) => { - console.log('got a inbox message') - console.log(req.body); - const myURL = new URL(req.body.actor); - let targetDomain = myURL.hostname; + console.log('got an inbox message of type', req.body.type, req.body) + + // validate the incoming message + const signature = req.get('Signature'); + let signature_header = signature.split(',').map(pair => { + return pair.split('=').map(value => { + return value.replace(/^"/g, '').replace(/"$/g, '') + }); + }).reduce((acc, el) => { + acc[el[0]] = el[1]; + return acc; + }, {}); + + // get the actor + 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 comparison_string = signature_header.headers.split(' ').map(header => { + if (header === '(request-target)') { + return '(request-target): post /activitypub/inbox'; + } + else { + return `${header}: ${req.get(header)}` + } + }).join('\n'); + + const verifier = crypto.createVerify('RSA-SHA256') + 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) + if (!result) { + res.status(401).send('Signature could not be verified.'); + } + else { + processInbox(req, res); + } + }); +}); + + +function processInbox(req, res) { + if (req.body.object) console.log('containing object of type', req.body.object.type) // if a Follow activity hits the inbox if (typeof req.body.object === 'string' && req.body.type === 'Follow') { - console.log('follow!') + const myURL = new URL(req.body.actor); + let targetDomain = myURL.hostname; let eventID = req.body.object.replace(`https://${domain}/`,''); - // Accept the follow request - sendAcceptMessage(req.body, eventID, domain, req, res, targetDomain); // Add the user to the DB of accounts that follow the account - console.log(req.body) - - const newFollower = { - account: req.body.actor, - followId: req.body.id - }; - - Event.findOne({ - id: eventID, - }, function(err,event) { - console.log(event.followers) - // if this account is NOT already in our followers list, add it - if (!event.followers.map(el => el.account).includes(req.body.actor)) { - event.followers.push(newFollower); - console.log(event.followers) - event.save() - .then(() => { - addToLog("addEventFollower", "success", "Follower added to event " + eventID); - console.log('successful follower add'); - // send a Question to the new follower - let inbox = req.body.actor+'/inbox'; - let myURL = new URL(req.body.actor); - let targetDomain = myURL.hostname; - const jsonObject = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://polls.example.org/question/1", - "name": `RSVP to ${event.name}`, - "type": "Question", - "content": `Will you attend ${event.name}?`, - "oneOf": [ - {"type":"Note","name": "Yes"}, - {"type":"Note","name": "No"}, - {"type":"Note","name": "Maybe"} - ], - "endTime":event.start.toISOString() + // get the follower's username + request({ + url: req.body.actor, + headers: { + 'Accept': 'application/activity+json', + 'Content-Type': 'application/activity+json' + }}, function (error, response, body) { + body = JSON.parse(body) + const name = body.preferredUsername || body.name || attributedTo; + const newFollower = { + actorId: req.body.actor, + followId: req.body.id, + name: name, + actorJson: JSON.stringify(body) + }; + Event.findOne({ + id: eventID, + }, function(err,event) { + // if this account is NOT already in our followers list, add it + if (event && !event.followers.map(el => el.actorId).includes(req.body.actor)) { + console.log('made it!') + event.followers.push(newFollower); + event.save() + .then(() => { + addToLog("addEventFollower", "success", "Follower added to event " + eventID); + console.log('successful follower add'); + // Accept the follow request + sendAcceptMessage(req.body, eventID, targetDomain, function(err, resp, status) { + if (err) { + console.log(`Didn't send Accept to ${req.body.actor}, status ${status} with error ${err}`); + } + else { + console.log('sent Accept to', req.body.actor); + // if users can self-RSVP, send a Question to the new follower + if (event.usersCanAttend) { + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "name": `RSVP to ${event.name}`, + "type": "Question", + "content": `@${name} Will you attend ${event.name}?`, + "oneOf": [ + {"type":"Note","name": "Yes"}, + {"type":"Note","name": "No"}, + {"type":"Note","name": "Maybe"} + ], + "endTime":event.start.toISOString(), + "tag":[{"type":"Mention","href":req.body.actor,"name":name}] + } + // send direct message to user + sendDirectMessage(jsonObject, req.body.actor, eventID, function (error, response, statuscode) { + if (error) { + console.log(error); + res.status(statuscode).json(error); + } + else { + res.status(statuscode).json({messageid: response}); + } + }); + } + } + }); + }) + .catch((err) => { res.status(500).send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err); + console.log('ERROR', err); + }); } - let message = rawMessage(jsonObject, eventID, domain, req.body.actor); - console.log('!!!!!!!!! sending') - console.log(message) - signAndSend(message, eventID, domain, req, res, targetDomain, inbox); }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err); - console.log('error', err) - }); - } - }); + }) //end request } // if an Undo activity with a Follow object hits the inbox if (req.body && req.body.type === 'Undo' && req.body.object && req.body.object.type === 'Follow') { - console.log('undo follow!') - console.log(req.body) // get the record of all followers for this account - let eventID = req.body.object.object.replace(`https://${domain}/`,''); + const eventID = req.body.object.object.replace(`https://${domain}/`,''); Event.findOne({ id: eventID, }, function(err,event) { + if (!event) return; // check to see if the Follow object's id matches the id we have on record - console.log(event.followers) // is this even someone who follows us - const indexOfFollower = event.followers.findIndex(el => {console.log(el.account, req.body.object.actor); return el.account === req.body.object.actor;}); - console.log(indexOfFollower) + const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.actor); if (indexOfFollower !== -1) { // does the id we have match the id we are being given if (event.followers[indexOfFollower].followId === req.body.object.id) { // we have a match and can trust the Undo! remove this person from the followers list event.followers.splice(indexOfFollower, 1); - console.log('new', indexOfFollower, event.followers); event.save() .then(() => { + res.send(200); addToLog("removeEventFollower", "success", "Follower removed from event " + eventID); console.log('successful follower removal') }) @@ -1495,7 +1731,210 @@ router.post('/activitypub/inbox', (req, res) => { } }); } -}); + // if a Create activity with a Note object hits the inbox, it might be a vote in a poll + if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.inReplyTo && req.body.object.to) { + console.log('create note inreplyto!!!') + // figure out what this is in reply to -- it should be addressed specifically to us + let {name, attributedTo, inReplyTo, to} = req.body.object; + // if it's an array just grab the first element, since a poll should only broadcast back to the pollster + if (Array.isArray(to)) { + to = to[0]; + } + const eventID = to.replace(`https://${domain}/`,''); + // make sure this person is actually a follower + Event.findOne({ + id: eventID, + }, function(err,event) { + if (!event) return; + // is this even someone who follows us + const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.attributedTo); + if (indexOfFollower !== -1) { + console.log('this person does follow us!') + // compare the inReplyTo to its stored message, if it exists and it's going to the right follower then this is a valid reply + const message = event.activityPubMessages.find(el => { + const content = JSON.parse(el.content); + return inReplyTo === (content.object && content.object.id); + }); + if (message) { + console.log(message); + const content = JSON.parse(message.content); + // check if the message we sent out was sent to the actor this incoming message is attributedTo + if (content.to[0] === attributedTo) { + // it's a match, this is a valid poll response, add RSVP to database + // fetch the profile information of the user + request({ + url: attributedTo, + headers: { + 'Accept': 'application/activity+json', + 'Content-Type': 'application/activity+json' + }}, function (error, response, body) { + body = JSON.parse(body) + // if this account is NOT already in our attendees list, add it + if (!event.attendees.map(el => el.id).includes(attributedTo)) { + const attendeeName = body.preferredUsername || body.name || attributedTo; + const newAttendee = { + name: attendeeName, + status: 'attending', + id: attributedTo + }; + event.attendees.push(newAttendee); + event.save() + .then(() => { + addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); + console.log('added attendee', attendeeName) + res.send(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); }); + } + }); + } + } + } + }); + } + if (req.body && req.body.type === 'Delete') { + // figure out if we have a matching comment by id + const deleteObjectId = req.body.object.id; + // find all events with comments from the author + Event.find({ + "comments.actorId":req.body.actor + }, function(err,events) { + if (!events) { + res.sendStatus(404); + return; + } + + // find the event with THIS comment from the author + let eventWithComment = events.find(event => { + let comments = event.comments; + return comments.find(comment => { + if (!comment.activityJson) { + return false; + } + return JSON.parse(comment.activityJson).object.id === req.body.object.id; + }) + }); + + if (!eventWithComment) { + res.sendStatus(404); + return; + } + + // delete the comment + // find the index of the comment + let indexOfComment = eventWithComment.comments.findIndex(comment => { + return JSON.parse(comment.activityJson).object.id === req.body.object.id; + }); + eventWithComment.comments.splice(indexOfComment, 1); + eventWithComment.save() + .then(() => { + addToLog("deleteComment", "success", "Comment deleted from event " + eventWithComment.id); + console.log('deleted comment!') + res.sendStatus(200); + }) + .catch((err) => { res.sendStatus(500); addToLog("deleteComment", "error", "Attempt to delete comment " + req.body.object.id + "from event " + eventWithComment.id + " failed with error: " + err);}); + }); + } + // 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 {name, attributedTo, inReplyTo, to, cc} = req.body.object; + // if it's an array just grab the first element, since a poll should only broadcast back to the pollster + if (Array.isArray(to)) { + to = to[0]; + } + + // normalize cc into an array + if (typeof cc === 'string') { + cc = [cc]; + } + + // if this is a public message (in the to or cc fields) + if (to === 'https://www.w3.org/ns/activitystreams#Public' || (Array.isArray(cc) && cc.includes('https://www.w3.org/ns/activitystreams#Public'))) { + // figure out which event(s) of ours it was addressing + ourEvents = cc.filter(el => el.includes(`https://${domain}/`)) + .map(el => el.replace(`https://${domain}/`,'')); + // comments should only be on one event. if more than one, ignore (spam, probably) + if (ourEvents.length === 1) { + let eventID = ourEvents[0]; + // add comment + let commentID = shortid.generate(); + // get the actor for the commenter + request({ + url: req.body.actor, + headers: { + 'Accept': 'application/activity+json', + 'Content-Type': 'application/activity+json' + }}, function (error, response, actor) { + if (!error) { + const parsedActor = JSON.parse(actor); + const name = parsedActor.preferredUsername || parsedActor.name || req.body.actor; + const newComment = { + id: commentID, + actorId: req.body.actor, + activityId: req.body.object.id, + author: name, + content: sanitizeHtml(req.body.object.content, {allowedTags: [], allowedAttributes: {}}).replace('@'+eventID,''), + timestamp: moment(), + activityJson: JSON.stringify(req.body), + actorJson: actor + }; + + Event.findOne({ + id: eventID, + }, function(err,event) { + if (!event) { + return res.sendStatus(404); + } + if (!event.usersCanComment) { + return res.sendStatus(200); + } + event.comments.push(newComment); + event.save() + .then(() => { + addToLog("addEventComment", "success", "Comment added to event " + eventID); + console.log('added comment'); + res.sendStatus(200); + }) + .catch((err) => { res.status(500).send('Database error, please try again :(' + err); addToLog("addEventComment", "error", "Attempt to add comment to event " + eventID + " failed with error: " + err); console.log('error', err)}); + }); + } + }); + } // end ourevent + } // end public message + // if it's not a public message, let them know that we only support public messages right now + else { + // 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}/`,'')); + // comments should only be on one event. if more than one, ignore (spam, probably) + if (ourEvents.length === 1) { + let eventID = ourEvents[0]; + // get the user's actor info + request({ + url: req.body.actor, + headers: { + 'Accept': 'application/activity+json', + 'Content-Type': 'application/activity+json' + }}, function (error, response, actor) { + actor = JSON.parse(actor); + const name = actor.preferredUsername || actor.name || req.body.actor; + const jsonObject = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "inReplyTo": req.body.object.id, + "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); + sendDirectMessage(jsonObject, req.body.actor, eventID); + } + ); + } + } + } +} router.use(function(req, res, next){ res.status(404); -- cgit v1.2.3 From 49817373b16a7b4c36d32a9d23563c95c40ca685 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sun, 15 Dec 2019 14:11:00 -0800 Subject: profile updates, better diffs --- routes.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 12 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 6d09ab7..2475904 100755 --- a/routes.js +++ b/routes.js @@ -144,7 +144,7 @@ function createWebfinger(eventID, domain) { }; } -function createActivityPubActor(eventID, domain, pubkey, description, name, location, imageFilename) { +function createActivityPubActor(eventID, domain, pubkey, description, name, location, imageFilename, startUTC, endUTC, timezone) { let actor = { '@context': [ 'https://www.w3.org/ns/activitystreams', @@ -156,7 +156,7 @@ function createActivityPubActor(eventID, domain, pubkey, description, name, loca 'preferredUsername': `${eventID}`, 'inbox': `https://${domain}/activitypub/inbox`, 'followers': `https://${domain}/${eventID}/followers`, - 'summary': description, + 'summary': `

${description}

`, 'name': name, 'publicKey': { @@ -166,7 +166,34 @@ function createActivityPubActor(eventID, domain, pubkey, description, name, loca } }; if (location) { - actor.summary += ` Location: ${location}.` + actor.summary += `

Location: ${location}.

` + } + let displayDate; + if (startUTC && timezone) { + displayDate = moment.tz(startUTC, timezone).format('D MMMM YYYY h:mm a'); + actor.summary += `

Starting ${displayDate}.

`; + } + if (imageFilename) { + actor.icon = { + 'type': 'Image', + 'mediaType': 'image/jpg', + 'url': `https://${domain}/events/${imageFilename}`, + }; + } + return JSON.stringify(actor); +} + +function updateActivityPubActor(actor, description, name, location, imageFilename, startUTC, endUTC, timezone) { + if (!actor) return; + actor.summary = `

${description}

`; + actor.name = name; + if (location) { + actor.summary += `

Location: ${location}.

` + } + let displayDate; + if (startUTC && timezone) { + displayDate = moment.tz(startUTC, timezone).format('D MMMM YYYY h:mm a'); + actor.summary += `

Starting ${displayDate}.

`; } if (imageFilename) { actor.icon = { @@ -301,6 +328,54 @@ function broadcastMessage(apObject, followers, eventID, callback) { } // end followers } +function broadcastUpdateMessage(apObject, followers, eventID, callback) { + callback = callback || function() {}; + let guidUpdate = crypto.randomBytes(16).toString('hex'); + console.log('broadcasting update'); + // iterate over followers + for (const follower of followers) { + 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; + console.log('found the inbox for', actorId) + const createMessage = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/m/${guidUpdate}`, + 'type': 'Update', + '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}`); + } + else { + console.log('sent to', actorId); + } + }); + } + else { + callback(`No follower found with the id ${actorId}`, null, 404); + } + } + else { + callback(`No event found with the id ${eventID}`, null, 404); + } + }); + } // end followers +} function signAndSend(message, eventID, targetDomain, inbox, callback) { let inboxFragment = inbox.replace('https://'+targetDomain,''); // get the private key @@ -803,7 +878,7 @@ router.post('/newevent', async (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, firstLoad: true, - activityPubActor: createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename), + 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), publicKey: pair.public, privateKey: pair.private }); @@ -1021,7 +1096,8 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null, - eventGroup: isPartOfEventGroup ? eventGroup._id : 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) } let diffText = '

This event was just updated with new information.

    '; let displayDate; @@ -1068,6 +1144,11 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { "content": `${diffText} See here: https://${domain}/${req.params.eventID}`, } 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) + // DM to attendees for (const attendee of attendees) { const jsonObject = { @@ -1672,7 +1753,7 @@ function processInbox(req, res) { "@context": "https://www.w3.org/ns/activitystreams", "name": `RSVP to ${event.name}`, "type": "Question", - "content": `@${name} Will you attend ${event.name}?`, + "content": `@${name} Will you attend ${event.name}? (If you reply "Yes", you'll be listed as an attendee on the event page.)`, "oneOf": [ {"type":"Note","name": "Yes"}, {"type":"Note","name": "No"}, @@ -1840,18 +1921,17 @@ function processInbox(req, res) { 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; - // if it's an array just grab the first element, since a poll should only broadcast back to the pollster - if (Array.isArray(to)) { - to = to[0]; - } - // normalize cc into an array if (typeof cc === 'string') { cc = [cc]; } + // normalize to into an array + if (typeof to === 'string') { + to = [to]; + } // if this is a public message (in the to or cc fields) - if (to === 'https://www.w3.org/ns/activitystreams#Public' || (Array.isArray(cc) && cc.includes('https://www.w3.org/ns/activitystreams#Public'))) { + if (to.includes('https://www.w3.org/ns/activitystreams#Public') || (Array.isArray(cc) && cc.includes('https://www.w3.org/ns/activitystreams#Public'))) { // figure out which event(s) of ours it was addressing ourEvents = cc.filter(el => el.includes(`https://${domain}/`)) .map(el => el.replace(`https://${domain}/`,'')); -- 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. --- routes.js | 370 ++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 202 insertions(+), 168 deletions(-) (limited to 'routes.js') 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); } ); -- 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. --- routes.js | 332 ++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 258 insertions(+), 74 deletions(-) (limited to 'routes.js') 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); }) -- cgit v1.2.3 From 7fc93dbe9d4d99457a0e85c6c532112f415b7af2 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 4 Jan 2020 22:52:54 -0800 Subject: add federation document --- routes.js | 2 -- 1 file changed, 2 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index eb17d37..5d2110a 100755 --- a/routes.js +++ b/routes.js @@ -2079,8 +2079,6 @@ 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 Event.find({ -- cgit v1.2.3 From dcbf6c7268261639d67b0c8502cd205f815ba2fa Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 4 Jan 2020 23:07:22 -0800 Subject: update federation doc --- routes.js | 101 +++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 57 insertions(+), 44 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 5d2110a..920d36b 100755 --- a/routes.js +++ b/routes.js @@ -189,6 +189,7 @@ function createActivityPubActor(eventID, domain, pubkey, description, name, loca 'type': 'Person', 'preferredUsername': `${eventID}`, 'inbox': `https://${domain}/activitypub/inbox`, + 'outbox': `https://${domain}/${eventID}/outbox`, 'followers': `https://${domain}/${eventID}/followers`, 'summary': `

      ${description}

      `, 'name': name, @@ -362,6 +363,57 @@ function broadcastMessage(apObject, followers, eventID, callback) { } // end followers } +// sends an Announce for the apObject +function broadcastAnnounceMessage(apObject, followers, eventID, callback) { + callback = callback || function() {}; + let guidUpdate = crypto.randomBytes(16).toString('hex'); + console.log('broadcasting announce'); + // iterate over followers + for (const follower of followers) { + 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 announceMessage = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${domain}/m/${guidUpdate}`, + 'cc': 'https://www.w3.org/ns/activitystreams#Public', + 'type': 'Announce', + 'actor': `https://${domain}/${eventID}`, + 'object': apObject, + 'to': actorId + }; + signAndSend(announceMessage, eventID, targetDomain, inbox, function(err, resp, status) { + if (err) { + console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); + } + else { + console.log('sent to', actorId); + } + }); + } + else { + console.log(`No follower found with the id ${actorId}`); + callback(`No follower found with the id ${actorId}`, null, 404); + } + } + else { + console.log(`No event found with the id ${eventID}`); + callback(`No event found with the id ${eventID}`, null, 404); + } + }); + } // end followers +} +// sends an Update for the apObject function broadcastUpdateMessage(apObject, followers, eventID, callback) { callback = callback || function() {}; let guidUpdate = crypto.randomBytes(16).toString('hex'); @@ -2120,7 +2172,7 @@ function processInbox(req, res) { .catch((err) => { res.sendStatus(500); addToLog("deleteComment", "error", "Attempt to delete comment " + req.body.object.id + "from event " + eventWithComment.id + " failed with error: " + err);}); }); } - // if we are CC'ed on a public or unlisted Create/Note, then this is a comment to us we should replicate + // if we are CC'ed on a public or unlisted Create/Note, then this is a comment to us we should boost (Announce) to our followers if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) { // figure out what this is in reply to -- it should be addressed specifically to us let {attributedTo, inReplyTo, to, cc} = req.body.object; @@ -2177,16 +2229,10 @@ 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) + const jsonObject = req.body.object; + jsonObject.attributedTo = newComment.actorId; + broadcastAnnounceMessage(jsonObject, event.followers, eventID) console.log('added comment'); res.sendStatus(200); }) @@ -2196,40 +2242,7 @@ function processInbox(req, res) { }); } // end ourevent } // end public message - // 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}/`,'')); - // comments should only be on one event. if more than one, ignore (spam, probably) - if (ourEvents.length === 1) { - let eventID = ourEvents[0]; - // get the user's actor info - request({ - url: req.body.actor, - headers: { - 'Accept': 'application/activity+json', - 'Content-Type': 'application/activity+json' - }}, function (error, response, actor) { - actor = JSON.parse(actor); - const name = actor.preferredUsername || actor.name || req.body.actor; - const jsonObject = { - "@context": "https://www.w3.org/ns/activitystreams", - "type": "Note", - "inReplyTo": req.body.object.id, - "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.sendStatus(200); - sendDirectMessage(jsonObject, req.body.actor, eventID); - } - ); - } - } - } + } // CC'ed } router.use(function(req, res, next){ -- cgit v1.2.3 From 8029cfcd9221da9164d731ab3e7c20740f52fab7 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 21:35:42 -0800 Subject: lots of refactoring --- routes.js | 1022 +++++++++---------------------------------------------------- 1 file changed, 152 insertions(+), 870 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 920d36b..203134c 100755 --- a/routes.js +++ b/routes.js @@ -14,7 +14,7 @@ const router = express.Router(); const Event = mongoose.model('Event'); const EventGroup = mongoose.model('EventGroup'); -const Log = mongoose.model('Log'); +const addToLog = require('./helpers.js').addToLog; var moment = require('moment-timezone'); @@ -27,47 +27,47 @@ 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'); +const ap = require('./activitypub.js'); // Extra marked renderer (used to render plaintext event description for page metadata) // Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ // ? to ? helper -htmlEscapeToText = function (text) { - return text.replace(/\&\#[0-9]*;|&/g, function (escapeCode) { - if (escapeCode.match(/amp/)) { - return '&'; - } - return String.fromCharCode(escapeCode.match(/[0-9]+/)); - }); +function htmlEscapeToText (text) { + return text.replace(/\&\#[0-9]*;|&/g, function (escapeCode) { + if (escapeCode.match(/amp/)) { + return '&'; + } + return String.fromCharCode(escapeCode.match(/[0-9]+/)); + }); } -render_plain = function () { - var render = new marked.Renderer(); - // render just the text of a link, strong, em - render.link = function (href, title, text) { - return text; - }; - render.strong = function(text) { - return text; - } - render.em = function(text) { - return text; - } - // render just the text of a paragraph - render.paragraph = function (text) { - return htmlEscapeToText(text)+'\r\n'; - }; - // render nothing for headings, images, and br - render.heading = function (text, level) { - return ''; - }; - render.image = function (href, title, text) { - return ''; - }; +function render_plain () { + var render = new marked.Renderer(); + // render just the text of a link, strong, em + render.link = function (href, title, text) { + return text; + }; + render.strong = function(text) { + return text; + } + render.em = function(text) { + return text; + } + // render just the text of a paragraph + render.paragraph = function (text) { + return htmlEscapeToText(text)+'\r\n'; + }; + // render nothing for headings, images, and br + render.heading = function (text, level) { + return ''; + }; + render.image = function (href, title, text) { + return ''; + }; render.br = function () { - return ''; + return ''; }; - return render; + return render; } const ical = require('ical'); @@ -86,17 +86,6 @@ const fileUpload = require('express-fileupload'); var Jimp = require('jimp'); router.use(fileUpload()); -// LOGGING - -function addToLog(process, status, message) { - let logEntry = new Log({ - status: status, - process: process, - message: message, - timestamp: moment() - }); - logEntry.save().catch(() => { console.log("Error saving log entry!") }); -} // SCHEDULED DELETION @@ -120,13 +109,16 @@ const deleteOldEvents = schedule.scheduleJob('59 23 * * *', function(fireDate){ // 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); + const jsonEventObject = JSON.parse(event.activityPubEvent); // 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); + ap.broadcastDeleteMessage(jsonUpdateObject, event.followers, event.id, function(statuses) { + ap.broadcastDeleteMessage(jsonEventObject, 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); + }); }); }); }) @@ -151,441 +143,7 @@ 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': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1' - ], - - 'id': `https://${domain}/${eventID}`, - 'type': 'Person', - 'preferredUsername': `${eventID}`, - 'inbox': `https://${domain}/activitypub/inbox`, - 'outbox': `https://${domain}/${eventID}/outbox`, - 'followers': `https://${domain}/${eventID}/followers`, - 'summary': `

      ${description}

      `, - 'name': name, - - 'publicKey': { - 'id': `https://${domain}/${eventID}#main-key`, - 'owner': `https://${domain}/${eventID}`, - 'publicKeyPem': pubkey - } - }; - if (location) { - actor.summary += `

      Location: ${location}.

      ` - } - let displayDate; - if (startUTC && timezone) { - displayDate = moment.tz(startUTC, timezone).format('D MMMM YYYY h:mm a'); - actor.summary += `

      Starting ${displayDate}.

      `; - } - if (imageFilename) { - actor.icon = { - 'type': 'Image', - 'mediaType': 'image/jpg', - 'url': `https://${domain}/events/${imageFilename}`, - }; - } - return JSON.stringify(actor); -} - -function updateActivityPubActor(actor, description, name, location, imageFilename, startUTC, endUTC, timezone) { - if (!actor) return; - actor.summary = `

      ${description}

      `; - actor.name = name; - if (location) { - actor.summary += `

      Location: ${location}.

      ` - } - let displayDate; - if (startUTC && timezone) { - displayDate = moment.tz(startUTC, timezone).format('D MMMM YYYY h:mm a'); - actor.summary += `

      Starting ${displayDate}.

      `; - } - if (imageFilename) { - actor.icon = { - 'type': 'Image', - 'mediaType': 'image/jpg', - 'url': `https://${domain}/events/${imageFilename}`, - }; - } - return JSON.stringify(actor); -} - -function sendAcceptMessage(thebody, eventID, targetDomain, callback) { - callback = callback || function() {}; - const guid = crypto.randomBytes(16).toString('hex'); - const actorId = thebody.actor; - let message = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${domain}/${guid}`, - 'type': 'Accept', - 'actor': `https://${domain}/${eventID}`, - 'object': thebody, - }; - // get the inbox - Event.findOne({ - id: eventID, - }, function(err, event) { - if (event) { - const follower = event.followers.find(el => el.actorId === actorId); - if (follower) { - const actorJson = JSON.parse(follower.actorJson); - const inbox = actorJson.inbox; - signAndSend(message, eventID, targetDomain, inbox, callback); - } - } - else { - callback(`Could not find event ${eventID}`, null, 404); - } - }); -} - -// this sends a message "to:" an individual fediverse user -function sendDirectMessage(apObject, actorId, eventID, callback) { - callback = callback || function() {}; - const guidCreate = crypto.randomBytes(16).toString('hex'); - const guidObject = crypto.randomBytes(16).toString('hex'); - let d = new Date(); - - apObject.published = d.toISOString(); - apObject.attributedTo = `https://${domain}/${eventID}`; - apObject.to = actorId; - apObject.id = `https://${domain}/m/${guidObject}`; - apObject.content = unescape(apObject.content) - - let createMessage = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${domain}/m/${guidCreate}`, - 'type': 'Create', - 'actor': `https://${domain}/${eventID}`, - 'to': [actorId], - 'object': apObject - }; - - let myURL = new URL(actorId); - let targetDomain = myURL.hostname; - // get the inbox - Event.findOne({ - id: eventID, - }, function(err, event) { - if (event) { - const follower = event.followers.find(el => el.actorId === actorId); - if (follower) { - const actorJson = JSON.parse(follower.actorJson); - const inbox = actorJson.inbox; - signAndSend(createMessage, eventID, targetDomain, inbox, callback); - } - else { - callback(`No follower found with the id ${actorId}`, null, 404); - } - } - else { - callback(`No event found with the id ${eventID}`, null, 404); - } - }); -} - -// this function sends something to the timeline of every follower in the followers array -function broadcastMessage(apObject, followers, eventID, callback) { - callback = callback || function() {}; - let guidCreate = crypto.randomBytes(16).toString('hex'); - console.log('broadcasting'); - // iterate over followers - for (const follower of followers) { - 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; - console.log('found the inbox for', actorId) - const createMessage = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${domain}/m/${guidCreate}`, - 'type': 'Create', - 'actor': `https://${domain}/${eventID}`, - 'to': [actorId], - 'object': apObject - }; - signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) { - if (err) { - console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); - } - else { - console.log('sent to', actorId); - } - }); - } - else { - callback(`No follower found with the id ${actorId}`, null, 404); - } - } - else { - callback(`No event found with the id ${eventID}`, null, 404); - } - }); - } // end followers -} -// sends an Announce for the apObject -function broadcastAnnounceMessage(apObject, followers, eventID, callback) { - callback = callback || function() {}; - let guidUpdate = crypto.randomBytes(16).toString('hex'); - console.log('broadcasting announce'); - // iterate over followers - for (const follower of followers) { - 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 announceMessage = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${domain}/m/${guidUpdate}`, - 'cc': 'https://www.w3.org/ns/activitystreams#Public', - 'type': 'Announce', - 'actor': `https://${domain}/${eventID}`, - 'object': apObject, - 'to': actorId - }; - signAndSend(announceMessage, eventID, targetDomain, inbox, function(err, resp, status) { - if (err) { - console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); - } - else { - console.log('sent to', actorId); - } - }); - } - else { - console.log(`No follower found with the id ${actorId}`); - callback(`No follower found with the id ${actorId}`, null, 404); - } - } - else { - console.log(`No event found with the id ${eventID}`); - callback(`No event found with the id ${eventID}`, null, 404); - } - }); - } // end followers -} -// sends an Update for the apObject -function broadcastUpdateMessage(apObject, followers, eventID, callback) { - callback = callback || function() {}; - let guidUpdate = crypto.randomBytes(16).toString('hex'); - console.log('broadcasting update'); - // iterate over followers - for (const follower of followers) { - 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': 'Update', - 'actor': `https://${domain}/${eventID}`, - 'object': apObject - }; - signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) { - if (err) { - console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`); - } - else { - console.log('sent to', actorId); - } - }); - } - else { - callback(`No follower found with the id ${actorId}`, null, 404); - } - } - else { - callback(`No event found with the id ${eventID}`, null, 404); - } - }); - } // 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 - Event.findOne({ - id: eventID - }) - .then((event) => { - if (event) { - const privateKey = event.privateKey; - const signer = crypto.createSign('sha256'); - let d = new Date(); - let stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`; - signer.update(stringToSign); - signer.end(); - const signature = signer.sign(privateKey); - const signature_b64 = signature.toString('base64'); - const header = `keyId="https://${domain}/${eventID}",headers="(request-target) host date",signature="${signature_b64}"`; - request({ - url: inbox, - headers: { - 'Host': targetDomain, - 'Date': d.toUTCString(), - 'Signature': header - }, - method: 'POST', - json: true, - body: message - }, function (error, response){ - if (error) { - console.log('Error:', error, response.body); - callback(error, null, 500); - } - else { - console.log('Response:', response.statusCode); - // Add the message to the database - const messageID = message.id; - const newMessage = { - id: message.id, - content: JSON.stringify(message) - }; - Event.findOne({ - id: eventID, - }, function(err,event) { - if (!event) return; - event.activityPubMessages.push(newMessage); - event.save() - .then(() => { - addToLog("addActivityPubMessage", "success", "ActivityPubMessage added to event " + eventID); - console.log('successful ActivityPubMessage add'); - callback(null, message.id, 200); - }) - .catch((err) => { addToLog("addActivityPubMessage", "error", "Attempt to add ActivityPubMessage to event " + eventID + " failed with error: " + err); - console.log('error', err) - callback(err, null, 500); - }); - }) - } - }); - } - else { - callback(`No record found for ${eventID}.`, null, 404); - } - }); -} // FRONTEND ROUTES @@ -614,7 +172,11 @@ router.get('/new', (req, res) => { //}); router.get('/new/event', (req, res) => { - res.render('newevent'); + res.render('newevent', { + domain: domain, + email: contactEmail, + siteName: siteName, + }); }); router.get('/new/event/public', (req, res) => { let isPrivate = false; @@ -640,12 +202,61 @@ router.get('/new/event/public', (req, res) => { isPublic: isPublic, isOrganisation: isOrganisation, isUnknownType: isUnknownType, - eventType: 'public' + eventType: 'public', + domain: domain, + email: contactEmail, + siteName: siteName, }); }) +router.get('/:eventID/featured', (req, res) => { + const {eventID} = req.params; + const guidObject = crypto.randomBytes(16).toString('hex'); + const featured = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": `https://${domain}/${eventID}/featured`, + "type": "OrderedCollection", + "orderedItems": [ + ap.createFeaturedPost(eventID) + ] + } + res.json(featured); +}); + +router.get('/:eventID/m/:hash', (req, res) => { + const {hash, eventID} = req.params; + const id = `https://${domain}/${eventID}/m/${hash}`; + console.log(id); + + Event.findOne({ + id: eventID + }) + .then((event) => { + if (!event) { + res.status(404); + res.render('404', { url: req.url }); + } + else { + const message = event.activityPubMessages.find(el => el.id === id); + if (message) { + return res.json(JSON.parse(message.content)); + } + else { + res.status(404); + return res.render('404', { url: req.url }); + } + } + }) + .catch((err) => { + addToLog("getActivityPubMessage", "error", "Attempt to get Activity Pub Message for " + id + " failed with error: " + err); + console.log(err) + res.status(404); + res.render('404', { url: req.url }); + return; + }); +}); + router.get('/.well-known/webfinger', (req, res) => { - console.log(req.query); let resource = req.query.resource; if (!resource || !resource.includes('acct:')) { return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.'); @@ -655,7 +266,6 @@ router.get('/.well-known/webfinger', (req, res) => { let activityPubAccount = resource.replace('acct:',''); // "foo" let eventID = activityPubAccount.replace(/@.*/,''); - console.log(eventID); Event.findOne({ id: eventID }) @@ -675,14 +285,6 @@ router.get('/.well-known/webfinger', (req, res) => { res.render('404', { url: req.url }); return; }); - //let db = req.app.get('db'); - //let result = db.prepare('select webfinger from accounts where name = ?').get(name); - //if (result === undefined) { - // return res.status(404).send(`No record found for ${name}.`); - //} - //else { - // res.json(JSON.parse(result.webfinger)); - //} } }); @@ -693,7 +295,8 @@ router.get('/:eventID', (req, res) => { .populate('eventGroup') .then((event) => { if (event) { - parsedLocation = event.location.replace(/\s+/g, '+'); + const parsedLocation = event.location.replace(/\s+/g, '+'); + let displayDate; if (moment.tz(event.end, event.timezone).isSame(event.start, 'day')){ // Happening during one day displayDate = moment.tz(event.start, event.timezone).format('dddd D MMMM YYYY [from] h:mm a') + moment.tz(event.end, event.timezone).format(' [to] h:mm a [](z)[]'); @@ -701,10 +304,10 @@ router.get('/:eventID', (req, res) => { else { displayDate = moment.tz(event.start, event.timezone).format('dddd D MMMM YYYY [at] h:mm a') + moment.tz(event.end, event.timezone).format(' [] dddd D MMMM YYYY [at] h:mm a [](z)[]'); } - eventStartISO = moment.tz(event.start, "Etc/UTC").toISOString(); - eventEndISO = moment.tz(event.end, "Etc/UTC").toISOString(); - parsedStart = moment.tz(event.start, event.timezone).format('YYYYMMDD[T]HHmmss'); - parsedEnd = moment.tz(event.end, event.timezone).format('YYYYMMDD[T]HHmmss'); + let eventStartISO = moment.tz(event.start, "Etc/UTC").toISOString(); + let eventEndISO = moment.tz(event.end, "Etc/UTC").toISOString(); + let parsedStart = moment.tz(event.start, event.timezone).format('YYYYMMDD[T]HHmmss'); + let parsedEnd = moment.tz(event.end, event.timezone).format('YYYYMMDD[T]HHmmss'); let eventHasConcluded = false; if (moment.tz(event.end, event.timezone).isBefore(moment.tz(event.timezone))){ eventHasConcluded = true; @@ -713,11 +316,11 @@ router.get('/:eventID', (req, res) => { if (moment.tz(event.start, event.timezone).isBefore(moment.tz(event.timezone))){ eventHasBegun = true; } - fromNow = moment.tz(event.start, event.timezone).fromNow(); - parsedDescription = marked(event.description); - eventEditToken = event.editToken; + let fromNow = moment.tz(event.start, event.timezone).fromNow(); + let parsedDescription = marked(event.description); + let eventEditToken = event.editToken; - escapedName = event.name.replace(/\s+/g, '+'); + let escapedName = event.name.replace(/\s+/g, '+'); let eventHasCoverImage = false; if( event.image ) { @@ -867,10 +470,10 @@ router.get('/group/:eventGroupID', (req, res) => { }) .then(async (eventGroup) => { if (eventGroup) { - parsedDescription = marked(eventGroup.description); - eventGroupEditToken = eventGroup.editToken; + let parsedDescription = marked(eventGroup.description); + let eventGroupEditToken = eventGroup.editToken; - escapedName = eventGroup.name.replace(/\s+/g, '+'); + let escapedName = eventGroup.name.replace(/\s+/g, '+'); let eventGroupHasCoverImage = false; if( eventGroup.image ) { @@ -998,8 +601,8 @@ router.post('/newevent', async (req, res) => { }); eventImageFilename = eventID + '.jpg'; } - startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); - endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); + let startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); + let endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); let eventGroup; if (req.body.eventGroupCheckbox) { eventGroup = await EventGroup.findOne({ @@ -1036,8 +639,9 @@ router.post('/newevent', async (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, 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), + activityPubActor: ap.createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), + activityPubEvent: ap.createActivityPubEvent(req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation), + activityPubMessages: [ { id: `https://${domain}/${eventID}/m/featuredPost`, content: JSON.stringify(ap.createFeaturedPost(eventID, req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation)) } ], publicKey: pair.public, privateKey: pair.private }); @@ -1074,10 +678,8 @@ router.post('/importevent', (req, res) => { let eventID = shortid.generate(); let editToken = randomstring.generate(); if (req.files && Object.keys(req.files).length !== 0) { - importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); - for (var key in importediCalObject) { - importedEventData = importediCalObject[key]; - } + let importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); + let importedEventData = importediCalObject; console.log(importedEventData) let creatorEmail; if (req.body.creatorEmail) { @@ -1222,12 +824,13 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { }); eventImageFilename = eventID + '.jpg'; } - startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); - endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); + let startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); + let endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); - var isPartOfEventGroup = false; + let isPartOfEventGroup = false; + let eventGroup; if (req.body.eventGroupCheckbox) { - var eventGroup = await EventGroup.findOne({ + eventGroup = await EventGroup.findOne({ id: req.body.eventGroupID, editToken: req.body.eventGroupEditToken }) @@ -1250,8 +853,8 @@ 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), - activityPubEvent: updateActivityPubEvent(JSON.parse(event.activityPubEvent), req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone), + activityPubActor: ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), + activityPubEvent: ap.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; @@ -1294,18 +897,19 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { const guidObject = crypto.randomBytes(16).toString('hex'); const jsonObject = { "@context": "https://www.w3.org/ns/activitystreams", - "id": `https://${domain}/m/${guidObject}`, + "id": `https://${domain}/${req.params.eventID}/m/${guidObject}`, "name": `RSVP to ${event.name}`, "type": "Note", - "content": `${diffText} See here: https://${domain}/${req.params.eventID}`, + 'cc': 'https://www.w3.org/ns/activitystreams#Public', + "content": `${diffText} See here: https://${domain}/${req.params.eventID}`, } - broadcastMessage(jsonObject, event.followers, eventID) + ap.broadcastCreateMessage(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 jsonUpdateObject = JSON.parse(event.activityPubActor); - broadcastUpdateMessage(jsonUpdateObject, event.followers, eventID) + ap.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) + ap.broadcastUpdateMessage(jsonEventObject, event.followers, eventID) // DM to attendees for (const attendee of attendees) { @@ -1317,13 +921,13 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { "tag":[{"type":"Mention","href":attendee.id,"name":attendee.name}] } // send direct message to user - sendDirectMessage(jsonObject, attendee.id, eventID); + ap.sendDirectMessage(jsonObject, attendee.id, eventID); } } }) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { - attendeeEmails = ids; + let attendeeEmails = ids; if (!error && attendeeEmails !== ""){ console.log("Sending emails to: " + attendeeEmails); req.app.get('hbsInstance').renderView('./views/emails/editevent.handlebars', {diffText, eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { @@ -1401,32 +1005,6 @@ router.post('/editeventgroup/:eventGroupID/:editToken', (req, res) => { }) .then(() => { addToLog("editEventGroup", "success", "Event group " + req.params.eventGroupID + " edited"); - // if (sendEmails) { - // Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { - // 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); - // } - // else { - // console.log("Nothing to send!"); - // } - // }) - // } res.writeHead(302, { 'Location': '/group/' + req.params.eventGroupID + '?e=' + req.params.editToken }); @@ -1461,7 +1039,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { 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) { + ap.broadcastDeleteMessage(jsonUpdateObject, event.followers, req.params.eventID, function(statuses) { Event.deleteOne({id: req.params.eventID}, function(err, raw) { if (err) { res.send(err); @@ -1490,7 +1068,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { // 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; + let 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) { @@ -1751,18 +1329,20 @@ router.post('/post/comment/:eventID', (req, res) => { .then(() => { addToLog("addEventComment", "success", "Comment added to event " + req.params.eventID); // broadcast an identical message to all followers, will show in their home timeline + // and in the home timeline of the event const guidObject = crypto.randomBytes(16).toString('hex'); const jsonObject = { "@context": "https://www.w3.org/ns/activitystreams", - "id": `https://${domain}/m/${guidObject}`, + "id": `https://${domain}/${req.params.eventID}/m/${guidObject}`, "name": `Comment on ${event.name}`, "type": "Note", + 'cc': 'https://www.w3.org/ns/activitystreams#Public', "content": `

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

        See the full conversation here.

        `, } - broadcastMessage(jsonObject, event.followers, req.params.eventID) + ap.broadcastCreateMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { - attendeeEmails = ids; + let attendeeEmails = ids; if (!error){ console.log("Sending emails to: " + attendeeEmails); 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) { @@ -1817,15 +1397,16 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { const guidObject = crypto.randomBytes(16).toString('hex'); const jsonObject = { "@context": "https://www.w3.org/ns/activitystreams", - "id": `https://${domain}/m/${guidObject}`, + "id": `https://${domain}/${req.params.eventID}/m/${guidObject}`, "name": `Comment on ${event.name}`, "type": "Note", + 'cc': 'https://www.w3.org/ns/activitystreams#Public', "content": `

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

        See the full conversation here.

        `, } - broadcastMessage(jsonObject, event.followers, req.params.eventID) + ap.broadcastCreateMessage(jsonObject, event.followers, req.params.eventID) if (sendEmails) { Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { - attendeeEmails = ids; + let attendeeEmails = ids; if (!error){ console.log("Sending emails to: " + attendeeEmails); 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) { @@ -1887,8 +1468,6 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => { }); router.post('/activitypub/inbox', (req, res) => { - console.log('got an inbox message of type', req.body.type, req.body) - // validate the incoming message const signature = req.get('Signature'); let signature_header = signature.split(',').map(pair => { @@ -1934,7 +1513,13 @@ router.post('/activitypub/inbox', (req, res) => { const signatureBuf = new Buffer(signature_header.signature, 'base64') try { const result = verifier.verify(publicKeyBuf, signatureBuf) - processInbox(req, res); + if (result) { + // actually process the ActivityPub message now that it's been verified + ap.processInbox(req, res); + } + else { + return res.status(401).send('Signature could not be verified.'); + } } catch(err) { return res.status(401).send('Signature could not be verified: ' + err); @@ -1942,309 +1527,6 @@ router.post('/activitypub/inbox', (req, res) => { }); }); - -function processInbox(req, res) { - if (req.body.object) console.log('containing object of type', req.body.object.type) - // if a Follow activity hits the inbox - if (typeof req.body.object === 'string' && req.body.type === 'Follow') { - const myURL = new URL(req.body.actor); - let targetDomain = myURL.hostname; - let eventID = req.body.object.replace(`https://${domain}/`,''); - // Add the user to the DB of accounts that follow the account - // get the follower's username - request({ - url: req.body.actor, - headers: { - 'Accept': 'application/activity+json', - 'Content-Type': 'application/activity+json' - }}, function (error, response, body) { - body = JSON.parse(body) - const name = body.preferredUsername || body.name || attributedTo; - const newFollower = { - actorId: req.body.actor, - followId: req.body.id, - name: name, - actorJson: JSON.stringify(body) - }; - Event.findOne({ - id: eventID, - }, function(err,event) { - // if this account is NOT already in our followers list, add it - if (event && !event.followers.map(el => el.actorId).includes(req.body.actor)) { - console.log('made it!') - event.followers.push(newFollower); - event.save() - .then(() => { - addToLog("addEventFollower", "success", "Follower added to event " + eventID); - console.log('successful follower add'); - // Accept the follow request - sendAcceptMessage(req.body, eventID, targetDomain, function(err, resp, status) { - if (err) { - console.log(`Didn't send Accept to ${req.body.actor}, status ${status} with error ${err}`); - } - 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 = { - "@context": "https://www.w3.org/ns/activitystreams", - "name": `RSVP to ${event.name}`, - "type": "Question", - "content": `@${name} Will you attend ${event.name}? (If you reply "Yes", you'll be listed as an attendee on the event page.)`, - "oneOf": [ - {"type":"Note","name": "Yes"}, - {"type":"Note","name": "No"}, - {"type":"Note","name": "Maybe"} - ], - "endTime":event.start.toISOString(), - "tag":[{"type":"Mention","href":req.body.actor,"name":name}] - } - // send direct message to user - sendDirectMessage(jsonObject, req.body.actor, eventID, function (error, response, statuscode) { - if (error) { - console.log(error); - res.status(statuscode).json(error); - } - else { - res.status(statuscode).json({messageid: response}); - } - }); - } - } - }); - }) - .catch((err) => { res.status(500).send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err); - console.log('ERROR', err); - }); - } - }) - }) //end request - } - // if an Undo activity with a Follow object hits the inbox - if (req.body && req.body.type === 'Undo' && req.body.object && req.body.object.type === 'Follow') { - // get the record of all followers for this account - const eventID = req.body.object.object.replace(`https://${domain}/`,''); - Event.findOne({ - id: eventID, - }, function(err,event) { - if (!event) return; - // check to see if the Follow object's id matches the id we have on record - // is this even someone who follows us - const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.actor); - if (indexOfFollower !== -1) { - // does the id we have match the id we are being given - if (event.followers[indexOfFollower].followId === req.body.object.id) { - // we have a match and can trust the Undo! remove this person from the followers list - event.followers.splice(indexOfFollower, 1); - event.save() - .then(() => { - res.send(200); - addToLog("removeEventFollower", "success", "Follower removed from event " + eventID); - console.log('successful follower removal') - }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("removeEventFollower", "error", "Attempt to remove follower from event " + eventID + " failed with error: " + err); - console.log('error', err) - }); - } - } - }); - } - // if a Create activity with a Note object hits the inbox, it might be a vote in a poll - if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.inReplyTo && req.body.object.to) { - console.log('create note inreplyto!!!') - // figure out what this is in reply to -- it should be addressed specifically to us - let {name, attributedTo, inReplyTo, to} = req.body.object; - // if it's an array just grab the first element, since a poll should only broadcast back to the pollster - if (Array.isArray(to)) { - to = to[0]; - } - const eventID = to.replace(`https://${domain}/`,''); - // make sure this person is actually a follower - Event.findOne({ - id: eventID, - }, function(err,event) { - if (!event) return; - // is this even someone who follows us - const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.attributedTo); - if (indexOfFollower !== -1) { - console.log('this person does follow us!') - // compare the inReplyTo to its stored message, if it exists and it's going to the right follower then this is a valid reply - const message = event.activityPubMessages.find(el => { - const content = JSON.parse(el.content); - return inReplyTo === (content.object && content.object.id); - }); - if (message) { - console.log(message); - const content = JSON.parse(message.content); - // check if the message we sent out was sent to the actor this incoming message is attributedTo - if (content.to[0] === attributedTo) { - // it's a match, this is a valid poll response, add RSVP to database - // fetch the profile information of the user - request({ - url: attributedTo, - headers: { - 'Accept': 'application/activity+json', - 'Content-Type': 'application/activity+json' - }}, function (error, response, body) { - body = JSON.parse(body) - // if this account is NOT already in our attendees list, add it - if (!event.attendees.map(el => el.id).includes(attributedTo)) { - const attendeeName = body.preferredUsername || body.name || attributedTo; - const newAttendee = { - name: attendeeName, - status: 'attending', - id: attributedTo - }; - event.attendees.push(newAttendee); - event.save() - .then((fullEvent) => { - addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID); - // 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 :('); }); - } - else { - // it's a duplicate and this person is already rsvped so just say OK - return res.status(200).send("Attendee is already registered."); - } - }); - } - } - } - }); - } - if (req.body && req.body.type === 'Delete') { - const deleteObjectId = req.body.object.id; - // find all events with comments from the author - Event.find({ - "comments.actorId":req.body.actor - }, function(err,events) { - if (!events) { - res.sendStatus(404); - return; - } - - // find the event with THIS comment from the author - let eventWithComment = events.find(event => { - let comments = event.comments; - return comments.find(comment => { - if (!comment.activityJson) { - return false; - } - return JSON.parse(comment.activityJson).object.id === req.body.object.id; - }) - }); - - if (!eventWithComment) { - res.sendStatus(404); - return; - } - - // delete 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 comment.activityJson && JSON.parse(comment.activityJson).object.id === req.body.object.id; - }); - eventWithComment.comments.splice(indexOfComment, 1); - eventWithComment.save() - .then(() => { - addToLog("deleteComment", "success", "Comment deleted from event " + eventWithComment.id); - console.log('deleted comment!') - res.sendStatus(200); - }) - .catch((err) => { res.sendStatus(500); addToLog("deleteComment", "error", "Attempt to delete comment " + req.body.object.id + "from event " + eventWithComment.id + " failed with error: " + err);}); - }); - } - // if we are CC'ed on a public or unlisted Create/Note, then this is a comment to us we should boost (Announce) to our followers - if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) { - // 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 - if (typeof cc === 'string') { - cc = [cc]; - } - // normalize to into an array - if (typeof to === 'string') { - to = [to]; - } - - // if this is a public message (in the to or cc fields) - if (to.includes('https://www.w3.org/ns/activitystreams#Public') || (Array.isArray(cc) && cc.includes('https://www.w3.org/ns/activitystreams#Public'))) { - // figure out which event(s) of ours it was addressing - ourEvents = cc.filter(el => el.includes(`https://${domain}/`)) - .map(el => el.replace(`https://${domain}/`,'')); - // comments should only be on one event. if more than one, ignore (spam, probably) - if (ourEvents.length === 1) { - let eventID = ourEvents[0]; - // add comment - let commentID = shortid.generate(); - // get the actor for the commenter - request({ - url: req.body.actor, - headers: { - 'Accept': 'application/activity+json', - 'Content-Type': 'application/activity+json' - }}, function (error, response, actor) { - if (!error) { - const parsedActor = JSON.parse(actor); - const name = parsedActor.preferredUsername || parsedActor.name || req.body.actor; - const newComment = { - id: commentID, - actorId: req.body.actor, - activityId: req.body.object.id, - author: name, - content: sanitizeHtml(req.body.object.content, {allowedTags: [], allowedAttributes: {}}).replace('@'+eventID,''), - timestamp: moment(), - activityJson: JSON.stringify(req.body), - actorJson: actor - }; - - Event.findOne({ - id: eventID, - }, function(err,event) { - if (!event) { - return res.sendStatus(404); - } - if (!event.usersCanComment) { - return res.sendStatus(200); - } - event.comments.push(newComment); - event.save() - .then(() => { - addToLog("addEventComment", "success", "Comment added to event " + eventID); - const guidObject = crypto.randomBytes(16).toString('hex'); - const jsonObject = req.body.object; - jsonObject.attributedTo = newComment.actorId; - broadcastAnnounceMessage(jsonObject, event.followers, eventID) - console.log('added comment'); - res.sendStatus(200); - }) - .catch((err) => { res.status(500).send('Database error, please try again :(' + err); addToLog("addEventComment", "error", "Attempt to add comment to event " + eventID + " failed with error: " + err); console.log('error', err)}); - }); - } - }); - } // end ourevent - } // end public message - } // CC'ed -} - router.use(function(req, res, next){ res.status(404); res.render('404', { url: req.url }); -- cgit v1.2.3 From 6dc03139921414d24f0e24efba224bf0c8e0581f Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 22:26:29 -0800 Subject: delete console logs --- routes.js | 7 ------- 1 file changed, 7 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 203134c..b9b9cdf 100755 --- a/routes.js +++ b/routes.js @@ -226,7 +226,6 @@ router.get('/:eventID/featured', (req, res) => { router.get('/:eventID/m/:hash', (req, res) => { const {hash, eventID} = req.params; const id = `https://${domain}/${eventID}/m/${hash}`; - console.log(id); Event.findOne({ id: eventID @@ -249,7 +248,6 @@ router.get('/:eventID/m/:hash', (req, res) => { }) .catch((err) => { addToLog("getActivityPubMessage", "error", "Attempt to get Activity Pub Message for " + id + " failed with error: " + err); - console.log(err) res.status(404); res.render('404', { url: req.url }); return; @@ -280,7 +278,6 @@ router.get('/.well-known/webfinger', (req, res) => { }) .catch((err) => { addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + err); - console.log(err) res.status(404); res.render('404', { url: req.url }); return; @@ -440,9 +437,7 @@ router.get('/:eventID/followers', (req, res) => { }) .then((event) => { if (event) { - console.log(event.followers); const followers = event.followers.map(el => el.actorId); - console.log(followers) let followersCollection = { "type": "OrderedCollection", "totalItems": followers.length, @@ -1237,13 +1232,11 @@ 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 -- cgit v1.2.3 From a4392c4c663b6b23da7320d95b5a4b23f474f213 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 23:16:52 -0800 Subject: minor fixes --- routes.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'routes.js') diff --git a/routes.js b/routes.js index cd81da5..e90360c 100755 --- a/routes.js +++ b/routes.js @@ -1309,6 +1309,11 @@ router.post('/unattendevent/:eventID', (req, res) => { }); router.get('/oneclickunattendevent/:eventID/:attendeeID', (req, res) => { + // Mastodon will "click" links that sent to its users, presumably as a prefetch? + // Anyway, this ignores the automated clicks that are done without the user's knowledge + if (req.headers['user-agent'] && req.headers['user-agent'].includes('Mastodon')) { + return res.sendStatus(200); + } Event.update( { id: req.params.eventID }, { $pull: { attendees: { _id: req.params.attendeeID } } } -- cgit v1.2.3 From 5ffe6115740cfa293322e1961e7c9996e32ce2a4 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 23:29:24 -0800 Subject: move function to activitypub file --- routes.js | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index e90360c..b988fee 100755 --- a/routes.js +++ b/routes.js @@ -132,24 +132,6 @@ const deleteOldEvents = schedule.scheduleJob('59 23 * * *', function(fireDate){ }); }); - -// ACTIVITYPUB HELPER FUNCTIONS -function createWebfinger(eventID, domain) { - return { - 'subject': `acct:${eventID}@${domain}`, - - 'links': [ - { - 'rel': 'self', - 'type': 'application/activity+json', - 'href': `https://${domain}/${eventID}` - } - ] - }; -} - - - // FRONTEND ROUTES router.get('/', (req, res) => { @@ -278,7 +260,7 @@ router.get('/.well-known/webfinger', (req, res) => { res.render('404', { url: req.url }); } else { - res.json(createWebfinger(eventID, domain)); + res.json(ap.createWebfinger(eventID, domain)); } }) .catch((err) => { -- cgit v1.2.3 From d2d43f9e1552e9403747e358dc1cf30872bf0941 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 23:31:52 -0800 Subject: adding comments --- routes.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'routes.js') diff --git a/routes.js b/routes.js index b988fee..b1abab5 100755 --- a/routes.js +++ b/routes.js @@ -196,6 +196,7 @@ router.get('/new/event/public', (req, res) => { }); }) +// return the JSON for the featured/pinned post for this event router.get('/:eventID/featured', (req, res) => { const {eventID} = req.params; const guidObject = crypto.randomBytes(16).toString('hex'); @@ -210,6 +211,7 @@ router.get('/:eventID/featured', (req, res) => { res.json(featured); }); +// return the JSON for a given activitypub message router.get('/:eventID/m/:hash', (req, res) => { const {hash, eventID} = req.params; const id = `https://${domain}/${eventID}/m/${hash}`; @@ -241,6 +243,7 @@ router.get('/:eventID/m/:hash', (req, res) => { }); }); +// return the webfinger record required for the initial activitypub handshake router.get('/.well-known/webfinger', (req, res) => { let resource = req.query.resource; if (!resource || !resource.includes('acct:')) { @@ -1290,6 +1293,8 @@ router.post('/unattendevent/:eventID', (req, res) => { }); }); +// this is a one-click unattend that requires a secret URL that only the person who RSVPed over +// activitypub knows router.get('/oneclickunattendevent/:eventID/:attendeeID', (req, res) => { // Mastodon will "click" links that sent to its users, presumably as a prefetch? // Anyway, this ignores the automated clicks that are done without the user's knowledge -- cgit v1.2.3 From 32398ef5dfa1fc85f2054cd9cb9f6716418cc7c8 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 23:33:20 -0800 Subject: minor fix --- routes.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index b1abab5..82f971c 100755 --- a/routes.js +++ b/routes.js @@ -266,12 +266,12 @@ router.get('/.well-known/webfinger', (req, res) => { res.json(ap.createWebfinger(eventID, domain)); } }) - .catch((err) => { - addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + err); - res.status(404); - res.render('404', { url: req.url }); - return; - }); + .catch((err) => { + addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + err); + res.status(404); + res.render('404', { url: req.url }); + return; + }); } }); -- cgit v1.2.3 From d4eb6e1ee5cd3bae8f0414e9ff054e0c4ab9952e Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 6 Jan 2020 23:34:39 -0800 Subject: minor fix --- routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 82f971c..e8b8d7f 100755 --- a/routes.js +++ b/routes.js @@ -283,7 +283,7 @@ router.get('/:eventID', (req, res) => { .then((event) => { if (event) { const parsedLocation = event.location.replace(/\s+/g, '+'); - let displayDate; + let displayDate; if (moment.tz(event.end, event.timezone).isSame(event.start, 'day')){ // Happening during one day displayDate = moment.tz(event.start, event.timezone).format('dddd D MMMM YYYY [from] h:mm a') + moment.tz(event.end, event.timezone).format(' [to] h:mm a [](z)[]'); -- cgit v1.2.3 From fa2ca34a5c8f0e10a902129caec36dea38eec13a Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sun, 12 Jan 2020 18:07:31 -0800 Subject: reverting ical import --- routes.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index e8b8d7f..f5ec0b3 100755 --- a/routes.js +++ b/routes.js @@ -698,9 +698,11 @@ router.post('/newevent', async (req, res) => { router.post('/importevent', (req, res) => { let eventID = shortid.generate(); let editToken = randomstring.generate(); - if (req.files && Object.keys(req.files).length !== 0) { - let importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); - let importedEventData = importediCalObject; + if (req.files && Object.keys(req.files).length != 0) { + importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); + for (var key in importediCalObject) { + importedEventData = importediCalObject[key]; + } console.log(importedEventData) let creatorEmail; if (req.body.creatorEmail) { -- cgit v1.2.3 From b2e6e2c92d0fda2966e424e29038e7e6f0e42823 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sun, 12 Jan 2020 18:17:54 -0800 Subject: fixup --- routes.js | 1 - 1 file changed, 1 deletion(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 8e480e6..d3b8c82 100755 --- a/routes.js +++ b/routes.js @@ -1092,7 +1092,6 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { eventImage = event.image; } -<<<<<<< HEAD // 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); -- cgit v1.2.3 From 464885e5bcc3b48ced0812b8f1dc388465237d57 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Fri, 17 Jan 2020 11:24:04 -0800 Subject: Adding Node version specification, fixup typos --- routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index d3b8c82..2b3c052 100755 --- a/routes.js +++ b/routes.js @@ -26,7 +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 +const siteName = require('./config/domain.js').sitename; const siteLogo = require('./config/domain.js').logo_url; const ap = require('./activitypub.js'); -- cgit v1.2.3 From d51ed04584ada2682e203cce143f31ca6d0866bd Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 18 Jan 2020 11:16:34 -0800 Subject: Adding Federation toggle There is now a toggle in the `config/domain-example.js` file called `isFederated`. When set to `true`, ActivityPub functions as normal. When set to false, the ActivityPub functions all immediately return without doing anything, and AP routes return 404s. --- routes.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 2b3c052..4172ab7 100755 --- a/routes.js +++ b/routes.js @@ -28,6 +28,7 @@ const domain = require('./config/domain.js').domain; const contactEmail = require('./config/domain.js').email; const siteName = require('./config/domain.js').sitename; const siteLogo = require('./config/domain.js').logo_url; +const isFederated = require('./config/domain.js').isFederated; const ap = require('./activitypub.js'); // Extra marked renderer (used to render plaintext event description for page metadata) @@ -199,6 +200,7 @@ router.get('/new/event/public', (req, res) => { // return the JSON for the featured/pinned post for this event router.get('/:eventID/featured', (req, res) => { + if (!isFederated) return res.sendStatus(404); const {eventID} = req.params; const guidObject = crypto.randomBytes(16).toString('hex'); const featured = { @@ -214,6 +216,7 @@ router.get('/:eventID/featured', (req, res) => { // return the JSON for a given activitypub message router.get('/:eventID/m/:hash', (req, res) => { + if (!isFederated) return res.sendStatus(404); const {hash, eventID} = req.params; const id = `https://${domain}/${eventID}/m/${hash}`; @@ -246,6 +249,7 @@ router.get('/:eventID/m/:hash', (req, res) => { // return the webfinger record required for the initial activitypub handshake router.get('/.well-known/webfinger', (req, res) => { + if (!isFederated) return res.sendStatus(404); let resource = req.query.resource; if (!resource || !resource.includes('acct:')) { return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.'); @@ -379,6 +383,7 @@ router.get('/:eventID', (req, res) => { res.set("X-Robots-Tag", "noindex"); res.render('event', { domain: domain, + isFederated: isFederated, email: contactEmail, title: event.name, escapedName: escapedName, @@ -422,6 +427,7 @@ router.get('/:eventID', (req, res) => { }) router.get('/:eventID/followers', (req, res) => { + if (!isFederated) return res.sendStatus(404); const eventID = req.params.eventID; Event.findOne({ id: eventID @@ -661,9 +667,9 @@ router.post('/newevent', async (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, firstLoad: true, - activityPubActor: ap.createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), - activityPubEvent: ap.createActivityPubEvent(req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation), - activityPubMessages: [ { id: `https://${domain}/${eventID}/m/featuredPost`, content: JSON.stringify(ap.createFeaturedPost(eventID, req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation)) } ], + activityPubActor: isFederated ? ap.createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) : null, + activityPubEvent: isFederated ? ap.createActivityPubEvent(req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation) : null, + activityPubMessages: isFederated ? [ { id: `https://${domain}/${eventID}/m/featuredPost`, content: JSON.stringify(ap.createFeaturedPost(eventID, req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation)) } ] : [], publicKey: pair.public, privateKey: pair.private }); @@ -875,8 +881,8 @@ 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: ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), - activityPubEvent: ap.updateActivityPubEvent(JSON.parse(event.activityPubEvent), req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone), + activityPubActor: isFederated ? ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) : null, + activityPubEvent: isFederated ? ap.updateActivityPubEvent(JSON.parse(event.activityPubEvent), req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone) : null, } let diffText = '

        This event was just updated with new information.

          '; let displayDate; @@ -1531,6 +1537,7 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => { }); router.post('/activitypub/inbox', (req, res) => { + if (!isFederated) return res.sendStatus(404); // validate the incoming message const signature = req.get('Signature'); let signature_header = signature.split(',').map(pair => { -- cgit v1.2.3 From fc42ff1944a60225e4f111e1ebb1175f0e82f604 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 20 Jan 2020 18:31:17 -0800 Subject: changing federation toggle --- routes.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'routes.js') diff --git a/routes.js b/routes.js index 4172ab7..5080cd0 100755 --- a/routes.js +++ b/routes.js @@ -28,7 +28,11 @@ const domain = require('./config/domain.js').domain; const contactEmail = require('./config/domain.js').email; const siteName = require('./config/domain.js').sitename; const siteLogo = require('./config/domain.js').logo_url; -const isFederated = require('./config/domain.js').isFederated; +let isFederated = require('./config/domain.js').isFederated; +// if the federation config isn't set, things are federated by default +if (isFederated === undefined) { + isFederated = true; +} const ap = require('./activitypub.js'); // Extra marked renderer (used to render plaintext event description for page metadata) @@ -667,9 +671,9 @@ router.post('/newevent', async (req, res) => { usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, firstLoad: true, - activityPubActor: isFederated ? ap.createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) : null, - activityPubEvent: isFederated ? ap.createActivityPubEvent(req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation) : null, - activityPubMessages: isFederated ? [ { id: `https://${domain}/${eventID}/m/featuredPost`, content: JSON.stringify(ap.createFeaturedPost(eventID, req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation)) } ] : [], + activityPubActor: ap.createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), + activityPubEvent: ap.createActivityPubEvent(req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation), + activityPubMessages: [ { id: `https://${domain}/${eventID}/m/featuredPost`, content: JSON.stringify(ap.createFeaturedPost(eventID, req.body.eventName, startUTC, endUTC, req.body.timezone, req.body.eventDescription, req.body.eventLocation)) } ], publicKey: pair.public, privateKey: pair.private }); @@ -881,8 +885,8 @@ 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: isFederated ? ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) : null, - activityPubEvent: isFederated ? ap.updateActivityPubEvent(JSON.parse(event.activityPubEvent), req.body.eventName, req.body.startUTC, req.body.endUTC, req.body.timezone) : null, + activityPubActor: ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone), + activityPubEvent: ap.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; -- cgit v1.2.3