From 5faad9ea56dcf2d715c9e11e07490f50115d25bb Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Wed, 11 Dec 2019 18:11:09 -0800 Subject: First pass at federation! --- .gitignore | 1 + README.md | 2 +- config/domain-example.js | 3 + models/Event.js | 14 ++- package-lock.json | 5 ++ package.json | 1 + routes.js | 195 +++++++++++++++++++++++++++++++++--------- views/event.handlebars | 4 +- views/eventgroup.handlebars | 6 +- views/layouts/main.handlebars | 8 +- 10 files changed, 186 insertions(+), 53 deletions(-) create mode 100644 config/domain-example.js diff --git a/.gitignore b/.gitignore index a31f385..16d3fa8 100755 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ config/api.js config/database.js +config/domain.js public/events/* !public/events/.gitkeep diff --git a/README.md b/README.md index d40c0f0..6b4025d 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,5 @@ You can use the publicly hosted version [here](https://gath.io). 1. Clone the repository 2. Open the directory, run `npm install` -3. Rename `config/api-example.js` and `config/database-example.js` to `config/api.js` and `config/database.js`. For locally hosted versions, the local MongoDB configuration will work fine. To send emails, you need to set up a Sendgrid account and get an API key, which you should paste into `config/api.js`. +3. Rename `config/api-example.js` and `config/database-example.js` and `config/domain-example.js` to `config/api.js` and `config/database.js` and `config/domain.js`. For locally hosted versions, the local MongoDB configuration will work fine. To send emails, you need to set up a Sendgrid account and get an API key, which you should paste into `config/api.js`. 4. Run `npm start`. Enjoy! diff --git a/config/domain-example.js b/config/domain-example.js new file mode 100644 index 0000000..3cb4c85 --- /dev/null +++ b/config/domain-example.js @@ -0,0 +1,3 @@ +module.exports = { + 'domain' : 'localhost:3000' // Your domain goes here +}; diff --git a/models/Event.js b/models/Event.js index 3c0bb8c..1de6088 100755 --- a/models/Event.js +++ b/models/Event.js @@ -159,7 +159,19 @@ const EventSchema = new mongoose.Schema({ maxAttendees: { type: Number }, - comments: [CommentSchema] + comments: [CommentSchema], + activityPubActor: { + type: String, + trim: true + }, + publicKey: { + type: String, + trim: true + }, + privateKey: { + type: String, + trim: true + } }); module.exports = mongoose.model('Event', EventSchema); diff --git a/package-lock.json b/package-lock.json index 2e1d899..58c466e 100755 --- a/package-lock.json +++ b/package-lock.json @@ -2560,6 +2560,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "generate-rsa-keypair": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/generate-rsa-keypair/-/generate-rsa-keypair-0.2.1.tgz", + "integrity": "sha512-vxLfzfy6WbMLtkKV4AJtg7QH0ZqGGNkSYM6S0Q72Z70QXsztLklKFtX15te3YLIqmiQAYi3g3MWsTfXd6djkpg==" + }, "get-stream": { "version": "3.0.0", "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", diff --git a/package.json b/package.json index fff1c2d..1368503 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "express-jwt": "^5.3.1", "express-session": "^1.15.6", "express-validator": "^5.3.0", + "generate-rsa-keypair": "^0.2.1", "greenlock": "^2.6.7", "greenlock-express": "^2.6.7", "ical": "^0.6.0", diff --git a/routes.js b/routes.js index ee723e2..c04077b 100755 --- a/routes.js +++ b/routes.js @@ -20,6 +20,11 @@ var moment = require('moment-timezone'); const marked = require('marked'); +const generateRSAKeypair = require('generate-rsa-keypair'); + +const domain = require('./config/domain.js').domain; +const contactEmail = require('./config/domain.js').email; + // Extra marked renderer (used to render plaintext event description for page metadata) // Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ // ? to ? helper @@ -121,10 +126,49 @@ const deleteOldEvents = schedule.scheduleJob('59 23 * * *', function(fireDate){ }); +// ACTIVITYPUB HELPER FUNCTIONS +function createWebfinger(eventID, domain) { + return { + 'subject': `acct:${eventID}@${domain}`, + + 'links': [ + { + 'rel': 'self', + 'type': 'application/activity+json', + 'href': `https://${domain}/${eventID}` + } + ] + }; +} + +function createActivityPubActor(eventID, domain, pubkey) { + return JSON.stringify({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + + 'id': `https://${domain}/u/${eventID}`, + 'type': 'Person', + 'preferredUsereventID': `${eventID}`, + 'inbox': `https://${domain}/api/inbox`, + 'followers': `https://${domain}/u/${eventID}/followers`, + + 'publicKey': { + 'id': `https://${domain}/u/${eventID}#main-key`, + 'owner': `https://${domain}/u/${eventID}`, + 'publicKeyPem': pubkey + } + }); +} + // FRONTEND ROUTES router.get('/', (req, res) => { - res.render('home'); + res.render('home', { + domain: domain, + email: contactEmail, + }); }); router.get('/new', (req, res) => { @@ -174,6 +218,48 @@ router.get('/new/event/public', (req, res) => { }); }) +router.get('/.well-known/webfinger', (req, res) => { + console.log(req.query); + let resource = req.query.resource; + if (!resource || !resource.includes('acct:')) { + return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.'); + } + else { + // "foo@domain" + let activityPubAccount = resource.replace('acct:',''); + // "foo" + let eventID = activityPubAccount.replace(/@.*/,''); + console.log(eventID); + Event.findOne({ + id: eventID + }) + .then((event) => { + if (!event) { + res.status(404); + res.render('404', { url: req.url }); + } + else { + res.json(createWebfinger(eventID, domain)); + } + }) + .catch((err) => { + addToLog("renderWebfinger", "error", "Attempt to render webfinger for " + req.params.eventID + " failed with error: " + err); + console.log(err) + res.status(404); + res.render('404', { url: req.url }); + return; + }); + //let db = req.app.get('db'); + //let result = db.prepare('select webfinger from accounts where name = ?').get(name); + //if (result === undefined) { + // return res.status(404).send(`No record found for ${name}.`); + //} + //else { + // res.json(JSON.parse(result.webfinger)); + //} + } +}); + router.get('/:eventID', (req, res) => { Event.findOne({ id: req.params.eventID @@ -256,34 +342,51 @@ router.get('/:eventID', (req, res) => { let metadata = { title: event.name, description: marked(event.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), - image: (eventHasCoverImage ? 'https://gath.io/events/' + event.image : null), - url: 'https://gath.io/' + req.params.eventID + image: (eventHasCoverImage ? `https://${domain}/events/` + event.image : null), + url: `https://${domain}/` + req.params.eventID }; - res.set("X-Robots-Tag", "noindex"); - res.render('event', { - title: event.name, - escapedName: escapedName, - eventData: event, - eventAttendees: eventAttendees, - spotsRemaining: spotsRemaining, - noMoreSpots: noMoreSpots, - eventStartISO: eventStartISO, - eventEndISO: eventEndISO, - parsedLocation: parsedLocation, - parsedStart: parsedStart, - parsedEnd: parsedEnd, - displayDate: displayDate, - fromNow: fromNow, - timezone: event.timezone, - parsedDescription: parsedDescription, - editingEnabled: editingEnabled, - eventHasCoverImage: eventHasCoverImage, - eventHasHost: eventHasHost, - firstLoad: firstLoad, - eventHasConcluded: eventHasConcluded, - eventHasBegun: eventHasBegun, - metadata: metadata, - }) + ///////////////////// + if (req.headers.accept && (req.headers.accept.includes('application/activity+json') || req.headers.accept.includes('application/json') || req.headers.accept.includes('application/json+ld'))) { + res.json(JSON.parse(event.activityPubActor)); + + //let tempActor = JSON.parse(result.actor); + //// Added this followers URI for Pleroma compatibility, see https://github.com/dariusk/rss-to-activitypub/issues/11#issuecomment-471390881 + //// New Actors should have this followers URI but in case of migration from an old version this will add it in on the fly + //if (tempActor.followers === undefined) { + // tempActor.followers = `https://${domain}/u/${username}/followers`; + //} + //res.json(tempActor); + } + ///////////////// + else { + res.set("X-Robots-Tag", "noindex"); + res.render('event', { + domain: domain, + email: contactEmail, + title: event.name, + escapedName: escapedName, + eventData: event, + eventAttendees: eventAttendees, + spotsRemaining: spotsRemaining, + noMoreSpots: noMoreSpots, + eventStartISO: eventStartISO, + eventEndISO: eventEndISO, + parsedLocation: parsedLocation, + parsedStart: parsedStart, + parsedEnd: parsedEnd, + displayDate: displayDate, + fromNow: fromNow, + timezone: event.timezone, + parsedDescription: parsedDescription, + editingEnabled: editingEnabled, + eventHasCoverImage: eventHasCoverImage, + eventHasHost: eventHasHost, + firstLoad: firstLoad, + eventHasConcluded: eventHasConcluded, + eventHasBegun: eventHasBegun, + metadata: metadata, + }) + } } else { res.status(404); @@ -375,11 +478,12 @@ router.get('/group/:eventGroupID', (req, res) => { let metadata = { title: eventGroup.name, description: marked(eventGroup.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), - image: (eventGroupHasCoverImage ? 'https://gath.io/events/' + eventGroup.image : null), - url: 'https://gath.io/' + req.params.eventID + image: (eventGroupHasCoverImage ? `https://${domain}/events/` + eventGroup.image : null), + url: `https://${domain}/` + req.params.eventID }; res.set("X-Robots-Tag", "noindex"); res.render('eventgroup', { + domain: domain, title: eventGroup.name, eventGroupData: eventGroup, escapedName: escapedName, @@ -445,6 +549,10 @@ router.post('/newevent', async (req, res) => { isPartOfEventGroup = true; } } + + // generate RSA keypair for ActivityPub + let pair = generateRSAKeypair(); + const event = new Event({ id: eventID, type: req.body.eventType, @@ -466,7 +574,10 @@ router.post('/newevent', async (req, res) => { showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendees, - firstLoad: true + firstLoad: true, + activityPubActor: createActivityPubActor(eventID, domain, pair.public), + publicKey: pair.public, + privateKey: pair.private }); event.save() .then((event) => { @@ -477,7 +588,7 @@ router.post('/newevent', async (req, res) => { to: req.body.creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-00330b8278ab463e9f88c16566487d97', dynamic_template_data: { @@ -547,7 +658,7 @@ router.post('/importevent', (req, res) => { to: creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-00330b8278ab463e9f88c16566487d97', dynamic_template_data: { @@ -609,7 +720,7 @@ router.post('/neweventgroup', (req, res) => { to: req.body.creatorEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-4c5ddcb34ac44ec5b2313c6da4e405f3', dynamic_template_data: { @@ -701,7 +812,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', dynamic_template_data: { @@ -781,7 +892,7 @@ router.post('/editeventgroup/:eventGroupID/:editToken', (req, res) => { // to: attendeeEmails, // from: { // name: 'Gathio', - // email: 'notifications@gath.io', + // email: contactEmail, // }, // templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', // dynamic_template_data: { @@ -837,7 +948,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', dynamic_template_data: { @@ -964,7 +1075,7 @@ router.post('/attendevent/:eventID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-977612474bba49c48b58e269f04f927c', dynamic_template_data: { @@ -999,7 +1110,7 @@ router.post('/unattendevent/:eventID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-56c97755d6394c23be212fef934b0f1f', dynamic_template_data: { @@ -1034,7 +1145,7 @@ router.post('/removeattendee/:eventID/:attendeeID', (req, res) => { to: req.body.attendeeEmail, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-f8ee9e1e2c8a48e3a329d1630d0d371f', dynamic_template_data: { @@ -1080,7 +1191,7 @@ router.post('/post/comment/:eventID', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-756d078561e047aba307155f02b6686d', dynamic_template_data: { @@ -1131,7 +1242,7 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => { to: attendeeEmails, from: { name: 'Gathio', - email: 'notifications@gath.io', + email: contactEmail, }, templateId: 'd-756d078561e047aba307155f02b6686d', dynamic_template_data: { diff --git a/views/event.handlebars b/views/event.handlebars index 4d0cf28..e2529b8 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -76,8 +76,8 @@ - gath.io/{{eventData.id}} - diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars index 00bae2c..dffb847 100755 --- a/views/eventgroup.handlebars +++ b/views/eventgroup.handlebars @@ -49,10 +49,10 @@ - - gath.io/group/{{eventGroupData.id}} + + {{domain}}/group/{{eventGroupData.id}} - diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index 4f0cfbc..3cde5d5 100755 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -18,13 +18,13 @@ - - + + - + {{#if title}}{{title}} · {{/if}}gathio @@ -73,7 +73,7 @@
- GitHub · Made with by Raphael · Need help? Email us.
+ GitHub · Made with by Raphael · Need help? Email us.
If you like gathio, you might like sweet, my utopian social network.
-- cgit v1.2.3