summaryrefslogtreecommitdiff
path: root/routes.js
diff options
context:
space:
mode:
Diffstat (limited to 'routes.js')
-rwxr-xr-xroutes.js719
1 files changed, 520 insertions, 199 deletions
diff --git a/routes.js b/routes.js
index eb3ac10..cd81da5 100755
--- a/routes.js
+++ b/routes.js
@@ -14,62 +14,67 @@ 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');
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;
const siteName = require('./config/domain.js').sitename
+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');
const icalGenerator = require('ical-generator');
const cal = icalGenerator({
- domain: 'gath.io',
- name: 'Gathio'
+ domain: domain,
+ name: siteName
});
const sgMail = require('@sendgrid/mail');
@@ -86,17 +91,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
@@ -117,12 +111,21 @@ 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);
+ const jsonEventObject = JSON.parse(event.activityPubEvent);
+ // first broadcast AP messages, THEN delete from DB
+ 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);
+ });
+ });
+ });
})
}).catch((err) => {
addToLog("deleteOldEvents", "error", "Attempt to delete old event "+event.id+" failed with error: " + err);
@@ -130,10 +133,31 @@ 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) => {
- res.render('home');
+ res.render('home', {
+ domain: domain,
+ email: contactEmail,
+ siteName: siteName,
+ });
});
router.get('/new', (req, res) => {
@@ -153,7 +177,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;
@@ -179,10 +207,89 @@ 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}`;
+
+ 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);
+ res.status(404);
+ res.render('404', { url: req.url });
+ return;
+ });
+});
+
+router.get('/.well-known/webfinger', (req, res) => {
+ 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(/@.*/,'');
+ 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);
+ res.status(404);
+ res.render('404', { url: req.url });
+ return;
+ });
+ }
+});
+
router.get('/:eventID', (req, res) => {
Event.findOne({
id: req.params.eventID
@@ -190,7 +297,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 [<span class="text-muted">from</span>] h:mm a') + moment.tz(event.end, event.timezone).format(' [<span class="text-muted">to</span>] h:mm a [<span class="text-muted">](z)[</span>]');
@@ -198,10 +306,10 @@ router.get('/:eventID', (req, res) => {
else {
displayDate = moment.tz(event.start, event.timezone).format('dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a') + moment.tz(event.end, event.timezone).format(' [<span class="text-muted">–</span>] dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a [<span class="text-muted">](z)[</span>]');
}
- 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;
@@ -210,11 +318,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 ) {
@@ -246,7 +354,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 {
@@ -275,34 +383,42 @@ 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));
+ }
+ 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,
+ siteName: siteName
+ })
+ }
}
else {
res.status(404);
@@ -319,16 +435,45 @@ router.get('/:eventID', (req, res) => {
});
})
+router.get('/:eventID/followers', (req, res) => {
+ const eventID = req.params.eventID;
+ Event.findOne({
+ id: eventID
+ })
+ .then((event) => {
+ if (event) {
+ const followers = event.followers.map(el => el.actorId);
+ 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
})
.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 ) {
@@ -363,7 +508,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;
}
@@ -383,7 +528,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 {
@@ -394,11 +539,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,
@@ -474,10 +620,13 @@ router.get('/exportevent/:eventID', (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);
@@ -488,8 +637,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({
@@ -500,6 +649,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,
@@ -521,7 +674,12 @@ 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: 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
});
event.save()
.then((event) => {
@@ -555,11 +713,9 @@ 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) {
- importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8'));
- for (var key in importediCalObject) {
- importedEventData = importediCalObject[key];
- }
+ if (req.files && Object.keys(req.files).length !== 0) {
+ let importediCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8'));
+ let importedEventData = importediCalObject;
console.log(importedEventData)
let creatorEmail;
if (req.body.creatorEmail) {
@@ -577,7 +733,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,
@@ -629,7 +785,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);
@@ -693,7 +849,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;
@@ -704,19 +860,20 @@ 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
})
if (eventGroup) {
isPartOfEventGroup = true;
}
- }
+ }
const updatedEvent = {
name: req.body.eventName,
location: req.body.eventLocation,
@@ -731,8 +888,34 @@ 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: 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 = '<p>This event was just updated with new information.</p><ul>';
+ let displayDate;
+ if (event.name !== updatedEvent.name) {
+ diffText += `<li>the event name changed to ${updatedEvent.name}</li>`;
+ }
+ if (event.location !== updatedEvent.location) {
+ diffText += `<li>the location changed to ${updatedEvent.location}</li>`;
+ }
+ if (event.start.toISOString() !== updatedEvent.start.toISOString()) {
+ displayDate = moment.tz(updatedEvent.start, updatedEvent.timezone).format('dddd D MMMM YYYY h:mm a');
+ diffText += `<li>the start time changed to ${displayDate}</li>`;
+ }
+ if (event.end.toISOString() !== updatedEvent.end.toISOString()) {
+ displayDate = moment.tz(updatedEvent.end, updatedEvent.timezone).format('dddd D MMMM YYYY h:mm a');
+ diffText += `<li>the end time changed to ${displayDate}</li>`;
+ }
+ if (event.timezone !== updatedEvent.timezone) {
+ console.log(typeof event.timezone, JSON.stringify(event.timezone), JSON.stringify(updatedEvent.timezone))
+ diffText += `<li>the time zone changed to ${updatedEvent.timezone}</li>`;
+ }
+ if (event.description !== updatedEvent.description) {
+ diffText += `<li>the event description changed</li>`;
+ }
+ diffText += `</ul>`;
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);
@@ -741,10 +924,47 @@ 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}/${req.params.eventID}/m/${guidObject}`,
+ "name": `RSVP to ${event.name}`,
+ "type": "Note",
+ 'cc': 'https://www.w3.org/ns/activitystreams#Public',
+ "content": `${diffText} See here: <a href="https://${domain}/${req.params.eventID}">https://${domain}/${req.params.eventID}</a>`,
+ }
+ 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);
+ 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);
+ ap.broadcastUpdateMessage(jsonEventObject, 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": `<span class=\"h-card\"><a href="${attendee.id}" class="u-url mention">@<span>${attendee.name}</span></a></span> ${diffText} See here: <a href="https://${domain}/${req.params.eventID}">https://${domain}/${req.params.eventID}</a>`,
+ "tag":[{"type":"Mention","href":attendee.id,"name":attendee.name}]
+ }
+ // send direct message to user
+ ap.sendDirectMessage(jsonObject, attendee.id, eventID);
+ }
+ }
+ })
if (sendEmails) {
Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) {
- attendeeEmails = ids;
- if (!error && attendeeEmails != ""){
+ 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) {
const msg = {
@@ -795,7 +1015,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;
@@ -821,32 +1041,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: 'notifications@gath.io',
- // },
- // 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
});
@@ -912,58 +1106,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
+ ap.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) {
+ 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) {
+ 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
@@ -1040,6 +1239,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(() => {
@@ -1108,6 +1308,42 @@ router.post('/unattendevent/:eventID', (req, res) => {
});
});
+router.get('/oneclickunattendevent/:eventID/:attendeeID', (req, res) => {
+ Event.update(
+ { id: req.params.eventID },
+ { $pull: { attendees: { _id: req.params.attendeeID } } }
+ )
+ .then(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 },
@@ -1157,13 +1393,26 @@ 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(() => {
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}/${req.params.eventID}/m/${guidObject}`,
+ "name": `Comment on ${event.name}`,
+ "type": "Note",
+ 'cc': 'https://www.w3.org/ns/activitystreams#Public',
+ "content": `<p>${req.body.commentAuthor} commented: ${req.body.commentContent}.</p><p><a href="https://${domain}/${req.params.eventID}/">See the full conversation here.</a></p>`,
+ }
+ 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) {
@@ -1208,14 +1457,26 @@ 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()
.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}/${req.params.eventID}/m/${guidObject}`,
+ "name": `Comment on ${event.name}`,
+ "type": "Note",
+ 'cc': 'https://www.w3.org/ns/activitystreams#Public',
+ "content": `<p>${req.body.replyAuthor} commented: ${req.body.replyContent}</p><p><a href="https://${domain}/${req.params.eventID}/">See the full conversation here.</a></p>`,
+ }
+ 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) {
@@ -1276,6 +1537,66 @@ 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) => {
+ // 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
+ // 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) {
+ 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)') {
+ 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')
+ try {
+ const result = verifier.verify(publicKeyBuf, signatureBuf)
+ 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);
+ }
+ });
+});
+
router.use(function(req, res, next){
res.status(404);
res.render('404', { url: req.url });