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

This event was just updated with new information.