diff --git a/package.json b/package.json index 1aa02fc96..85dfac6a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/activitypub/ActivityPub.js b/src/activitypub/ActivityPub.js index b4d9677c2..74e047e12 100644 --- a/src/activitypub/ActivityPub.js +++ b/src/activitypub/ActivityPub.js @@ -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) + } } } } diff --git a/src/activitypub/NitroDatasource.js b/src/activitypub/NitroDatasource.js index b723fa72c..19e931eef 100644 --- a/src/activitypub/NitroDatasource.js +++ b/src/activitypub/NitroDatasource.js @@ -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 } diff --git a/src/activitypub/routes/inbox.js b/src/activitypub/routes/inbox.js index e8af10c2c..eb720e042 100644 --- a/src/activitypub/routes/inbox.js +++ b/src/activitypub/routes/inbox.js @@ -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': diff --git a/src/activitypub/utils/serveUser.js b/src/activitypub/routes/serveUser.js similarity index 97% rename from src/activitypub/utils/serveUser.js rename to src/activitypub/routes/serveUser.js index 9a13275eb..f65876741 100644 --- a/src/activitypub/utils/serveUser.js +++ b/src/activitypub/routes/serveUser.js @@ -1,4 +1,4 @@ -import { createActor } from '../utils' +import { createActor } from '../utils/actor' const gql = require('graphql-tag') const debug = require('debug')('ea:serveUser') diff --git a/src/activitypub/routes/user.js b/src/activitypub/routes/user.js index 36bb7c2db..2316d319c 100644 --- a/src/activitypub/routes/user.js +++ b/src/activitypub/routes/user.js @@ -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)) diff --git a/src/activitypub/routes/webfinger.js b/src/activitypub/routes/webfinger.js index 9e51dcdfd..ad1c806ad 100644 --- a/src/activitypub/routes/webfinger.js +++ b/src/activitypub/routes/webfinger.js @@ -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() diff --git a/src/activitypub/utils/activity.js b/src/activitypub/utils/activity.js new file mode 100644 index 000000000..afe13dfca --- /dev/null +++ b/src/activitypub/utils/activity.js @@ -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') +} diff --git a/src/activitypub/utils/actor.js b/src/activitypub/utils/actor.js new file mode 100644 index 000000000..0b8cc7454 --- /dev/null +++ b/src/activitypub/utils/actor.js @@ -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}` + } + ] + } +} diff --git a/src/activitypub/utils/collection.js b/src/activitypub/utils/collection.js new file mode 100644 index 000000000..379febe43 --- /dev/null +++ b/src/activitypub/utils/collection.js @@ -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() + }) +} diff --git a/src/activitypub/utils/index.js b/src/activitypub/utils/index.js index 9df537e79..0cb4b7d0d 100644 --- a/src/activitypub/utils/index.js +++ b/src/activitypub/utils/index.js @@ -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}`) } diff --git a/src/graphql-schema.js b/src/graphql-schema.js index e5da2ce54..65dfc8cda 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -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) diff --git a/yarn.lock b/yarn.lock index 78cabe011..05299ad6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"