diff --git a/server/api/models/event.js b/server/api/models/event.js index fbfe85f1..8581f7b6 100644 --- a/server/api/models/event.js +++ b/server/api/models/event.js @@ -37,17 +37,36 @@ module.exports = (sequelize, DataTypes) => { } // - event.prototype.toAP = function (username, follower) { - const tags = this.tags && '-' + this.tags.map(t => '#' + t.tag).join(' ') - const content = `${this.title} @${this.place.name} - ${moment.unix(this.start_datetime).format('dddd, D MMMM (HH:mm)')}
- ${this.description.length > 200 ? this.description.substr(0, 200) + '...' : this.description} ${tags}
` + event.prototype.toAP = function (username=config.admin, follower) { + const tags = this.tags && this.tags.map(t => '#' + t.tag).join(' ') + const content = `${this.title}
+ 📍${this.place.name}
+ ⏰ ${moment.unix(this.start_datetime).format('dddd, D MMMM (HH:mm)')}

+ ${this.description.length > 200 ? this.description.substr(0, 200) + '...' : this.description}
+ ${tags}
` + + let attachment = [] + if (this.image_path) { + attachment.push({ + type: 'Document', + mediaType: 'image/jpeg', + url: `${config.baseurl}/media/${this.image_path}`, + name: null, + blurHash: null + }) + } return { id: `${config.baseurl}/federation/m/c_${this.id}`, type: 'Create', actor: `${config.baseurl}/federation/u/${username}`, + url: `${config.baseurl}/federation/m/${this.id}`, object: { + attachment, + tag: this.tags.map(tag => ({ + type: 'Hashtag', + name: '#' + tag.tag + })), id: `${config.baseurl}/federation/m/${this.id}`, type: 'Note', published: this.createdAt, diff --git a/server/federation/follows.js b/server/federation/follows.js index 31b7bad0..5a7c5294 100644 --- a/server/federation/follows.js +++ b/server/federation/follows.js @@ -27,13 +27,23 @@ module.exports = { 'type': 'Accept', 'actor': `${config.baseurl}/federation/u/${user.username}`, 'object': body, - } + } Helpers.signAndSend(message, user, body.actor) res.sendStatus(200) }, // unfollow request from fediverse - unfollow () { - console.error('inside unfollow') + async unfollow (req, res) { + debug("Unfollow UNFOLLOW!") + const body = req.body + const username = body.object.object.replace(`${config.baseurl}/federation/u/`, '') + const user = await User.findOne({ where: { username }}) + if (!user) return res.status(404).send('User not found') + + if (body.actor !== body.object.actor) return res.status(400).send('Bad things') + user.followers = user.followers.filter(follower => follower !== username) + debug('%s unfollowed by %s (%d)', username, body.actor, user.followers.length) + await user.save() + res.sendStatus(200) } } diff --git a/server/federation/helpers.js b/server/federation/helpers.js index 35fc7a0f..a4f9c693 100644 --- a/server/federation/helpers.js +++ b/server/federation/helpers.js @@ -4,32 +4,29 @@ const crypto = require('crypto') const config = require('config') const httpSignature = require('http-signature') const debug = require('debug')('fediverse:helpers') +const { user: User } = require('../api/models') +const url = require('url') const actorCache = [] const Helpers = { async signAndSend(message, user, to) { - // get the URI of the actor object and append 'inbox' to it const toInbox = to + '/inbox' - const toOrigin = new URL(to) - const toPath = toInbox.replace(toOrigin.origin, '') + const toOrigin = url.parse(to) + const toPath = toOrigin.path + '/inbox' // get the private key const privkey = user.rsa.privateKey const signer = crypto.createSign('sha256') const d = new Date() const stringToSign = `(request-target): post ${toPath}\nhost: ${toOrigin.hostname}\ndate: ${d.toUTCString()}` - console.error('stringToSign ', stringToSign) signer.update(stringToSign) signer.end() const signature = signer.sign(privkey) const signature_b64 = signature.toString('base64') const header = `keyId="${config.baseurl}/federation/u/${user.username}",headers="(request-target) host date",signature="${signature_b64}"` - console.error('header ', header) - console.error('requestTo ', toInbox) - console.error('host ', toOrigin.hostname) - const response = await fetch(toInbox, { + return await fetch(toInbox, { headers: { 'Host': toOrigin.hostname, 'Date': d.toUTCString(), @@ -40,16 +37,29 @@ const Helpers = { method: 'POST', body: JSON.stringify(message) }) - console.log('Response:', response.body, response.statusCode, response.status, response.statusMessage) }, + async sendEvent(event, user) { - const followers = user.followers - for(let follower of followers) { + // TODO: has to use sharedInbox! + // event is sent by user that published it and by the admin instance + const instanceAdmin = await User.findOne({where: { email: config.admin }}) + if(!instanceAdmin) return + + for(let follower of instanceAdmin.followers) { + debug('Notify %s with event %s', follower, event.title) + const body = event.toAP(instanceAdmin.username, follower) + body['@context'] = 'https://www.w3.org/ns/activitystreams' + Helpers.signAndSend(body, user, follower) + } + + // in case the event is published by the Admin itself do not republish + if (instanceAdmin.id === user.id) return + for(let follower of user.followers) { debug('Notify %s with event %s', follower, event.title) const body = event.toAP(user.username, follower) body['@context'] = 'https://www.w3.org/ns/activitystreams' Helpers.signAndSend(body, user, follower) - } + } }, async getFederatedUser(address) { @@ -77,6 +87,7 @@ const Helpers = { // ref: https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/ async verifySignature(req, res, next) { + debug('Inside verify signature', req.body.actor) let user = await Helpers.getActor(req.body.actor) if (!user) return res.status(401).send('Actor not found') @@ -95,6 +106,7 @@ const Helpers = { if (httpSignature.verifySignature(parsed, user.publicKey.publicKeyPem)) return next() // still not valid + debug('Invalid signature from user %s', req.body.actor) res.send('Request signature could not be verified', 401) } } diff --git a/server/federation/index.js b/server/federation/index.js index 9560694a..00768c7c 100644 --- a/server/federation/index.js +++ b/server/federation/index.js @@ -4,7 +4,7 @@ const config = require('config') const cors = require('cors') const Follows = require('./follows') const Users = require('./users') -const { event: Event, user: User } = require('../api/models') +const { event: Event, user: User, tag: Tag, place: Place } = require('../api/models') const Comments = require('./comments') const Helpers = require('./helpers') const Ego = require('./ego') @@ -19,9 +19,9 @@ router.use(express.json({type: ['application/json', 'application/activity+json', router.get('/m/:event_id', async (req, res) => { const event_id = req.params.event_id - if (req.accepts('html')) return res.redirect(301, `/event/${event_id}`) + // if (req.accepts('html')) return res.redirect(301, `/event/${event_id}`) - const event = await Event.findByPk(req.params.event_id, { include: [ User ] }) + const event = await Event.findByPk(req.params.event_id, { include: [ User, Tag, Place ] }) if (!event) return res.status(404).send('Not found') return res.json(event.toAP(event.user.username)) })