mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 6436-show-the-event-on-the-map
This commit is contained in:
commit
64f159e82f
@ -11,7 +11,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 57,
|
lines: 70,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1100,7 +1100,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
huey.relateTo(p9, 'shouted'),
|
huey.relateTo(p9, 'shouted'),
|
||||||
louie.relateTo(p10, 'shouted'),
|
louie.relateTo(p10, 'shouted'),
|
||||||
])
|
])
|
||||||
|
|
||||||
const reports = await Promise.all([
|
const reports = await Promise.all([
|
||||||
Factory.build('report'),
|
Factory.build('report'),
|
||||||
Factory.build('report'),
|
Factory.build('report'),
|
||||||
@ -1113,32 +1112,31 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
const reportAgainstDewey = reports[3]
|
const reportAgainstDewey = reports[3]
|
||||||
|
|
||||||
// report resource first time
|
// report resource first time
|
||||||
await Promise.all([
|
|
||||||
reportAgainstDagobert.relateTo(jennyRostock, 'filed', {
|
await reportAgainstDagobert.relateTo(jennyRostock, 'filed', {
|
||||||
resourceId: 'u7',
|
resourceId: 'u7',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonCategory: 'discrimination_etc',
|
||||||
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||||
}),
|
})
|
||||||
reportAgainstDagobert.relateTo(dagobert, 'belongsTo'),
|
await reportAgainstDagobert.relateTo(dagobert, 'belongsTo')
|
||||||
reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', {
|
await reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', {
|
||||||
resourceId: 'p2',
|
resourceId: 'p2',
|
||||||
reasonCategory: 'doxing',
|
reasonCategory: 'doxing',
|
||||||
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
||||||
}),
|
})
|
||||||
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
|
await reportAgainstTrollingPost.relateTo(p2, 'belongsTo')
|
||||||
reportAgainstTrollingComment.relateTo(huey, 'filed', {
|
await reportAgainstTrollingComment.relateTo(huey, 'filed', {
|
||||||
resourceId: 'c1',
|
resourceId: 'c1',
|
||||||
reasonCategory: 'other',
|
reasonCategory: 'other',
|
||||||
reasonDescription: 'This comment is bigoted',
|
reasonDescription: 'This comment is bigoted',
|
||||||
}),
|
})
|
||||||
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
await reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo')
|
||||||
reportAgainstDewey.relateTo(dagobert, 'filed', {
|
await reportAgainstDewey.relateTo(dagobert, 'filed', {
|
||||||
resourceId: 'u5',
|
resourceId: 'u5',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonCategory: 'discrimination_etc',
|
||||||
reasonDescription: 'This user is harassing me!',
|
reasonDescription: 'This user is harassing me!',
|
||||||
}),
|
})
|
||||||
reportAgainstDewey.relateTo(dewey, 'belongsTo'),
|
await reportAgainstDewey.relateTo(dewey, 'belongsTo')
|
||||||
])
|
|
||||||
|
|
||||||
// report resource a second time
|
// report resource a second time
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
@ -21,11 +21,13 @@ export const messageQuery = () => {
|
|||||||
return gql`
|
return gql`
|
||||||
query($roomId: ID!) {
|
query($roomId: ID!) {
|
||||||
Message(roomId: $roomId) {
|
Message(roomId: $roomId) {
|
||||||
|
_id
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
author {
|
senderId
|
||||||
id
|
username
|
||||||
}
|
avatar
|
||||||
|
date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export const createRoomMutation = () => {
|
|||||||
userId: $userId
|
userId: $userId
|
||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
|
roomId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -19,8 +20,15 @@ export const roomQuery = () => {
|
|||||||
query {
|
query {
|
||||||
Room {
|
Room {
|
||||||
id
|
id
|
||||||
|
roomId
|
||||||
|
roomName
|
||||||
users {
|
users {
|
||||||
|
_id
|
||||||
id
|
id
|
||||||
|
name
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import { applyMiddleware } from 'graphql-middleware'
|
import { applyMiddleware } from 'graphql-middleware'
|
||||||
import CONFIG from './../config'
|
import CONFIG from './../config'
|
||||||
|
|
||||||
import activityPub from './activityPubMiddleware'
|
|
||||||
import softDelete from './softDelete/softDeleteMiddleware'
|
import softDelete from './softDelete/softDeleteMiddleware'
|
||||||
import sluggify from './sluggifyMiddleware'
|
import sluggify from './sluggifyMiddleware'
|
||||||
import excerpt from './excerptMiddleware'
|
import excerpt from './excerptMiddleware'
|
||||||
@ -22,7 +20,6 @@ export default (schema) => {
|
|||||||
sentry,
|
sentry,
|
||||||
permissions,
|
permissions,
|
||||||
xss,
|
xss,
|
||||||
activityPub,
|
|
||||||
validation,
|
validation,
|
||||||
sluggify,
|
sluggify,
|
||||||
excerpt,
|
excerpt,
|
||||||
|
|||||||
@ -184,20 +184,23 @@ describe('Message', () => {
|
|||||||
|
|
||||||
describe('room exists with authenticated user chatting', () => {
|
describe('room exists with authenticated user chatting', () => {
|
||||||
it('returns the messages', async () => {
|
it('returns the messages', async () => {
|
||||||
await expect(query({
|
const result = await query({
|
||||||
query: messageQuery(),
|
query: messageQuery(),
|
||||||
variables: {
|
variables: {
|
||||||
roomId,
|
roomId,
|
||||||
},
|
},
|
||||||
})).resolves.toMatchObject({
|
})
|
||||||
|
expect(result).toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Message: [{
|
Message: [{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
_id: result.data.Message[0].id,
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
author: {
|
senderId: 'chatting-user',
|
||||||
id: 'chatting-user',
|
username: 'Chatting User',
|
||||||
},
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -209,9 +212,17 @@ describe('Message', () => {
|
|||||||
mutation: createMessageMutation(),
|
mutation: createMessageMutation(),
|
||||||
variables: {
|
variables: {
|
||||||
roomId,
|
roomId,
|
||||||
content: 'Another nice message to other chatting user',
|
content: 'A nice response message to chatting user',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
authenticatedUser = await chattingUser.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: createMessageMutation(),
|
||||||
|
variables: {
|
||||||
|
roomId,
|
||||||
|
content: 'And another nice message to other chatting user',
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the messages', async () => {
|
it('returns the messages', async () => {
|
||||||
@ -227,22 +238,32 @@ describe('Message', () => {
|
|||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
author: {
|
senderId: 'chatting-user',
|
||||||
id: 'chatting-user',
|
username: 'Chatting User',
|
||||||
},
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
content: 'Another nice message to other chatting user',
|
content: 'A nice response message to chatting user',
|
||||||
author: {
|
senderId: 'other-chatting-user',
|
||||||
id: 'other-chatting-user',
|
username: 'Other Chatting User',
|
||||||
},
|
avatar: expect.any(String),
|
||||||
}
|
date: expect.any(String),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(String),
|
||||||
|
content: 'And another nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('room exists, authenticated user not in room', () => {
|
describe('room exists, authenticated user not in room', () => {
|
||||||
|
|||||||
@ -13,7 +13,13 @@ export default {
|
|||||||
id: context.user.id,
|
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: {
|
Mutation: {
|
||||||
@ -24,11 +30,11 @@ export default {
|
|||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
const createMessageCypher = `
|
const createMessageCypher = `
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
||||||
MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room)
|
CREATE (currentUser)-[:CREATED]->(message:Message {
|
||||||
ON CREATE SET
|
createdAt: toString(datetime()),
|
||||||
message.createdAt = toString(datetime()),
|
id: apoc.create.uuid(),
|
||||||
message.id = apoc.create.uuid(),
|
content: $content
|
||||||
message.content = $content
|
})-[:INSIDE]->(room)
|
||||||
RETURN message { .* }
|
RETURN message { .* }
|
||||||
`
|
`
|
||||||
const createMessageTxResponse = await transaction.run(
|
const createMessageTxResponse = await transaction.run(
|
||||||
|
|||||||
@ -102,11 +102,12 @@ describe('Room', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
roomId = result.data.CreateRoom.id
|
roomId = result.data.CreateRoom.id
|
||||||
await expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
CreateRoom: {
|
CreateRoom: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
roomId: result.data.CreateRoom.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -153,18 +154,31 @@ describe('Room', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns the room', async () => {
|
it('returns the room', async () => {
|
||||||
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
|
const result = await query({ query: roomQuery() })
|
||||||
|
expect(result).toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Room: [
|
Room: [
|
||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
roomId: result.data.Room[0].id,
|
||||||
|
roomName: 'Other Chatting User',
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
|
_id: 'chatting-user',
|
||||||
id: 'chatting-user',
|
id: 'chatting-user',
|
||||||
|
name: 'Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: 'other-chatting-user',
|
||||||
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 () => {
|
it('returns the room', async () => {
|
||||||
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
|
const result = await query({ query: roomQuery() })
|
||||||
|
expect(result).toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Room: [
|
Room: [
|
||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
roomId: result.data.Room[0].id,
|
||||||
|
roomName: 'Chatting User',
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
|
_id: 'chatting-user',
|
||||||
id: 'chatting-user',
|
id: 'chatting-user',
|
||||||
|
name: 'Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: 'other-chatting-user',
|
||||||
id: 'other-chatting-user',
|
id: 'other-chatting-user',
|
||||||
|
name: 'Other Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,18 @@ export default {
|
|||||||
params.filter.users_some = {
|
params.filter.users_some = {
|
||||||
id: context.user.id,
|
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: {
|
Mutation: {
|
||||||
@ -37,6 +48,9 @@ export default {
|
|||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const room = await writeTxResultPromise
|
const room = await writeTxResultPromise
|
||||||
|
if (room) {
|
||||||
|
room.roomId = room.id
|
||||||
|
}
|
||||||
return room
|
return room
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
|
|||||||
@ -11,6 +11,11 @@ type Message {
|
|||||||
|
|
||||||
author: User! @relation(name: "CREATED", direction: "IN")
|
author: User! @relation(name: "CREATED", direction: "IN")
|
||||||
room: Room! @relation(name: "INSIDE", direction: "OUT")
|
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 {
|
type Mutation {
|
||||||
|
|||||||
@ -10,7 +10,10 @@ type Room {
|
|||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
|
||||||
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
||||||
|
|
||||||
|
roomId: String! @cypher(statement: "RETURN this.id")
|
||||||
|
roomName: String! ## @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user[0].name")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import middleware from './middleware'
|
|||||||
import { getNeode, getDriver } from './db/neo4j'
|
import { getNeode, getDriver } from './db/neo4j'
|
||||||
import decode from './jwt/decode'
|
import decode from './jwt/decode'
|
||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
import webfinger from './activitypub/routes/webfinger'
|
|
||||||
import { RedisPubSub } from 'graphql-redis-subscriptions'
|
import { RedisPubSub } from 'graphql-redis-subscriptions'
|
||||||
import { PubSub } from 'graphql-subscriptions'
|
import { PubSub } from 'graphql-subscriptions'
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis'
|
||||||
@ -89,7 +88,6 @@ const createServer = (options?) => {
|
|||||||
(CONFIG.DEBUG && { contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }) || {},
|
(CONFIG.DEBUG && { contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }) || {},
|
||||||
) as any,
|
) as any,
|
||||||
)
|
)
|
||||||
app.use('/.well-known/', webfinger())
|
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
app.use(bodyParser.json({ limit: '10mb' }) as any)
|
app.use(bodyParser.json({ limit: '10mb' }) as any)
|
||||||
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }) as any)
|
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }) as any)
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -89,13 +89,13 @@
|
|||||||
<label for="checkbox0">
|
<label for="checkbox0">
|
||||||
{{ $t('components.registration.create-user-account.termsAndCondsEtcConfirmed') }}
|
{{ $t('components.registration.create-user-account.termsAndCondsEtcConfirmed') }}
|
||||||
<br />
|
<br />
|
||||||
<a :href="'/terms-and-conditions'" target="_blank">
|
<page-params-link :pageParams="links.TERMS_AND_CONDITIONS" forceTargetBlank>
|
||||||
{{ $t('site.termsAndConditions') }}
|
{{ $t('site.termsAndConditions') }}
|
||||||
</a>
|
</page-params-link>
|
||||||
<br />
|
<br />
|
||||||
<a :href="'/data-privacy'" target="_blank">
|
<page-params-link :pageParams="links.DATA_PRIVACY" forceTargetBlank>
|
||||||
{{ $t('site.data-privacy') }}
|
{{ $t('site.data-privacy') }}
|
||||||
</a>
|
</page-params-link>
|
||||||
</label>
|
</label>
|
||||||
</ds-text>
|
</ds-text>
|
||||||
<ds-text>
|
<ds-text>
|
||||||
@ -123,20 +123,22 @@
|
|||||||
import { VERSION } from '~/constants/terms-and-conditions-version.js'
|
import { VERSION } from '~/constants/terms-and-conditions-version.js'
|
||||||
import links from '~/constants/links'
|
import links from '~/constants/links'
|
||||||
import emails from '~/constants/emails'
|
import emails from '~/constants/emails'
|
||||||
|
import { SignupVerificationMutation } from '~/graphql/Registration.js'
|
||||||
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
import PasswordStrength from '~/components/Password/Strength'
|
import PasswordStrength from '~/components/Password/Strength'
|
||||||
import EmailDisplayAndVerify from './EmailDisplayAndVerify'
|
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 PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
import { SignupVerificationMutation } from '~/graphql/Registration.js'
|
|
||||||
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RegistrationSlideCreate',
|
name: 'RegistrationSlideCreate',
|
||||||
components: {
|
components: {
|
||||||
PasswordStrength,
|
|
||||||
EmailDisplayAndVerify,
|
EmailDisplayAndVerify,
|
||||||
SweetalertIcon,
|
PageParamsLink,
|
||||||
|
PasswordStrength,
|
||||||
ShowPassword,
|
ShowPassword,
|
||||||
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
sliderData: { type: Object, required: true },
|
sliderData: { type: Object, required: true },
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-if="pageParams.isInternalPage"
|
v-if="isInternalLink"
|
||||||
:to="pageParams.internalPage.pageRoute"
|
:to="pageParams.internalPage.pageRoute"
|
||||||
:data-test="pageParams.name + '-nuxt-link'"
|
:data-test="pageParams.name + '-nuxt-link'"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<a
|
<a v-else :href="href" :target="target" :data-test="pageParams.name + '-link'">
|
||||||
v-else
|
|
||||||
:href="pageParams.externalLink.url"
|
|
||||||
:target="pageParams.externalLink.target"
|
|
||||||
:data-test="pageParams.name + '-link'"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
@ -21,6 +16,24 @@ export default {
|
|||||||
name: 'PageParamsLink',
|
name: 'PageParamsLink',
|
||||||
props: {
|
props: {
|
||||||
pageParams: { type: Object, required: true },
|
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>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user