Skip to content

Commit df50579

Browse files
🎨 Removed any from activitypub (#28557)
no ref This PR is another small `any` cleanup, like the previous ones. Removes the loose `any` index signatures from the shared ActivityPub API types and replaces them with the props that are already consumed in the ActivityPub app.
1 parent 46d19eb commit df50579

7 files changed

Lines changed: 144 additions & 23 deletions

File tree

apps/activitypub/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tryghost/activitypub",
3-
"version": "3.1.37",
3+
"version": "3.1.40",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/activitypub/src/components/feed/feed-item.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import FeedItemMenu from './feed-item-menu';
22
import React, {useEffect, useRef, useState} from 'react';
3-
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
3+
import {ActivityPubAttachment, ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
44
import {Button, Skeleton} from '@tryghost/shade/components';
55
import {H4} from '@tryghost/shade/primitives';
66
import {LucideIcon} from '@tryghost/shade/utils';
@@ -20,11 +20,18 @@ import {renderTimestamp} from '../../utils/render-timestamp';
2020
import {useDeleteMutationForUser, useFollowMutationForUser, useUnfollowMutationForUser} from '../../hooks/use-activity-pub-queries';
2121
import {useNavigateWithBasePath} from '@src/hooks/use-navigate-with-base-path';
2222

23-
export function getAttachment(object: ObjectProperties) {
24-
let attachment;
23+
export function getAttachment(object: ObjectProperties): ActivityPubAttachment | ActivityPubAttachment[] | null {
24+
let attachment: ActivityPubAttachment | ActivityPubAttachment[] | undefined;
2525

2626
if (object.image) {
27-
attachment = object.image;
27+
attachment = typeof object.image === 'string' ? {
28+
type: 'Image',
29+
url: object.image
30+
} : {
31+
type: object.image.type ?? 'Image',
32+
mediaType: object.image.mediaType,
33+
url: object.image.url
34+
};
2835
}
2936

3037
if (object.type === 'Note' && !attachment) {

apps/activitypub/src/utils/pending-activity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function generatePendingActivity(actorProps: ActorProperties, id: string,
5858
// These are used in the app, but are not part of the ObjectProperties type
5959
id,
6060
replyCount: 0,
61+
likeCount: 0,
6162
liked: false,
6263
reposted: false,
6364
repostCount: 0,

apps/activitypub/src/views/notifications/notifications.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {useEffect, useRef} from 'react';
22
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
33
import {Button, LoadingIndicator, Skeleton} from '@tryghost/shade/components';
4-
import {LucideIcon} from '@tryghost/shade/utils';
4+
import {LucideIcon, formatNumber} from '@tryghost/shade/utils';
55

66
import APAvatar from '@components/global/ap-avatar';
77
import AppError from '@components/layout/error';
@@ -113,6 +113,7 @@ function groupNotifications(notifications: Notification[]): NotificationGroup[]
113113
const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps> = ({group}) => {
114114
const [firstActor, ...otherActors] = group.actors;
115115
const hasOthers = otherActors.length > 0;
116+
const otherActorsCount = otherActors.length;
116117

117118
const actorClass = 'cursor-pointer font-semibold hover:underline text-black dark:text-white';
118119

@@ -125,13 +126,15 @@ const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps>
125126
className={actorClass}
126127
onClick={(e) => {
127128
e?.stopPropagation();
128-
handleProfileClick(firstActor.handle, navigate);
129+
if (firstActor.handle) {
130+
handleProfileClick(firstActor.handle, navigate);
131+
}
129132
}}
130133
>
131134
{firstActor.name}
132135
</span>
133136
</ProfilePreviewHoverCard>
134-
{hasOthers && ` and ${otherActors.length} ${otherActors.length > 1 ? 'others' : 'other'}`}
137+
{hasOthers && ` and ${formatNumber(otherActorsCount)} ${otherActorsCount > 1 ? 'others' : 'other'}`}
135138
</>
136139
);
137140

@@ -271,7 +274,7 @@ const Notifications: React.FC = () => {
271274
case 'follow':
272275
if (group.actors.length > 1) {
273276
toggleOpen(group.id || `${group.type}_${index}`);
274-
} else {
277+
} else if (group.actors[0]?.handle) {
275278
handleProfileClick(group.actors[0].handle, navigate);
276279
}
277280
break;
@@ -349,7 +352,7 @@ const Notifications: React.FC = () => {
349352
))}
350353
{group.actors.length > maxAvatars && (!openStates[group.id || `${group.type}_${index}`]) && (
351354
<div className='absolute right-[28px] z-10 flex size-9 items-center justify-center rounded-full bg-black/50 text-base font-semibold tracking-tightest text-white'>
352-
{`+${group.actors.length - maxAvatars}`}
355+
{`+${formatNumber(group.actors.length - maxAvatars)}`}
353356
</div>
354357
)}
355358

@@ -372,7 +375,9 @@ const Notifications: React.FC = () => {
372375
className='group/item break-anywhere flex items-center justify-between gap-4'
373376
onClick={(e) => {
374377
e?.stopPropagation();
375-
handleProfileClick(actor.handle, navigate);
378+
if (actor.handle) {
379+
handleProfileClick(actor.handle, navigate);
380+
}
376381
}}
377382
>
378383
<div className='flex min-w-0 items-center'>
@@ -386,7 +391,7 @@ const Notifications: React.FC = () => {
386391
<span className='ml-2 line-clamp-1 text-base font-semibold group-hover/item:underline dark:text-white'>{actor.name}</span>
387392
<span className='ml-1 line-clamp-1 text-base text-gray-700 dark:text-gray-600'>{actor.handle}</span>
388393
</div>
389-
{group.type === 'follow' && !actor.followedByMe && (
394+
{group.type === 'follow' && !actor.followedByMe && actor.handle && (
390395
<FollowButton
391396
following={false}
392397
handle={actor.handle}
@@ -418,7 +423,7 @@ const Notifications: React.FC = () => {
418423
}
419424
</div>
420425
{/* Follow button for singular follow, reply, and mention */}
421-
{group.actors.length === 1 && (group.type === 'follow' || group.type === 'reply' || group.type === 'mention') && !group.actors[0].followedByMe && (
426+
{group.actors.length === 1 && (group.type === 'follow' || group.type === 'reply' || group.type === 'mention') && !group.actors[0].followedByMe && group.actors[0].handle && (
422427
<FollowButton
423428
following={false}
424429
handle={group.actors[0].handle}

apps/activitypub/test/unit/utils/pending-activity.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ describe('Pending Activity Utils', function () {
6565
expect(pendingActivity.object.preview!.type).toBe('Note');
6666
expect(pendingActivity.object.preview!.content).toBe(content);
6767
expect(pendingActivity.object.id).toBe(id);
68+
expect(pendingActivity.object.attachment).toEqual([]);
69+
expect(pendingActivity.object.replyCount).toBe(0);
70+
expect(pendingActivity.object.liked).toBe(false);
71+
expect(pendingActivity.object.reposted).toBe(false);
72+
expect(pendingActivity.object.repostCount).toBe(0);
73+
expect(pendingActivity.object.authored).toBe(true);
6874
expect(pendingActivity.object.attributedTo).toBeDefined();
6975
expect(pendingActivity.object.attributedTo).toBeInstanceOf(Object);
7076

@@ -80,6 +86,39 @@ describe('Pending Activity Utils', function () {
8086
expect(attributedToName).toEqual(actor.name);
8187
expect(attributedToPreferredUsername).toEqual(actor.preferredUsername);
8288
});
89+
90+
it('should attach an uploaded image url to the pending activity', function () {
91+
const actor: ActorProperties = {
92+
id: 'https://example.com/actor',
93+
icon: {
94+
url: 'https://example.com/icon.png'
95+
},
96+
name: 'Example Actor',
97+
preferredUsername: 'example',
98+
'@context': '',
99+
discoverable: false,
100+
featured: '',
101+
followers: '',
102+
following: '',
103+
image: {url: ''},
104+
inbox: '',
105+
manuallyApprovesFollowers: false,
106+
outbox: '',
107+
publicKey: {
108+
id: '',
109+
owner: '',
110+
publicKeyPem: ''
111+
},
112+
published: '',
113+
summary: '',
114+
type: 'Person',
115+
url: 'https://example.com/actor'
116+
};
117+
const id = generatePendingActivityId();
118+
const pendingActivity = generatePendingActivity(actor, id, 'Pending note', 'https://example.com/image.png');
119+
120+
expect(pendingActivity.object.image).toBe('https://example.com/image.png');
121+
});
83122
});
84123

85124
describe('formatPendingActivityContent', function () {

apps/activitypub/test/unit/utils/posts.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,49 @@ describe('mapPostToActivity', function () {
146146
expect(object.preview.content).toBe('Test Excerpt');
147147
expect(object.id).toBe('123');
148148
expect(object.replyCount).toBe(3);
149+
expect(object.likeCount).toBe(2);
149150
expect(object.liked).toBe(true);
150151
expect(object.reposted).toBe(false);
151152
expect(object.repostCount).toBe(5);
153+
expect(object.authored).toBe(true);
154+
});
155+
156+
test('it preserves object metadata', function () {
157+
const object = mapPostToActivity({
158+
...post,
159+
metadata: {
160+
ghostAuthors: [{
161+
name: 'Ghost Author',
162+
profile_image: 'https://example.com/authors/ghost-author.jpg'
163+
}]
164+
}
165+
}).object;
166+
167+
expect(object.metadata).toEqual({
168+
ghostAuthors: [{
169+
name: 'Ghost Author',
170+
profile_image: 'https://example.com/authors/ghost-author.jpg'
171+
}]
172+
});
173+
});
174+
175+
test('it maps object engagement properties', function () {
176+
const object = mapPostToActivity({
177+
...post,
178+
replyCount: 0,
179+
likeCount: 0,
180+
likedByMe: false,
181+
repostedByMe: true,
182+
repostCount: 0,
183+
authoredByMe: false
184+
}).object;
185+
186+
expect(object.replyCount).toBe(0);
187+
expect(object.likeCount).toBe(0);
188+
expect(object.liked).toBe(false);
189+
expect(object.reposted).toBe(true);
190+
expect(object.repostCount).toBe(0);
191+
expect(object.authored).toBe(false);
152192
});
153193

154194
test('it sets the correct attachments', function () {
@@ -180,6 +220,7 @@ describe('mapPostToActivity', function () {
180220
});
181221

182222
expect(activity.actor.followedByMe).toBe(true);
223+
expect(activity.actor.handle).toBe('@testuser@example.com');
183224

184225
// Test for reposts
185226
const repostActivity = mapPostToActivity({

apps/admin-x-framework/src/api/activitypub.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,56 @@
11
import {createMutation, createQueryWithId} from '../utils/api/hooks';
2+
import {JSONObject} from './config';
3+
4+
export type ActivityPubContext = string | (string | JSONObject)[];
5+
6+
export type ActivityPubAttachment = {
7+
type: string;
8+
mediaType?: string;
9+
name?: string;
10+
url: string;
11+
};
12+
13+
export type ObjectMetadata = {
14+
ghostAuthors?: Array<{
15+
name: string;
16+
profile_image: string;
17+
}>;
18+
};
219

320
export type FollowItem = {
421
id: string;
5-
preferredUsername: string,
6-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7-
[x: string]: any
22+
preferredUsername: string;
823
};
924

1025
export type ObjectProperties = {
11-
'@context': string | (string | object)[];
26+
'@context': ActivityPubContext;
27+
id: string;
1228
type: 'Article' | 'Link' | 'Note' | 'Tombstone';
1329
name: string;
1430
content: string | null;
1531
summary: string | null;
1632
url?: string | undefined;
17-
attributedTo?: object | string | object[] | undefined;
33+
attributedTo?: ActorProperties | string | ActorProperties[] | JSONObject | JSONObject[];
1834
image?: string | {
1935
url: string;
2036
mediaType?: string;
2137
type?: string;
2238
};
39+
attachment?: ActivityPubAttachment | ActivityPubAttachment[];
2340
published?: string;
41+
createdAt?: string;
2442
preview?: {type: string, content: string | null};
25-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26-
[x: string]: any;
43+
replyCount: number;
44+
likeCount: number;
45+
liked?: boolean;
46+
reposted?: boolean;
47+
repostCount: number;
48+
authored: boolean;
49+
metadata?: ObjectMetadata;
2750
}
2851

2952
export type ActorProperties = {
30-
'@context': string | (string | object)[];
53+
'@context': ActivityPubContext;
3154
attachment?: {
3255
type: string;
3356
name: string;
@@ -49,6 +72,13 @@ export type ActorProperties = {
4972
name: string;
5073
outbox: string;
5174
preferredUsername: string;
75+
handle?: string;
76+
followedByMe?: boolean;
77+
followsMe?: boolean;
78+
followingCount?: number;
79+
followerCount?: number;
80+
bio?: string;
81+
avatarUrl?: string | null;
5282
publicKey: {
5383
id: string;
5484
owner: string;
@@ -58,8 +88,6 @@ export type ActorProperties = {
5888
summary: string;
5989
type: 'Person';
6090
url: string;
61-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62-
[x: string]: any;
6391
}
6492

6593
export type Activity = {

0 commit comments

Comments
 (0)