mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Handle update, like, dislike and accept activities and also sending activities through the ActivityPub protocol + refactoring
This commit is contained in:
parent
e48ce8a94e
commit
adb674b98d
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createActor } from '../utils'
|
||||
import { createActor } from '../utils/actor'
|
||||
const gql = require('graphql-tag')
|
||||
const debug = require('debug')('ea:serveUser')
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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()
|
||||
|
||||
82
src/activitypub/utils/activity.js
Normal file
82
src/activitypub/utils/activity.js
Normal 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')
|
||||
}
|
||||
40
src/activitypub/utils/actor.js
Normal file
40
src/activitypub/utils/actor.js
Normal 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}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
70
src/activitypub/utils/collection.js
Normal file
70
src/activitypub/utils/collection.js
Normal 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()
|
||||
})
|
||||
}
|
||||
@ -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}`)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user