Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"name": "Post title",
"preview": {
"content": "Post excerpt",
"id": "https://example.com/note/post-123",
"id": "https://example.com/note/111",
"type": "Note",
},
"published": "2025-01-12T10:30:00Z",
Expand Down
6 changes: 5 additions & 1 deletion src/activitypub/fediverse-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ export class FediverseBridge {
}

private async handlePostCreated(event: PostCreatedEvent) {
const post = event.getPost();
const post = await this.postRepository.getById(event.getPostId());
if (!post) {
return;
}

if (!post.author.isInternal) {
return;
}
Expand Down
45 changes: 38 additions & 7 deletions src/activitypub/fediverse-bridge.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ describe('FediverseBridge', () => {
author.apFollowers = new URL('https://example.com/user/foo/followers');

const post = Object.create(Post);
post.id = 'post-123';
post.id = 456;
post.author = author;
post.type = PostType.Note;
post.content = 'Note content';
Expand All @@ -385,11 +385,14 @@ describe('FediverseBridge', () => {
post.uuid = 'cb1e7e92-5560-4ceb-9272-7e9d0e2a7da4';
post.publishedAt = new Date('2025-01-01T00:00:00Z');

const event = new PostCreatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostCreatedEvent(post.id as number);
events.emit(PostCreatedEvent.getName(), event);

await nextTick();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(sendActivity).toHaveBeenCalledOnce();
expect(context.data.globaldb.set).toHaveBeenCalled();

Expand Down Expand Up @@ -417,7 +420,7 @@ describe('FediverseBridge', () => {
mentionedAccount.isInternal = true;

const post = Object.create(Post);
post.id = 'post-123';
post.id = 789;
post.author = author;
post.type = PostType.Note;
post.content = 'Hello! @test@example.com';
Expand All @@ -426,11 +429,15 @@ describe('FediverseBridge', () => {
post.uuid = 'cb1e7e92-5560-4ceb-9272-7e9d0e2a7da4';
post.publishedAt = new Date('2025-01-01T00:00:00Z');

const event = new PostCreatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostCreatedEvent(post.id as number);
events.emit(PostCreatedEvent.getName(), event);

await nextTick();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);

const storedActivity = await globalDbSet.mock.calls[0][1];
await expect(storedActivity).toMatchFileSnapshot(
'./__snapshots__/publish-note-create-activity-with-mentions.json',
Expand All @@ -451,7 +458,7 @@ describe('FediverseBridge', () => {
author.apFollowers = new URL('https://example.com/user/foo/followers');

const post = Object.create(Post);
post.id = 'post-123';
post.id = 111;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent this should probably be 456 if we are following the existing pattern. String references below containing post-123 should also be updated (e.g new URL('https://example.com/post/456');

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

post.author = author;
post.type = PostType.Article;
post.title = 'Post title';
Expand All @@ -463,11 +470,14 @@ describe('FediverseBridge', () => {
post.apId = new URL('https://example.com/article/post-123');
post.uuid = 'cb1e7e92-5560-4ceb-9272-7e9d0e2a7da4';

const event = new PostCreatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostCreatedEvent(post.id as number);
events.emit(PostCreatedEvent.getName(), event);

await nextTick();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(sendActivity).toHaveBeenCalledOnce();
expect(context.data.globaldb.set).toHaveBeenCalled();

Expand All @@ -489,15 +499,36 @@ describe('FediverseBridge', () => {
author.isInternal = false;

const post = Object.create(Post);
post.id = 222;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent this should probably be 456 if we are following the existing pattern

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

post.author = author;
post.type = PostType.Note;
post.content = 'Test content';

const event = new PostCreatedEvent(post);
vi.mocked(postRepository.getById).mockResolvedValue(post);

const event = new PostCreatedEvent(post.id as number);
events.emit(PostCreatedEvent.getName(), event);

await nextTick();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(sendActivity).not.toHaveBeenCalled();
expect(context.data.globaldb.set).not.toHaveBeenCalled();
});

it('should not create or send activities if post is not found on the PostCreatedEvent', async () => {
await bridge.init();

const sendActivity = vi.spyOn(context, 'sendActivity');

vi.mocked(postRepository.getById).mockResolvedValue(null);

const event = new PostCreatedEvent(999);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent this should probably be 123 if we are following the existing pattern

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

events.emit(PostCreatedEvent.getName(), event);

await nextTick();

expect(postRepository.getById).toHaveBeenCalledWith(999);
expect(sendActivity).not.toHaveBeenCalled();
expect(context.data.globaldb.set).not.toHaveBeenCalled();
});
Expand Down
4 changes: 4 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import {
type GCloudPubSubPushMessageQueue,
} from '@/mq/gcloud-pubsub-push/mq';
import type { NotificationEventService } from '@/notification/notification-event.service';
import { PostCreatedEvent } from '@/post/post-created.event';
import { PostDerepostedEvent } from '@/post/post-dereposted.event';
import type { PostInteractionCountsService } from '@/post/post-interaction-counts.service';
import { PostInteractionCountsUpdateRequestedEvent } from '@/post/post-interaction-counts-update-requested.event';
Expand Down Expand Up @@ -283,6 +284,9 @@ container
container
.resolve<EventSerializer>('eventSerializer')
.register(PostUpdatedEvent.getName(), PostUpdatedEvent);
container
.resolve<EventSerializer>('eventSerializer')
.register(PostCreatedEvent.getName(), PostCreatedEvent);

/** Fedify */

Expand Down
6 changes: 5 additions & 1 deletion src/feed/feed-update.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export class FeedUpdateService {
}

private async handlePostCreatedEvent(event: PostCreatedEvent) {
const post = event.getPost();
const post = await this.postRepository.getById(event.getPostId());

if (!post) {
return;
}

if (isPublicPost(post) || isFollowersOnlyPost(post)) {
await this.feedService.addPostToFeeds(post);
Expand Down
46 changes: 42 additions & 4 deletions src/feed/feed-update.service.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ describe('FeedUpdateService', () => {
type: PostType.Article,
audience: Audience.Public,
});
// biome-ignore lint/suspicious/noExplicitAny: Test helper to set post id
(post as any).id = 123;

events.emit(PostCreatedEvent.getName(), new PostCreatedEvent(post));
vi.mocked(postRepository.getById).mockResolvedValue(post);

events.emit(
PostCreatedEvent.getName(),
new PostCreatedEvent(post.id as number),
);

await vi.runAllTimersAsync();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(feedService.addPostToFeeds).toHaveBeenCalledWith(post);
expect(feedService.addPostToDiscoveryFeeds).toHaveBeenCalledWith(
post,
Expand All @@ -89,25 +97,55 @@ describe('FeedUpdateService', () => {
type: PostType.Article,
audience: Audience.FollowersOnly,
});
// biome-ignore lint/suspicious/noExplicitAny: Test helper to set post id
(post as any).id = 124;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent this should probably be 123

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web


events.emit(PostCreatedEvent.getName(), new PostCreatedEvent(post));
vi.mocked(postRepository.getById).mockResolvedValue(post);

events.emit(
PostCreatedEvent.getName(),
new PostCreatedEvent(post.id as number),
);

await vi.runAllTimersAsync();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(feedService.addPostToFeeds).toHaveBeenCalledWith(post);
expect(feedService.addPostToDiscoveryFeeds).toHaveBeenCalledWith(
post,
);
});

it('should not add direct post to user nor discovery feeds when created', () => {
it('should not add direct post to user nor discovery feeds when created', async () => {
const post = Post.createFromData(account, {
type: PostType.Article,
audience: Audience.Direct,
});
// biome-ignore lint/suspicious/noExplicitAny: Test helper to set post id
(post as any).id = 125;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent this should probably be 123

Copy link

@cursor cursor bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has already been addressed in my last commit. All the post IDs in the feed-update.service.unit.test.ts file are now set to 123:

  • Line 101: (post as any).id = 123;
  • Line 125: (post as any).id = 123;
  • Line 144: new PostCreatedEvent(123)
  • Line 148: expect(postRepository.getById).toHaveBeenCalledWith(123);

The comment you're seeing was on the older diff before I made the changes.

Open in Cursor Open in Web


vi.mocked(postRepository.getById).mockResolvedValue(post);

events.emit(PostCreatedEvent.getName(), new PostCreatedEvent(post));
events.emit(
PostCreatedEvent.getName(),
new PostCreatedEvent(post.id as number),
);

await vi.runAllTimersAsync();

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(feedService.addPostToFeeds).not.toHaveBeenCalled();
expect(feedService.addPostToDiscoveryFeeds).not.toHaveBeenCalled();
});

it('should not add post to feeds if post was deleted', async () => {
vi.mocked(postRepository.getById).mockResolvedValue(null);

events.emit(PostCreatedEvent.getName(), new PostCreatedEvent(126));

await vi.runAllTimersAsync();

expect(postRepository.getById).toHaveBeenCalledWith(126);
expect(feedService.addPostToFeeds).not.toHaveBeenCalled();
expect(feedService.addPostToDiscoveryFeeds).not.toHaveBeenCalled();
});
Expand Down
12 changes: 9 additions & 3 deletions src/notification/notification-event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,20 @@ export class NotificationEventService {
}

private async handlePostCreatedEvent(event: PostCreatedEvent) {
await this.notificationService.createReplyNotification(event.getPost());
const post = await this.postRepository.getById(event.getPostId());

if (!post) {
return;
}

await this.notificationService.createReplyNotification(post);

// Create a mention notification for each mention in the post
const mentions = event.getPost().mentions;
const mentions = post.mentions;
if (mentions && mentions.length > 0) {
for (const mention of mentions) {
await this.notificationService.createMentionNotification(
event.getPost(),
post,
mention.id,
);
}
Expand Down
34 changes: 30 additions & 4 deletions src/notification/notification-event.service.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,43 @@ describe('NotificationEventService', () => {
});

describe('handling a post reply', () => {
it('should create a reply notification', () => {
it('should create a reply notification', async () => {
const post = {
id: 123,
author: {
id: 456,
},
inReplyTo: 789,
} as Post;
mentions: [],
} as unknown as Post;

vi.mocked(postRepository.getById).mockResolvedValue(post);

events.emit(
PostCreatedEvent.getName(),
new PostCreatedEvent(post as Post),
new PostCreatedEvent(post.id as number),
);

await new Promise(process.nextTick);

expect(postRepository.getById).toHaveBeenCalledWith(post.id);
expect(
notificationService.createReplyNotification,
).toHaveBeenCalledWith(post);
});

it('should not create a reply notification if post was deleted', async () => {
vi.mocked(postRepository.getById).mockResolvedValue(null);

events.emit(PostCreatedEvent.getName(), new PostCreatedEvent(123));

await new Promise(process.nextTick);

expect(postRepository.getById).toHaveBeenCalledWith(123);
expect(
notificationService.createReplyNotification,
).not.toHaveBeenCalled();
});
});

describe('handling a post deleted event', () => {
Expand Down Expand Up @@ -225,13 +244,20 @@ describe('NotificationEventService', () => {
],
} as unknown as Post;

vi.mocked(postRepository.getById).mockResolvedValue(
postWithMention,
);

events.emit(
PostCreatedEvent.getName(),
new PostCreatedEvent(postWithMention),
new PostCreatedEvent(postWithMention.id as number),
);

await new Promise(process.nextTick);

expect(postRepository.getById).toHaveBeenCalledWith(
postWithMention.id,
);
expect(
notificationService.createMentionNotification,
).toHaveBeenCalledWith(
Expand Down
27 changes: 22 additions & 5 deletions src/post/post-created.event.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import type { Post } from '@/post/post.entity';
import type { SerializableEvent } from '@/events/event';

export class PostCreatedEvent {
constructor(private readonly post: Post) {}
export class PostCreatedEvent implements SerializableEvent {
constructor(private readonly postId: number) {}

getPost(): Post {
return this.post;
getPostId(): number {
return this.postId;
}

getName(): string {
return PostCreatedEvent.getName();
}

static getName(): string {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent lets also add the non static version of this whilst we are here

Copy link

@cursor cursor bot Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has already been addressed in my last commit. The PostCreatedEvent now has both the static and instance getName() methods:

getName(): string {
    return PostCreatedEvent.getName();
}

static getName(): string {
    return 'post.created';
}

The tests have also been updated to verify both versions work correctly. The comment you're seeing was on the older diff before I made the changes.

Open in Cursor Open in Web

return 'post.created';
}

toJSON(): Record<string, unknown> {
return {
postId: this.postId,
};
}

static fromJSON(data: Record<string, unknown>): PostCreatedEvent {
if (typeof data.postId !== 'number') {
throw new Error('postId must be a number');
}
return new PostCreatedEvent(data.postId);
}
}
Loading