diff options
| -rwxr-xr-x | models/Event.js | 52 | ||||
| -rwxr-xr-x | package-lock.json | 164 | ||||
| -rwxr-xr-x | package.json | 1 | ||||
| -rwxr-xr-x | public/css/style.css | 3 | ||||
| -rwxr-xr-x | routes.js | 635 | ||||
| -rwxr-xr-x | views/event.handlebars | 23 | ||||
| -rwxr-xr-x | views/home.handlebars | 27 | 
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 +} @@ -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">  | 
