diff --git a/backend/jest.config.js b/backend/jest.config.js index 9837dca0d..264ad13c0 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -11,7 +11,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 57, + lines: 70, }, }, testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], diff --git a/backend/src/activitypub/ActivityPub.ts b/backend/src/activitypub/ActivityPub.ts deleted file mode 100644 index 5f2bcd8ea..000000000 --- a/backend/src/activitypub/ActivityPub.ts +++ /dev/null @@ -1,244 +0,0 @@ -// 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: any = null - -export { activityPub } - -export default class ActivityPub { - endpoint: any - dataSource: any - collections: any - host: any - 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: any = 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: any = 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: any = 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(() => { - return this.trySend(activity, fromName, host, url, --tries) - }, 20000) - } - } - } -} diff --git a/backend/src/activitypub/Collections.ts b/backend/src/activitypub/Collections.ts deleted file mode 100644 index c0fd6dd71..000000000 --- a/backend/src/activitypub/Collections.ts +++ /dev/null @@ -1,30 +0,0 @@ -export default class Collections { - dataSource: any - constructor(dataSource) { - this.dataSource = dataSource - } - - getFollowersCollection(actorId) { - return this.dataSource.getFollowersCollection(actorId) - } - - getFollowersCollectionPage(actorId) { - return this.dataSource.getFollowersCollectionPage(actorId) - } - - getFollowingCollection(actorId) { - return this.dataSource.getFollowingCollection(actorId) - } - - getFollowingCollectionPage(actorId) { - return this.dataSource.getFollowingCollectionPage(actorId) - } - - getOutboxCollection(actorId) { - return this.dataSource.getOutboxCollection(actorId) - } - - getOutboxCollectionPage(actorId) { - return this.dataSource.getOutboxCollectionPage(actorId) - } -} diff --git a/backend/src/activitypub/NitroDataSource.ts b/backend/src/activitypub/NitroDataSource.ts deleted file mode 100644 index 88b9ae5e9..000000000 --- a/backend/src/activitypub/NitroDataSource.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { - throwErrorIfApolloErrorOccurred, - extractIdFromActivityId, - extractNameFromId, - constructIdFromName, -} from './utils' -import { createOrderedCollection, createOrderedCollectionPage } from './utils/collection' -import { createArticleObject, isPublicAddressed } from './utils/activity' -import crypto from 'crypto' -import gql from 'graphql-tag' -import { createHttpLink } from 'apollo-link-http' -import { setContext } from 'apollo-link-context' -import { InMemoryCache } from 'apollo-cache-inmemory' -import fetch from 'node-fetch' -import { ApolloClient } from 'apollo-client' -import trunc from 'trunc-html' -const debug = require('debug')('ea:datasource') - -export default class NitroDataSource { - uri: any - client: any - constructor(uri) { - this.uri = uri - const defaultOptions: any = { - query: { - fetchPolicy: 'network-only', - errorPolicy: 'all', - }, - } - const link = createHttpLink({ uri: this.uri, fetch: fetch } as any) // eslint-disable-line - const cache = new InMemoryCache() - const authLink = setContext((_, { headers }) => { - // generate the authentication token (maybe from env? Which user?) - 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}` : '', - }, - } - }) - this.client = new ApolloClient({ - link: authLink.concat(link), - cache: cache, - defaultOptions, - }) - } - - async getFollowersCollection(actorId) { - const slug = extractNameFromId(actorId) - debug(`slug= ${slug}`) - const result = await this.client.query({ - query: gql` - query { - User(slug: "${slug}") { - followedByCount - } - } - `, - }) - debug('successfully fetched followers') - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const followersCount = actor.followedByCount - - const followersCollection = createOrderedCollection(slug, 'followers') - followersCollection.totalItems = followersCount - - return followersCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async getFollowersCollectionPage(actorId) { - const slug = extractNameFromId(actorId) - debug(`getFollowersPage slug = ${slug}`) - const result = await this.client.query({ - query: gql` - query { - User(slug:"${slug}") { - followedBy { - slug - } - followedByCount - } - } - `, - }) - - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const followers = actor.followedBy - const followersCount = actor.followedByCount - - const followersCollection: any = createOrderedCollectionPage(slug, 'followers') - followersCollection.totalItems = followersCount - debug(`followers = ${JSON.stringify(followers, null, 2)}`) - await Promise.all( - followers.map(async (follower) => { - followersCollection.orderedItems.push(constructIdFromName(follower.slug)) - }), - ) - - return followersCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async getFollowingCollection(actorId) { - const slug = extractNameFromId(actorId) - const result = await this.client.query({ - query: gql` - query { - User(slug:"${slug}") { - followingCount - } - } - `, - }) - - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const followingCount = actor.followingCount - - const followingCollection = createOrderedCollection(slug, 'following') - followingCollection.totalItems = followingCount - - return followingCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async getFollowingCollectionPage(actorId) { - const slug = extractNameFromId(actorId) - const result = await this.client.query({ - query: gql` - query { - User(slug:"${slug}") { - following { - slug - } - followingCount - } - } - `, - }) - - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const following = actor.following - const followingCount = actor.followingCount - - const followingCollection: any = createOrderedCollectionPage(slug, 'following') - followingCollection.totalItems = followingCount - - await Promise.all( - following.map(async (user) => { - followingCollection.orderedItems.push(await constructIdFromName(user.slug)) - }), - ) - - return followingCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async getOutboxCollection(actorId) { - const slug = extractNameFromId(actorId) - const result = await this.client.query({ - query: gql` - query { - User(slug:"${slug}") { - contributions { - title - slug - content - contentExcerpt - createdAt - } - } - } - `, - }) - - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const posts = actor.contributions - - const outboxCollection = createOrderedCollection(slug, 'outbox') - outboxCollection.totalItems = posts.length - - return outboxCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async getOutboxCollectionPage(actorId) { - const slug = extractNameFromId(actorId) - debug(`inside getting outbox collection page => ${slug}`) - const result = await this.client.query({ - query: gql` - query { - User(slug:"${slug}") { - actorId - contributions { - id - activityId - objectId - title - slug - content - contentExcerpt - createdAt - author { - slug - } - } - } - } - `, - }) - - debug(result.data) - if (result.data) { - const actor = result.data.User[0] - const posts = actor.contributions - - const outboxCollection: any = createOrderedCollectionPage(slug, 'outbox') - outboxCollection.totalItems = posts.length - await Promise.all( - posts.map(async (post) => { - outboxCollection.orderedItems.push( - await createArticleObject( - post.activityId, - post.objectId, - post.content, - post.author.slug, - post.id, - post.createdAt, - ), - ) - }), - ) - - debug('after createNote') - return outboxCollection - } else { - throwErrorIfApolloErrorOccurred(result) - } - } - - async undoFollowActivity(fromActorId, toActorId) { - const fromUserId = await this.ensureUser(fromActorId) - const toUserId = await this.ensureUser(toActorId) - const result = await this.client.mutate({ - mutation: gql` - mutation { - RemoveUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { - from { name } - } - } - `, - }) - debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`) - throwErrorIfApolloErrorOccurred(result) - } - - async saveFollowersCollectionPage(followersCollection, onlyNewestItem = true) { - debug('inside saveFollowers') - let orderedItems = followersCollection.orderedItems - const toUserName = extractNameFromId(followersCollection.id) - const toUserId = await this.ensureUser(constructIdFromName(toUserName)) - orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems - - return Promise.all( - orderedItems.map(async (follower) => { - debug(`follower = ${follower}`) - const fromUserId = await this.ensureUser(follower) - debug(`fromUserId = ${fromUserId}`) - debug(`toUserId = ${toUserId}`) - const result = await this.client.mutate({ - mutation: gql` - mutation { - AddUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { - from { name } - } - } - `, - }) - debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`) - throwErrorIfApolloErrorOccurred(result) - debug('saveFollowers: added follow edge successfully') - }), - ) - } - - async saveFollowingCollectionPage(followingCollection, onlyNewestItem = true) { - debug('inside saveFollowers') - let orderedItems = followingCollection.orderedItems - const fromUserName = extractNameFromId(followingCollection.id) - const fromUserId = await this.ensureUser(constructIdFromName(fromUserName)) - orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems - return Promise.all( - orderedItems.map(async (following) => { - debug(`follower = ${following}`) - const toUserId = await this.ensureUser(following) - debug(`fromUserId = ${fromUserId}`) - debug(`toUserId = ${toUserId}`) - const result = await this.client.mutate({ - mutation: gql` - mutation { - AddUserFollowing(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { - from { name } - } - } - `, - }) - debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`) - throwErrorIfApolloErrorOccurred(result) - debug('saveFollowing: added follow edge successfully') - }), - ) - } - - async createPost(activity) { - // TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient - // createPost - const postObject = activity.object - if (!isPublicAddressed(postObject)) { - return debug( - 'createPost: not send to public (sending to specific persons is not implemented yet)', - ) - } - 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 { - CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc( - postObject.content, - 120, - )}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${ - activity.id - }") { - id - } - } - `, - }) - - throwErrorIfApolloErrorOccurred(result) - - // 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}"}) { - from { - name - } - } - } - `, - }) - - throwErrorIfApolloErrorOccurred(result) - } - - async deletePost(activity) { - const result = await this.client.mutate({ - mutation: gql` - mutation { - DeletePost(id: "${extractIdFromActivityId(activity.object.id)}") { - title - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - } - - async updatePost(activity) { - const postObject = activity.object - const postId = extractIdFromActivityId(postObject.id) - const date = postObject.updated ? postObject.updated : new Date().toISOString() - const result = await this.client.mutate({ - mutation: gql` - mutation { - UpdatePost(content: "${postObject.content}", contentExcerpt: "${ - trunc(postObject.content, 120).html - }", id: "${postId}", updatedAt: "${date}") { - title - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - } - - async createShouted(activity) { - const userId = await this.ensureUser(activity.actor) - const postId = extractIdFromActivityId(activity.object) - const result = await this.client.mutate({ - mutation: gql` - mutation { - AddUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) { - from { - name - } - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - if (!result.data.AddUserShouted) { - debug('something went wrong shouting post') - throw Error('User or Post not exists') - } - } - - async deleteShouted(activity) { - const userId = await this.ensureUser(activity.actor) - const postId = extractIdFromActivityId(activity.object) - const result = await this.client.mutate({ - mutation: gql` - mutation { - RemoveUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) { - from { - name - } - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - if (!result.data.AddUserShouted) { - debug('something went wrong disliking a post') - throw Error('User or Post not exists') - } - } - - async getSharedInboxEndpoints() { - const result = await this.client.query({ - query: gql` - query { - SharedInboxEndpoint { - uri - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - return result.data.SharedInboxEnpoint - } - - async addSharedInboxEndpoint(uri) { - try { - const result = await this.client.mutate({ - mutation: gql` - mutation { - CreateSharedInboxEndpoint(uri: "${uri}") - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - return true - } catch (e) { - return false - } - } - - async createComment(activity) { - const postObject = activity.object - let result = await this.client.mutate({ - mutation: gql` - mutation { - CreateComment(content: "${ - postObject.content - }", activityId: "${extractIdFromActivityId(activity.id)}") { - id - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - - const toUserId = await this.ensureUser(activity.actor) - const result2 = await this.client.mutate({ - mutation: gql` - mutation { - AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) { - id - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result2) - - const postId = extractIdFromActivityId(postObject.inReplyTo) - result = await this.client.mutate({ - mutation: gql` - mutation { - AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) { - id - } - } - `, - }) - - throwErrorIfApolloErrorOccurred(result) - } - - /** - * This function will search for user existence and will create a disabled user with a random 16 bytes password when no user is found. - * - * @param actorId - * @returns {Promise<*>} - */ - async ensureUser(actorId) { - debug(`inside ensureUser = ${actorId}`) - const name = extractNameFromId(actorId) - const queryResult = await this.client.query({ - query: gql` - query { - User(slug: "${name}") { - id - } - } - `, - }) - - if ( - queryResult.data && - Array.isArray(queryResult.data.User) && - queryResult.data.User.length > 0 - ) { - debug('ensureUser: user exists.. return id') - // user already exists.. return the id - return queryResult.data.User[0].id - } else { - 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:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") { - id - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - - return result.data.CreateUser.id - } - } -} diff --git a/backend/src/activitypub/routes/inbox.ts b/backend/src/activitypub/routes/inbox.ts deleted file mode 100644 index f0f88f7e6..000000000 --- a/backend/src/activitypub/routes/inbox.ts +++ /dev/null @@ -1,54 +0,0 @@ -import express from 'express' -import { activityPub } from '../ActivityPub' - -const debug = require('debug')('ea:inbox') - -const router = express.Router() - -// Shared Inbox endpoint (federated Server) -// For now its only able to handle Note Activities!! -router.post('/', async function (req, res, next) { - debug(`Content-Type = ${req.get('Content-Type')}`) - debug(`body = ${JSON.stringify(req.body, null, 2)}`) - debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`) - switch (req.body.type) { - case 'Create': - await activityPub.handleCreateActivity(req.body).catch(next) - break - case 'Undo': - await activityPub.handleUndoActivity(req.body).catch(next) - break - // case 'Follow': - // await activityPub.handleFollowActivity(req.body).catch(next) - // break - case 'Delete': - await activityPub.handleDeleteActivity(req.body).catch(next) - break - /* 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)) - } - /* eslint-enable */ - res.status(200).end() -}) - -export default router diff --git a/backend/src/activitypub/routes/index.ts b/backend/src/activitypub/routes/index.ts deleted file mode 100644 index dc34da3ab..000000000 --- a/backend/src/activitypub/routes/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import user from './user' -import inbox from './inbox' -import express from 'express' -import cors from 'cors' -import verify from './verify' - -export default function () { - const router = express.Router() - router.use( - '/activitypub/users', - cors(), - express.json({ - type: ['application/activity+json', 'application/ld+json', 'application/json'], - }) as any, - express.urlencoded({ extended: true }) as any, - user, - ) - router.use( - '/activitypub/inbox', - cors(), - express.json({ - type: ['application/activity+json', 'application/ld+json', 'application/json'], - }) as any, - express.urlencoded({ extended: true }) as any, - verify, - inbox, - ) - return router -} diff --git a/backend/src/activitypub/routes/serveUser.ts b/backend/src/activitypub/routes/serveUser.ts deleted file mode 100644 index dd7d80811..000000000 --- a/backend/src/activitypub/routes/serveUser.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createActor } from '../utils/actor' -const gql = require('graphql-tag') -const debug = require('debug')('ea:serveUser') - -export async function serveUser(req, res, next) { - let name = req.params.name - - if (name.startsWith('@')) { - name = name.slice(1) - } - - debug(`name = ${name}`) - const result = await req.app - .get('ap') - .dataSource.client.query({ - query: gql` - query { - User(slug: "${name}") { - publicKey - } - } - `, - }) - .catch((reason) => { - debug(`serveUser User fetch error: ${reason}`) - }) - - if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) { - const publicKey = result.data.User[0].publicKey - const actor = createActor(name, publicKey) - debug(`actor = ${JSON.stringify(actor, null, 2)}`) - debug( - `accepts json = ${req.accepts([ - 'application/activity+json', - 'application/ld+json', - 'application/json', - ])}`, - ) - if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) { - return res.json(actor) - } else if (req.accepts('text/html')) { - // TODO show user's profile page instead of the actor object - /* const outbox = JSON.parse(result.outbox) - const posts = outbox.orderedItems.filter((el) => { return el.object.type === 'Note'}) - const actor = result.actor - debug(posts) */ - // res.render('user', { user: actor, posts: JSON.stringify(posts)}) - return res.json(actor) - } - } else { - debug(`error getting publicKey for actor ${name}`) - next() - } -} diff --git a/backend/src/activitypub/routes/user.ts b/backend/src/activitypub/routes/user.ts deleted file mode 100644 index 8dfdbc91d..000000000 --- a/backend/src/activitypub/routes/user.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { sendCollection } from '../utils/collection' -import express from 'express' -import { serveUser } from './serveUser' -import { activityPub } from '../ActivityPub' -import verify from './verify' - -const router = express.Router() -const debug = require('debug')('ea:user') - -router.get('/:name', async function (req, res, next) { - debug('inside user.js -> serveUser') - await serveUser(req, res, next) -}) - -router.get('/:name/following', (req, res) => { - debug('inside user.js -> serveFollowingCollection') - const name = req.params.name - if (!name) { - res.status(400).send('Bad request! Please specify a name.') - } else { - const collectionName = req.query.page ? 'followingPage' : 'following' - sendCollection(collectionName, req, res) - } -}) - -router.get('/:name/followers', (req, res) => { - debug('inside user.js -> serveFollowersCollection') - const name = req.params.name - if (!name) { - return res.status(400).send('Bad request! Please specify a name.') - } else { - const collectionName = req.query.page ? 'followersPage' : 'followers' - sendCollection(collectionName, req, res) - } -}) - -router.get('/:name/outbox', (req, res) => { - debug('inside user.js -> serveOutboxCollection') - const name = req.params.name - if (!name) { - return res.status(400).send('Bad request! Please specify a name.') - } else { - const collectionName = req.query.page ? 'outboxPage' : 'outbox' - sendCollection(collectionName, req, res) - } -}) - -router.post('/:name/inbox', verify, async function (req, res, next) { - debug(`body = ${JSON.stringify(req.body, null, 2)}`) - debug(`actorId = ${req.body.actor}`) - // const result = await saveActorId(req.body.actor) - switch (req.body.type) { - case 'Create': - await activityPub.handleCreateActivity(req.body).catch(next) - break - case 'Undo': - await activityPub.handleUndoActivity(req.body).catch(next) - break - // case 'Follow': - // await activityPub.handleFollowActivity(req.body).catch(next) - // break - case 'Delete': - await activityPub.handleDeleteActivity(req.body).catch(next) - break - /* 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)) - } - /* eslint-enable */ - res.status(200).end() -}) - -export default router diff --git a/backend/src/activitypub/routes/verify.ts b/backend/src/activitypub/routes/verify.ts deleted file mode 100644 index 33603805f..000000000 --- a/backend/src/activitypub/routes/verify.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { verifySignature } from '../security' -const debug = require('debug')('ea:verify') - -export default async (req, res, next) => { - debug(`actorId = ${req.body.actor}`) - // TODO stop if signature validation fails - if ( - await verifySignature( - `${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`, - req.headers, - ) - ) { - debug('verify = true') - next() - } else { - // throw Error('Signature validation failed!') - debug('verify = false') - next() - } -} diff --git a/backend/src/activitypub/routes/webfinger.spec.ts b/backend/src/activitypub/routes/webfinger.spec.ts deleted file mode 100644 index 33b4f552f..000000000 --- a/backend/src/activitypub/routes/webfinger.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { handler } from './webfinger' -import Factory, { cleanDatabase } from '../../db/factories' -import { getDriver } from '../../db/neo4j' -import CONFIG from '../../config' - -let resource, res, json, status, contentType - -const driver = getDriver() - -const request = () => { - json = jest.fn() - status = jest.fn(() => ({ json })) - contentType = jest.fn(() => ({ json })) - res = { status, contentType } - const req = { - app: { - get: (key) => { - return { - driver, - }[key] - }, - }, - query: { - resource, - }, - } - return handler(req, res) -} - -beforeAll(async () => { - await cleanDatabase() -}) - -afterAll(async () => { - await cleanDatabase() - driver.close() -}) - -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 -afterEach(async () => { - await cleanDatabase() -}) - -describe('webfinger', () => { - describe('no ressource', () => { - beforeEach(() => { - resource = undefined - }) - - it('sends HTTP 400', async () => { - await request() - expect(status).toHaveBeenCalledWith(400) - expect(json).toHaveBeenCalledWith({ - error: 'Query parameter "?resource=acct:@" is missing.', - }) - }) - }) - - describe('?resource query param', () => { - describe('is missing acct:', () => { - beforeEach(() => { - resource = 'some-user@domain' - }) - - it('sends HTTP 400', async () => { - await request() - expect(status).toHaveBeenCalledWith(400) - expect(json).toHaveBeenCalledWith({ - error: 'Query parameter "?resource=acct:@" is missing.', - }) - }) - }) - - describe('has no domain', () => { - beforeEach(() => { - resource = 'acct:some-user@' - }) - - it('sends HTTP 400', async () => { - await request() - expect(status).toHaveBeenCalledWith(400) - expect(json).toHaveBeenCalledWith({ - error: 'Query parameter "?resource=acct:@" is missing.', - }) - }) - }) - - describe('with acct:', () => { - beforeEach(() => { - resource = 'acct:some-user@domain' - }) - - it('returns error as json', async () => { - await request() - expect(status).toHaveBeenCalledWith(404) - expect(json).toHaveBeenCalledWith({ - error: 'No record found for "some-user@domain".', - }) - }) - - describe('given a user for acct', () => { - beforeEach(async () => { - await Factory.build('user', { slug: 'some-user' }) - }) - - it('returns user object', async () => { - await request() - expect(contentType).toHaveBeenCalledWith('application/jrd+json') - expect(json).toHaveBeenCalledWith({ - links: [ - { - href: `${CONFIG.CLIENT_URI}/activitypub/users/some-user`, - rel: 'self', - type: 'application/activity+json', - }, - ], - subject: `acct:some-user@${new URL(CONFIG.CLIENT_URI).host}`, - }) - }) - }) - }) - }) -}) diff --git a/backend/src/activitypub/routes/webfinger.ts b/backend/src/activitypub/routes/webfinger.ts deleted file mode 100644 index 894314f7b..000000000 --- a/backend/src/activitypub/routes/webfinger.ts +++ /dev/null @@ -1,59 +0,0 @@ -import express from 'express' -import CONFIG from '../../config/' -import cors from 'cors' - -const debug = require('debug')('ea:webfinger') -const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/ - -const createWebFinger = (name) => { - const { host } = new URL(CONFIG.CLIENT_URI) - return { - subject: `acct:${name}@${host}`, - links: [ - { - rel: 'self', - type: 'application/activity+json', - href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`, - }, - ], - } -} - -export async function handler(req, res) { - const { resource = '' } = req.query - // eslint-disable-next-line no-unused-vars - const [_, name, domain] = resource.match(regex) || [] - if (!(name && domain)) - return res.status(400).json({ - error: 'Query parameter "?resource=acct:@" is missing.', - }) - - const session = req.app.get('driver').session() - try { - const [slug] = await session.readTransaction(async (t) => { - const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', { - slug: name, - }) - return result.records.map((record) => record.get('slug')) - }) - if (!slug) - return res.status(404).json({ - error: `No record found for "${name}@${domain}".`, - }) - const webFinger = createWebFinger(name) - return res.contentType('application/jrd+json').json(webFinger) - } catch (error) { - debug(error) - return res.status(500).json({ - error: `Something went terribly wrong. Please visit ${CONFIG.SUPPORT_URL}`, - }) - } finally { - session.close() - } -} - -export default function () { - const router = express.Router() - router.use('/webfinger', cors(), express.urlencoded({ extended: true }) as any, handler) - return router -} diff --git a/backend/src/activitypub/security/httpSignature.spec.ts b/backend/src/activitypub/security/httpSignature.spec.ts deleted file mode 100644 index 0c6fbb8b5..000000000 --- a/backend/src/activitypub/security/httpSignature.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { generateRsaKeyPair, createSignature, verifySignature } from '.' -import crypto from 'crypto' -import request from 'request' -jest.mock('request') - -let privateKey -let publicKey -let headers -const passphrase = 'a7dsf78sadg87ad87sfagsadg78' - -describe('activityPub/security', () => { - beforeEach(() => { - const pair = generateRsaKeyPair({ passphrase }) - privateKey = pair.privateKey - publicKey = pair.publicKey - headers = { - Date: '2019-03-08T14:35:45.759Z', - Host: 'democracy-app.de', - 'Content-Type': 'application/json', - } - }) - - describe('createSignature', () => { - describe('returned http signature', () => { - let signatureB64 - let httpSignature - - beforeEach(() => { - const signer = crypto.createSign('rsa-sha256') - signer.update( - '(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json', - ) - signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64') - httpSignature = createSignature({ - privateKey, - keyId: 'https://human-connection.org/activitypub/users/lea#main-key', - url: 'https://democracy-app.de/activitypub/users/max/inbox', - headers, - passphrase, - }) - }) - - it('contains keyId', () => { - expect(httpSignature).toContain( - 'keyId="https://human-connection.org/activitypub/users/lea#main-key"', - ) - }) - - it('contains default algorithm "rsa-sha256"', () => { - expect(httpSignature).toContain('algorithm="rsa-sha256"') - }) - - it('contains headers', () => { - expect(httpSignature).toContain('headers="(request-target) date host content-type"') - }) - - it('contains signature', () => { - expect(httpSignature).toContain('signature="' + signatureB64 + '"') - }) - }) - }) - - describe('verifySignature', () => { - let httpSignature - - beforeEach(() => { - httpSignature = createSignature({ - privateKey, - keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', - url: 'https://democracy-app.de/activitypub/users/max/inbox', - headers, - passphrase, - }) - const body = { - publicKey: { - id: 'https://localhost:4001/activitypub/users/test-user#main-key', - owner: 'https://localhost:4001/activitypub/users/test-user', - publicKeyPem: publicKey, - }, - } - - const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body))) - request.mockImplementation(mockedRequest) - }) - - it('resolves false', async () => { - await expect( - verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers), - ).resolves.toEqual(false) - }) - - describe('valid signature', () => { - beforeEach(() => { - headers.Signature = httpSignature - }) - - it('resolves true', async () => { - await expect( - verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers), - ).resolves.toEqual(true) - }) - }) - }) -}) diff --git a/backend/src/activitypub/security/index.ts b/backend/src/activitypub/security/index.ts deleted file mode 100644 index 271aa5995..000000000 --- a/backend/src/activitypub/security/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -// import dotenv from 'dotenv' -// import { resolve } from 'path' -import crypto from 'crypto' -import request from 'request' -import CONFIG from './../../config' -const debug = require('debug')('ea:security') - -// TODO Does this reference a local config? Why? -// dotenv.config({ path: resolve('src', 'activitypub', '.env') }) - -export function generateRsaKeyPair(options: any = {}) { - const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options - return crypto.generateKeyPairSync('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: 'aes-256-cbc', - passphrase, - }, - }) -} - -// signing -export function createSignature(options) { - const { - privateKey, - keyId, - url, - headers = {}, - algorithm = 'rsa-sha256', - passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE, - } = options - if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { - throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) - } - const signer = crypto.createSign(algorithm) - const signingString = constructSigningString(url, headers) - signer.update(signingString) - const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64') - const headersString = Object.keys(headers).reduce((result, key) => { - return result + ' ' + key.toLowerCase() - }, '') - return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"` -} - -// verifying -export function verifySignature(url, headers) { - return new Promise((resolve, reject) => { - const signatureHeader = headers.signature ? headers.signature : headers.Signature - if (!signatureHeader) { - debug('No Signature header present!') - resolve(false) - } - debug(`Signature Header = ${signatureHeader}`) - const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature') - const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm') - const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers') - const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId') - - if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { - debug('Unsupported hash algorithm specified!') - resolve(false) - } - - const usedHeaders = headersString.split(' ') - const verifyHeaders = {} - Object.keys(headers).forEach((key) => { - if (usedHeaders.includes(key.toLowerCase())) { - verifyHeaders[key.toLowerCase()] = headers[key] - } - }) - const signingString = constructSigningString(url, verifyHeaders) - debug(`keyId= ${keyId}`) - request( - { - url: keyId, - headers: { - Accept: 'application/json', - }, - }, - (err, response, body) => { - if (err) reject(err) - debug(`body = ${body}`) - const actor = JSON.parse(body) - const publicKeyPem = actor.publicKey.publicKeyPem - resolve(httpVerify(publicKeyPem, signature, signingString, algorithm)) - }, - ) - }) -} - -// private: signing -function constructSigningString(url, headers) { - const urlObj = new URL(url) - const signingString = `(request-target): post ${urlObj.pathname}${ - urlObj.search !== '' ? urlObj.search : '' - }` - return Object.keys(headers).reduce((result, key) => { - return result + `\n${key.toLowerCase()}: ${headers[key]}` - }, signingString) -} - -// private: verifying -function httpVerify(pubKey, signature, signingString, algorithm) { - if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { - throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) - } - const verifier = crypto.createVerify(algorithm) - verifier.update(signingString) - return verifier.verify(pubKey, signature, 'base64') -} - -// private: verifying -// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header. -// Just pass what you want as key -function extractKeyValueFromSignatureHeader(signatureHeader, key) { - const keyString = signatureHeader.split(',').filter((el) => { - return !!el.startsWith(key) - })[0] - - let firstEqualIndex = keyString.search('=') - // When headers are requested add 17 to the index to remove "(request-target) " from the string - if (key === 'headers') { - firstEqualIndex += 17 - } - return keyString.substring(firstEqualIndex + 2, keyString.length - 1) -} - -// Obtained from invoking crypto.getHashes() -export const SUPPORTED_HASH_ALGORITHMS = [ - 'rsa-md4', - 'rsa-md5', - 'rsa-mdC2', - 'rsa-ripemd160', - 'rsa-sha1', - 'rsa-sha1-2', - 'rsa-sha224', - 'rsa-sha256', - 'rsa-sha384', - 'rsa-sha512', - 'blake2b512', - 'blake2s256', - 'md4', - 'md4WithRSAEncryption', - 'md5', - 'md5-sha1', - 'md5WithRSAEncryption', - 'mdc2', - 'mdc2WithRSA', - 'ripemd', - 'ripemd160', - 'ripemd160WithRSA', - 'rmd160', - 'sha1', - 'sha1WithRSAEncryption', - 'sha224', - 'sha224WithRSAEncryption', - 'sha256', - 'sha256WithRSAEncryption', - 'sha384', - 'sha384WithRSAEncryption', - 'sha512', - 'sha512WithRSAEncryption', - 'ssl3-md5', - 'ssl3-sha1', - 'whirlpool', -] diff --git a/backend/src/activitypub/utils/activity.ts b/backend/src/activitypub/utils/activity.ts deleted file mode 100644 index 3d85d7f08..000000000 --- a/backend/src/activitypub/utils/activity.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { activityPub } from '../ActivityPub' -import { throwErrorIfApolloErrorOccurred } from './index' -// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index' - -import crypto from 'crypto' -// import as from 'activitystrea.ms' -import gql from 'graphql-tag' -// const debug = require('debug')('ea:utils:activity') - -export function createNoteObject(text, name, id, published) { - const createUuid = crypto.randomBytes(16).toString('hex') - - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`, - type: 'Create', - actor: `${activityPub.endpoint}/activitypub/users/${name}`, - object: { - id: `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`, - type: 'Note', - published: published, - attributedTo: `${activityPub.endpoint}/activitypub/users/${name}`, - content: text, - to: 'https://www.w3.org/ns/activitystreams#Public', - }, - } -} - -export async function createArticleObject(activityId, objectId, text, name, id, published) { - const actorId = await getActorId(name) - - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${activityId}`, - type: 'Create', - actor: `${actorId}`, - object: { - id: `${objectId}`, - type: 'Article', - published: published, - attributedTo: `${actorId}`, - content: text, - to: 'https://www.w3.org/ns/activitystreams#Public', - }, - } -} - -export async function getActorId(name) { - const result = await activityPub.dataSource.client.query({ - query: gql` - query { - User(slug: "${name}") { - actorId - } - } - `, - }) - throwErrorIfApolloErrorOccurred(result) - if (Array.isArray(result.data.User) && result.data.User[0]) { - return result.data.User[0].actorId - } else { - throw Error(`No user with name: ${name}`) - } -} - -// export function sendAcceptActivity(theBody, name, targetDomain, url) { -// as.accept() -// .id( -// `${activityPub.endpoint}/activitypub/users/${name}/status/` + -// crypto.randomBytes(16).toString('hex'), -// ) -// .actor(`${activityPub.endpoint}/activitypub/users/${name}`) -// .object(theBody) -// .prettyWrite((err, doc) => { -// if (!err) { -// return signAndSend(doc, name, targetDomain, url) -// } else { -// debug(`error serializing Accept object: ${err}`) -// throw new Error('error serializing Accept object') -// } -// }) -// } - -// export function sendRejectActivity(theBody, name, targetDomain, url) { -// as.reject() -// .id( -// `${activityPub.endpoint}/activitypub/users/${name}/status/` + -// crypto.randomBytes(16).toString('hex'), -// ) -// .actor(`${activityPub.endpoint}/activitypub/users/${name}`) -// .object(theBody) -// .prettyWrite((err, doc) => { -// if (!err) { -// return signAndSend(doc, name, targetDomain, url) -// } else { -// debug(`error serializing Accept object: ${err}`) -// throw new Error('error serializing Accept object') -// } -// }) -// } - -export function isPublicAddressed(postObject) { - const result: { to: any[]} = { to: []} - if (typeof postObject.to === 'string') { - result.to = [postObject.to] - } - if (typeof postObject === 'string') { - result.to = [postObject] - } - if (Array.isArray(postObject)) { - result.to = postObject - } - return ( - result.to.includes('Public') || - result.to.includes('as:Public') || - result.to.includes('https://www.w3.org/ns/activitystreams#Public') - ) -} diff --git a/backend/src/activitypub/utils/actor.ts b/backend/src/activitypub/utils/actor.ts deleted file mode 100644 index e07397bdc..000000000 --- a/backend/src/activitypub/utils/actor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { activityPub } from '../ActivityPub' - -export function createActor(name, pubkey) { - return { - '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], - id: `${activityPub.endpoint}/activitypub/users/${name}`, - type: 'Person', - preferredUsername: `${name}`, - name: `${name}`, - following: `${activityPub.endpoint}/activitypub/users/${name}/following`, - followers: `${activityPub.endpoint}/activitypub/users/${name}/followers`, - inbox: `${activityPub.endpoint}/activitypub/users/${name}/inbox`, - outbox: `${activityPub.endpoint}/activitypub/users/${name}/outbox`, - url: `${activityPub.endpoint}/activitypub/@${name}`, - endpoints: { - sharedInbox: `${activityPub.endpoint}/activitypub/inbox`, - }, - publicKey: { - id: `${activityPub.endpoint}/activitypub/users/${name}#main-key`, - owner: `${activityPub.endpoint}/activitypub/users/${name}`, - publicKeyPem: pubkey, - }, - } -} diff --git a/backend/src/activitypub/utils/collection.ts b/backend/src/activitypub/utils/collection.ts deleted file mode 100644 index 9cb71fe39..000000000 --- a/backend/src/activitypub/utils/collection.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { activityPub } from '../ActivityPub' -import { constructIdFromName } from './index' -const debug = require('debug')('ea:utils:collections') - -export function createOrderedCollection(name, collectionName) { - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`, - summary: `${name}s ${collectionName} collection`, - type: 'OrderedCollection', - first: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`, - totalItems: 0, - } -} - -export function createOrderedCollectionPage(name, collectionName) { - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`, - summary: `${name}s ${collectionName} collection`, - type: 'OrderedCollectionPage', - totalItems: 0, - partOf: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`, - orderedItems: [], - } -} -export function sendCollection(collectionName, req, res) { - const name = req.params.name - const id = constructIdFromName(name) - - switch (collectionName) { - case 'followers': - attachThenCatch(activityPub.collections.getFollowersCollection(id), res) - break - - case 'followersPage': - attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res) - break - - case 'following': - attachThenCatch(activityPub.collections.getFollowingCollection(id), res) - break - - case 'followingPage': - attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res) - break - - case 'outbox': - attachThenCatch(activityPub.collections.getOutboxCollection(id), res) - break - - case 'outboxPage': - attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res) - break - - default: - res.status(500).end() - } -} - -function attachThenCatch(promise, res) { - return promise - .then((collection) => { - res.status(200).contentType('application/activity+json').send(collection) - }) - .catch((err) => { - debug(`error getting a Collection: = ${err}`) - res.status(500).end() - }) -} diff --git a/backend/src/activitypub/utils/index.ts b/backend/src/activitypub/utils/index.ts deleted file mode 100644 index 360125f73..000000000 --- a/backend/src/activitypub/utils/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { activityPub } from '../ActivityPub' -import gql from 'graphql-tag' -import { createSignature } from '../security' -import request from 'request' -import CONFIG from './../../config' -const debug = require('debug')('ea:utils') - -export function extractNameFromId(uri) { - const urlObject = new URL(uri) - const pathname = urlObject.pathname - const splitted = pathname.split('/') - - return splitted[splitted.indexOf('users') + 1] -} - -export function extractIdFromActivityId(uri) { - const urlObject = new URL(uri) - const pathname = urlObject.pathname - const splitted = pathname.split('/') - - return splitted[splitted.indexOf('status') + 1] -} -export function constructIdFromName(name, fromDomain = activityPub.endpoint) { - return `${fromDomain}/activitypub/users/${name}` -} - -export function extractDomainFromUrl(url) { - return new URL(url).host -} - -export function throwErrorIfApolloErrorOccurred(result) { - if (result.error && (result.error.message || result.error.errors)) { - throw new Error( - `${result.error.message ? result.error.message : result.error.errors[0].message}`, - ) - } -} - -export function signAndSend(activity, fromName, targetDomain, url) { - // fix for development: replace with http - url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url - debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`) - return new Promise((resolve, reject) => { - debug('inside signAndSend') - // get the private key - activityPub.dataSource.client - .query({ - query: gql` - query { - User(slug: "${fromName}") { - privateKey - } - } - `, - }) - .then((result) => { - if (result.error) { - reject(result.error) - } else { - // add security context - const parsedActivity = JSON.parse(activity) - if (Array.isArray(parsedActivity['@context'])) { - parsedActivity['@context'].push('https://w3id.org/security/v1') - } else { - const context = [parsedActivity['@context']] - context.push('https://w3id.org/security/v1') - parsedActivity['@context'] = context - } - - // deduplicate context strings - parsedActivity['@context'] = [...new Set(parsedActivity['@context'])] - const privateKey = result.data.User[0].privateKey - const date = new Date().toUTCString() - - debug(`url = ${url}`) - request( - { - url: url, - headers: { - Host: targetDomain, - Date: date, - Signature: createSignature({ - privateKey, - keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`, - url, - headers: { - Host: targetDomain, - Date: date, - 'Content-Type': 'application/activity+json', - }, - }), - 'Content-Type': 'application/activity+json', - }, - method: 'POST', - body: JSON.stringify(parsedActivity), - }, - (error, response) => { - if (error) { - debug(`Error = ${JSON.stringify(error, null, 2)}`) - reject(error) - } else { - debug('Response Headers:', JSON.stringify(response.headers, null, 2)) - debug('Response Body:', JSON.stringify(response.body, null, 2)) - resolve(response) - } - }, - ) - } - }) - }) -} diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 9eb67abbf..d594f4852 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1100,7 +1100,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] huey.relateTo(p9, 'shouted'), louie.relateTo(p10, 'shouted'), ]) - const reports = await Promise.all([ Factory.build('report'), Factory.build('report'), @@ -1113,32 +1112,31 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const reportAgainstDewey = reports[3] // report resource first time - await Promise.all([ - reportAgainstDagobert.relateTo(jennyRostock, 'filed', { - resourceId: 'u7', - reasonCategory: 'discrimination_etc', - reasonDescription: 'This user is harassing me with bigoted remarks!', - }), - reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), - reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', { - resourceId: 'p2', - reasonCategory: 'doxing', - reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", - }), - reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), - reportAgainstTrollingComment.relateTo(huey, 'filed', { - resourceId: 'c1', - reasonCategory: 'other', - reasonDescription: 'This comment is bigoted', - }), - reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), - reportAgainstDewey.relateTo(dagobert, 'filed', { - resourceId: 'u5', - reasonCategory: 'discrimination_etc', - reasonDescription: 'This user is harassing me!', - }), - reportAgainstDewey.relateTo(dewey, 'belongsTo'), - ]) + + await reportAgainstDagobert.relateTo(jennyRostock, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }) + await reportAgainstDagobert.relateTo(dagobert, 'belongsTo') + await reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', { + resourceId: 'p2', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", + }) + await reportAgainstTrollingPost.relateTo(p2, 'belongsTo') + await reportAgainstTrollingComment.relateTo(huey, 'filed', { + resourceId: 'c1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }) + await reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo') + await reportAgainstDewey.relateTo(dagobert, 'filed', { + resourceId: 'u5', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me!', + }) + await reportAgainstDewey.relateTo(dewey, 'belongsTo') // report resource a second time await Promise.all([ diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts index 59694914a..c51950cc8 100644 --- a/backend/src/graphql/messages.ts +++ b/backend/src/graphql/messages.ts @@ -21,11 +21,13 @@ export const messageQuery = () => { return gql` query($roomId: ID!) { Message(roomId: $roomId) { + _id id content - author { - id - } + senderId + username + avatar + date } } ` diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/rooms.ts index 38d10a1d8..1c2120fb0 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/rooms.ts @@ -9,6 +9,7 @@ export const createRoomMutation = () => { userId: $userId ) { id + roomId } } ` @@ -19,8 +20,15 @@ export const roomQuery = () => { query { Room { id + roomId + roomName users { + _id id + name + avatar { + url + } } } } diff --git a/backend/src/middleware/activityPubMiddleware.ts b/backend/src/middleware/activityPubMiddleware.ts deleted file mode 100644 index 712ca6c8a..000000000 --- a/backend/src/middleware/activityPubMiddleware.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { generateRsaKeyPair } from '../activitypub/security' -import { activityPub } from '../activitypub/ActivityPub' -// import as from 'activitystrea.ms' - -// const debug = require('debug')('backend:schema') - -export default { - Mutation: { - // CreatePost: async (resolve, root, args, context, info) => { - // args.activityId = activityPub.generateStatusId(context.user.slug) - // args.objectId = activityPub.generateStatusId(context.user.slug) - - // const post = await resolve(root, args, context, info) - - // const { user: author } = context - // const actorId = author.actorId - // debug(`actorId = ${actorId}`) - // const createActivity = await new Promise((resolve, reject) => { - // as.create() - // .id(`${actorId}/status/${args.activityId}`) - // .actor(`${actorId}`) - // .object( - // as - // .article() - // .id(`${actorId}/status/${post.id}`) - // .content(post.content) - // .to('https://www.w3.org/ns/activitystreams#Public') - // .publishedNow() - // .attributedTo(`${actorId}`), - // ) - // .prettyWrite((err, doc) => { - // if (err) { - // reject(err) - // } else { - // debug(doc) - // const parsedDoc = JSON.parse(doc) - // parsedDoc.send = true - // resolve(JSON.stringify(parsedDoc)) - // } - // }) - // }) - // try { - // await activityPub.sendActivity(createActivity) - // } catch (e) { - // debug(`error sending post activity\n${e}`) - // } - // return post - // }, - SignupVerification: async (resolve, root, args, context, info) => { - const keys = generateRsaKeyPair() - Object.assign(args, keys) - args.actorId = `${activityPub.host}/activitypub/users/${args.slug}` - return resolve(root, args, context, info) - }, - }, -} diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 22e92e1a3..813bbe9a7 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,7 +1,5 @@ import { applyMiddleware } from 'graphql-middleware' import CONFIG from './../config' - -import activityPub from './activityPubMiddleware' import softDelete from './softDelete/softDeleteMiddleware' import sluggify from './sluggifyMiddleware' import excerpt from './excerptMiddleware' @@ -22,7 +20,6 @@ export default (schema) => { sentry, permissions, xss, - activityPub, validation, sluggify, excerpt, diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index e9cf26a22..3ee905be9 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -184,20 +184,23 @@ describe('Message', () => { describe('room exists with authenticated user chatting', () => { it('returns the messages', async () => { - await expect(query({ + const result = await query({ query: messageQuery(), variables: { roomId, }, - })).resolves.toMatchObject({ + }) + expect(result).toMatchObject({ errors: undefined, data: { Message: [{ id: expect.any(String), + _id: result.data.Message[0].id, content: 'Some nice message to other chatting user', - author: { - id: 'chatting-user', - }, + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), }], }, }) @@ -209,9 +212,17 @@ describe('Message', () => { mutation: createMessageMutation(), variables: { roomId, - content: 'Another nice message to other chatting user', + content: 'A nice response message to chatting user', } }) + authenticatedUser = await chattingUser.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'And another nice message to other chatting user', + } + }) }) it('returns the messages', async () => { @@ -227,22 +238,32 @@ describe('Message', () => { { id: expect.any(String), content: 'Some nice message to other chatting user', - author: { - id: 'chatting-user', - }, + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), }, { id: expect.any(String), - content: 'Another nice message to other chatting user', - author: { - id: 'other-chatting-user', - }, - } + content: 'A nice response message to chatting user', + senderId: 'other-chatting-user', + username: 'Other Chatting User', + avatar: expect.any(String), + date: expect.any(String), + }, + { + id: expect.any(String), + content: 'And another nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + }, ], }, }) }) - }) + }) }) describe('room exists, authenticated user not in room', () => { diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index 2cf72e9fe..0be0298d1 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -13,7 +13,13 @@ export default { id: context.user.id, }, } - return neo4jgraphql(object, params, context, resolveInfo) + const resolved = await neo4jgraphql(object, params, context, resolveInfo) + if (resolved) { + resolved.forEach((message) => { + message._id = message.id + }) + } + return resolved }, }, Mutation: { @@ -24,11 +30,11 @@ export default { const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createMessageCypher = ` MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) - MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room) - ON CREATE SET - message.createdAt = toString(datetime()), - message.id = apoc.create.uuid(), - message.content = $content + CREATE (currentUser)-[:CREATED]->(message:Message { + createdAt: toString(datetime()), + id: apoc.create.uuid(), + content: $content + })-[:INSIDE]->(room) RETURN message { .* } ` const createMessageTxResponse = await transaction.run( diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 8c4d887cb..945facd05 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -102,11 +102,12 @@ describe('Room', () => { }, }) roomId = result.data.CreateRoom.id - await expect(result).toMatchObject({ + expect(result).toMatchObject({ errors: undefined, data: { CreateRoom: { id: expect.any(String), + roomId: result.data.CreateRoom.id, }, }, }) @@ -153,18 +154,31 @@ describe('Room', () => { }) it('returns the room', async () => { - await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + const result = await query({ query: roomQuery() }) + expect(result).toMatchObject({ errors: undefined, data: { Room: [ { id: expect.any(String), + roomId: result.data.Room[0].id, + roomName: 'Other Chatting User', users: expect.arrayContaining([ { + _id: 'chatting-user', id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, }, { + _id: 'other-chatting-user', id: 'other-chatting-user', + name: 'Other Chatting User', + avatar: { + url: expect.any(String), + }, }, ]), }, @@ -180,18 +194,31 @@ describe('Room', () => { }) it('returns the room', async () => { - await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + const result = await query({ query: roomQuery() }) + expect(result).toMatchObject({ errors: undefined, data: { Room: [ { id: expect.any(String), + roomId: result.data.Room[0].id, + roomName: 'Chatting User', users: expect.arrayContaining([ { + _id: 'chatting-user', id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, }, { + _id: 'other-chatting-user', id: 'other-chatting-user', + name: 'Other Chatting User', + avatar: { + url: expect.any(String), + }, }, ]), }, diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index f3ea05cc9..bf0e6b8a6 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -8,7 +8,18 @@ export default { params.filter.users_some = { id: context.user.id, } - return neo4jgraphql(object, params, context, resolveInfo) + const resolved = await neo4jgraphql(object, params, context, resolveInfo) + if (resolved) { + resolved.forEach((room) => { + if (room.users) { + room.roomName = room.users.filter((user) => user.id !== context.user.id)[0].name + room.users.forEach((user) => { + user._id = user.id + }) + } + }) + } + return resolved }, }, Mutation: { @@ -37,6 +48,9 @@ export default { }) try { const room = await writeTxResultPromise + if (room) { + room.roomId = room.id + } return room } catch (error) { throw new Error(error) diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/schema/types/type/Message.gql index 5b14104db..4a3346079 100644 --- a/backend/src/schema/types/type/Message.gql +++ b/backend/src/schema/types/type/Message.gql @@ -11,6 +11,11 @@ type Message { author: User! @relation(name: "CREATED", direction: "IN") room: Room! @relation(name: "INSIDE", direction: "OUT") + + senderId: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.id") + username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name") + avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url") + date: String! @cypher(statement: "RETURN this.createdAt") } type Mutation { diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 8792aa56a..c90ebda3a 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -10,7 +10,10 @@ type Room { createdAt: String updatedAt: String - users: [User]! @relation(name: "CHATS_IN", direction: "IN") + users: [User]! @relation(name: "CHATS_IN", direction: "IN") + + roomId: String! @cypher(statement: "RETURN this.id") + roomName: String! ## @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user[0].name") } type Mutation { diff --git a/backend/src/server.ts b/backend/src/server.ts index a76735147..b4d63c007 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -7,7 +7,6 @@ import middleware from './middleware' import { getNeode, getDriver } from './db/neo4j' import decode from './jwt/decode' import schema from './schema' -import webfinger from './activitypub/routes/webfinger' import { RedisPubSub } from 'graphql-redis-subscriptions' import { PubSub } from 'graphql-subscriptions' import Redis from 'ioredis' @@ -89,7 +88,6 @@ const createServer = (options?) => { (CONFIG.DEBUG && { contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }) || {}, ) as any, ) - app.use('/.well-known/', webfinger()) app.use(express.static('public')) app.use(bodyParser.json({ limit: '10mb' }) as any) app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }) as any) diff --git a/backend/test/features/activity-delete.feature b/backend/test/features/activity-delete.feature deleted file mode 100644 index 76c734952..000000000 --- a/backend/test/features/activity-delete.feature +++ /dev/null @@ -1,55 +0,0 @@ -Feature: Delete an object - I want to delete objects - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | bernd-das-brot| - And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://aronda.org/users/bernd-das-brot/status/lka7dfzkjn2398hsfd", - "type": "Create", - "actor": "https://aronda.org/users/bernd-das-brot", - "object": { - "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", - "content": "Hi Max, how are you?", - "to": "https://www.w3.org/ns/activitystreams#Public" - } - } - """ - - Scenario: Deleting a post (Article Object) - When I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2", - "type": "Delete", - "object": { - "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", - "type": "Article", - "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", - "content": "Hi Max, how are you?", - "to": "https://www.w3.org/ns/activitystreams#Public" - } - } - """ - Then I expect the status code to be 200 - And the object is removed from the outbox collection of "bernd-das-brot" - """ - { - "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", - "type": "Article", - "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", - "content": "Hi Max, how are you?", - "to": "https://www.w3.org/ns/activitystreams#Public" - } - """ diff --git a/backend/test/features/activity-follow.feature b/backend/test/features/activity-follow.feature deleted file mode 100644 index 7aa0c447d..000000000 --- a/backend/test/features/activity-follow.feature +++ /dev/null @@ -1,51 +0,0 @@ -Feature: Follow a user - I want to be able to follow a user on another instance. - Also if I do not want to follow a previous followed user anymore, - I want to undo the follow. - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | stuart-little | - | tero-vota | - - @wip - Scenario: Send a follow to a user inbox and make sure it's added to the right followers collection - When I send a POST request with the following activity to "/activitypub/users/tero-vota/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83", - "type": "Follow", - "actor": "http://localhost:4123/activitypub/users/stuart-little", - "object": "http://localhost:4123/activitypub/users/tero-vota" - } - """ - Then I expect the status code to be 200 - And the follower is added to the followers collection of "tero-vota" - """ - http://localhost:4123/activitypub/users/stuart-little - """ - - Scenario: Send an undo activity to revert the previous follow activity - When I send a POST request with the following activity to "/activitypub/users/stuart-little/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2", - "type": "Undo", - "actor": "http://localhost:4123/activitypub/users/tero-vota", - "object": { - "id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83", - "type": "Follow", - "actor": "http://localhost:4123/activitypub/users/stuart-little", - "object": "http://localhost:4123/activitypub/users/tero-vota" - } - } - """ - Then I expect the status code to be 200 - And the follower is removed from the followers collection of "tero-vota" - """ - http://localhost:4123/activitypub/users/stuart-little - """ diff --git a/backend/test/features/activity-like.feature b/backend/test/features/activity-like.feature deleted file mode 100644 index 26ef9c857..000000000 --- a/backend/test/features/activity-like.feature +++ /dev/null @@ -1,43 +0,0 @@ -Feature: Like an object like an article or note - As a user I want to like others posts - Also if I do not want to follow a previous followed user anymore, - I want to undo the follow. - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | karl-heinz | - | peter-lustiger | - And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd", - "type": "Create", - "actor": "http://localhost:4123/activitypub/users/karl-heinz", - "object": { - "id": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf", - "type": "Article", - "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "http://localhost:4123/activitypub/users/karl-heinz", - "content": "Hi Max, how are you?", - "to": "https://www.w3.org/ns/activitystreams#Public" - } - } - """ - - @wip - Scenario: Send a like of a person to an users inbox and make sure it's added to the likes collection - When I send a POST request with the following activity to "/activitypub/users/karl-heinz/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83", - "type": "Like", - "actor": "http://localhost:4123/activitypub/users/peter-lustiger", - "object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf" - } - """ - Then I expect the status code to be 200 - And the post with id "dkasfljsdfaafg9843jknsdf" has been liked by "peter-lustiger" diff --git a/backend/test/features/collection.feature b/backend/test/features/collection.feature deleted file mode 100644 index 1bb4737e0..000000000 --- a/backend/test/features/collection.feature +++ /dev/null @@ -1,101 +0,0 @@ -Feature: Receiving collections - As a member of the Fediverse I want to be able of fetching collections - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | renate-oberdorfer | - - Scenario: Send a request to the outbox URI of peter-lustig and expect a ordered collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox", - "summary": "renate-oberdorfers outbox collection", - "type": "OrderedCollection", - "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true", - "totalItems": 0 - } - """ - - Scenario: Send a request to the following URI of peter-lustig and expect a ordered collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/following" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following", - "summary": "renate-oberdorfers following collection", - "type": "OrderedCollection", - "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true", - "totalItems": 0 - } - """ - - Scenario: Send a request to the followers URI of peter-lustig and expect a ordered collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/followers" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers", - "summary": "renate-oberdorfers followers collection", - "type": "OrderedCollection", - "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true", - "totalItems": 0 - } - """ - - Scenario: Send a request to the outbox URI of peter-lustig and expect a paginated outbox collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox?page=true" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true", - "summary": "renate-oberdorfers outbox collection", - "type": "OrderedCollectionPage", - "totalItems": 0, - "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox", - "orderedItems": [] - } - """ - - Scenario: Send a request to the following URI of peter-lustig and expect a paginated following collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/following?page=true" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true", - "summary": "renate-oberdorfers following collection", - "type": "OrderedCollectionPage", - "totalItems": 0, - "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/following", - "orderedItems": [] - } - """ - - Scenario: Send a request to the followers URI of peter-lustig and expect a paginated followers collection - When I send a GET request to "/activitypub/users/renate-oberdorfer/followers?page=true" - Then I expect the status code to be 200 - And I receive the following json: - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true", - "summary": "renate-oberdorfers followers collection", - "type": "OrderedCollectionPage", - "totalItems": 0, - "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers", - "orderedItems": [] - } - """ diff --git a/backend/test/features/object-article.feature b/backend/test/features/object-article.feature deleted file mode 100644 index 030e408e9..000000000 --- a/backend/test/features/object-article.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Send and receive Articles - I want to send and receive article's via ActivityPub - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | marvin | - | max | - - Scenario: Send an article to a user inbox and make sure it's added to the inbox - When I send a POST request with the following activity to "/activitypub/users/max/inbox": - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://aronda.org/users/marvin/status/lka7dfzkjn2398hsfd", - "type": "Create", - "actor": "https://aronda.org/users/marvin", - "object": { - "id": "https://aronda.org/users/marvin/status/kljsdfg9843jknsdf", - "type": "Article", - "published": "2019-02-07T19:37:55.002Z", - "attributedTo": "https://aronda.org/users/marvin", - "content": "Hi Max, how are you?", - "to": "as:Public" - } - } - """ - Then I expect the status code to be 200 - And the post with id "kljsdfg9843jknsdf" to be created diff --git a/backend/test/features/support/steps.ts b/backend/test/features/support/steps.ts deleted file mode 100644 index c62b286cd..000000000 --- a/backend/test/features/support/steps.ts +++ /dev/null @@ -1,157 +0,0 @@ -// features/support/steps.js -import { Given, When, Then, AfterAll } from 'cucumber' -import { expect } from 'chai' -// import { client } from '../../../src/activitypub/apollo-client' -import { GraphQLClient } from 'graphql-request' -import Factory from '../../../src/db/factories' -const debug = require('debug')('ea:test:steps') -const host: any = null -const client = new GraphQLClient(host) - -function createUser (slug) { - debug(`creating user ${slug}`) - return Factory.build('user', { - name: slug, - }, { - password: '1234', - email: 'example@test.org', - }) - // await login({ email: 'example@test.org', password: '1234' }) -} - -Given('our own server runs at {string}', function (string) { - // just documenation -}) - -Given('we have the following users in our database:', function (dataTable) { - return Promise.all(dataTable.hashes().map((user) => { - return createUser(user.Slug) - })) -}) - -When('I send a GET request to {string}', async function (pathname) { - const response = await this.get(pathname) - this.lastContentType = response.lastContentType - - this.lastResponses.push(response.lastResponse) - this.statusCode = response.statusCode -}) - -When('I send a POST request with the following activity to {string}:', async function (inboxUrl, activity) { - debug(`inboxUrl = ${inboxUrl}`) - debug(`activity = ${activity}`) - const splitted = inboxUrl.split('/') - const slug = splitted[splitted.indexOf('users') + 1] - let result - do { - result = await client.request(` - query { - User(slug: "${slug}") { - id - slug - actorId - } - } - `) - } while (result.User.length === 0) - this.lastInboxUrl = inboxUrl - this.lastActivity = activity - const response = await this.post(inboxUrl, activity) - - this.lastResponses.push(response.lastResponse) - this.lastResponse = response.lastResponse - this.statusCode = response.statusCode -}) - -Then('I receive the following json:', function (docString) { - const parsedDocString = JSON.parse(docString) - const parsedLastResponse = JSON.parse(this.lastResponses.shift()) - if (Array.isArray(parsedDocString.orderedItems)) { - parsedDocString.orderedItems.forEach((el) => { - delete el.id - if (el.object) delete el.object.published - }) - parsedLastResponse.orderedItems.forEach((el) => { - delete el.id - if (el.object) delete el.object.published - }) - } - if (parsedDocString.publicKey && parsedDocString.publicKey.publicKeyPem) { - delete parsedDocString.publicKey.publicKeyPem - delete parsedLastResponse.publicKey.publicKeyPem - } - expect(parsedDocString).to.eql(parsedLastResponse) -}) - -Then('I expect the Content-Type to be {string}', function (contentType) { - expect(this.lastContentType).to.equal(contentType) -}) - -Then('I expect the status code to be {int}', function (statusCode) { - expect(this.statusCode).to.equal(statusCode) -}) - -Then('the activity is added to the {string} collection', async function (collectionName) { - const response = await this.get(this.lastInboxUrl.replace('inbox', collectionName) + '?page=true') - debug(`orderedItems = ${JSON.parse(response.lastResponse).orderedItems}`) - expect(JSON.parse(response.lastResponse).orderedItems).to.include(JSON.parse(this.lastActivity).object) -}) - -Then('the follower is added to the followers collection of {string}', async function (userName, follower) { - const response = await this.get(`/activitypub/users/${userName}/followers?page=true`) - const responseObject = JSON.parse(response.lastResponse) - expect(responseObject.orderedItems).to.include(follower) -}) - -Then('the follower is removed from the followers collection of {string}', async function (userName, follower) { - const response = await this.get(`/activitypub/users/${userName}/followers?page=true`) - const responseObject = JSON.parse(response.lastResponse) - expect(responseObject.orderedItems).to.not.include(follower) -}) - -Then('the post with id {string} to be created', async function (id) { - let result - do { - result = await client.request(` - query { - Post(id: "${id}") { - title - } - } - `) - } while (result.Post.length === 0) - - expect(result.Post).to.be.an('array').that.is.not.empty // eslint-disable-line -}) - -Then('the object is removed from the outbox collection of {string}', async function (name, object) { - const response = await this.get(`/activitypub/users/${name}/outbox?page=true`) - const parsedResponse = JSON.parse(response.lastResponse) - expect(parsedResponse.orderedItems).to.not.include(object) -}) - -Then('I send a GET request to {string} and expect a ordered collection', () => { - -}) - -Then('the activity is added to the users inbox collection', async function () { - -}) - -Then('the post with id {string} has been liked by {string}', async function (id, slug) { - let result - do { - result = await client.request(` - query { - Post(id: "${id}") { - shoutedBy { - slug - } - } - } - `) - } while (result.Post.length === 0) - - expect(result.Post[0].shoutedBy).to.be.an('array').that.is.not.empty // eslint-disable-line - expect(result.Post[0].shoutedBy[0].slug).to.equal(slug) -}) diff --git a/backend/test/features/webfinger.feature b/backend/test/features/webfinger.feature deleted file mode 100644 index cbca5ac10..000000000 --- a/backend/test/features/webfinger.feature +++ /dev/null @@ -1,39 +0,0 @@ -Feature: Webfinger discovery - From an external server, e.g. Mastodon - I want to search for an actor alias - In order to follow the actor - - Background: - Given our own server runs at "http://localhost:4123" - And we have the following users in our database: - | Slug | - | peter-lustiger | - - Scenario: Receiving an actor object - When I send a GET request to "/activitypub/users/peter-lustiger" - Then I receive the following json: - """ - { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1" - ], - "id": "http://localhost:4123/activitypub/users/peter-lustiger", - "type": "Person", - "preferredUsername": "peter-lustiger", - "name": "peter-lustiger", - "following": "http://localhost:4123/activitypub/users/peter-lustiger/following", - "followers": "http://localhost:4123/activitypub/users/peter-lustiger/followers", - "inbox": "http://localhost:4123/activitypub/users/peter-lustiger/inbox", - "outbox": "http://localhost:4123/activitypub/users/peter-lustiger/outbox", - "url": "http://localhost:4123/activitypub/@peter-lustiger", - "endpoints": { - "sharedInbox": "http://localhost:4123/activitypub/inbox" - }, - "publicKey": { - "id": "http://localhost:4123/activitypub/users/peter-lustiger#main-key", - "owner": "http://localhost:4123/activitypub/users/peter-lustiger", - "publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..." - } - } - """ diff --git a/backend/test/features/world.ts b/backend/test/features/world.ts deleted file mode 100644 index 72e120dc7..000000000 --- a/backend/test/features/world.ts +++ /dev/null @@ -1,63 +0,0 @@ -// features/support/world.js -import { setWorldConstructor } from 'cucumber' -import request from 'request' -const debug = require('debug')('ea:test:world') - -class CustomWorld { - lastResponses: any - lastContentType: any - lastInboxUrl: any - lastActivity: any - statusCode: any - constructor () { - // webFinger.feature - this.lastResponses = [] - this.lastContentType = null - this.lastInboxUrl = null - this.lastActivity = null - // object-article.feature - this.statusCode = null - } - get (pathname) { - return new Promise((resolve, reject) => { - request(`http://localhost:4123/${this.replaceSlashes(pathname)}`, { - headers: { - 'Accept': 'application/activity+json' - }}, function (error, response, body) { - if (!error) { - debug(`get content-type = ${response.headers['content-type']}`) - debug(`get body = ${JSON.stringify(typeof body === 'string' ? JSON.parse(body) : body, null, 2)}`) - resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode }) - } else { - reject(error) - } - }) - }) - } - - replaceSlashes (pathname) { - return pathname.replace(/^\/+/, '') - } - - post (pathname, activity) { - return new Promise((resolve, reject) => { - request({ - url: `http://localhost:4123/${this.replaceSlashes(pathname)}`, - method: 'POST', - headers: { - 'Content-Type': 'application/activity+json' - }, - body: activity - }, function (error, response, body) { - if (!error) { - debug(`post response = ${response.headers['content-type']}`) - resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode }) - } else { - reject(error) - } - }) - }) - } -} - -setWorldConstructor(CustomWorld) diff --git a/webapp/components/Registration/RegistrationSlideCreate.vue b/webapp/components/Registration/RegistrationSlideCreate.vue index 0906fc8a4..141db1c4a 100644 --- a/webapp/components/Registration/RegistrationSlideCreate.vue +++ b/webapp/components/Registration/RegistrationSlideCreate.vue @@ -89,13 +89,13 @@ @@ -123,20 +123,22 @@ import { VERSION } from '~/constants/terms-and-conditions-version.js' import links from '~/constants/links' import emails from '~/constants/emails' +import { SignupVerificationMutation } from '~/graphql/Registration.js' +import { SweetalertIcon } from 'vue-sweetalert-icons' import PasswordStrength from '~/components/Password/Strength' import EmailDisplayAndVerify from './EmailDisplayAndVerify' -import { SweetalertIcon } from 'vue-sweetalert-icons' +import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink' import PasswordForm from '~/components/utils/PasswordFormHelper' -import { SignupVerificationMutation } from '~/graphql/Registration.js' import ShowPassword from '../ShowPassword/ShowPassword.vue' export default { name: 'RegistrationSlideCreate', components: { - PasswordStrength, EmailDisplayAndVerify, - SweetalertIcon, + PageParamsLink, + PasswordStrength, ShowPassword, + SweetalertIcon, }, props: { sliderData: { type: Object, required: true }, diff --git a/webapp/components/_new/features/PageParamsLink/PageParamsLink.vue b/webapp/components/_new/features/PageParamsLink/PageParamsLink.vue index 391c4722e..5d7cdeea2 100644 --- a/webapp/components/_new/features/PageParamsLink/PageParamsLink.vue +++ b/webapp/components/_new/features/PageParamsLink/PageParamsLink.vue @@ -1,17 +1,12 @@ @@ -21,6 +16,24 @@ export default { name: 'PageParamsLink', props: { pageParams: { type: Object, required: true }, + forceTargetBlank: { type: Boolean, default: false }, + }, + computed: { + href() { + return this.pageParams.isInternalPage + ? this.pageParams.internalPage.pageRoute + : this.pageParams.externalLink.url + }, + target() { + return this.forceTargetBlank + ? '_blank' + : !this.pageParams.isInternalPage + ? this.pageParams.externalLink.target + : '' + }, + isInternalLink() { + return !this.forceTargetBlank && this.pageParams.isInternalPage + }, }, }