Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/account/account-updated.event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export class AccountUpdatedEvent {
return 'account.updated';
}

getName(): string {
return 'account.updated';
}

constructor(private readonly account: Account) {}

getAccount(): Account {
Expand Down
36 changes: 27 additions & 9 deletions src/account/account.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AccountBlockedEvent } from './account-blocked.event';
import { AccountFollowedEvent } from './account-followed.event';
import { AccountUnblockedEvent } from './account-unblocked.event';
import { AccountUnfollowedEvent } from './account-unfollowed.event';
import { AccountUpdatedEvent } from './account-updated.event';
import { DomainBlockedEvent } from './domain-blocked.event';
import { DomainUnblockedEvent } from './domain-unblocked.event';

Expand Down Expand Up @@ -142,19 +143,36 @@ export class AccountEntity implements Account {
return new URL(`/.ghost/activitypub/${type}/${post.uuid}`, this.apId);
}

updateProfile(params: ProfileUpdateParams) {
updateProfile(params: ProfileUpdateParams): Account {
type P = ProfileUpdateParams;
const get = <K extends keyof P>(prop: K): P[K] =>
params[prop] === undefined ? this[prop] : params[prop];

return AccountEntity.create({
...this,
username: get('username'),
name: get('name'),
bio: get('bio'),
avatarUrl: get('avatarUrl'),
bannerImageUrl: get('bannerImageUrl'),
}) as Account;
const account = AccountEntity.create(
{
...this,
username: get('username'),
name: get('name'),
bio: get('bio'),
avatarUrl: get('avatarUrl'),
bannerImageUrl: get('bannerImageUrl'),
},
this.events,
);

if (
account.username !== this.username ||
account.name !== this.name ||
account.bio !== this.bio ||
account.avatarUrl?.href !== this.avatarUrl?.href ||
account.bannerImageUrl?.href !== this.bannerImageUrl?.href
) {
account.events = account.events.concat(
new AccountUpdatedEvent(account),
);
}

return account;
}

unblock(account: Account): Account {
Expand Down
62 changes: 62 additions & 0 deletions src/account/account.entity.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AccountBlockedEvent } from './account-blocked.event';
import { AccountFollowedEvent } from './account-followed.event';
import { AccountUnblockedEvent } from './account-unblocked.event';
import { AccountUnfollowedEvent } from './account-unfollowed.event';
import { AccountUpdatedEvent } from './account-updated.event';
import { DomainBlockedEvent } from './domain-blocked.event';
import { DomainUnblockedEvent } from './domain-unblocked.event';

Expand Down Expand Up @@ -371,6 +372,67 @@ describe('AccountEntity', () => {
'http://example.com/original-banner.png',
);
});

it('should emit AccountUpdatedEvent when data changes', () => {
const draft = AccountEntity.draft({
isInternal: true,
host: new URL('http://example.com'),
username: 'testuser',
name: 'Original Name',
bio: 'Original Bio',
url: new URL('http://example.com/url'),
avatarUrl: new URL('http://example.com/original-avatar.png'),
bannerImageUrl: new URL(
'http://example.com/original-banner.png',
),
});

const account = AccountEntity.create({
id: 1,
...draft,
});

const updated = account.updateProfile({
name: 'Updated Name',
});

const events = AccountEntity.pullEvents(updated);
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(AccountUpdatedEvent);
});

it('should not emit AccountUpdatedEvent when data is the same', () => {
const draft = AccountEntity.draft({
isInternal: true,
host: new URL('http://example.com'),
username: 'testuser',
name: 'Original Name',
bio: 'Original Bio',
url: new URL('http://example.com/url'),
avatarUrl: new URL('http://example.com/original-avatar.png'),
bannerImageUrl: new URL(
'http://example.com/original-banner.png',
),
});

const account = AccountEntity.create({
id: 1,
...draft,
});

const updated = account.updateProfile({
name: 'Original Name',
bio: 'Original Bio',
username: 'testuser',
avatarUrl: new URL('http://example.com/original-avatar.png'),
bannerImageUrl: new URL(
'http://example.com/original-banner.png',
),
});

const events = AccountEntity.pullEvents(updated);
expect(events).toHaveLength(0);
});
});

