summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xmodels/Event.js52
-rwxr-xr-xpackage-lock.json164
-rwxr-xr-xpackage.json1
-rwxr-xr-xpublic/css/style.css3
-rwxr-xr-xroutes.js635
-rwxr-xr-xviews/event.handlebars23
-rwxr-xr-xviews/home.handlebars27
7 files changed, 798 insertions, 107 deletions
diff --git a/models/Event.js b/models/Event.js
index d68621a..9ab455f 100755
--- a/models/Event.js
+++ b/models/Event.js
@@ -12,18 +12,33 @@ const Attendees = new mongoose.Schema({
email: {
type: String,
trim: true
- }
+ },
+ id: {
+ type: String,
+ trim: true
+ }
})
const Followers = new mongoose.Schema({
+ // this is the id of the original follow *request*, which we use to validate Undo events
followId: {
type: String,
trim: true
},
- account: {
+ // this is the actual remote user profile id
+ actorId: {
type: String,
trim: true
- }
+ },
+ // this is the stringified JSON of the entire user profile
+ actorJson: {
+ type: String,
+ trim: true
+ },
+ name: {
+ type: String,
+ trim: true
+ },
}, {_id: false})
const ReplySchema = new mongoose.Schema({
@@ -50,6 +65,20 @@ const ReplySchema = new mongoose.Schema({
}
})
+const ActivityPubMessages = new mongoose.Schema({
+ id: {
+ type: String,
+ required: true,
+ unique: true,
+ sparse: true
+ },
+ content: {
+ type: String,
+ trim: true,
+ required: true
+ }
+})
+
const CommentSchema = new mongoose.Schema({
id: {
type: String,
@@ -72,6 +101,22 @@ const CommentSchema = new mongoose.Schema({
trim: true,
required: true
},
+ activityJson: {
+ type: String,
+ trim: true
+ },
+ actorJson: {
+ type: String,
+ trim: true
+ },
+ activityId: {
+ type: String,
+ trim: true
+ },
+ actorId: {
+ type: String,
+ trim: true
+ },
replies: [ReplySchema]
})
@@ -184,6 +229,7 @@ const EventSchema = new mongoose.Schema({
trim: true
},
followers: [Followers],
+ activityPubMessages: [ActivityPubMessages]
});
module.exports = mongoose.model('Event', EventSchema);
diff --git a/package-lock.json b/package-lock.json
index 24f5ad7..e9b5b66 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1803,11 +1803,54 @@
"esutils": "^2.0.2"
}
},
+ "dom-serializer": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+ "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+ "requires": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ },
+ "dependencies": {
+ "domelementtype": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+ "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
+ },
+ "entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
+ "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
+ }
+ }
+ },
"dom-walk": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
"integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg="
},
+ "domelementtype": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
+ },
+ "domhandler": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+ "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+ "requires": {
+ "domelementtype": "1"
+ }
+ },
+ "domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "requires": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
@@ -1866,6 +1909,11 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
+ "entities": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
+ },
"es-abstract": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
@@ -2765,6 +2813,31 @@
}
}
},
+ "htmlparser2": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+ "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+ "requires": {
+ "domelementtype": "^1.3.1",
+ "domhandler": "^2.3.0",
+ "domutils": "^1.5.1",
+ "entities": "^1.1.1",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.1.1"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+ "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ }
+ }
+ },
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
@@ -3386,6 +3459,16 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+ },
+ "lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
+ },
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -3416,6 +3499,11 @@
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
+ "lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
+ },
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -3850,6 +3938,11 @@
"path-key": "^2.0.0"
}
},
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+ },
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -4138,6 +4231,51 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
+ "postcss": {
+ "version": "7.0.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.24.tgz",
+ "integrity": "sha512-Xl0XvdNWg+CblAXzNvbSOUvgJXwSjmbAKORqyw9V2AlHrm1js2gFw9y3jibBAhpKZi8b5JzJCVh/FyzPsTtgTA==",
+ "requires": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -4502,6 +4640,23 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
+ "sanitize-html": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz",
+ "integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==",
+ "requires": {
+ "chalk": "^2.4.1",
+ "htmlparser2": "^3.10.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.mergewith": "^4.6.1",
+ "postcss": "^7.0.5",
+ "srcset": "^1.0.0",
+ "xtend": "^4.0.1"
+ }
+ },
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
@@ -4807,6 +4962,15 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
+ "srcset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz",
+ "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=",
+ "requires": {
+ "array-uniq": "^1.0.2",
+ "number-is-nan": "^1.0.0"
+ }
+ },
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
diff --git a/package.json b/package.json
index 5c3c0ed..b957322 100755
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"node-schedule": "^1.3.1",
"randomstring": "^1.1.5",
"request": "^2.88.0",
+ "sanitize-html": "^1.20.1",
"shortid": "^2.2.14"
},
"devDependencies": {
diff --git a/public/css/style.css b/public/css/style.css
index 4085875..681273b 100755
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -268,6 +268,7 @@ body, html {
text-overflow: "";
overflow: hidden;
max-width: 62px;
+ color: #fff;
}
.remove-attendee {
@@ -337,4 +338,4 @@ body, html {
.code {
font-family: 'Courier New', Courier, monospace;
overflow-wrap: anywhere;
-} \ No newline at end of file
+}
diff --git a/routes.js b/routes.js
index 836a55c..6d09ab7 100755
--- a/routes.js
+++ b/routes.js
@@ -26,6 +26,7 @@ const request = require('request');
const domain = require('./config/domain.js').domain;
const contactEmail = require('./config/domain.js').email;
+var sanitizeHtml = require('sanitize-html');
// Extra marked renderer (used to render plaintext event description for page metadata)
// Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/
@@ -143,8 +144,8 @@ function createWebfinger(eventID, domain) {
};
}
-function createActivityPubActor(eventID, domain, pubkey) {
- return JSON.stringify({
+function createActivityPubActor(eventID, domain, pubkey, description, name, location, imageFilename) {
+ let actor = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
@@ -155,17 +156,32 @@ function createActivityPubActor(eventID, domain, pubkey) {
'preferredUsername': `${eventID}`,
'inbox': `https://${domain}/activitypub/inbox`,
'followers': `https://${domain}/${eventID}/followers`,
+ 'summary': description,
+ 'name': name,
'publicKey': {
'id': `https://${domain}/${eventID}#main-key`,
'owner': `https://${domain}/${eventID}`,
'publicKeyPem': pubkey
}
- });
+ };
+ if (location) {
+ actor.summary += ` Location: ${location}.`
+ }
+ if (imageFilename) {
+ actor.icon = {
+ 'type': 'Image',
+ 'mediaType': 'image/jpg',
+ 'url': `https://${domain}/events/${imageFilename}`,
+ };
+ }
+ return JSON.stringify(actor);
}
-function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) {
+function sendAcceptMessage(thebody, eventID, targetDomain, callback) {
+ callback = callback || function() {};
const guid = crypto.randomBytes(16).toString('hex');
+ const actorId = thebody.actor;
let message = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${domain}/${guid}`,
@@ -173,41 +189,119 @@ function sendAcceptMessage(thebody, eventID, domain, req, res, targetDomain) {
'actor': `https://${domain}/${eventID}`,
'object': thebody,
};
- // get the URI of the actor object and append 'inbox' to it
- let inbox = message.object.actor+'/inbox';
- signAndSend(message, eventID, domain, req, res, targetDomain, inbox);
+ // get the inbox
+ Event.findOne({
+ id: eventID,
+ }, function(err, event) {
+ if (event) {
+ const follower = event.followers.find(el => el.actorId === actorId);
+ if (follower) {
+ const actorJson = JSON.parse(follower.actorJson);
+ const inbox = actorJson.inbox;
+ signAndSend(message, eventID, targetDomain, inbox, callback);
+ }
+ }
+ else {
+ callback(`Could not find event ${eventID}`, null, 404);
+ }
+ });
}
-function rawMessage(json, eventID, domain, follower) {
+// this sends a message "to:" an individual fediverse user
+function sendDirectMessage(apObject, actorId, eventID, callback) {
+ callback = callback || function() {};
const guidCreate = crypto.randomBytes(16).toString('hex');
- const guidNote = crypto.randomBytes(16).toString('hex');
- // let db = req.app.get('db');
+ const guidObject = crypto.randomBytes(16).toString('hex');
let d = new Date();
- let rawMessagePayload = json;
-
- rawMessagePayload.published = d.toISOString();
- rawMessagePayload.attributedTo = `https://${domain}/${eventID}`;
- rawMessagePayload.to = [follower];
- rawMessagePayload.id = `https://${domain}/m/${guidNote}`;
- rawMessagePayload.content = unescape(rawMessagePayload.content)
+ apObject.published = d.toISOString();
+ apObject.attributedTo = `https://${domain}/${eventID}`;
+ apObject.to = actorId;
+ apObject.id = `https://${domain}/m/${guidObject}`;
+ apObject.content = unescape(apObject.content)
let createMessage = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${domain}/m/${guidCreate}`,
'type': 'Create',
'actor': `https://${domain}/${eventID}`,
- 'to': [follower],
- 'object': rawMessagePayload
+ 'to': [actorId],
+ 'object': apObject
};
- //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidCreate, JSON.stringify(createMessage));
- //db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidNote, JSON.stringify(rawMessagePayload));
+ let myURL = new URL(actorId);
+ let targetDomain = myURL.hostname;
+ // get the inbox
+ Event.findOne({
+ id: eventID,
+ }, function(err, event) {
+ if (event) {
+ const follower = event.followers.find(el => el.actorId === actorId);
+ if (follower) {
+ const actorJson = JSON.parse(follower.actorJson);
+ const inbox = actorJson.inbox;
+ signAndSend(createMessage, eventID, targetDomain, inbox, callback);
+ }
+ else {
+ callback(`No follower found with the id ${actorId}`, null, 404);
+ }
+ }
+ else {
+ callback(`No event found with the id ${eventID}`, null, 404);
+ }
+ });
+}
- return createMessage;
+// this function sends something to the timeline of every follower in the followers array
+function broadcastMessage(apObject, followers, eventID, callback) {
+ callback = callback || function() {};
+ let guidCreate = crypto.randomBytes(16).toString('hex');
+ console.log('broadcasting');
+ // iterate over followers
+ for (const follower of followers) {
+ let actorId = follower.actorId;
+ let myURL = new URL(actorId);
+ let targetDomain = myURL.hostname;
+ // get the inbox
+ Event.findOne({
+ id: eventID,
+ }, function(err, event) {
+ console.log('found the event for broadcast')
+ if (event) {
+ const follower = event.followers.find(el => el.actorId === actorId);
+ if (follower) {
+ const actorJson = JSON.parse(follower.actorJson);
+ const inbox = actorJson.inbox;
+ console.log('found the inbox for', actorId)
+ const createMessage = {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ 'id': `https://${domain}/m/${guidCreate}`,
+ 'type': 'Create',
+ 'actor': `https://${domain}/${eventID}`,
+ 'to': [actorId],
+ 'object': apObject
+ };
+ signAndSend(createMessage, eventID, targetDomain, inbox, function(err, resp, status) {
+ if (err) {
+ console.log(`Didn't sent to ${actorId}, status ${status} with error ${err}`);
+ }
+ else {
+ console.log('sent to', actorId);
+ }
+ });
+ }
+ else {
+ callback(`No follower found with the id ${actorId}`, null, 404);
+ }
+ }
+ else {
+ callback(`No event found with the id ${eventID}`, null, 404);
+ }
+ });
+ } // end followers
}
-function signAndSend(message, eventID, domain, req, res, targetDomain, inbox) {
+function signAndSend(message, eventID, targetDomain, inbox, callback) {
let inboxFragment = inbox.replace('https://'+targetDomain,'');
// get the private key
Event.findOne({
@@ -237,15 +331,37 @@ function signAndSend(message, eventID, domain, req, res, targetDomain, inbox) {
}, function (error, response){
if (error) {
console.log('Error:', error, response.body);
+ callback(error, null, 500);
}
else {
- console.log('Response:', response.body);
+ console.log('Response:', response.statusCode);
+ // Add the message to the database
+ const messageID = message.id;
+ const newMessage = {
+ id: message.id,
+ content: JSON.stringify(message)
+ };
+ Event.findOne({
+ id: eventID,
+ }, function(err,event) {
+ if (!event) return;
+ event.activityPubMessages.push(newMessage);
+ event.save()
+ .then(() => {
+ addToLog("addActivityPubMessage", "success", "ActivityPubMessage added to event " + eventID);
+ console.log('successful ActivityPubMessage add');
+ callback(null, message.id, 200);
+ })
+ .catch((err) => { addToLog("addActivityPubMessage", "error", "Attempt to add ActivityPubMessage to event " + eventID + " failed with error: " + err);
+ console.log('error', err)
+ callback(err, null, 500);
+ });
+ })
}
});
- return res.status(200);
}
else {
- return res.status(404).send(`No record found for ${eventID}.`);
+ callback(`No record found for ${eventID}.`, null, 404);
}
});
}
@@ -411,7 +527,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 {
@@ -489,7 +605,7 @@ router.get('/:eventID/followers', (req, res) => {
.then((event) => {
if (event) {
console.log(event.followers);
- const followers = event.followers.map(el => el.account);
+ const followers = event.followers.map(el => el.actorId);
console.log(followers)
let followersCollection = {
"type": "OrderedCollection",
@@ -556,7 +672,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;
}
@@ -576,7 +692,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 {
@@ -632,10 +748,13 @@ router.get('/group/:eventGroupID', (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);
@@ -684,7 +803,7 @@ router.post('/newevent', async (req, res) => {
usersCanComment: req.body.interactionCheckbox ? true : false,
maxAttendees: req.body.maxAttendees,
firstLoad: true,
- activityPubActor: createActivityPubActor(eventID, domain, pair.public),
+ activityPubActor: createActivityPubActor(eventID, domain, pair.public, marked(req.body.eventDescription), req.body.eventName, req.body.eventLocation, eventImageFilename),
publicKey: pair.public,
privateKey: pair.private
});
@@ -722,7 +841,7 @@ 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) {
+ 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];
@@ -744,7 +863,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,
@@ -798,7 +917,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);
@@ -864,7 +983,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;
@@ -887,7 +1006,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => {
if (eventGroup) {
isPartOfEventGroup = true;
}
- }
+ }
const updatedEvent = {
name: req.body.eventName,
location: req.body.eventLocation,
@@ -904,6 +1023,28 @@ router.post('/editevent/:eventID/:editToken', (req, res) => {
maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null,
eventGroup: isPartOfEventGroup ? eventGroup._id : null
}
+ let diffText = '<p>This event was just updated with new information.</p><ul>';
+ let displayDate;
+ // TODO: send an Update Profile message if needed?
+ 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);
@@ -912,10 +1053,39 @@ 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}/m/${guidObject}`,
+ "name": `RSVP to ${event.name}`,
+ "type": "Note",
+ "content": `${diffText} See here: <a href="https://${domain}/${req.params.eventID}">https://${domain}/${req.params.eventID}</a>`,
+ }
+ broadcastMessage(jsonObject, 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
+ sendDirectMessage(jsonObject, attendee.id, eventID);
+ }
+ }
+ })
if (sendEmails) {
Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) {
attendeeEmails = ids;
- if (!error && attendeeEmails != ""){
+ if (!error && attendeeEmails !== ""){
console.log("Sending emails to: " + attendeeEmails);
const msg = {
to: attendeeEmails,
@@ -966,7 +1136,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;
@@ -1174,6 +1344,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(() => {
@@ -1287,6 +1458,7 @@ 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(() => {
@@ -1337,6 +1509,7 @@ 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()
@@ -1405,86 +1578,149 @@ router.post('/deletecomment/:eventID/:commentID/:editToken', (req, res) => {
});
router.post('/activitypub/inbox', (req, res) => {
- console.log('got a inbox message')
- console.log(req.body);
- const myURL = new URL(req.body.actor);
- let targetDomain = myURL.hostname;
+ console.log('got an inbox message of type', req.body.type, req.body)
+
+ // 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
+ request({
+ url: signature_header.keyId,
+ headers: {
+ 'Accept': 'application/activity+json',
+ 'Content-Type': 'application/activity+json'
+ }}, function (error, response, actor) {
+ publicKey = JSON.parse(actor).publicKey.publicKeyPem;
+
+ 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')
+ const result = verifier.verify(publicKeyBuf, signatureBuf)
+ console.log('VALIDATE RESULT:', result)
+ if (!result) {
+ res.status(401).send('Signature could not be verified.');
+ }
+ else {
+ processInbox(req, res);
+ }
+ });
+});
+
+
+function processInbox(req, res) {
+ if (req.body.object) console.log('containing object of type', req.body.object.type)
// if a Follow activity hits the inbox
if (typeof req.body.object === 'string' && req.body.type === 'Follow') {
- console.log('follow!')
+ const myURL = new URL(req.body.actor);
+ let targetDomain = myURL.hostname;
let eventID = req.body.object.replace(`https://${domain}/`,'');
- // Accept the follow request
- sendAcceptMessage(req.body, eventID, domain, req, res, targetDomain);
// Add the user to the DB of accounts that follow the account
- console.log(req.body)
-
- const newFollower = {
- account: req.body.actor,
- followId: req.body.id
- };
-
- Event.findOne({
- id: eventID,
- }, function(err,event) {
- console.log(event.followers)
- // if this account is NOT already in our followers list, add it
- if (!event.followers.map(el => el.account).includes(req.body.actor)) {
- event.followers.push(newFollower);
- console.log(event.followers)
- event.save()
- .then(() => {
- addToLog("addEventFollower", "success", "Follower added to event " + eventID);
- console.log('successful follower add');
- // send a Question to the new follower
- let inbox = req.body.actor+'/inbox';
- let myURL = new URL(req.body.actor);
- let targetDomain = myURL.hostname;
- const jsonObject = {
- "@context": "https://www.w3.org/ns/activitystreams",
- "id": "http://polls.example.org/question/1",
- "name": `RSVP to ${event.name}`,
- "type": "Question",
- "content": `Will you attend ${event.name}?`,
- "oneOf": [
- {"type":"Note","name": "Yes"},
- {"type":"Note","name": "No"},
- {"type":"Note","name": "Maybe"}
- ],
- "endTime":event.start.toISOString()
+ // get the follower's username
+ request({
+ url: req.body.actor,
+ headers: {
+ 'Accept': 'application/activity+json',
+ 'Content-Type': 'application/activity+json'
+ }}, function (error, response, body) {
+ body = JSON.parse(body)
+ const name = body.preferredUsername || body.name || attributedTo;
+ const newFollower = {
+ actorId: req.body.actor,
+ followId: req.body.id,
+ name: name,
+ actorJson: JSON.stringify(body)
+ };
+ Event.findOne({
+ id: eventID,
+ }, function(err,event) {
+ // if this account is NOT already in our followers list, add it
+ if (event && !event.followers.map(el => el.actorId).includes(req.body.actor)) {
+ console.log('made it!')
+ event.followers.push(newFollower);
+ event.save()
+ .then(() => {
+ addToLog("addEventFollower", "success", "Follower added to event " + eventID);
+ console.log('successful follower add');
+ // Accept the follow request
+ sendAcceptMessage(req.body, eventID, targetDomain, function(err, resp, status) {
+ if (err) {
+ console.log(`Didn't send Accept to ${req.body.actor}, status ${status} with error ${err}`);
+ }
+ else {
+ console.log('sent Accept to', req.body.actor);
+ // if users can self-RSVP, send a Question to the new follower
+ if (event.usersCanAttend) {
+ const jsonObject = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "name": `RSVP to ${event.name}`,
+ "type": "Question",
+ "content": `<span class=\"h-card\"><a href="${req.body.actor}" class="u-url mention">@<span>${name}</span></a></span> Will you attend ${event.name}?`,
+ "oneOf": [
+ {"type":"Note","name": "Yes"},
+ {"type":"Note","name": "No"},
+ {"type":"Note","name": "Maybe"}
+ ],
+ "endTime":event.start.toISOString(),
+ "tag":[{"type":"Mention","href":req.body.actor,"name":name}]
+ }
+ // send direct message to user
+ sendDirectMessage(jsonObject, req.body.actor, eventID, function (error, response, statuscode) {
+ if (error) {
+ console.log(error);
+ res.status(statuscode).json(error);
+ }
+ else {
+ res.status(statuscode).json({messageid: response});
+ }
+ });
+ }
+ }
+ });
+ })
+ .catch((err) => { res.status(500).send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err);
+ console.log('ERROR', err);
+ });
}
- let message = rawMessage(jsonObject, eventID, domain, req.body.actor);
- console.log('!!!!!!!!! sending')
- console.log(message)
- signAndSend(message, eventID, domain, req, res, targetDomain, inbox);
})
- .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventFollower", "error", "Attempt to add follower to event " + eventID + " failed with error: " + err);
- console.log('error', err)
- });
- }
- });
+ }) //end request
}
// if an Undo activity with a Follow object hits the inbox
if (req.body && req.body.type === 'Undo' && req.body.object && req.body.object.type === 'Follow') {
- console.log('undo follow!')
- console.log(req.body)
// get the record of all followers for this account
- let eventID = req.body.object.object.replace(`https://${domain}/`,'');
+ const eventID = req.body.object.object.replace(`https://${domain}/`,'');
Event.findOne({
id: eventID,
}, function(err,event) {
+ if (!event) return;
// check to see if the Follow object's id matches the id we have on record
- console.log(event.followers)
// is this even someone who follows us
- const indexOfFollower = event.followers.findIndex(el => {console.log(el.account, req.body.object.actor); return el.account === req.body.object.actor;});
- console.log(indexOfFollower)
+ const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.actor);
if (indexOfFollower !== -1) {
// does the id we have match the id we are being given
if (event.followers[indexOfFollower].followId === req.body.object.id) {
// we have a match and can trust the Undo! remove this person from the followers list
event.followers.splice(indexOfFollower, 1);
- console.log('new', indexOfFollower, event.followers);
event.save()
.then(() => {
+ res.send(200);
addToLog("removeEventFollower", "success", "Follower removed from event " + eventID);
console.log('successful follower removal')
})
@@ -1495,7 +1731,210 @@ router.post('/activitypub/inbox', (req, res) => {
}
});
}
-});
+ // if a Create activity with a Note object hits the inbox, it might be a vote in a poll
+ if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.inReplyTo && req.body.object.to) {
+ console.log('create note inreplyto!!!')
+ // figure out what this is in reply to -- it should be addressed specifically to us
+ let {name, attributedTo, inReplyTo, to} = req.body.object;
+ // if it's an array just grab the first element, since a poll should only broadcast back to the pollster
+ if (Array.isArray(to)) {
+ to = to[0];
+ }
+ const eventID = to.replace(`https://${domain}/`,'');
+ // make sure this person is actually a follower
+ Event.findOne({
+ id: eventID,
+ }, function(err,event) {
+ if (!event) return;
+ // is this even someone who follows us
+ const indexOfFollower = event.followers.findIndex(el => el.actorId === req.body.object.attributedTo);
+ if (indexOfFollower !== -1) {
+ console.log('this person does follow us!')
+ // compare the inReplyTo to its stored message, if it exists and it's going to the right follower then this is a valid reply
+ const message = event.activityPubMessages.find(el => {
+ const content = JSON.parse(el.content);
+ return inReplyTo === (content.object && content.object.id);
+ });
+ if (message) {
+ console.log(message);
+ const content = JSON.parse(message.content);
+ // check if the message we sent out was sent to the actor this incoming message is attributedTo
+ if (content.to[0] === attributedTo) {
+ // it's a match, this is a valid poll response, add RSVP to database
+ // fetch the profile information of the user
+ request({
+ url: attributedTo,
+ headers: {
+ 'Accept': 'application/activity+json',
+ 'Content-Type': 'application/activity+json'
+ }}, function (error, response, body) {
+ body = JSON.parse(body)
+ // if this account is NOT already in our attendees list, add it
+ if (!event.attendees.map(el => el.id).includes(attributedTo)) {
+ const attendeeName = body.preferredUsername || body.name || attributedTo;
+ const newAttendee = {
+ name: attendeeName,
+ status: 'attending',
+ id: attributedTo
+ };
+ event.attendees.push(newAttendee);
+ event.save()
+ .then(() => {
+ addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID);
+ console.log('added attendee', attendeeName)
+ res.send(200);
+ })
+ .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); });
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+ if (req.body && req.body.type === 'Delete') {
+ // figure out if we have a matching comment by id
+ const deleteObjectId = req.body.object.id;
+ // find all events with comments from the author
+ Event.find({
+ "comments.actorId":req.body.actor
+ }, function(err,events) {
+ if (!events) {
+ res.sendStatus(404);
+ return;
+ }
+
+ // find the event with THIS comment from the author
+ let eventWithComment = events.find(event => {
+ let comments = event.comments;
+ return comments.find(comment => {
+ if (!comment.activityJson) {
+ return false;
+ }
+ return JSON.parse(comment.activityJson).object.id === req.body.object.id;
+ })
+ });
+
+ if (!eventWithComment) {
+ res.sendStatus(404);
+ return;
+ }
+
+ // delete the comment
+ // find the index of the comment
+ let indexOfComment = eventWithComment.comments.findIndex(comment => {
+ return JSON.parse(comment.activityJson).object.id === req.body.object.id;
+ });
+ eventWithComment.comments.splice(indexOfComment, 1);
+ eventWithComment.save()
+ .then(() => {
+ addToLog("deleteComment", "success", "Comment deleted from event " + eventWithComment.id);
+ console.log('deleted comment!')
+ res.sendStatus(200);
+ })
+ .catch((err) => { res.sendStatus(500); addToLog("deleteComment", "error", "Attempt to delete comment " + req.body.object.id + "from event " + eventWithComment.id + " failed with error: " + err);});
+ });
+ }
+ // if we are CC'ed on a public or unlisted Create/Note, then this is a comment to us we should replicate
+ if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) {
+ console.log('create note!!')
+ // figure out what this is in reply to -- it should be addressed specifically to us
+ let {name, attributedTo, inReplyTo, to, cc} = req.body.object;
+ // if it's an array just grab the first element, since a poll should only broadcast back to the pollster
+ if (Array.isArray(to)) {
+ to = to[0];
+ }
+
+ // normalize cc into an array
+ if (typeof cc === 'string') {
+ cc = [cc];
+ }
+
+ // if this is a public message (in the to or cc fields)
+ if (to === 'https://www.w3.org/ns/activitystreams#Public' || (Array.isArray(cc) && cc.includes('https://www.w3.org/ns/activitystreams#Public'))) {
+ // figure out which event(s) of ours it was addressing
+ ourEvents = cc.filter(el => el.includes(`https://${domain}/`))
+ .map(el => el.replace(`https://${domain}/`,''));
+ // comments should only be on one event. if more than one, ignore (spam, probably)
+ if (ourEvents.length === 1) {
+ let eventID = ourEvents[0];
+ // add comment
+ let commentID = shortid.generate();
+ // get the actor for the commenter
+ request({
+ url: req.body.actor,
+ headers: {
+ 'Accept': 'application/activity+json',
+ 'Content-Type': 'application/activity+json'
+ }}, function (error, response, actor) {
+ if (!error) {
+ const parsedActor = JSON.parse(actor);
+ const name = parsedActor.preferredUsername || parsedActor.name || req.body.actor;
+ const newComment = {
+ id: commentID,
+ actorId: req.body.actor,
+ activityId: req.body.object.id,
+ author: name,
+ content: sanitizeHtml(req.body.object.content, {allowedTags: [], allowedAttributes: {}}).replace('@'+eventID,''),
+ timestamp: moment(),
+ activityJson: JSON.stringify(req.body),
+ actorJson: actor
+ };
+
+ Event.findOne({
+ id: eventID,
+ }, function(err,event) {
+ if (!event) {
+ return res.sendStatus(404);
+ }
+ if (!event.usersCanComment) {
+ return res.sendStatus(200);
+ }
+ event.comments.push(newComment);
+ event.save()
+ .then(() => {
+ addToLog("addEventComment", "success", "Comment added to event " + eventID);
+ console.log('added comment');
+ res.sendStatus(200);
+ })
+ .catch((err) => { res.status(500).send('Database error, please try again :(' + err); addToLog("addEventComment", "error", "Attempt to add comment to event " + eventID + " failed with error: " + err); console.log('error', err)});
+ });
+ }
+ });
+ } // end ourevent
+ } // end public message
+ // if it's not a public message, let them know that we only support public messages right now
+ else {
+ // figure out which event(s) of ours it was addressing
+ ourEvents = cc.concat(to).filter(el => el.includes(`https://${domain}/`))
+ .map(el => el.replace(`https://${domain}/`,''));
+ // comments should only be on one event. if more than one, ignore (spam, probably)
+ if (ourEvents.length === 1) {
+ let eventID = ourEvents[0];
+ // get the user's actor info
+ request({
+ url: req.body.actor,
+ headers: {
+ 'Accept': 'application/activity+json',
+ 'Content-Type': 'application/activity+json'
+ }}, function (error, response, actor) {
+ actor = JSON.parse(actor);
+ const name = actor.preferredUsername || actor.name || req.body.actor;
+ const jsonObject = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "type": "Note",
+ "inReplyTo": req.body.object.id,
+ "content": `<span class=\"h-card\"><a href="${req.body.actor}" class="u-url mention">@<span>${name}</span></a></span> Sorry, this service only supports posting public messages to the event page. Try contacting the event organizer directly if you need to have a private conversation.`,
+ "tag":[{"type":"Mention","href":req.body.actor,"name":name}]
+ }
+ res.send(200);
+ sendDirectMessage(jsonObject, req.body.actor, eventID);
+ }
+ );
+ }
+ }
+ }
+}
router.use(function(req, res, next){
res.status(404);
diff --git a/views/event.handlebars b/views/event.handlebars
index e2529b8..a57f62a 100755
--- a/views/event.handlebars
+++ b/views/event.handlebars
@@ -81,6 +81,15 @@
<i class="fas fa-copy"></i> Copy
</button>
</li>
+ <li>
+ <span class="fa-li">
+ <i class="fas fa-fw fa-share-square"></i>
+ </span>
+ @{{eventData.id}}@{{domain}}
+ <button type="button" id="copyAPLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="@{{eventData.id}}@{{domain}}">
+ <i class="fas fa-copy"></i> Copy
+ </button>
+ </li>
</ul>
</div>
</div>
@@ -125,7 +134,7 @@
{{#if eventAttendees}}
<ul class="attendeesList">
{{#each eventAttendees}}
- <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}><span class="attendee-name">{{this.name}}</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
+ <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}><a href="{{this.id}}"><span class="attendee-name">{{this.name}}</span></a>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
{{/each}}
</ul>
{{else}}
@@ -246,7 +255,11 @@
<div class="comment">
<div class="row commentContainer">
<div class="col-lg commentText">
- <p class="mb-0"><strong>{{this.author}}</strong> <small class="commentTimestamp text-muted">{{this.timestamp}}</small></p>
+ {{#if this.actorId}}
+ <p class="mb-0"><a href="{{this.actorId}}"><strong>{{this.author}}</strong></a> <a href="{{this.activityId}}"><small class="commentTimestamp text-muted">{{this.timestamp}}</small></a></p>
+ {{else}}
+ <p class="mb-0"><strong>{{this.author}}</strong> <small class="commentTimestamp text-muted">{{this.timestamp}}</small></p>
+ {{/if}}
<p>{{this.content}}</p>
{{#if this.replies}}
<hr>
@@ -398,6 +411,12 @@
$(this).html('<i class="fas fa-copy"></i> Copied!');
setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
})
+ new ClipboardJS('#copyAPLink');
+ $("#copyAPLink").click(function(){
+ console.log('hhhhh')
+ $(this).html('<i class="fas fa-copy"></i> Copied!');
+ setTimeout(function(){ $("#copyAPLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
+ })
$(".daysToDeletion").html(moment("{{eventEndISO}}").add(7, 'days').fromNow());
if ($("#joinCheckbox").is(':checked')){
$("#maxAttendeesCheckboxContainer").css("display","flex");
diff --git a/views/home.handlebars b/views/home.handlebars
index c1a610f..6f62a16 100755
--- a/views/home.handlebars
+++ b/views/home.handlebars
@@ -1,11 +1,32 @@
-<h1>Organise all the things</h1>
-
+<h1>🚨Experimental Fediverse Event Software🚨</h1>
<p class="lead">
- <strong>gath<span class="text-muted">io</span></strong> is a quick and easy way to make and share events which respects your privacy.
+ This is experimental software from <a href="https://tinysubversions.com">Darius Kazemi</a>.
</p>
<hr>
+<p>The Fediverse needs an event organizing system, so I've taken the incredibly lovely open source event organizing software <strong><a href="https://gath.io">gath.io</a></strong> and added my <a href="https://github.com/dariusk/express-activitypub">lightweight ActivityPub server</a> to the mix.</p>
+
+<p>I fully expect this to break, and I would love early testers. I'm especially interested to know how this interacts with people who aren't on Mastodon.</p>
+
+<h2>Directions</h2>
+
+<p>Hit the green New Event button and put in your event details. Your event will have a nice looking home page and it will also have an ActivityPub-compatible account and profile. It will give you the account handle for the event, which will look something like <strong>@aB3_2HI@{{domain}}</strong> and can be shared with people on the Fediverse who want to follow your event.</p>
+
+<p>When a person follows your event, they'll follow a feed that updates whenever you update event details. After a person follows, this software will DM the person a poll (aka an ActivityPub "Question") where, at least in Mastodon, they can vote Yes/No/Maybe. If they vote Yes, then they will be registered as an attendee on your page. No and Maybe don't do anything yet.</p>
+
+<p>Also when you update your event (changing any of the fields), you send a DM notification to every user who is registered as attending.</p>
+
+<h2>Further info</h2>
+
+<p>I'll publish the source code soon, but it's in crummy and undocumented shape right now and I'd rather see it perform "in the wild" before I do an official release.</p>
+
+<p>If you'd like to chat with me about this software, I can be reached at <a href="https://friend.camp/@darius">@darius@friend.camp</a>.</p>
+
+<p>What follows is the documentation from <strong>gath.io</strong>.</p>
+
+<hr>
+
<p>You don't need to sign up for an account - we just use your email to send you a secret link you can use to edit or delete your event. Send all your guests the public link, and all your co-hosts the editing link. A week after the event finishes, it's deleted from our servers for ever, and your email goes with it.</p>
<div id="example-event" class="text-center w-100 mt-4 mb-5">