From 00ba891cbf9ddd67438842942950e6c5fa37652e Mon Sep 17 00:00:00 2001 From: Armin Date: Tue, 12 Mar 2019 22:50:25 +0100 Subject: [PATCH] Some refactoring + add timeout functions to some step definitions + add objectId to Post and schema --- src/activitypub/ActivityPub.js | 29 ++++++++++++++++++--------- src/activitypub/NitroDataSource.js | 26 +++++++++++++++++------- src/activitypub/routes/inbox.js | 26 ++++++++++++------------ src/activitypub/utils/activity.js | 6 ++++++ src/middleware/userMiddleware.js | 5 ++--- src/resolvers/posts.js | 11 +++++----- src/schema.graphql | 1 + test/features/activity-delete.feature | 12 +++++------ test/features/support/steps.js | 15 +++++++++----- 9 files changed, 82 insertions(+), 49 deletions(-) diff --git a/src/activitypub/ActivityPub.js b/src/activitypub/ActivityPub.js index ade2362bc..ed64816b9 100644 --- a/src/activitypub/ActivityPub.js +++ b/src/activitypub/ActivityPub.js @@ -14,6 +14,7 @@ import NitroDataSource from './NitroDataSource' import router from './routes' import dotenv from 'dotenv' import Collections from './Collections' +import uuid from 'uuid/v4' const debug = require('debug')('ea') let activityPub = null @@ -27,6 +28,7 @@ export default class ActivityPub { this.dataSource = new NitroDataSource(uri) this.collections = new Collections(this.dataSource) } + static init (server) { if (!activityPub) { dotenv.config() @@ -179,30 +181,37 @@ export default class ActivityPub { }) } + generateStatusId (slug) { + return `http://${this.domain}/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((el) => { - return this.trySend(activity, fromName, new URL(el).host, el) + sharedInboxEndpoints.map((sharedInbox) => { + return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox) }) - activity.to = activity.to.filter((el) => { - return !(isPublicAddressed({ to: el })) + activity.to = activity.to.filter((recipient) => { + return !(isPublicAddressed({ to: recipient })) }) // serve the rest - activity.to.map(async (el) => { - const actorObject = await this.getActorObject(el) - return this.trySend(activity, fromName, new URL(el).host, actorObject.inbox) + 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 (el) => { - const actorObject = await this.getActorObject(el) - return this.trySend(activity, fromName, new URL(el).host, actorObject.inbox) + activity.to.map(async (recipient) => { + const actorObject = await this.getActorObject(recipient) + return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) }) } } diff --git a/src/activitypub/NitroDataSource.js b/src/activitypub/NitroDataSource.js index c5d520dce..4225e02ea 100644 --- a/src/activitypub/NitroDataSource.js +++ b/src/activitypub/NitroDataSource.js @@ -19,8 +19,6 @@ import { setContext } from 'apollo-link-context' import { InMemoryCache } from 'apollo-cache-inmemory' import fetch from 'node-fetch' import { ApolloClient } from 'apollo-client' -import uuid from 'uuid' -import encode from '../jwt/encode' import trunc from 'trunc-html' const debug = require('debug')('ea:nitro-datasource') @@ -37,12 +35,12 @@ export default class NitroDataSource { const cache = new InMemoryCache() const authLink = setContext((_, { headers }) => { // generate the authentication token (maybe from env? Which user?) - const token = encode({ name: 'ActivityPub', id: uuid() }) + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw' // return the headers to the context so httpLink can read them return { headers: { ...headers, - authorization: token ? `Bearer ${token}` : '' + Authorization: token ? `Bearer ${token}` : '' } } }) @@ -213,6 +211,7 @@ export default class NitroDataSource { async getOutboxCollectionPage (actorId) { const slug = extractNameFromId(actorId) + debug(`inside getting outbox collection page => ${slug}`) const result = await this.client.query({ query: gql` query { @@ -220,11 +219,16 @@ export default class NitroDataSource { actorId contributions { id + activityId + objectId title slug content contentExcerpt createdAt + author { + name + } } } } @@ -240,7 +244,7 @@ export default class NitroDataSource { outboxCollection.totalItems = posts.length await Promise.all( posts.map(async (post) => { - outboxCollection.orderedItems.push(await createArticleObject(post.activityId, post.objectId, post.content, extractNameFromId(post.id), post.id, post.createdAt)) + outboxCollection.orderedItems.push(await createArticleObject(post.activityId, post.objectId, post.content, post.author.name, post.id, post.createdAt)) }) ) @@ -332,6 +336,7 @@ export default class NitroDataSource { } const title = postObject.summary ? postObject.summary : postObject.content.split(' ').slice(0, 5).join(' ') const postId = extractIdFromActivityId(postObject.id) + debug('inside create post') let result = await this.client.mutate({ mutation: gql` mutation { @@ -346,10 +351,16 @@ export default class NitroDataSource { // ensure user and add author to post const userId = await this.ensureUser(postObject.attributedTo) + debug(`userId = ${userId}`) + debug(`postId = ${postId}`) result = await this.client.mutate({ mutation: gql` mutation { - AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) + AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) { + from { + name + } + } } ` }) @@ -523,10 +534,11 @@ export default class NitroDataSource { debug('ensureUser: user not exists.. createUser') // user does not exist.. create it const pw = crypto.randomBytes(16).toString('hex') + const slug = name.toLowerCase().split(' ').join('-') const result = await this.client.mutate({ mutation: gql` mutation { - CreateUser(password: "${pw}", slug:"${name}", actorId: "${actorId}", name: "${name}") { + CreateUser(password: "${pw}", slug:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") { id } } diff --git a/src/activitypub/routes/inbox.js b/src/activitypub/routes/inbox.js index 062f4b916..f9cfb3794 100644 --- a/src/activitypub/routes/inbox.js +++ b/src/activitypub/routes/inbox.js @@ -13,36 +13,36 @@ router.post('/', async function (req, res, next) { debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`) switch (req.body.type) { case 'Create': - if (req.body.send) { - await activityPub.sendActivity(req.body).catch(next) - break - } await activityPub.handleCreateActivity(req.body).catch(next) break case 'Undo': await activityPub.handleUndoActivity(req.body).catch(next) break case 'Follow': - debug('handleFollow') - await activityPub.handleFollowActivity(req.body) - debug('handledFollow') + await activityPub.handleFollowActivity(req.body).catch(next) break case 'Delete': await activityPub.handleDeleteActivity(req.body).catch(next) break - /* eslint-disable */ + /* eslint-disable */ case 'Update': - + await activityPub.handleUpdateActivity(req.body).catch(next) + break case 'Accept': await activityPub.handleAcceptActivity(req.body).catch(next) case 'Reject': - + // Do nothing + break case 'Add': - + break case 'Remove': - + break case 'Like': - + await activityPub.handleLikeActivity(req.body).catch(next) + break + case 'Dislike': + await activityPub.handleDislikeActivity(req.body).catch(next) + break case 'Announce': debug('else!!') debug(JSON.stringify(req.body, null, 2)) diff --git a/src/activitypub/utils/activity.js b/src/activitypub/utils/activity.js index 00a91813d..53bcd37ea 100644 --- a/src/activitypub/utils/activity.js +++ b/src/activitypub/utils/activity.js @@ -96,6 +96,12 @@ export function isPublicAddressed (postObject) { if (typeof postObject.to === 'string') { postObject.to = [postObject.to] } + if (typeof postObject === 'string') { + postObject.to = [postObject] + } + if (Array.isArray(postObject)) { + postObject.to = postObject + } return postObject.to.includes('Public') || postObject.to.includes('as:Public') || postObject.to.includes('https://www.w3.org/ns/activitystreams#Public') diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 9f3864dfe..a85bd1244 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,16 +1,15 @@ import createOrUpdateLocations from './nodes/locations' import { generateRsaKeyPair } from '../activitypub/security' import dotenv from 'dotenv' -import { resolve } from 'path' -dotenv.config({ path: resolve('src', 'activitypub', '.env') }) +dotenv.config() export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { const keys = generateRsaKeyPair() Object.assign(args, keys) - args.actorId = `${process.env.ACTIVITYPUB_URI}/activitypub/users/${args.slug}` + args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}` const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index d895b29a3..b7a6d8a2a 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,22 +1,23 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import { activityPub } from '../activitypub/ActivityPub' -import uuid from 'uuid/v4' import as from 'activitystrea.ms' +import dotenv from 'dotenv' /* import as from 'activitystrea.ms' import request from 'request' */ const debug = require('debug')('backend:schema') +dotenv.config() export default { Mutation: { CreatePost: async (object, params, context, resolveInfo) => { - params.activityId = uuid() + params.activityId = activityPub.generateStatusId(context.user.slug) + params.objectId = activityPub.generateStatusId(context.user.slug) const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() - const author = await session.run( 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MERGE (post)<-[:WROTE]-(author) ' + @@ -25,6 +26,7 @@ export default { postId: result.id } ) + session.close() debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`) if (Array.isArray(author.records) && author.records.length > 0) { @@ -54,10 +56,9 @@ export default { try { await activityPub.sendActivity(createActivity) } catch (e) { - debug(`error sending post activity = ${JSON.stringify(e, null, 2)}`) + debug(`error sending post activity\n${e}`) } } - session.close() return result } diff --git a/src/schema.graphql b/src/schema.graphql index cd6573e83..a57e43373 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -158,6 +158,7 @@ type User { type Post { id: ID! activityId: String + objectId: String author: User @relation(name: "WROTE", direction: "IN") title: String! slug: String diff --git a/test/features/activity-delete.feature b/test/features/activity-delete.feature index 9bdcbe9ab..b5ef7158a 100644 --- a/test/features/activity-delete.feature +++ b/test/features/activity-delete.feature @@ -14,7 +14,7 @@ Feature: Delete an object "type": "Create", "actor": "https://aronda.org/users/bernd-das-brot", "object": { - "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf", + "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf234", "type": "Article", "published": "2019-02-07T19:37:55.002Z", "attributedTo": "https://aronda.org/users/bernd-das-brot", @@ -29,13 +29,13 @@ Feature: Delete an object """ { "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost:4123/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2", + "id": "https://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2", "type": "Delete", "object": { - "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf", + "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", "type": "Article", "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "https://aronda.org/users/bernd-das-brot", + "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", "content": "Hi Max, how are you?", "to": "https://localhost:4123/activitypub/users/moritz" } @@ -45,10 +45,10 @@ Feature: Delete an object And the object is removed from the outbox collection of "bernd-das-brot" """ { - "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf", + "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", "type": "Article", "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "https://aronda.org/users/bernd-das-brot", + "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", "content": "Hi Max, how are you?", "to": "https://localhost:4123/activitypub/users/moritz" } diff --git a/test/features/support/steps.js b/test/features/support/steps.js index f2aaf6ffd..d7581f549 100644 --- a/test/features/support/steps.js +++ b/test/features/support/steps.js @@ -105,14 +105,17 @@ Then('the activity is added to the users inbox collection', async function () { }) Then('the post with id {string} to be created', async function (id) { - const result = await client.request(` + setTimeout(async () => { + const result = await client.request(` query { Post(id: "${id}") { title } } `) - expect(result.data.Post).to.be.an('array').that.is.not.empty // eslint-disable-line + + expect(result.data.Post).to.be.an('array').that.is.not.empty // eslint-disable-line + }, 2000) }) Then('the object is removed from the outbox collection of {string}', async function (name, object) { @@ -126,7 +129,8 @@ Then('I send a GET request to {string} and expect a ordered collection', () => { }) Then('the post with id {string} has been liked by {string}', async function (id, slug) { - const result = await client.request(` + setTimeout(async () => { + const result = await client.request(` query { Post(id: "${id}") { shoutedBy { @@ -135,6 +139,7 @@ Then('the post with id {string} has been liked by {string}', async function (id, } } `) - expect(result.data.Post[0].shoutedBy).to.be.an('array').that.is.not.empty // eslint-disable-line - expect(result.data.Post[0].shoutedBy[0].slug).to.equal(slug) + expect(result.data.Post[0].shoutedBy).to.be.an('array').that.is.not.empty // eslint-disable-line + expect(result.data.Post[0].shoutedBy[0].slug).to.equal(slug) + }, 2000) })