describe('block and unblock', () => {
Expand Down
6 changes: 0 additions & 6 deletions src/account/account.repository.knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { AccountBlockedEvent } from './account-blocked.event';
import { AccountFollowedEvent } from './account-followed.event';
import { AccountUnblockedEvent } from './account-unblocked.event';
import { AccountUnfollowedEvent } from './account-unfollowed.event';
import { AccountUpdatedEvent } from './account-updated.event';
import { type Account, AccountEntity } from './account.entity';
import { DomainBlockedEvent } from './domain-blocked.event';
import { DomainUnblockedEvent } from './domain-unblocked.event';
Expand Down Expand Up @@ -150,11 +149,6 @@ export class KnexAccountRepository {
for (const event of events) {
await this.events.emitAsync(event.getName(), event);
}

await this.events.emitAsync(
AccountUpdatedEvent.getName(),
new AccountUpdatedEvent(account),
);
}

/**
Expand Down
12 changes: 0 additions & 12 deletions src/account/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,18 +610,6 @@ export class AccountService {
: null,
};

if (
account.name === profileData.name &&
account.bio === profileData.bio &&
account.username === profileData.username &&
account.avatarUrl?.toString() ===
profileData.avatarUrl?.toString() &&
account.bannerImageUrl?.toString() ===
profileData.bannerImageUrl?.toString()
) {
return;
}

const updated = account.updateProfile(profileData);

await this.accountRepository.save(updated);
Expand Down
29 changes: 0 additions & 29 deletions src/account/account.service.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,6 @@ describe('AccountService', () => {
expect(knexAccountRepository.save).toHaveBeenCalledWith(updated);
});

it('should do nothing if the provided data is the same as the existing account profile', async () => {
const data = {
name: 'Alice',
bio: 'Eiusmod in cillum elit sit cupidatat reprehenderit ad quis qui consequat officia elit.',
username: 'alice',
avatarUrl: 'https://example.com/avatar/alice.png',
bannerImageUrl: 'https://example.com/banner/alice.png',
};

const account = {
name: data.name,
bio: data.bio,
username: data.username,
avatarUrl: new URL(data.avatarUrl),
bannerImageUrl: new URL(data.bannerImageUrl),
updateProfile: vi.fn(),
} as unknown as AccountEntity;

await accountService.updateAccountProfile(account, {
name: data.name,
bio: data.bio,
username: data.username,
avatarUrl: data.avatarUrl,
bannerImageUrl: data.bannerImageUrl,
});

expect(knexAccountRepository.save).not.toHaveBeenCalled();
});

it('should handle empty values for avatarUrl and bannerImageUrl', async () => {
const account = {
updateProfile: vi.fn(),
Expand Down
77 changes: 39 additions & 38 deletions src/activitypub/fediverse-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type EventEmitter from 'node:events';
import {
type Activity,
Article,
Create,
Delete,
Expand All @@ -14,6 +15,7 @@ import {
import { Temporal } from '@js-temporal/polyfill';
import { AccountBlockedEvent } from 'account/account-blocked.event';
import { AccountUpdatedEvent } from 'account/account-updated.event';
import type { Account } from 'account/account.entity';
import type { AccountService } from 'account/account.service';
import { PostCreatedEvent } from 'post/post-created.event';
import { PostDeletedEvent } from 'post/post-deleted.event';
Expand Down Expand Up @@ -47,6 +49,39 @@ export class FediverseBridge {
);
}

private async sendActivityToInbox(
account: Account,
recipient: Account,
activity: Activity,
) {
const ctx = this.fedifyContextFactory.getFedifyContext();

await ctx.sendActivity(
{ username: account.username },
{
id: recipient.apId,
inboxId: recipient.apInbox,
},
activity,
);
}

private async sendActivityToFollowers(
account: Account,
activity: Activity,
) {
const ctx = this.fedifyContextFactory.getFedifyContext();

await ctx.sendActivity(
{ username: account.username },
'followers',
activity,
{
preferSharedInbox: true,
},
);
}

private async handlePostCreated(event: PostCreatedEvent) {
const post = event.getPost();
if (!post.author.isInternal) {
Expand Down Expand Up @@ -138,16 +173,7 @@ export class FediverseBridge {
await fedifyObject.toJsonLd(),
);

await ctx.sendActivity(
{
username: post.author.username,
},
'followers',
createActivity,
{
preferSharedInbox: true,
},
);
await this.sendActivityToFollowers(post.author, createActivity);
}

private async handlePostDeleted(event: PostDeletedEvent) {
Expand All @@ -169,16 +195,7 @@ export class FediverseBridge {
await deleteActivity.toJsonLd(),
);

await ctx.sendActivity(
{
username: post.author.username,
},
'followers',
deleteActivity,
{
preferSharedInbox: true,
},
);
await this.sendActivityToFollowers(post.author, deleteActivity);
}

private async handleAccountUpdatedEvent(event: AccountUpdatedEvent) {
Expand All @@ -199,16 +216,7 @@ export class FediverseBridge {

await ctx.data.globaldb.set([update.id!.href], await update.toJsonLd());

await ctx.sendActivity(
{
username: account.username,
},
'followers',
update,
{
preferSharedInbox: true,
},
);
await this.sendActivityToFollowers(account, update);
}

private async handleAccountBlockedEvent(event: AccountBlockedEvent) {
Expand Down Expand Up @@ -241,13 +249,6 @@ export class FediverseBridge {

await ctx.data.globaldb.set([reject.id!.href], await reject.toJsonLd());

await ctx.sendActivity(
{ username: blockerAccount.username },
{
id: blockedAccount.apId,
inboxId: blockedAccount.apInbox,
},
reject,
);
await this.sendActivityToInbox(blockerAccount, blockedAccount, reject);
}
}