diff options
author | Darius Kazemi <darius.kazemi@gmail.com> | 2019-12-11 18:11:09 -0800 |
---|---|---|
committer | Darius Kazemi <darius.kazemi@gmail.com> | 2019-12-11 18:11:09 -0800 |
commit | 5faad9ea56dcf2d715c9e11e07490f50115d25bb (patch) | |
tree | a9816a1fa77b8784e8a22120a7ae9715e01a8aa8 | |
parent | 0c46e2d2d148efb7cfabcf82c12266a8d788f551 (diff) |
First pass at federation!
-rwxr-xr-x | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | config/domain-example.js | 3 | ||||
-rwxr-xr-x | models/Event.js | 14 | ||||
-rwxr-xr-x | package-lock.json | 5 | ||||
-rwxr-xr-x | package.json | 1 | ||||
-rwxr-xr-x | routes.js | 195 | ||||
-rwxr-xr-x | views/event.handlebars | 4 | ||||
-rwxr-xr-x | views/eventgroup.handlebars | 6 | ||||
-rwxr-xr-x | views/layouts/main.handlebars | 8 |
10 files changed, 186 insertions, 53 deletions
@@ -4,6 +4,7 @@ config/api.js config/database.js +config/domain.js public/events/* !public/events/.gitkeep @@ -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", @@ -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 @@ <span class="fa-li"> <i class="fas fa-fw fa-share-square"></i> </span> - <a href="https://gath.io/{{eventData.id}}">gath.io/{{eventData.id}}</a> - <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://gath.io/{{eventData.id}}"> + <a href="https://{{domain}}/{{eventData.id}}">{{domain}}/{{eventData.id}}</a> + <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://{{domain}}/{{eventData.id}}"> <i class="fas fa-copy"></i> Copy </button> </li> 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 @@ <span class="fa-li"> <i class="fas fa-share-square"></i> </span> - <a href="https://gath.io/group/{{eventGroupData.id}}"> - gath.io/group/{{eventGroupData.id}} + <a href="https://{{domain}}/group/{{eventGroupData.id}}"> + {{domain}}/group/{{eventGroupData.id}} </a> - <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://gath.io/group/{{eventGroupData.id}}"> + <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://{{domain}}/group/{{eventGroupData.id}}"> <i class="fas fa-copy"></i> Copy </button> </li> 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 @@ <meta property="og:image:height" content="260"> <meta property="og:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}An easier, quicker, and much less privacy-invading way to make and share events{{/if}}"> <meta property="og:title" content="{{#if metadata.title}}{{metadata.title}} · gathio{{else}}gathio{{/if}}"> - <meta property="og:url" content="{{#if metadata.url}}{{metadata.url}}{{else}}https://gath.io/{{/if}}"> - <meta property="og:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}http://gath.io/og-image.jpg{{/if}}"> + <meta property="og:url" content="{{#if metadata.url}}{{metadata.url}}{{else}}https://{{domain}}/{{/if}}"> + <meta property="og:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}http://{{domain}}/og-image.jpg{{/if}}"> <meta name="twitter:card" content="summary"> <meta name="twitter:title" content="{{#if metadata.title}}{{metadata.title}} · gathio{{else}}gathio{{/if}}"> <meta name="twitter:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}An easier, quicker, and much less privacy-invading way to make and share events{{/if}}"> - <meta name="twitter:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}http://gath.io/og-image.jpg{{/if}}"> + <meta name="twitter:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}http://{{domain}}/og-image.jpg{{/if}}"> <title>{{#if title}}{{title}} · {{/if}}gathio</title> @@ -73,7 +73,7 @@ </div> <div id="footerContainer"> <small class="text-muted"> - <a href="https://github.com/lowercasename/gathio">GitHub</a> · Made with <i class="far fa-heart"></i> by <a href="http://raphaelkabo.com">Raphael</a> · Need help? <a href="mailto:support@gath.io">Email us</a>.<br /> + <a href="https://github.com/lowercasename/gathio">GitHub</a> · Made with <i class="far fa-heart"></i> by <a href="http://raphaelkabo.com">Raphael</a> · Need help? <a href="mailto:{{email}}">Email us</a>.<br /> If you like gathio, you might like <strong><a href="http://sweet.sh/" style="color:#ed5e5e;">sweet</a></strong>, my utopian social network. </small> </div> |