mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
remove activity pub related stuff - since its unused
This commit is contained in:
parent
5567ae475a
commit
c063682560
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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',
|
||||
]
|
||||
@ -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')
|
||||
)
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
"""
|
||||
@ -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
|
||||
"""
|
||||
@ -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"
|
||||
@ -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": []
|
||||
}
|
||||
"""
|
||||
@ -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
|
||||
@ -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)
|
||||
})
|
||||
@ -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..."
|
||||
}
|
||||
}
|
||||
"""
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user