From 453c08463bf46e568c8a5018416ad88498a73f29 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Fri, 12 May 2023 16:54:06 +0100 Subject: linting: source files --- src/routes.js | 2903 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 1903 insertions(+), 1000 deletions(-) (limited to 'src/routes.js') diff --git a/src/routes.js b/src/routes.js index c9867e3..052446c 100755 --- a/src/routes.js +++ b/src/routes.js @@ -1,45 +1,48 @@ -const fs = require('fs'); +const fs = require("fs"); -const express = require('express'); +const express = require("express"); -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); // This alphabet (used to generate all event, group, etc. IDs) is missing '-' // because ActivityPub doesn't like it in IDs -const { customAlphabet } = require('nanoid'); -const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_', 21); +const { customAlphabet } = require("nanoid"); +const nanoid = customAlphabet( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_", + 21 +); const randomstring = require("randomstring"); -const { body, validationResult } = require('express-validator'); +const { body, validationResult } = require("express-validator"); const router = express.Router(); -const Event = mongoose.model('Event'); -const EventGroup = mongoose.model('EventGroup'); -const addToLog = require('./helpers.js').addToLog; +const Event = mongoose.model("Event"); +const EventGroup = mongoose.model("EventGroup"); +const addToLog = require("./helpers.js").addToLog; -var moment = require('moment-timezone'); +var moment = require("moment-timezone"); -const marked = require('marked'); +const marked = require("marked"); -const generateRSAKeypair = require('generate-rsa-keypair'); -const crypto = require('crypto'); -const request = require('request'); -const niceware = require('niceware'); +const generateRSAKeypair = require("generate-rsa-keypair"); +const crypto = require("crypto"); +const request = require("request"); +const niceware = require("niceware"); -const domain = require('./config/domain.js').domain; -const contactEmail = require('./config/domain.js').email; -const mailService = require('./config/domain.js').mailService; -const siteName = require('./config/domain.js').sitename; -const siteLogo = require('./config/domain.js').logo_url; -let isFederated = require('./config/domain.js').isFederated; -let showKofi = require('./config/domain.js').showKofi; +const domain = require("./config/domain.js").domain; +const contactEmail = require("./config/domain.js").email; +const mailService = require("./config/domain.js").mailService; +const siteName = require("./config/domain.js").sitename; +const siteLogo = require("./config/domain.js").logo_url; +let isFederated = require("./config/domain.js").isFederated; +let showKofi = require("./config/domain.js").showKofi; // if the federation config isn't set, things are federated by default if (isFederated === undefined) { isFederated = true; } -const ap = require('./activitypub.js'); +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/ @@ -47,7 +50,7 @@ const ap = require('./activitypub.js'); function htmlEscapeToText(text) { return text.replace(/\&\#[0-9]*;|&/g, function (escapeCode) { if (escapeCode.match(/amp/)) { - return '&'; + return "&"; } return String.fromCharCode(escapeCode.match(/[0-9]+/)); }); @@ -61,45 +64,45 @@ function render_plain() { }; 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'; + return htmlEscapeToText(text) + "\r\n"; }; // render nothing for headings, images, and br render.heading = function (text, level) { - return ''; + return ""; }; render.image = function (href, title, text) { - return ''; + return ""; }; render.br = function () { - return ''; + return ""; }; return render; } -const ical = require('ical'); -const { exportIcal } = require('./helpers.js'); +const ical = require("ical"); +const { exportIcal } = require("./helpers.js"); -const sgMail = require('@sendgrid/mail'); +const sgMail = require("@sendgrid/mail"); const nodemailer = require("nodemailer"); -const apiCredentials = require('./config/api.js'); +const apiCredentials = require("./config/api.js"); let sendEmails = false; let nodemailerTransporter; if (mailService) { switch (mailService) { - case 'sendgrid': + case "sendgrid": sgMail.setApiKey(apiCredentials.sendgrid); console.log("Sendgrid is ready to send emails."); sendEmails = true; break; - case 'nodemailer': + case "nodemailer": nodemailerTransporter = nodemailer.createTransport({ host: apiCredentials.smtpServer, port: apiCredentials.smtpPort, @@ -119,60 +122,104 @@ if (mailService) { }); break; default: - console.error('You have not configured this Gathio instance to send emails! This means that event creators will not receive emails when their events are created, which means they may end up locked out of editing events. Consider setting up an email service.') + console.error( + "You have not configured this Gathio instance to send emails! This means that event creators will not receive emails when their events are created, which means they may end up locked out of editing events. Consider setting up an email service." + ); } } -const fileUpload = require('express-fileupload'); -var Jimp = require('jimp'); +const fileUpload = require("express-fileupload"); +var Jimp = require("jimp"); router.use(fileUpload()); // SCHEDULED DELETION -const schedule = require('node-schedule'); -schedule.scheduleJob('59 23 * * *', function (fireDate) { - const too_old = moment.tz('Etc/UTC').subtract(7, 'days').toDate(); - console.log("Old event deletion running! Deleting all events concluding before ", too_old); - - Event.find({ end: { $lte: too_old } }).then((oldEvents) => { - oldEvents.forEach(event => { - const deleteEventFromDB = (id) => { - Event.remove({ "_id": id }) - .then(response => { - addToLog("deleteOldEvents", "success", "Old event " + id + " deleted"); - }).catch((err) => { - addToLog("deleteOldEvents", "error", "Attempt to delete old event " + id + " failed with error: " + err); - }); - } +const schedule = require("node-schedule"); +schedule.scheduleJob("59 23 * * *", function (fireDate) { + const too_old = moment.tz("Etc/UTC").subtract(7, "days").toDate(); + console.log( + "Old event deletion running! Deleting all events concluding before ", + too_old + ); + + Event.find({ end: { $lte: too_old } }) + .then((oldEvents) => { + oldEvents.forEach((event) => { + const deleteEventFromDB = (id) => { + Event.remove({ _id: id }) + .then((response) => { + addToLog( + "deleteOldEvents", + "success", + "Old event " + id + " deleted" + ); + }) + .catch((err) => { + addToLog( + "deleteOldEvents", + "error", + "Attempt to delete old event " + + id + + " failed with error: " + + err + ); + }); + }; - if (event.image) { - fs.unlink(global.appRoot + '/public/events/' + event.image, (err) => { - if (err) { - addToLog("deleteOldEvents", "error", "Attempt to delete event image for old event " + event.id + " failed with error: " + err); - } - // Image removed - addToLog("deleteOldEvents", "error", "Image deleted for old event " + event.id); - }) - } - // Check if event has ActivityPub fields - if (event.activityPubActor && event.activityPubEvent) { - // 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) { - deleteEventFromDB(event._id); + if (event.image) { + fs.unlink(global.appRoot + "/public/events/" + event.image, (err) => { + if (err) { + addToLog( + "deleteOldEvents", + "error", + "Attempt to delete event image for old event " + + event.id + + " failed with error: " + + err + ); + } + // Image removed + addToLog( + "deleteOldEvents", + "error", + "Image deleted for old event " + event.id + ); }); - }); - } else { - // No ActivityPub data - simply delete the event - deleteEventFromDB(event._id); - } + } + // Check if event has ActivityPub fields + if (event.activityPubActor && event.activityPubEvent) { + // 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) { + deleteEventFromDB(event._id); + } + ); + } + ); + } else { + // No ActivityPub data - simply delete the event + deleteEventFromDB(event._id); + } + }); }) - }).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 + ); + }); // TODO: While we're here, also remove all provisioned event attendees over a day // old (they're not going to become active) @@ -180,8 +227,8 @@ schedule.scheduleJob('59 23 * * *', function (fireDate) { // FRONTEND ROUTES -router.get('/', (req, res) => { - res.render('home', { +router.get("/", (req, res) => { + res.render("home", { domain, email: contactEmail, siteName, @@ -189,192 +236,236 @@ router.get('/', (req, res) => { }); }); -router.get('/new', (req, res) => { - res.render('home'); +router.get("/new", (req, res) => { + res.render("home"); }); -router.get('/new/event', (req, res) => { - res.render('newevent', { +router.get("/new/event", (req, res) => { + res.render("newevent", { domain: domain, email: contactEmail, siteName: siteName, }); }); -router.get('/new/event/public', (req, res) => { +router.get("/new/event/public", (req, res) => { let isPrivate = false; let isPublic = true; let isOrganisation = false; let isUnknownType = false; - res.render('newevent', { - title: 'New event', + res.render("newevent", { + title: "New event", isPrivate: isPrivate, isPublic: isPublic, isOrganisation: isOrganisation, isUnknownType: isUnknownType, - eventType: 'public', + eventType: "public", domain: domain, email: contactEmail, siteName: siteName, }); -}) +}); // return the JSON for the featured/pinned post for this event -router.get('/:eventID/featured', (req, res) => { +router.get("/:eventID/featured", (req, res) => { if (!isFederated) return res.sendStatus(404); const { eventID } = req.params; - const guidObject = crypto.randomBytes(16).toString('hex'); + 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) - ] - } + id: `https://${domain}/${eventID}/featured`, + type: "OrderedCollection", + orderedItems: [ap.createFeaturedPost(eventID)], + }; res.json(featured); }); // return the JSON for a given activitypub message -router.get('/:eventID/m/:hash', (req, res) => { +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}`; Event.findOne({ - id: eventID + 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); + 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 { + } else { res.status(404); - return res.render('404', { url: req.url }); + return res.render("404", { url: req.url }); } } }) .catch((err) => { - addToLog("getActivityPubMessage", "error", "Attempt to get Activity Pub Message for " + id + " failed with error: " + 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 }); + res.render("404", { url: req.url }); return; }); }); // return the webfinger record required for the initial activitypub handshake -router.get('/.well-known/webfinger', (req, res) => { +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.'); - } - else { + 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:', ''); + let activityPubAccount = resource.replace("acct:", ""); // "foo" - let eventID = activityPubAccount.replace(/@.*/, ''); + let eventID = activityPubAccount.replace(/@.*/, ""); Event.findOne({ - id: eventID + id: eventID, }) .then((event) => { if (!event) { res.status(404); - res.render('404', { url: req.url }); - } - else { + res.render("404", { url: req.url }); + } else { res.json(ap.createWebfinger(eventID, domain)); } }) .catch((err) => { - addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + 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 }); + res.render("404", { url: req.url }); return; }); } }); -router.get('/:eventID', (req, res) => { +router.get("/:eventID", (req, res) => { Event.findOne({ - id: req.params.eventID + id: req.params.eventID, }) .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is - .populate('eventGroup') + .populate("eventGroup") .then((event) => { if (event) { - const 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')) { + 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)[]'); - } - 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)[]'); + 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)[]' + ); + } 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)[]' + ); } 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 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))) { + if ( + moment + .tz(event.end, event.timezone) + .isBefore(moment.tz(event.timezone)) + ) { eventHasConcluded = true; } let eventHasBegun = false; - if (moment.tz(event.start, event.timezone).isBefore(moment.tz(event.timezone))) { + if ( + moment + .tz(event.start, event.timezone) + .isBefore(moment.tz(event.timezone)) + ) { eventHasBegun = true; } let fromNow = moment.tz(event.start, event.timezone).fromNow(); let parsedDescription = marked.parse(event.description); let eventEditToken = event.editToken; - let escapedName = event.name.replace(/\s+/g, '+'); + let escapedName = event.name.replace(/\s+/g, "+"); let eventHasCoverImage = false; if (event.image) { eventHasCoverImage = true; - } - else { + } else { eventHasCoverImage = false; } let eventHasHost = false; if (event.hostName) { eventHasHost = true; - } - else { + } else { eventHasHost = false; } let firstLoad = false; if (event.firstLoad === true) { firstLoad = true; - Event.findOneAndUpdate({ id: req.params.eventID }, { firstLoad: false }, function (err, raw) { - if (err) { - res.send(err); + Event.findOneAndUpdate( + { id: req.params.eventID }, + { firstLoad: false }, + function (err, raw) { + if (err) { + res.send(err); + } } - }); + ); } let editingEnabled = false; if (Object.keys(req.query).length !== 0) { if (!req.query.e) { editingEnabled = false; console.log("No edit token set"); - } - else { + } else { if (req.query.e === eventEditToken) { editingEnabled = true; - } - else { + } else { editingEnabled = false; } } } - let eventAttendees = event.attendees.sort((a, b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)) - .map(el => { + 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; } @@ -384,12 +475,15 @@ router.get('/:eventID', (req, res) => { return el; }) .filter((obj, pos, arr) => { - return obj.status === 'attending' && arr.map(mapObj => mapObj.id).indexOf(obj.id) === pos; + return ( + obj.status === "attending" && + arr.map((mapObj) => mapObj.id).indexOf(obj.id) === pos + ); }); let spotsRemaining, noMoreSpots; let numberOfAttendees = eventAttendees.reduce((acc, attendee) => { - if (attendee.status === 'attending') { + if (attendee.status === "attending") { return acc + attendee.number || 1; } return acc; @@ -402,16 +496,27 @@ router.get('/:eventID', (req, res) => { } let metadata = { title: event.name, - description: marked.parse(event.description, { renderer: render_plain() }).split(" ").splice(0, 40).join(" ").trim(), - image: (eventHasCoverImage ? `https://${domain}/events/` + event.image : null), - url: `https://${domain}/` + req.params.eventID + description: marked + .parse(event.description, { renderer: render_plain() }) + .split(" ") + .splice(0, 40) + .join(" ") + .trim(), + 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'))) { + 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 { + } else { res.set("X-Robots-Tag", "noindex"); - res.render('event', { + res.render("event", { domain: domain, isFederated: isFederated, email: contactEmail, @@ -438,58 +543,61 @@ router.get('/:eventID', (req, res) => { eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, metadata: metadata, - siteName: siteName - }) + siteName: siteName, + }); } - } - else { + } else { res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); } - }) .catch((err) => { - addToLog("displayEvent", "error", "Attempt to display event " + req.params.eventID + " failed with error: " + err); - console.log(err) + addToLog( + "displayEvent", + "error", + "Attempt to display event " + + req.params.eventID + + " failed with error: " + + err + ); + console.log(err); res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); return; }); -}) +}); -router.get('/:eventID/followers', (req, res) => { +router.get("/:eventID/followers", (req, res) => { if (!isFederated) return res.sendStatus(404); 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.'); - } - }) -}) + 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) => { +router.get("/group/:eventGroupID", (req, res) => { EventGroup.findOne({ - id: req.params.eventGroupID + id: req.params.eventGroupID, }) .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is .then(async (eventGroup) => { @@ -497,78 +605,99 @@ router.get('/group/:eventGroupID', (req, res) => { let parsedDescription = marked.parse(eventGroup.description); let eventGroupEditToken = eventGroup.editToken; - let escapedName = eventGroup.name.replace(/\s+/g, '+'); + let escapedName = eventGroup.name.replace(/\s+/g, "+"); let eventGroupHasCoverImage = false; if (eventGroup.image) { eventGroupHasCoverImage = true; - } - else { + } else { eventGroupHasCoverImage = false; } let eventGroupHasHost = false; if (eventGroup.hostName) { eventGroupHasHost = true; - } - else { + } else { eventGroupHasHost = false; } - let events = await Event.find({ eventGroup: eventGroup._id }).lean().sort('start'); + let events = await Event.find({ eventGroup: eventGroup._id }) + .lean() + .sort("start"); - events.map(event => { - if (moment.tz(event.end, event.timezone).isSame(event.start, 'day')) { + events.map((event) => { + if (moment.tz(event.end, event.timezone).isSame(event.start, "day")) { // Happening during one day - event.displayDate = moment.tz(event.start, event.timezone).format('D MMM YYYY'); - } - else { - event.displayDate = moment.tz(event.start, event.timezone).format('D MMM YYYY') + moment.tz(event.end, event.timezone).format(' - D MMM YYYY'); + event.displayDate = moment + .tz(event.start, event.timezone) + .format("D MMM YYYY"); + } else { + event.displayDate = + moment.tz(event.start, event.timezone).format("D MMM YYYY") + + moment.tz(event.end, event.timezone).format(" - D MMM YYYY"); } - if (moment.tz(event.end, event.timezone).isBefore(moment.tz(event.timezone))) { + if ( + moment + .tz(event.end, event.timezone) + .isBefore(moment.tz(event.timezone)) + ) { event.eventHasConcluded = true; } else { event.eventHasConcluded = false; } - return (({ id, name, displayDate, eventHasConcluded }) => ({ id, name, displayDate, eventHasConcluded }))(event); + return (({ id, name, displayDate, eventHasConcluded }) => ({ + id, + name, + displayDate, + eventHasConcluded, + }))(event); }); let upcomingEventsExist = false; - if (events.some(e => e.eventHasConcluded === false)) { + if (events.some((e) => e.eventHasConcluded === false)) { upcomingEventsExist = true; } let firstLoad = false; if (eventGroup.firstLoad === true) { firstLoad = true; - EventGroup.findOneAndUpdate({ id: req.params.eventGroupID }, { firstLoad: false }, function (err, raw) { - if (err) { - res.send(err); + EventGroup.findOneAndUpdate( + { id: req.params.eventGroupID }, + { firstLoad: false }, + function (err, raw) { + if (err) { + res.send(err); + } } - }); + ); } let editingEnabled = false; if (Object.keys(req.query).length !== 0) { if (!req.query.e) { editingEnabled = false; console.log("No edit token set"); - } - else { + } else { if (req.query.e === eventGroupEditToken) { editingEnabled = true; - } - else { + } else { editingEnabled = false; } } } let metadata = { title: eventGroup.name, - description: marked.parse(eventGroup.description, { renderer: render_plain() }).split(" ").splice(0, 40).join(" ").trim(), - image: (eventGroupHasCoverImage ? `https://${domain}/events/` + eventGroup.image : null), - url: `https://${domain}/` + req.params.eventID + description: marked + .parse(eventGroup.description, { renderer: render_plain() }) + .split(" ") + .splice(0, 40) + .join(" ") + .trim(), + image: eventGroupHasCoverImage + ? `https://${domain}/events/` + eventGroup.image + : null, + url: `https://${domain}/` + req.params.eventID, }; res.set("X-Robots-Tag", "noindex"); - res.render('eventgroup', { + res.render("eventgroup", { domain: domain, title: eventGroup.name, eventGroupData: eventGroup, @@ -580,51 +709,65 @@ router.get('/group/:eventGroupID', (req, res) => { eventGroupHasCoverImage: eventGroupHasCoverImage, eventGroupHasHost: eventGroupHasHost, firstLoad: firstLoad, - metadata: metadata - }) - } - else { + metadata: metadata, + }); + } else { res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); } - }) .catch((err) => { - addToLog("displayEventGroup", "error", "Attempt to display event group " + req.params.eventGroupID + " failed with error: " + err); - console.log(err) + addToLog( + "displayEventGroup", + "error", + "Attempt to display event group " + + req.params.eventGroupID + + " failed with error: " + + err + ); + console.log(err); res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); return; }); -}) +}); -router.get('/group/:eventGroupID/feed.ics', (req, res) => { +router.get("/group/:eventGroupID/feed.ics", (req, res) => { EventGroup.findOne({ - id: req.params.eventGroupID + id: req.params.eventGroupID, }) .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is .then(async (eventGroup) => { if (eventGroup) { - let events = await Event.find({ eventGroup: eventGroup._id }).lean().sort('start'); + let events = await Event.find({ eventGroup: eventGroup._id }) + .lean() + .sort("start"); const string = exportIcal(events, eventGroup.name); - res.set('Content-Type', 'text/calendar'); + res.set("Content-Type", "text/calendar"); return res.send(string); } }) .catch((err) => { - addToLog("eventGroupFeed", "error", "Attempt to display event group feed for " + req.params.eventGroupID + " failed with error: " + err); - console.log(err) + addToLog( + "eventGroupFeed", + "error", + "Attempt to display event group feed for " + + req.params.eventGroupID + + " failed with error: " + + err + ); + console.log(err); res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); return; }); }); -router.get('/exportevent/:eventID', (req, res) => { +router.get("/exportevent/:eventID", (req, res) => { Event.findOne({ - id: req.params.eventID + id: req.params.eventID, }) - .populate('eventGroup') + .populate("eventGroup") .then((event) => { if (event) { const string = exportIcal([event]); @@ -632,38 +775,54 @@ router.get('/exportevent/:eventID', (req, res) => { } }) .catch((err) => { - addToLog("exportEvent", "error", "Attempt to export event " + req.params.eventID + " failed with error: " + err); - console.log(err) + addToLog( + "exportEvent", + "error", + "Attempt to export event " + + req.params.eventID + + " failed with error: " + + err + ); + console.log(err); res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); return; }); }); -router.get('/exportgroup/:eventGroupID', (req, res) => { +router.get("/exportgroup/:eventGroupID", (req, res) => { EventGroup.findOne({ - id: req.params.eventGroupID + id: req.params.eventGroupID, }) .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is .then(async (eventGroup) => { if (eventGroup) { - let events = await Event.find({ eventGroup: eventGroup._id }).lean().sort('start'); + let events = await Event.find({ eventGroup: eventGroup._id }) + .lean() + .sort("start"); const string = exportIcal(events); res.send(string); } }) .catch((err) => { - addToLog("exportEvent", "error", "Attempt to export event group " + req.params.eventGroupID + " failed with error: " + err); - console.log(err) + addToLog( + "exportEvent", + "error", + "Attempt to export event group " + + req.params.eventGroupID + + " failed with error: " + + err + ); + console.log(err); res.status(404); - res.render('404', { url: req.url }); + res.render("404", { url: req.url }); return; }); }); // BACKEND ROUTES -router.post('/newevent', async (req, res) => { +router.post("/newevent", async (req, res) => { let eventID = nanoid(); let editToken = randomstring.generate(); let eventImageFilename = ""; @@ -671,26 +830,38 @@ router.post('/newevent', async (req, res) => { if (req.files && Object.keys(req.files).length !== 0) { let eventImageBuffer = req.files.imageUpload.data; eventImageFilename = await Jimp.read(eventImageBuffer) - .then(img => { + .then((img) => { img .resize(920, Jimp.AUTO) // resize .quality(80) // set JPEG quality - .write('./public/events/' + eventID + '.jpg'); // save - const filename = eventID + '.jpg'; + .write("./public/events/" + eventID + ".jpg"); // save + const filename = eventID + ".jpg"; return filename; - }) - .catch(err => { - addToLog("Jimp", "error", "Attempt to edit image failed with error: " + err); + }) + .catch((err) => { + addToLog( + "Jimp", + "error", + "Attempt to edit image failed with error: " + err + ); }); } - 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 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({ id: req.body.eventGroupID, - editToken: req.body.eventGroupEditToken - }) + editToken: req.body.eventGroupEditToken, + }); if (eventGroup) { isPartOfEventGroup = true; } @@ -721,97 +892,171 @@ 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.parse(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: ap.createActivityPubActor( + eventID, + domain, + pair.public, + marked.parse(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 + privateKey: pair.private, }); - event.save() + event + .save() .then((event) => { addToLog("createEvent", "success", "Event " + eventID + "created"); // Send email with edit link if (req.body.creatorEmail && sendEmails) { - req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', { eventID, editToken, siteName, siteLogo, domain, cache: true, layout: 'email.handlebars' }, function (err, html) { - const msg = { - to: req.body.creatorEmail, - from: { - name: siteName, - email: contactEmail, - address: contactEmail + req.app + .get("hbsInstance") + .renderView( + "./views/emails/createevent.handlebars", + { + eventID, + editToken, + siteName, + siteLogo, + domain, + cache: true, + layout: "email.handlebars", }, - subject: `${siteName}: ${req.body.eventName}`, - html, - }; - switch (mailService) { - case 'sendgrid': - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case 'nodemailer': - nodemailerTransporter.sendMail(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - } - }); + function (err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + address: contactEmail, + }, + subject: `${siteName}: ${req.body.eventName}`, + html, + }; + switch (mailService) { + case "sendgrid": + sgMail.send(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + case "nodemailer": + nodemailerTransporter.sendMail(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + } + } + ); } // If the event was added to a group, send an email to any group // subscribers if (event.eventGroup && sendEmails) { - EventGroup.findOne({ _id: event.eventGroup._id }) - .then((eventGroup) => { - const subscribers = eventGroup.subscribers.reduce((acc, current) => { - if (acc.includes(current.email)) { - return acc; - } - return [current.email, ...acc]; - }, []); - subscribers.forEach(emailAddress => { - req.app.get('hbsInstance').renderView('./views/emails/eventgroupupdated.handlebars', { siteName, siteLogo, domain, eventID: req.params.eventID, eventGroupName: eventGroup.name, eventName: event.name, eventID: event.id, eventGroupID: eventGroup.id, emailAddress: encodeURIComponent(emailAddress), cache: true, layout: 'email.handlebars' }, function (err, html) { - const msg = { - to: emailAddress, - from: { - name: siteName, - email: contactEmail, - }, - subject: `${siteName}: New event in ${eventGroup.name}`, - html, - }; - switch (mailService) { - case 'sendgrid': - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case 'nodemailer': - nodemailerTransporter.sendMail(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; + EventGroup.findOne({ _id: event.eventGroup._id }).then((eventGroup) => { + const subscribers = eventGroup.subscribers.reduce((acc, current) => { + if (acc.includes(current.email)) { + return acc; + } + return [current.email, ...acc]; + }, []); + subscribers.forEach((emailAddress) => { + req.app + .get("hbsInstance") + .renderView( + "./views/emails/eventgroupupdated.handlebars", + { + siteName, + siteLogo, + domain, + eventID: req.params.eventID, + eventGroupName: eventGroup.name, + eventName: event.name, + eventID: event.id, + eventGroupID: eventGroup.id, + emailAddress: encodeURIComponent(emailAddress), + cache: true, + layout: "email.handlebars", + }, + function (err, html) { + const msg = { + to: emailAddress, + from: { + name: siteName, + email: contactEmail, + }, + subject: `${siteName}: New event in ${eventGroup.name}`, + html, + }; + switch (mailService) { + case "sendgrid": + sgMail.send(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + case "nodemailer": + nodemailerTransporter.sendMail(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + } } - }); - }); + ); }); + }); } res.writeHead(302, { - 'Location': '/' + eventID + '?e=' + editToken + Location: "/" + eventID + "?e=" + editToken, }); res.end(); }) - .catch((err) => { res.status(500).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) => { +router.post("/importevent", (req, res) => { let eventID = nanoid(); let editToken = randomstring.generate(); if (req.files && Object.keys(req.files).length !== 0) { - let iCalObject = ical.parseICS(req.files.icsImportControl.data.toString('utf8')); + let iCalObject = ical.parseICS( + req.files.icsImportControl.data.toString("utf8") + ); let importedEventData = iCalObject[Object.keys(iCalObject)]; let creatorEmail; @@ -823,84 +1068,115 @@ router.post('/importevent', (req, res) => { const event = new Event({ id: eventID, - type: 'public', + type: "public", name: importedEventData.summary, 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: '', + image: "", creatorEmail: creatorEmail, - url: '', - hostName: importedEventData.organizer ? importedEventData.organizer.params.CN.replace(/["]+/g, '') : "", - viewPassword: '', - editPassword: '', + url: "", + hostName: importedEventData.organizer + ? importedEventData.organizer.params.CN.replace(/["]+/g, "") + : "", + viewPassword: "", + editPassword: "", editToken: editToken, usersCanAttend: false, showUsersList: false, usersCanComment: false, - firstLoad: true + firstLoad: true, }); - event.save() + event + .save() .then(() => { addToLog("createEvent", "success", "Event " + eventID + " created"); // Send email with edit link if (creatorEmail && sendEmails) { - req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', { eventID, editToken, siteName, siteLogo, domain, cache: true, layout: 'email.handlebars' }, function (err, html) { - const msg = { - to: req.body.creatorEmail, - from: { - name: siteName, - email: contactEmail, - address: contactEmail + req.app + .get("hbsInstance") + .renderView( + "./views/emails/createevent.handlebars", + { + eventID, + editToken, + siteName, + siteLogo, + domain, + cache: true, + layout: "email.handlebars", }, - subject: `${siteName}: ${importedEventData.summary}`, - html, - }; - switch (mailService) { - case 'sendgrid': - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case 'nodemailer': - nodemailerTransporter.sendMail(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - } - }); + function (err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + address: contactEmail, + }, + subject: `${siteName}: ${importedEventData.summary}`, + html, + }; + switch (mailService) { + case "sendgrid": + sgMail.send(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + case "nodemailer": + nodemailerTransporter.sendMail(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + } + } + ); } res.writeHead(302, { - 'Location': '/' + eventID + '?e=' + editToken + Location: "/" + eventID + "?e=" + editToken, }); res.end(); }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err); }); - } - else { - console.log("Files array is empty!") + .catch((err) => { + res.send("Database error, please try again :("); + addToLog( + "createEvent", + "error", + "Attempt to create event failed with error: " + err + ); + }); + } else { + console.log("Files array is empty!"); res.status(500).end(); } }); -router.post('/neweventgroup', (req, res) => { +router.post("/neweventgroup", (req, res) => { let eventGroupID = nanoid(); let editToken = randomstring.generate(); let eventGroupImageFilename = ""; 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); + if (err) + addToLog( + "Jimp", + "error", + "Attempt to edit image failed with error: " + err + ); img .resize(920, Jimp.AUTO) // resize .quality(80) // set JPEG quality - .write('./public/events/' + eventGroupID + '.jpg'); // save + .write("./public/events/" + eventGroupID + ".jpg"); // save }); - eventGroupImageFilename = eventGroupID + '.jpg'; + eventGroupImageFilename = eventGroupID + ".jpg"; } const eventGroup = new EventGroup({ id: eventGroupID, @@ -911,76 +1187,99 @@ router.post('/neweventgroup', (req, res) => { url: req.body.eventGroupURL, hostName: req.body.hostName, editToken: editToken, - firstLoad: true + firstLoad: true, }); - eventGroup.save() + eventGroup + .save() .then(() => { - addToLog("createEventGroup", "success", "Event group " + eventGroupID + " created"); + addToLog( + "createEventGroup", + "success", + "Event group " + eventGroupID + " created" + ); // Send email with edit link if (req.body.creatorEmail && sendEmails) { - req.app.get('hbsInstance').renderView('./views/emails/createeventgroup.handlebars', { eventGroupID, editToken, siteName, siteLogo, domain, cache: true, layout: 'email.handlebars' }, function (err, html) { - const msg = { - to: req.body.creatorEmail, - from: { - name: siteName, - email: contactEmail, - address: contactEmail + req.app + .get("hbsInstance") + .renderView( + "./views/emails/createeventgroup.handlebars", + { + eventGroupID, + editToken, + siteName, + siteLogo, + domain, + cache: true, + layout: "email.handlebars", }, - subject: `${siteName}: ${req.body.eventGroupName}`, - html, - }; - switch (mailService) { - case 'sendgrid': - sgMail.send(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case 'nodemailer': - nodemailerTransporter.sendMail(msg).catch(e => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - } - }); + function (err, html) { + const msg = { + to: req.body.creatorEmail, + from: { + name: siteName, + email: contactEmail, + address: contactEmail, + }, + subject: `${siteName}: ${req.body.eventGroupName}`, + html, + }; + switch (mailService) { + case "sendgrid": + sgMail.send(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + case "nodemailer": + nodemailerTransporter.sendMail(msg).catch((e) => { + console.error(e.toString()); + res.status(500).end(); + }); + break; + } + } + ); } res.writeHead(302, { - 'Location': '/group/' + eventGroupID + '?e=' + editToken + Location: "/group/" + eventGroupID + "?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.send("Database error, please try again :( - " + err); + addToLog( + "createEvent", + "error", + "Attempt to create event failed with error: " + err + ); + }); }); -router.post('/verifytoken/event/:eventID', (req, res) => { +router.post("/verifytoken/event/:eventID", (req, res) => { Event.findOne({ id: req.params.eventID, editToken: req.body.editToken, - }) - .then(event => { - if (event) return res.sendStatus(200); - return res.sendStatus(404); - }) + }).then((event) => { + if (event) return res.sendStatus(200); + return res.sendStatus(404); + }); }); -router.post('/verifytoken/group/:eventGroupID', (req, res) => { +router.post("/verifytoken/group/:eventGroupID", (req, res) => { EventGroup.findOne({ id: req.params.eventGroupID, editToken: req.body.editToken, - }) - .then(group => { - if (group) return res.sendStatus(200); - return res.sendStatus(404); - }) + }).then((group) => { + if (group) return res.sendStatus(200); + return res.sendStatus(404); + }); }); - -router.post('/editevent/:eventID/:editToken', (req, res) => { +router.post("/editevent/:eventID/:editToken", (req, res) => { let submittedEditToken = req.params.editToken; - Event.findOne(({ + Event.findOne({ id: req.params.eventID, - })) + }) .then(async (event) => { if (event.editToken === submittedEditToken) { // Token matches @@ -995,20 +1294,28 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { img .resize(920, Jimp.AUTO) // resize .quality(80) // set JPEG - .write('./public/events/' + eventID + '.jpg'); // save + .write("./public/events/" + eventID + ".jpg"); // save }); - eventImageFilename = eventID + '.jpg'; + eventImageFilename = eventID + ".jpg"; } - 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 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 isPartOfEventGroup = false; let eventGroup; if (req.body.eventGroupCheckbox) { eventGroup = await EventGroup.findOne({ id: req.body.eventGroupID, - editToken: req.body.eventGroupEditToken - }) + editToken: req.body.eventGroupEditToken, + }); if (eventGroup) { isPartOfEventGroup = true; } @@ -1026,12 +1333,34 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { usersCanAttend: req.body.joinCheckbox ? true : false, showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, - maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null, + maxAttendees: req.body.maxAttendeesCheckbox + ? req.body.maxAttendees + : null, eventGroup: isPartOfEventGroup ? eventGroup._id : null, - activityPubActor: event.activityPubActor ? ap.updateActivityPubActor(JSON.parse(event.activityPubActor), req.body.eventDescription, req.body.eventName, req.body.eventLocation, eventImageFilename, startUTC, endUTC, req.body.timezone) : null, - activityPubEvent: event.activityPubEvent ? 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.