import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils' import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity' import request from 'request' import as from 'activitystrea.ms' import NitroDataSource from './NitroDataSource' import router from './routes' import Collections from './Collections' import uuid from 'uuid/v4' import CONFIG from '../config' const debug = require('debug')('ea') let activityPub = null export { activityPub } export default class ActivityPub { constructor(activityPubEndpointUri, internalGraphQlUri) { this.endpoint = activityPubEndpointUri this.dataSource = new NitroDataSource(internalGraphQlUri) this.collections = new Collections(this.dataSource) } static init(server) { if (!activityPub) { activityPub = new ActivityPub(CONFIG.CLIENT_URI, CONFIG.GRAPHQL_URI) // integrate into running graphql express server server.express.set('ap', activityPub) server.express.use(router) console.log('-> ActivityPub middleware added to the graphql express server') // eslint-disable-line no-console } else { console.log('-> ActivityPub middleware already added to the graphql express server') // eslint-disable-line no-console } } handleFollowActivity(activity) { debug(`inside FOLLOW ${activity.actor}`) const toActorName = extractNameFromId(activity.object) const fromDomain = extractDomainFromUrl(activity.actor) const dataSource = this.dataSource return new Promise((resolve, reject) => { request( { url: activity.actor, headers: { Accept: 'application/activity+json', }, }, async (err, response, toActorObject) => { if (err) return reject(err) // save shared inbox toActorObject = JSON.parse(toActorObject) await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox) const followersCollectionPage = await this.dataSource.getFollowersCollectionPage( activity.object, ) const followActivity = as .follow() .id(activity.id) .actor(activity.actor) .object(activity.object) // add follower if not already in collection if (followersCollectionPage.orderedItems.includes(activity.actor)) { debug('follower already in collection!') debug(`inbox = ${toActorObject.inbox}`) resolve( sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox), ) } else { followersCollectionPage.orderedItems.push(activity.actor) } debug(`toActorObject = ${toActorObject}`) toActorObject = typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`) debug(`inbox = ${toActorObject.inbox}`) debug(`outbox = ${toActorObject.outbox}`) debug(`followers = ${toActorObject.followers}`) debug(`following = ${toActorObject.following}`) try { await dataSource.saveFollowersCollectionPage(followersCollectionPage) debug('follow activity saved') resolve( sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox), ) } catch (e) { debug('followers update error!', e) resolve( sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox), ) } }, ) }) } handleUndoActivity(activity) { debug('inside UNDO') switch (activity.object.type) { case 'Follow': { const followActivity = activity.object return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object) } case 'Like': { return this.dataSource.deleteShouted(activity) } } } handleCreateActivity(activity) { debug('inside create') switch (activity.object.type) { case 'Note': { const articleObject = activity.object if (articleObject.inReplyTo) { return this.dataSource.createComment(activity) } else { return this.dataSource.createPost(activity) } } } } handleDeleteActivity(activity) { debug('inside delete') switch (activity.object.type) { case 'Article': case 'Note': return this.dataSource.deletePost(activity) default: } } handleUpdateActivity(activity) { debug('inside update') switch (activity.object.type) { case 'Note': case 'Article': return this.dataSource.updatePost(activity) default: } } handleLikeActivity(activity) { // TODO differ if activity is an Article/Note/etc. return this.dataSource.createShouted(activity) } handleDislikeActivity(activity) { // TODO differ if activity is an Article/Note/etc. return this.dataSource.deleteShouted(activity) } async handleAcceptActivity(activity) { debug('inside accept') switch (activity.object.type) { case 'Follow': { const followObject = activity.object const followingCollectionPage = await this.collections.getFollowingCollectionPage( followObject.actor, ) followingCollectionPage.orderedItems.push(followObject.object) await this.dataSource.saveFollowingCollectionPage(followingCollectionPage) } } } getActorObject(url) { return new Promise((resolve, reject) => { request( { url: url, headers: { Accept: 'application/json', }, }, (err, response, body) => { if (err) { reject(err) } resolve(JSON.parse(body)) }, ) }) } generateStatusId(slug) { return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}` } async sendActivity(activity) { delete activity.send const fromName = extractNameFromId(activity.actor) if (Array.isArray(activity.to) && isPublicAddressed(activity)) { debug('is public addressed') const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints() // serve shared inbox endpoints sharedInboxEndpoints.map(sharedInbox => { return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox) }) activity.to = activity.to.filter(recipient => { return !isPublicAddressed({ to: recipient }) }) // serve the rest activity.to.map(async recipient => { debug('serve rest') const actorObject = await this.getActorObject(recipient) return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) }) } else if (typeof activity.to === 'string') { debug('is string') const actorObject = await this.getActorObject(activity.to) return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox) } else if (Array.isArray(activity.to)) { activity.to.map(async recipient => { const actorObject = await this.getActorObject(recipient) return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) }) } } async trySend(activity, fromName, host, url, tries = 5) { try { return await signAndSend(activity, fromName, host, url) } catch (e) { if (tries > 0) { setTimeout(function() { return this.trySend(activity, fromName, host, url, --tries) }, 20000) } } } }