Skip to content

Commit 8bad9bd

Browse files
allouismike182uk
andauthored
Handled disabling ActivityPub
ref https://linear.app/ghost/issue/PROD-2351 When ActivityPub is disabled we want to cease federating content, this is done by removng all webhooks for the ActivityPub integration. We also request the disable site handler on ActivityPub, which currently noops, but may eventually disable webfinger, actor lookups etc... --------- Co-authored-by: Michael Barrett <[email protected]>
1 parent fda5776 commit 8bad9bd

File tree

3 files changed

+103
-22
lines changed

3 files changed

+103
-22
lines changed

ghost/core/core/server/services/activitypub/ActivityPubService.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,7 @@ export class ActivityPubService {
8181

8282
async getWebhookSecret(): Promise<string | null> {
8383
try {
84-
const ownerUser = await this.knex('users')
85-
.select('users.*')
86-
.join('roles_users', 'users.id', 'roles_users.user_id')
87-
.join('roles', 'roles.id', 'roles_users.role_id')
88-
.where('roles.name', 'Owner')
89-
.first();
90-
const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
84+
const token = await this.getOwnerUserToken();
9185

9286
const res = await fetch(new URL('.ghost/activitypub/v1/site', this.siteUrl), {
9387
headers: {
@@ -104,6 +98,34 @@ export class ActivityPubService {
10498
}
10599
}
106100

101+
async disable() {
102+
await this.removeWebhooks();
103+
await this.disableSite();
104+
}
105+
106+
async enable() {
107+
await this.initialiseWebhooks();
108+
}
109+
110+
async removeWebhooks() {
111+
const integration = await this.knex
112+
.select('*')
113+
.from('integrations')
114+
.where('slug', '=', 'ghost-activitypub')
115+
.andWhere('type', '=', 'internal')
116+
.first();
117+
118+
if (!integration) {
119+
this.logging.error('No ActivityPub integration found - cannot remove webhooks');
120+
return;
121+
}
122+
123+
await this.knex
124+
.del()
125+
.from('webhooks')
126+
.where('integration_id', '=', integration.id);
127+
}
128+
107129
async initialiseWebhooks() {
108130
const integration = await this.knex
109131
.select('*')
@@ -155,4 +177,30 @@ export class ActivityPubService {
155177
.insert(webhooksToInsert)
156178
.into('webhooks');
157179
}
180+
181+
async disableSite() {
182+
try {
183+
const token = await this.getOwnerUserToken();
184+
185+
await fetch(new URL('.ghost/activitypub/v1/site', this.siteUrl), {
186+
method: 'DELETE',
187+
headers: {
188+
Authorization: `Bearer ${token}`
189+
}
190+
});
191+
} catch (err: unknown) {
192+
this.logging.error(`Could not disable ActivityPub for site: ${this.siteUrl} due to: ${err}`);
193+
}
194+
}
195+
196+
private async getOwnerUserToken() {
197+
const ownerUser = await this.knex('users')
198+
.select('users.*')
199+
.join('roles_users', 'users.id', 'roles_users.user_id')
200+
.join('roles', 'roles.id', 'roles_users.role_id')
201+
.where('roles.name', 'Owner')
202+
.first();
203+
204+
return await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
205+
}
158206
}

ghost/core/core/server/services/activitypub/ActivityPubServiceWrapper.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,23 @@ module.exports = class ActivityPubServiceWrapper {
3232
IdentityTokenServiceWrapper.instance
3333
);
3434

35-
const initActivityPubService = async () => {
36-
await ActivityPubServiceWrapper.instance.initialiseWebhooks();
37-
ActivityPubServiceWrapper.initialised = true;
38-
};
39-
40-
if (settingsCache.get('social_web_enabled')) {
41-
await initActivityPubService();
42-
} else {
43-
const initActivityPubServiceLater = async () => {
44-
if (settingsCache.get('social_web_enabled') && !ActivityPubServiceWrapper.initialised) {
45-
await initActivityPubService();
35+
async function configureActivityPub() {
36+
if (settingsCache.get('social_web_enabled')) {
37+
if (!ActivityPubServiceWrapper.initialised) {
38+
await ActivityPubServiceWrapper.instance.enable();
39+
ActivityPubServiceWrapper.initialised = true;
4640
}
47-
};
48-
49-
events.on('settings.labs.edited', initActivityPubServiceLater);
50-
events.on('settings.social_web.edited', initActivityPubServiceLater);
41+
} else {
42+
if (ActivityPubServiceWrapper.initialised) {
43+
await ActivityPubServiceWrapper.instance.disable();
44+
ActivityPubServiceWrapper.initialised = false;
45+
}
46+
}
5147
}
48+
49+
events.on('settings.labs.edited', configureActivityPub);
50+
events.on('settings.social_web.edited', configureActivityPub);
51+
52+
configureActivityPub();
5253
}
5354
};

ghost/core/test/unit/server/services/activitypub/ActivityPubService.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,36 @@ describe('ActivityPubService', function () {
307307

308308
await knexInstance.destroy();
309309
});
310+
311+
it('Can disable the site', async function () {
312+
const knexInstance = await getKnexInstance();
313+
await addOwnerUser(knexInstance);
314+
await addActivityPubIntegration(knexInstance);
315+
316+
const siteUrl = new URL('http://fake-site-url');
317+
const scope = nock(siteUrl)
318+
.delete('/.ghost/activitypub/v1/site')
319+
.matchHeader('authorization', 'Bearer token:[email protected]:Owner')
320+
.reply(200);
321+
322+
const logging = console;
323+
const identityTokenService = {
324+
getTokenForUser(email: string, role: string) {
325+
return `token:${email}:${role}`;
326+
}
327+
};
328+
329+
const service = new ActivityPubService(
330+
knexInstance,
331+
siteUrl,
332+
logging,
333+
identityTokenService as unknown as IdentityTokenService
334+
);
335+
336+
await service.disableSite();
337+
338+
assert(scope.isDone(), 'Expected the ActivityPub site endpoint to be called');
339+
340+
await knexInstance.destroy();
341+
});
310342
});

0 commit comments

Comments
 (0)