Handle update, like, dislike and accept activities and also sending activities through the ActivityPub protocol + refactoring

This commit is contained in:
Armin 2019-02-27 02:42:34 +01:00
parent e48ce8a94e
commit adb674b98d
13 changed files with 460 additions and 245 deletions

View File

@ -39,6 +39,7 @@
"activitystrea.ms": "^2.1.3",
"apollo-cache-inmemory": "~1.4.3",
"apollo-client": "~2.4.13",
"apollo-link-context": "^1.0.14",
"apollo-link-http": "~1.5.11",
"apollo-server": "~2.4.2",
"bcryptjs": "~2.4.3",

View File

@ -1,10 +1,13 @@
import {
sendAcceptActivity,
sendRejectActivity,
extractNameFromId,
extractDomainFromUrl,
signAndSend
} from './utils'
import {
isPublicAddressed,
sendAcceptActivity,
sendRejectActivity
} from './utils/activity'
import request from 'request'
import as from 'activitystrea.ms'
import NitroDatasource from './NitroDatasource'
@ -30,9 +33,9 @@ export default class ActivityPub {
activityPub = new ActivityPub(process.env.ACTIVITYPUB_DOMAIN || 'localhost', process.env.ACTIVITYPUB_PORT || 4100)
server.express.set('ap', activityPub)
server.express.use(router)
debug('ActivityPub service added to graphql endpoint')
debug('ActivityPub middleware added to the express service')
} else {
debug('ActivityPub service already added to graphql endpoint')
debug('ActivityPub middleware already added to the express service')
}
}
@ -101,7 +104,6 @@ export default class ActivityPub {
debug(`followers = ${toActorObject.followers}`)
debug(`following = ${toActorObject.following}`)
// TODO save after accept activity for the corresponding follow is received
try {
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
debug('follow activity saved')
@ -141,18 +143,77 @@ export default class ActivityPub {
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) {
return this.dataSource.createShouted(activity)
}
handleDislikeActivity (activity) {
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.getFollowingCollectionPage(followObject.actor)
followingCollectionPage.orderedItems.push(followObject.object)
await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
}
}
async sendActivity (activity) {
if (Array.isArray(activity.to) && activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
delete activity.send
const fromName = extractNameFromId(activity.actor)
delete activity.send
const fromName = extractNameFromId(activity.actor)
if (Array.isArray(activity.to) && isPublicAddressed(activity)) {
const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
await Promise.all(
sharedInboxEndpoints.map((el) => {
return signAndSend(activity, fromName, new URL(el).host, el)
})
)
// serve shared inbox endpoints
sharedInboxEndpoints.map((el) => {
return this.trySend(activity, fromName, new URL(el).host, el)
})
activity.to = activity.to.filter((el) => {
return !(isPublicAddressed({ to: el }))
})
// serve the rest
activity.to.map((el) => {
return this.trySend(activity, fromName, new URL(el).host, el)
})
} else if (typeof activity.to === 'string') {
return this.trySend(activity, fromName, new URL(activity.to).host, activity.to)
} else if (Array.isArray(activity.to)) {
activity.to.map((el) => {
return this.trySend(activity, fromName, new URL(el).host, el)
})
}
}
async trySend (activity, fromName, host, url, tries = 5) {
try {
return await signAndSend(activity, fromName, host, url)
} catch (e) {
if (tries > 0) {
setTimeout(function () {
return this.trySend(activity, fromName, host, url, --tries)
}, 20000)
}
}
}
}

View File

@ -1,22 +1,32 @@
import {
throwErrorIfGraphQLErrorOccurred,
throwErrorIfApolloErrorOccurred,
extractIdFromActivityId,
createOrderedCollection,
createOrderedCollectionPage,
extractNameFromId,
createArticleActivity,
constructIdFromName
} from './utils'
import {
createOrderedCollection,
createOrderedCollectionPage
} from './utils/collection'
import {
createArticleActivity,
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 dotenv from 'dotenv'
import uuid from 'uuid'
import generateJwtToken from '../jwt/generateToken'
import { resolve } from 'path'
import trunc from 'trunc-html'
const debug = require('debug')('ea:nitro-datasource')
dotenv.config()
dotenv.config({ path: resolve('src', 'activitypub', '.env') })
export default class NitroDatasource {
constructor (domain) {
@ -29,8 +39,19 @@ export default class NitroDatasource {
}
const link = createHttpLink({ uri: process.env.GRAPHQL_URI, fetch: fetch }) // eslint-disable-line
const cache = new InMemoryCache()
const authLink = setContext((_, { headers }) => {
// generate the authentication token (maybe from env? Which user?)
const token = generateJwtToken({ name: 'ActivityPub', id: uuid() })
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
}
})
this.client = new ApolloClient({
link: link,
link: authLink.concat(link),
cache: cache,
defaultOptions
})
@ -59,7 +80,7 @@ export default class NitroDatasource {
return followersCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -96,7 +117,7 @@ export default class NitroDatasource {
return followersCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -122,7 +143,7 @@ export default class NitroDatasource {
return followingCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -158,7 +179,7 @@ export default class NitroDatasource {
return followingCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -190,7 +211,7 @@ export default class NitroDatasource {
return outboxCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -229,7 +250,7 @@ export default class NitroDatasource {
debug('after createNote')
return outboxCollection
} else {
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
}
@ -246,7 +267,7 @@ export default class NitroDatasource {
`
})
debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
async saveFollowersCollectionPage (followersCollection, onlyNewestItem = true) {
@ -257,7 +278,7 @@ export default class NitroDatasource {
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
return Promise.all(
await Promise.all(orderedItems.map(async (follower) => {
orderedItems.map(async (follower) => {
debug(`follower = ${follower}`)
const fromUserId = await this.ensureUser(follower)
debug(`fromUserId = ${fromUserId}`)
@ -272,9 +293,36 @@ export default class NitroDatasource {
`
})
debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
throwErrorIfGraphQLErrorOccurred(result)
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')
})
)
}
@ -282,6 +330,9 @@ export default class NitroDatasource {
// 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)
const activityId = extractIdFromActivityId(activity.id)
@ -295,7 +346,7 @@ export default class NitroDatasource {
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
// ensure user and add author to post
const userId = await this.ensureUser(postObject.attributedTo)
@ -307,7 +358,78 @@ export default class NitroDatasource {
`
})
throwErrorIfGraphQLErrorOccurred(result)
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 () {
@ -320,7 +442,7 @@ export default class NitroDatasource {
}
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
return result.data.SharedInboxEnpoint
}
async addSharedInboxEndpoint (uri) {
@ -332,7 +454,7 @@ export default class NitroDatasource {
}
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
return true
} catch (e) {
return false
@ -351,7 +473,7 @@ export default class NitroDatasource {
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
const postId = extractIdFromActivityId(postObject.inReplyTo)
result = await this.client.mutate({
@ -364,7 +486,7 @@ export default class NitroDatasource {
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
}
/**
@ -375,10 +497,11 @@ export default class NitroDatasource {
*/
async ensureUser (actorId) {
debug(`inside ensureUser = ${actorId}`)
const name = extractNameFromId(actorId)
const queryResult = await this.client.query({
query: gql`
query {
User(slug: "${extractNameFromId(actorId)}") {
User(slug: "${name}") {
id
}
}
@ -392,16 +515,17 @@ export default class NitroDatasource {
} else {
debug('ensureUser: user not exists.. createUser')
// user does not exist.. create it
const pw = crypto.randomBytes(16).toString('hex')
const result = await this.client.mutate({
mutation: gql`
mutation {
CreateUser(password: "${crypto.randomBytes(16).toString('hex')}", slug:"${extractNameFromId(actorId)}", actorId: "${actorId}", name: "${extractNameFromId(actorId)}") {
CreateUser(password: "${pw}", slug:"${name}", actorId: "${actorId}", name: "${name}") {
id
}
}
`
})
throwErrorIfGraphQLErrorOccurred(result)
throwErrorIfApolloErrorOccurred(result)
return result.data.CreateUser.id
}

View File

@ -1,5 +1,7 @@
import express from 'express'
import { verifySignature } from '../security'
import { activityPub } from '../ActivityPub'
const debug = require('debug')('ea:inbox')
const router = express.Router()
@ -10,31 +12,32 @@ 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)}`)
// TODO stop if signature validation fails
debug(`verify = ${await verifySignature(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
switch (req.body.type) {
case 'Create':
if (req.body.send) {
await req.app.get('ap').sendActivity(req.body).catch(next)
await activityPub.sendActivity(req.body).catch(next)
break
}
await req.app.get('ap').handleCreateActivity(req.body).catch(next)
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await req.app.get('ap').handleUndoActivity(req.body).catch(next)
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
debug('handleFollow')
await req.app.get('ap').handleFollowActivity(req.body)
await activityPub.handleFollowActivity(req.body)
debug('handledFollow')
break
case 'Delete':
await req.app.get('ap').handleDeleteActivity(req.body).catch(next)
await activityPub.handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Update':
case 'Accept':
await activityPub.handleAcceptActivity(req.body).catch(next)
case 'Reject':
case 'Add':

View File

@ -1,4 +1,4 @@
import { createActor } from '../utils'
import { createActor } from '../utils/actor'
const gql = require('graphql-tag')
const debug = require('debug')('ea:serveUser')

View File

@ -1,7 +1,8 @@
import { sendCollection } from '../utils'
import { sendCollection } from '../utils/collection'
import express from 'express'
import { serveUser } from '../utils/serveUser'
import { serveUser } from './serveUser'
import { verifySignature } from '../security'
import { activityPub } from '../ActivityPub'
const router = express.Router()
const debug = require('debug')('ea:user')
@ -47,36 +48,41 @@ router.get('/:name/outbox', (req, res) => {
router.post('/:name/inbox', async function (req, res, next) {
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`actorId = ${req.body.actor}`)
// TODO stop if signature validation fails
debug(`verify = ${await verifySignature(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
// const result = await saveActorId(req.body.actor)
switch (req.body.type) {
case 'Create':
await req.app.get('ap').handleCreateActivity(req.body).catch(next)
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await req.app.get('ap').handleUndoActivity(req.body).catch(next)
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
debug('handleFollow')
await req.app.get('ap').handleFollowActivity(req.body).catch(next)
debug('handledFollow')
await activityPub.handleFollowActivity(req.body).catch(next)
break
case 'Delete':
req.app.get('ap').handleDeleteActivity(req.body).catch(next)
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))

View File

@ -1,5 +1,5 @@
import express from 'express'
import { createWebFinger } from '../utils'
import { createWebFinger } from '../utils/actor'
import gql from 'graphql-tag'
const router = express.Router()

View File

@ -0,0 +1,82 @@
import crypto from 'crypto'
import { activityPub } from '../ActivityPub'
import as from 'activitystrea.ms'
import { signAndSend } from './index'
const debug = require('debug')('ea:utils:activity')
export function createNoteActivity (text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
'object': {
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
'type': 'Note',
'published': published,
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
}
}
export function createArticleActivity (text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
'object': {
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
'type': 'Article',
'published': published,
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
}
}
export function sendAcceptActivity (theBody, name, targetDomain, url) {
as.accept()
.id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`https://${activityPub.domain}/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(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`https://${activityPub.domain}/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) {
if (typeof postObject.to === 'string') {
postObject.to = [postObject.to]
}
return postObject.to.includes('Public') ||
postObject.to.includes('as:Public') ||
postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
}

View File

@ -0,0 +1,40 @@
import { activityPub } from '../ActivityPub'
export function createActor (name, pubkey) {
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
'id': `https://${activityPub.domain}/activitypub/users/${name}`,
'type': 'Person',
'preferredUsername': `${name}`,
'name': `${name}`,
'following': `https://${activityPub.domain}/activitypub/users/${name}/following`,
'followers': `https://${activityPub.domain}/activitypub/users/${name}/followers`,
'inbox': `https://${activityPub.domain}/activitypub/users/${name}/inbox`,
'outbox': `https://${activityPub.domain}/activitypub/users/${name}/outbox`,
'url': `https://${activityPub.domain}/activitypub/@${name}`,
'endpoints': {
'sharedInbox': `https://${activityPub.domain}/activitypub/inbox`
},
'publicKey': {
'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`,
'owner': `https://${activityPub.domain}/activitypub/users/${name}`,
'publicKeyPem': pubkey
}
}
}
export function createWebFinger (name) {
return {
'subject': `acct:${name}@${activityPub.domain}`,
'links': [
{
'rel': 'self',
'type': 'application/activity+json',
'href': `https://${activityPub.domain}/users/${name}`
}
]
}
}

View File

@ -0,0 +1,70 @@
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': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollection',
'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'totalItems': 0
}
}
export function createOrderedCollectionPage (name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollectionPage',
'totalItems': 0,
'partOf': `https://${activityPub.domain}/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.getFollowersCollection(id), res)
break
case 'followersPage':
attachThenCatch(activityPub.getFollowersCollectionPage(id), res)
break
case 'following':
attachThenCatch(activityPub.getFollowingCollection(id), res)
break
case 'followingPage':
attachThenCatch(activityPub.getFollowingCollectionPage(id), res)
break
case 'outbox':
attachThenCatch(activityPub.getOutboxCollection(id), res)
break
case 'outboxPage':
attachThenCatch(activityPub.getOutboxCollectionPage(id), res)
break
default:
res.status(500).end()
}
}
function attachThenCatch (promise, res) {
return promise
.then((collection) => {
res.status(200).contentType('application/activity+json').send(collection)
})
.catch((err) => {
debug(`error getting a Collection: = ${err}`)
res.status(500).end()
})
}

View File

@ -1,5 +1,3 @@
import crypto from 'crypto'
import as from 'activitystrea.ms'
import { activityPub } from '../ActivityPub'
import gql from 'graphql-tag'
import { createSignature } from '../security'
@ -23,194 +21,14 @@ export function extractIdFromActivityId (uri) {
}
export function constructIdFromName (name, fromDomain = activityPub.domain) {
return `https://${fromDomain}/activitypub/users/${name}`
return `http://${fromDomain}/activitypub/users/${name}`
}
export function extractDomainFromUrl (url) {
return new URL(url).hostname
}
export async function getActorIdByName (name) {
debug(`name = ${name}`)
return Promise.resolve()
}
export function sendCollection (collectionName, req, res) {
const name = req.params.name
const id = constructIdFromName(name)
switch (collectionName) {
case 'followers':
attachThenCatch(activityPub.getFollowersCollection(id), res)
break
case 'followersPage':
attachThenCatch(activityPub.getFollowersCollectionPage(id), res)
break
case 'following':
attachThenCatch(activityPub.getFollowingCollection(id), res)
break
case 'followingPage':
attachThenCatch(activityPub.getFollowingCollectionPage(id), res)
break
case 'outbox':
attachThenCatch(activityPub.getOutboxCollection(id), res)
break
case 'outboxPage':
attachThenCatch(activityPub.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()
})
}
export function createActor (name, pubkey) {
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
'id': `https://${activityPub.domain}/activitypub/users/${name}`,
'type': 'Person',
'preferredUsername': `${name}`,
'name': `${name}`,
'following': `https://${activityPub.domain}/activitypub/users/${name}/following`,
'followers': `https://${activityPub.domain}/activitypub/users/${name}/followers`,
'inbox': `https://${activityPub.domain}/activitypub/users/${name}/inbox`,
'outbox': `https://${activityPub.domain}/activitypub/users/${name}/outbox`,
'url': `https://${activityPub.domain}/activitypub/@${name}`,
'endpoints': {
'sharedInbox': `https://${activityPub.domain}/activitypub/inbox`
},
'publicKey': {
'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`,
'owner': `https://${activityPub.domain}/activitypub/users/${name}`,
'publicKeyPem': pubkey
}
}
}
export function createWebFinger (name) {
return {
'subject': `acct:${name}@${activityPub.domain}`,
'links': [
{
'rel': 'self',
'type': 'application/activity+json',
'href': `https://${activityPub.domain}/users/${name}`
}
]
}
}
export function createOrderedCollection (name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollection',
'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'totalItems': 0
}
}
export function createOrderedCollectionPage (name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollectionPage',
'totalItems': 0,
'partOf': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
'orderedItems': []
}
}
export function createNoteActivity (text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
'object': {
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
'type': 'Note',
'published': published,
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
}
}
export function createArticleActivity (text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
'object': {
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
'type': 'Article',
'published': published,
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
}
}
export function sendAcceptActivity (theBody, name, targetDomain, url) {
as.accept()
.id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`https://${activityPub.domain}/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(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`https://${activityPub.domain}/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 throwErrorIfGraphQLErrorOccurred (result) {
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}`)
}

View File

@ -169,6 +169,7 @@ export const resolvers = {
CreatePost: async (object, params, ctx, resolveInfo) => {
params.activityId = uuid()
const result = await neo4jgraphql(object, params, ctx, resolveInfo, false)
debug(`user = ${JSON.stringify(ctx.user, null, 2)}`)
const session = ctx.driver.session()
const author = await session.run(
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
@ -177,7 +178,7 @@ export const resolvers = {
userId: ctx.user.id,
postId: result.id
})
session.close()
debug(`author = ${JSON.stringify(author, null, 2)}`)
const actorId = author.records[0]._fields[0].properties.actorId
const createActivity = await new Promise((resolve, reject) => {
as.create()
@ -188,6 +189,7 @@ export const resolvers = {
.id(`${actorId}/status/${result.id}`)
.content(result.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`)
).prettyWrite((err, doc) => {
if (err) {
@ -200,6 +202,7 @@ export const resolvers = {
}
})
})
session.close()
// try sending post via ActivityPub
await new Promise((resolve) => {
const url = new URL(actorId)

View File

@ -1072,6 +1072,13 @@ apollo-graphql@^0.1.0:
dependencies:
lodash.sortby "^4.7.0"
apollo-link-context@^1.0.14:
version "1.0.14"
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.14.tgz#6265eef49bedadddbbcff4026d04cd351094cd6c"
integrity sha512-l6SIN7Fwqhgg5C5eA8xSrt8gulHBmYTE3J4z5/Q2hP/8Kok0rQ/z5q3uy42/hkdYlnaktOvpz+ZIwEFzcXwujQ==
dependencies:
apollo-link "^1.2.8"
apollo-link-dedup@^1.0.0:
version "1.0.11"
resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.11.tgz#6f34ea748d2834850329ad03111ef18445232b05"