From e9914d8fe525a9c2167b12daa74633bfad3e0ca8 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Tue, 22 Oct 2024 20:21:12 +0100 Subject: [PATCH] Fixed followers list on profile in admin-x-activitypub app (#21370) refs [AP-489](https://linear.app/ghost/issue/AP-489/followers-showing-unknown-on-user-profile) Fixed the followers list on profile in admin-x-activitypub app by utilising a custom endpoint to fetch a list of expanded followers seeming though the followers endpoint only returns follower id's --- apps/admin-x-activitypub/package.json | 2 +- .../src/api/activitypub.test.ts | 98 +++++++++++++++++++ .../src/api/activitypub.ts | 15 +++ .../src/components/Profile.tsx | 4 +- .../src/hooks/useActivityPubQueries.ts | 11 +++ 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index 40a318e381..27d1c650ee 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.1.5", + "version": "0.1.6", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index ba3cbe9e18..8eacb186ec 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -562,6 +562,104 @@ describe('ActivityPubAPI', function () { }); }); + describe('getFollowersExpanded', function () { + test('It passes the token to the followers endpoint', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/followers-expanded/index': { + async assert(_resource, init) { + const headers = new Headers(init?.headers); + expect(headers.get('Authorization')).toContain('fake-token'); + }, + response: JSONResponse({ + type: 'Collection', + orderedItems: [] + }) + } + }); + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + await api.getFollowersExpanded(); + }); + + test('Returns an empty array when the followers is empty', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/followers-expanded/index': { + response: JSONResponse({ + type: 'Collection', + orderedItems: [] + }) + } + }); + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersExpanded(); + const expected: never[] = []; + + expect(actual).toEqual(expected); + }); + + test('Returns all the items array when the followers is not empty', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/followers-expanded/index': { + response: + JSONResponse({ + type: 'Collection', + orderedItems: [{ + type: 'Person' + }] + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersExpanded(); + const expected: Activity[] = [ + { + type: 'Person' + } + ]; + + expect(actual).toEqual(expected); + }); + }); + describe('follow', function () { test('It passes the token to the follow endpoint', async function () { const fakeFetch = Fetch({ diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index d1ca115075..c5b1b1d341 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -159,6 +159,21 @@ export class ActivityPubAPI { return 0; } + get followersExpandedApiUrl() { + return new URL(`.ghost/activitypub/followers-expanded/${this.handle}`, this.apiUrl); + } + + async getFollowersExpanded(): Promise { + const json = await this.fetchJSON(this.followersExpandedApiUrl); + if (json === null) { + return []; + } + if ('orderedItems' in json) { + return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems]; + } + return []; + } + async getFollowersForProfile(handle: string, next?: string): Promise { const url = new URL(`.ghost/activitypub/profile/${handle}/followers`, this.apiUrl); if (next) { diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index ffcdaad8fa..20c9275b1f 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -8,7 +8,7 @@ import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {Button, Heading, List, NoValueLabel, Tab, TabView} from '@tryghost/admin-x-design-system'; import { useFollowersCountForUser, - useFollowersForUser, + useFollowersExpandedForUser, useFollowingCountForUser, useFollowingForUser, useLikedForUser, @@ -22,7 +22,7 @@ const Profile: React.FC = ({}) => { const {data: followersCount = 0} = useFollowersCountForUser('index'); const {data: followingCount = 0} = useFollowingCountForUser('index'); const {data: following = []} = useFollowingForUser('index'); - const {data: followers = []} = useFollowersForUser('index'); + const {data: followers = []} = useFollowersExpandedForUser('index'); const {data: liked = []} = useLikedForUser('index'); const {data: posts = []} = useOutboxForUser('index'); diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index df983a409e..dd2c550791 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -193,6 +193,17 @@ export function useFollowersForUser(handle: string) { }); } +export function useFollowersExpandedForUser(handle: string) { + return useQuery({ + queryKey: [`followers_expanded:${handle}`], + async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowersExpanded(); + } + }); +} + export function useAllActivitiesForUser({ handle, includeOwn = false,