diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index 88b7dab110..ba3cbe9e18 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -182,6 +182,153 @@ describe('ActivityPubAPI', function () { }); }); + describe('getOutbox', function () { + test('It passes the token to the outbox endpoint', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/outbox/index': { + async assert(_resource, init) { + const headers = new Headers(init?.headers); + expect(headers.get('Authorization')).toContain('fake-token'); + }, + response: JSONResponse({ + type: 'Collection', + items: [] + }) + } + }); + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + await api.getOutbox(); + }); + + test('Returns an empty array when the outbox is empty', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/outbox/index': { + response: JSONResponse({ + type: 'Collection', + items: [] + }) + } + }); + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getOutbox(); + const expected: never[] = []; + + expect(actual).toEqual(expected); + }); + + test('Returns all the items array when the outbox is not empty', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/outbox/index': { + response: + JSONResponse({ + type: 'Collection', + orderedItems: [{ + type: 'Create', + object: { + type: 'Note' + } + }] + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getOutbox(); + const expected: Activity[] = [ + { + type: 'Create', + object: { + type: 'Note' + } + } + ]; + + expect(actual).toEqual(expected); + }); + + test('Returns an array when the orderedItems key is a single object', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/outbox/index': { + response: + JSONResponse({ + type: 'Collection', + orderedItems: { + type: 'Create', + object: { + type: 'Note' + } + } + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getOutbox(); + const expected: Activity[] = [ + { + type: 'Create', + object: { + type: 'Note' + } + } + ]; + + expect(actual).toEqual(expected); + }); + }); + describe('getFollowing', function () { test('It passes the token to the following 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 1a95d1791c..d1ca115075 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -86,6 +86,24 @@ export class ActivityPubAPI { return []; } + get outboxApiUrl() { + return new URL(`.ghost/activitypub/outbox/${this.handle}`, this.apiUrl); + } + + async getOutbox(): Promise { + const json = await this.fetchJSON(this.outboxApiUrl); + if (json === null) { + return []; + } + if ('orderedItems' in json) { + return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems]; + } + if ('items' in json) { + return Array.isArray(json.items) ? json.items : [json.items]; + } + return []; + } + get followingApiUrl() { return new URL(`.ghost/activitypub/following/${this.handle}`, this.apiUrl); } diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index e16be21549..ffcdaad8fa 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -12,6 +12,7 @@ import { useFollowingCountForUser, useFollowingForUser, useLikedForUser, + useOutboxForUser, useUserDataForUser } from '../hooks/useActivityPubQueries'; @@ -23,6 +24,8 @@ const Profile: React.FC = ({}) => { const {data: following = []} = useFollowingForUser('index'); const {data: followers = []} = useFollowersForUser('index'); const {data: liked = []} = useLikedForUser('index'); + const {data: posts = []} = useOutboxForUser('index'); + // Replace 'index' with the actual handle of the user const {data: userProfile} = useUserDataForUser('index') as {data: ActorProperties | null}; @@ -36,10 +39,36 @@ const Profile: React.FC = ({}) => { { id: 'posts', title: 'Posts', - contents: (
- You haven't posted anything yet. -
), - counter: 240 + contents: ( +
+ {posts.length === 0 ? ( + + You haven't posted anything yet. + + ) : ( +
    + {posts.map((activity, index) => ( +
  • + {}} + /> + {index < posts.length - 1 && ( +
    + )} +
  • + ))} +
+ )} +
+ ), + counter: posts.length }, { id: 'likes', diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 1e4881c157..df983a409e 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -362,3 +362,14 @@ export function useProfileForUser(handle: string, fullHandle: string) { } }); } + +export function useOutboxForUser(handle: string) { + return useQuery({ + queryKey: [`outbox:${handle}`], + async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getOutbox(); + } + }); +}