Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 6436-show-the-event-on-the-map

This commit is contained in:
Wolfgang Huß 2023-06-27 12:54:15 +02:00
commit 64f159e82f
39 changed files with 171 additions and 2553 deletions

View File

@ -11,7 +11,7 @@ module.exports = {
],
coverageThreshold: {
global: {
lines: 57,
lines: 70,
},
},
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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:<USER>@<DOMAIN>" 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:<USER>@<DOMAIN>" 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:<USER>@<DOMAIN>" 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}`,
})
})
})
})
})
})

View File

@ -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:<USER>@<DOMAIN>" 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
}

View File

@ -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)
})
})
})
})

View File

@ -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',
]

View File

@ -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')
)
}

View File

@ -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,
},
}
}

View File

@ -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()
})
}

View File

@ -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)
}
},
)
}
})
})
}

View File

@ -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', {
await 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', {
})
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!",
}),
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
reportAgainstTrollingComment.relateTo(huey, 'filed', {
})
await reportAgainstTrollingPost.relateTo(p2, 'belongsTo')
await reportAgainstTrollingComment.relateTo(huey, 'filed', {
resourceId: 'c1',
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
}),
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
reportAgainstDewey.relateTo(dagobert, 'filed', {
})
await reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo')
await reportAgainstDewey.relateTo(dagobert, 'filed', {
resourceId: 'u5',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This user is harassing me!',
}),
reportAgainstDewey.relateTo(dewey, 'belongsTo'),
])
})
await reportAgainstDewey.relateTo(dewey, 'belongsTo')
// report resource a second time
await Promise.all([

View File

@ -21,11 +21,13 @@ export const messageQuery = () => {
return gql`
query($roomId: ID!) {
Message(roomId: $roomId) {
_id
id
content
author {
id
}
senderId
username
avatar
date
}
}
`

View File

@ -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
}
}
}
}

View File

@ -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)
},
},
}

View File

@ -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,

View File

@ -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,7 +212,15 @@ 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',
}
})
})
@ -227,17 +238,27 @@ 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),
},
}
],
},
})

View File

@ -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(

View File

@ -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),
},
},
]),
},

View File

@ -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)

View File

@ -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 {

View File

@ -11,6 +11,9 @@ type Room {
updatedAt: String
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 {

View File

@ -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)

View File

@ -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"
}
"""

View File

@ -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
"""

View File

@ -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"

View File

@ -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": []
}
"""

View File

@ -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

View File

@ -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)
})

View File

@ -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..."
}
}
"""

View File

@ -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)

View File

@ -89,13 +89,13 @@
<label for="checkbox0">
{{ $t('components.registration.create-user-account.termsAndCondsEtcConfirmed') }}
<br />
<a :href="'/terms-and-conditions'" target="_blank">
<page-params-link :pageParams="links.TERMS_AND_CONDITIONS" forceTargetBlank>
{{ $t('site.termsAndConditions') }}
</a>
</page-params-link>
<br />
<a :href="'/data-privacy'" target="_blank">
<page-params-link :pageParams="links.DATA_PRIVACY" forceTargetBlank>
{{ $t('site.data-privacy') }}
</a>
</page-params-link>
</label>
</ds-text>
<ds-text>
@ -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 },

View File

@ -1,17 +1,12 @@
<template>
<nuxt-link
v-if="pageParams.isInternalPage"
v-if="isInternalLink"
:to="pageParams.internalPage.pageRoute"
:data-test="pageParams.name + '-nuxt-link'"
>
<slot />
</nuxt-link>
<a
v-else
:href="pageParams.externalLink.url"
:target="pageParams.externalLink.target"
:data-test="pageParams.name + '-link'"
>
<a v-else :href="href" :target="target" :data-test="pageParams.name + '-link'">
<slot />
</a>
</template>
@ -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
},
},
}
</script>