0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Updated activitypub app to utilise new activities endpoint (#21025)

refs
[AP-377](https://linear.app/tryghost/issue/AP-377/inbox-returning-33mb-of-data),
[TryGhost/ActivityPub#40](https://github.com/TryGhost/ActivityPub/pull/40)

Updated activitypub app to utilise new activities endpoint which returns
a paginated list of activities
This commit is contained in:
Michael Barrett 2024-09-18 10:33:52 +01:00 committed by GitHub
parent 8d957c3ec1
commit d7ee3b2e42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 105 additions and 8 deletions

View file

@ -443,4 +443,45 @@ describe('ActivityPubAPI', function () {
await api.follow('@user@domain.com'); await api.follow('@user@domain.com');
}); });
}); });
describe('getAllActivities', function () {
test('It fetches all activities navigating pagination', async function () {
const fakeFetch = Fetch({
'https://auth.api/': {
response: JSONResponse({
identities: [{
token: 'fake-token'
}]
})
},
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50': {
response: JSONResponse({
items: [{type: 'Create', object: {type: 'Note'}}],
nextCursor: 'next-cursor'
})
},
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&cursor=next-cursor': {
response: JSONResponse({
items: [{type: 'Announce', object: {type: 'Article'}}],
nextCursor: null
})
}
});
const api = new ActivityPubAPI(
new URL('https://activitypub.api'),
new URL('https://auth.api'),
'index',
fakeFetch
);
const actual = await api.getAllActivities();
const expected: Activity[] = [
{type: 'Create', object: {type: 'Note'}},
{type: 'Announce', object: {type: 'Article'}}
];
expect(actual).toEqual(expected);
});
});
}); });

View file

@ -160,4 +160,52 @@ export class ActivityPubAPI {
const url = new URL(`.ghost/activitypub/actions/unlike/${encodeURIComponent(id)}`, this.apiUrl); const url = new URL(`.ghost/activitypub/actions/unlike/${encodeURIComponent(id)}`, this.apiUrl);
await this.fetchJSON(url, 'POST'); await this.fetchJSON(url, 'POST');
} }
get activitiesApiUrl() {
return new URL(`.ghost/activitypub/activities/${this.handle}`, this.apiUrl);
}
async getAllActivities(): Promise<Activity[]> {
const LIMIT = 50;
const fetchActivities = async (url: URL): Promise<Activity[]> => {
const json = await this.fetchJSON(url);
// If the response is null, return early
if (json === null) {
return [];
}
// If the response doesn't have an items array, return early
if (!('items' in json)) {
return [];
}
// If the response has an items property, but it's not an array
// use an empty array
const items = Array.isArray(json.items) ? json.items : [];
// If the response has a nextCursor property, fetch the next page
// recursively and concatenate the results
if ('nextCursor' in json && typeof json.nextCursor === 'string') {
const nextUrl = new URL(url);
nextUrl.searchParams.set('cursor', json.nextCursor);
nextUrl.searchParams.set('limit', LIMIT.toString());
const nextItems = await fetchActivities(nextUrl);
return items.concat(nextItems);
}
return items;
};
// Make a copy of the activities API URL and set the limit
const url = new URL(this.activitiesApiUrl);
url.searchParams.set('limit', LIMIT.toString());
// Fetch the activities
return fetchActivities(url);
}
} }

View file

@ -7,7 +7,7 @@ import React, {useState} from 'react';
import {type Activity} from './activities/ActivityItem'; import {type Activity} from './activities/ActivityItem';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading} from '@tryghost/admin-x-design-system'; import {Button, Heading} from '@tryghost/admin-x-design-system';
import {useBrowseInboxForUser} from '../MainContent'; import {useAllActivitiesForUser} from '../hooks/useActivityPubQueries';
interface InboxProps {} interface InboxProps {}
@ -16,18 +16,15 @@ const Inbox: React.FC<InboxProps> = ({}) => {
const [, setArticleActor] = useState<ActorProperties | null>(null); const [, setArticleActor] = useState<ActorProperties | null>(null);
const [layout, setLayout] = useState('inbox'); const [layout, setLayout] = useState('inbox');
// Retrieve activities from the inbox // Retrieve all activities for the user
const {data: inboxActivities = []} = useBrowseInboxForUser('index'); let {data: activities = []} = useAllActivitiesForUser('index');
const activities = inboxActivities.filter((activity: Activity) => { activities = activities.filter((activity: Activity) => {
const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type); const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type);
const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note'; const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note';
return isCreate || isAnnounce; return isCreate || isAnnounce;
}) });
// API endpoint currently returns items oldest-newest, so reverse them
// to show the most recent activities first
.reverse();
// Create a map of activity comments, grouping them by the parent activity // Create a map of activity comments, grouping them by the parent activity
// This allows us to quickly look up all comments for a given activity // This allows us to quickly look up all comments for a given activity

View file

@ -165,3 +165,14 @@ export function useFollowersForUser(handle: string) {
} }
}); });
} }
export function useAllActivitiesForUser(handle: string) {
const siteUrl = useSiteUrl();
const api = createActivityPubAPI(handle, siteUrl);
return useQuery({
queryKey: [`activities:${handle}`],
async queryFn() {
return api.getAllActivities();
}
});
}