// import { extractDomainFromUrl, signAndSend } from './utils' import { extractNameFromId, signAndSend } from './utils' import { isPublicAddressed } from './utils/activity' // 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 { v4 as uuid } from 'uuid' 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) } } } }