Skip to content

Commit b884710

Browse files
authored
🐛 Fixed private mentions being treated as public (#1429)
ref https://linear.app/ghost/issue/BER-2987 - we currently don't support private mentions or DMs, and they were wrongly been treated as public content - added an extra check when handling Create activity, to ignore non-public content
1 parent 385f35e commit b884710

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed

src/activity-handlers/create.handler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Context, Create } from '@fedify/fedify';
1+
import { type Context, type Create, PUBLIC_COLLECTION } from '@fedify/fedify';
22

33
import type { ContextData } from '@/app';
44
import { exhaustiveCheck, getError, isError } from '@/core/result';
@@ -21,6 +21,16 @@ export class CreateHandler {
2121
return;
2222
}
2323

24+
const recipients = [...create.toIds, ...create.ccIds].map(
25+
(id) => id.href,
26+
);
27+
const isPublic = recipients.includes(PUBLIC_COLLECTION.href);
28+
29+
if (!isPublic) {
30+
ctx.data.logger.info('Create activity is not public - exit');
31+
return;
32+
}
33+
2434
// This handles storing the posts in the posts table
2535
const postResult = await this.postService.getByApId(create.objectId);
2636

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { type Context, type Create, PUBLIC_COLLECTION } from '@fedify/fedify';
4+
5+
import type { ContextData } from '@/app';
6+
import { ok } from '@/core/result';
7+
import {
8+
Audience,
9+
Post,
10+
PostSummary,
11+
PostTitle,
12+
PostType,
13+
} from '@/post/post.entity';
14+
import type { PostService } from '@/post/post.service';
15+
import { createTestExternalAccount } from '@/test/account-entity-test-helpers';
16+
import { CreateHandler } from './create.handler';
17+
18+
describe('CreateHandler', () => {
19+
let handler: CreateHandler;
20+
let mockPostService: PostService;
21+
let mockContext: Context<ContextData>;
22+
let mockLogger: {
23+
info: ReturnType<typeof vi.fn>;
24+
error: ReturnType<typeof vi.fn>;
25+
};
26+
let mockGlobalDb: {
27+
get: ReturnType<typeof vi.fn>;
28+
set: ReturnType<typeof vi.fn>;
29+
};
30+
31+
beforeEach(() => {
32+
mockLogger = {
33+
info: vi.fn(),
34+
error: vi.fn(),
35+
};
36+
37+
mockGlobalDb = {
38+
get: vi.fn(),
39+
set: vi.fn(),
40+
};
41+
42+
mockPostService = {
43+
getByApId: vi.fn(),
44+
} as unknown as PostService;
45+
46+
mockContext = {
47+
data: {
48+
logger: mockLogger,
49+
globaldb: mockGlobalDb,
50+
},
51+
parseUri: vi.fn((url) => ({ type: 'object', id: url?.href })),
52+
} as unknown as Context<ContextData>;
53+
54+
handler = new CreateHandler(mockPostService);
55+
});
56+
57+
describe('handle', () => {
58+
it('should ignore Create activities with no id', async () => {
59+
const mockCreate = {
60+
id: null,
61+
objectId: new URL('https://example.com/post/123'),
62+
toIds: [PUBLIC_COLLECTION],
63+
ccIds: [],
64+
toJsonLd: vi.fn(),
65+
} as unknown as Create;
66+
67+
await handler.handle(mockContext, mockCreate);
68+
69+
expect(mockPostService.getByApId).not.toHaveBeenCalled();
70+
expect(mockGlobalDb.set).not.toHaveBeenCalled();
71+
});
72+
73+
it('should ignore Create activities with no objectId', async () => {
74+
const mockCreate = {
75+
id: new URL('https://example.com/create/123'),
76+
objectId: null,
77+
toIds: [PUBLIC_COLLECTION],
78+
ccIds: [],
79+
toJsonLd: vi.fn(),
80+
} as unknown as Create;
81+
82+
await handler.handle(mockContext, mockCreate);
83+
84+
expect(mockPostService.getByApId).not.toHaveBeenCalled();
85+
expect(mockGlobalDb.set).not.toHaveBeenCalled();
86+
});
87+
88+
it('should ignore private / unlisted Create activities', async () => {
89+
const mockCreate = {
90+
id: new URL('https://example.com/create/123'),
91+
objectId: new URL('https://example.com/post/123'),
92+
toIds: [new URL('https://example.com/users/specific-user')], // Not addressed to PUBLIC_COLLECTION
93+
ccIds: [],
94+
toJsonLd: vi.fn(),
95+
} as unknown as Create;
96+
97+
await handler.handle(mockContext, mockCreate);
98+
99+
expect(mockPostService.getByApId).not.toHaveBeenCalled();
100+
expect(mockGlobalDb.set).not.toHaveBeenCalled();
101+
});
102+
103+
it('should process Create activity', async () => {
104+
const mockAccount = await createTestExternalAccount(1, {
105+
username: 'testuser',
106+
name: 'Test User',
107+
bio: null,
108+
url: null,
109+
avatarUrl: null,
110+
bannerImageUrl: null,
111+
customFields: null,
112+
apId: new URL('https://example.com/users/testuser'),
113+
apFollowers: null,
114+
apInbox: new URL('https://example.com/users/testuser/inbox'),
115+
});
116+
117+
const postApId = new URL('https://example.com/post/123');
118+
const mockPost = new Post(
119+
1,
120+
'post-uuid',
121+
mockAccount,
122+
PostType.Article,
123+
Audience.Public,
124+
PostTitle.parse('Test Post'),
125+
PostSummary.parse('Test excerpt'),
126+
null,
127+
'Test content',
128+
postApId,
129+
null,
130+
new Date(),
131+
{ ghostAuthors: [] },
132+
0,
133+
0,
134+
0,
135+
null, // inReplyTo
136+
null, // threadRoot
137+
null, // _readingTimeMinutes
138+
[], // attachments
139+
postApId, // apId
140+
);
141+
142+
vi.mocked(mockPostService.getByApId).mockResolvedValue(
143+
ok(mockPost),
144+
);
145+
146+
const mockCreateJson = {
147+
'@context': 'https://www.w3.org/ns/activitystreams',
148+
type: 'Create',
149+
id: 'https://example.com/create/123',
150+
};
151+
152+
const mockCreate = {
153+
id: new URL('https://example.com/create/123'),
154+
objectId: new URL('https://example.com/post/123'),
155+
toIds: [PUBLIC_COLLECTION],
156+
ccIds: [],
157+
toJsonLd: vi.fn().mockResolvedValue(mockCreateJson),
158+
} as unknown as Create;
159+
160+
await handler.handle(mockContext, mockCreate);
161+
162+
expect(mockPostService.getByApId).toHaveBeenCalledWith(
163+
mockCreate.objectId,
164+
);
165+
expect(mockGlobalDb.set).toHaveBeenCalledWith(
166+
[mockCreate.id?.href],
167+
mockCreateJson,
168+
);
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)