-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor webhook post published handling into a more modular structure (
#257)
- Loading branch information
Showing
18 changed files
with
887 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { Activity, Actor } from '@fedify/fedify'; | ||
|
||
import type { FedifyRequestContext } from '../app'; | ||
|
||
/** | ||
* Sends an ActivityPub activity to other Fediverse actors | ||
* | ||
* @template TActivity Type of activity to send | ||
* @template TActor Type of actor to utilise when sending an activity | ||
*/ | ||
export interface ActivitySender<TActivity, TActor> { | ||
/** | ||
* Send an activity to the followers of an actor | ||
* | ||
* @param activity Activity to send | ||
* @param actor Actor whose followers will receive the activity | ||
*/ | ||
sendActivityToActorFollowers( | ||
activity: TActivity, | ||
actor: TActor, | ||
): Promise<void>; | ||
} | ||
|
||
/** | ||
* ActivitySender implementation using Fedify's RequestContext | ||
*/ | ||
export class FedifyActivitySender implements ActivitySender<Activity, Actor> { | ||
constructor(private readonly fedifyCtx: FedifyRequestContext) {} | ||
|
||
async sendActivityToActorFollowers(activity: Activity, actor: Actor) { | ||
await this.fedifyCtx.sendActivity( | ||
{ handle: String(actor.preferredUsername) }, | ||
'followers', | ||
activity, | ||
{ | ||
preferSharedInbox: true, | ||
}, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import type { Activity, Actor } from '@fedify/fedify'; | ||
|
||
import type { FedifyRequestContext } from '../app'; | ||
|
||
import { FedifyActivitySender } from './activity'; | ||
|
||
describe('FedifyActivitySender', () => { | ||
describe('sendActivityToActorFollowers', () => { | ||
it('should send an Activity to the followers of an Actor', async () => { | ||
const handle = 'foo'; | ||
|
||
const mockActor = { | ||
preferredUsername: handle, | ||
} as Actor; | ||
const mockActivity = {} as Activity; | ||
const mockFedifyCtx = { | ||
sendActivity: vi.fn(), | ||
} as unknown as FedifyRequestContext; | ||
|
||
const sender = new FedifyActivitySender(mockFedifyCtx); | ||
|
||
await sender.sendActivityToActorFollowers(mockActivity, mockActor); | ||
|
||
expect(mockFedifyCtx.sendActivity).toHaveBeenCalledWith( | ||
{ handle }, | ||
'followers', | ||
mockActivity, | ||
{ | ||
preferSharedInbox: true, | ||
}, | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { Actor } from '@fedify/fedify'; | ||
|
||
import type { FedifyRequestContext } from '../app'; | ||
|
||
/** | ||
* Resolves an ActivityPub actor | ||
* | ||
* @template TActor Type of actor to resolve | ||
*/ | ||
export interface ActorResolver<TActor> { | ||
/** | ||
* Resolve an ActivityPub actor by their handle | ||
* | ||
* @param handle Handle of the actor to resolve | ||
*/ | ||
resolveActorByHandle(handle: string): Promise<TActor | null>; | ||
} | ||
|
||
/** | ||
* ActorResolver implementation using Fedify's RequestContext | ||
*/ | ||
export class FedifyActorResolver implements ActorResolver<Actor> { | ||
constructor(private readonly fedifyCtx: FedifyRequestContext) {} | ||
|
||
async resolveActorByHandle(handle: string) { | ||
return this.fedifyCtx.getActor(handle); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import type { Actor } from '@fedify/fedify'; | ||
|
||
import type { FedifyRequestContext } from '../app'; | ||
|
||
import { FedifyActorResolver } from './actor'; | ||
|
||
describe('FedifyActorResolver', () => { | ||
describe('resolveActorByHandle', () => { | ||
it('should resolve an actor by handle', async () => { | ||
const handle = 'foo'; | ||
|
||
const mockActor = {} as Actor; | ||
|
||
const mockFedifyCtx = { | ||
getActor: vi.fn().mockImplementation((identifier) => { | ||
if (identifier === handle) { | ||
return mockActor; | ||
} | ||
|
||
return null; | ||
}), | ||
} as unknown as FedifyRequestContext; | ||
|
||
const resolver = new FedifyActorResolver(mockFedifyCtx); | ||
|
||
const result = await resolver.resolveActorByHandle(handle); | ||
|
||
expect(mockFedifyCtx.getActor).toHaveBeenCalledWith(handle); | ||
expect(result).toBe(mockActor); | ||
}); | ||
|
||
it('should return null if the actor can not be resolved', async () => { | ||
const handle = 'foo'; | ||
|
||
const mockFedifyCtx = { | ||
getActor: vi.fn().mockImplementation(() => { | ||
return null; | ||
}), | ||
} as unknown as FedifyRequestContext; | ||
|
||
const resolver = new FedifyActorResolver(mockFedifyCtx); | ||
|
||
const result = await resolver.resolveActorByHandle(handle); | ||
|
||
expect(mockFedifyCtx.getActor).toHaveBeenCalledWith(handle); | ||
expect(result).toBeNull(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './actor'; | ||
export * from './activity'; | ||
export * from './object'; | ||
export * from './outbox'; | ||
export * from './uri'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import type { Object as FedifyObject, KvStore } from '@fedify/fedify'; | ||
|
||
/** | ||
* Stores ActivityPub objects | ||
* | ||
* @template TObject Type of the object to store | ||
*/ | ||
export interface ObjectStore<TObject> { | ||
/** | ||
* Store an ActivityPub object | ||
* | ||
* @param object Object to store | ||
*/ | ||
store(object: TObject): Promise<void>; | ||
} | ||
|
||
/** | ||
* ObjectStore implementation using Fedify's KvStore | ||
*/ | ||
export class FedifyKvStoreObjectStore implements ObjectStore<FedifyObject> { | ||
constructor(private readonly db: KvStore) {} | ||
|
||
async store(object: FedifyObject) { | ||
if (object.id === null) { | ||
throw new Error('Object can not be stored without an ID'); | ||
} | ||
|
||
await this.db.set([object.id.href], await object.toJsonLd()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import type { Article, KvStore } from '@fedify/fedify'; | ||
|
||
import { FedifyKvStoreObjectStore } from './object'; | ||
|
||
describe('FedifyKvStoreObjectStore', () => { | ||
describe('store', () => { | ||
it('should store an object', async () => { | ||
const mockArticleId = new URL( | ||
'https://example.com/article/abc-123', | ||
); | ||
const mockKvStore = { | ||
set: vi.fn(), | ||
} as unknown as KvStore; | ||
const mockArticleJsonLd = { | ||
id: mockArticleId, | ||
type: 'Article', | ||
}; | ||
const mockArticle = { | ||
id: mockArticleId, | ||
toJsonLd: vi.fn().mockResolvedValue(mockArticleJsonLd), | ||
} as unknown as Article; | ||
|
||
const store = new FedifyKvStoreObjectStore(mockKvStore); | ||
|
||
await store.store(mockArticle); | ||
|
||
expect(mockKvStore.set).toHaveBeenCalledWith( | ||
[mockArticleId.href], | ||
mockArticleJsonLd, | ||
); | ||
}); | ||
|
||
it('should throw an error if the object has no ID', async () => { | ||
const mockKvStore = { | ||
set: vi.fn(), | ||
} as unknown as KvStore; | ||
const mockArticle = { | ||
id: null, | ||
toJsonLd: vi.fn().mockResolvedValue({}), | ||
} as unknown as Article; | ||
|
||
const store = new FedifyKvStoreObjectStore(mockKvStore); | ||
|
||
await expect(store.store(mockArticle)).rejects.toThrow( | ||
'Object can not be stored without an ID', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import type { Activity, KvStore } from '@fedify/fedify'; | ||
|
||
import { addToList } from '../kv-helpers'; | ||
|
||
/** | ||
* ActivityPub outbox collection | ||
* | ||
* @template TActivity Type of activity in the outbox | ||
*/ | ||
export interface Outbox<TActivity> { | ||
/** | ||
* Add an activity to the outbox | ||
* | ||
* @param activity Activity to add | ||
*/ | ||
add(activity: TActivity): Promise<void>; | ||
} | ||
|
||
/** | ||
* Outbox implementation using Fedify's KvStore | ||
*/ | ||
export class FedifyKvStoreOutbox implements Outbox<Activity> { | ||
constructor(private readonly db: KvStore) {} | ||
|
||
async add(activity: Activity) { | ||
if (activity.id === null) { | ||
throw new Error( | ||
'Activity can not be added to outbox without an ID', | ||
); | ||
} | ||
|
||
await addToList(this.db, ['outbox'], activity.id.href); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import type { Activity, KvStore } from '@fedify/fedify'; | ||
|
||
import * as kvHelpers from '../kv-helpers'; | ||
|
||
import { FedifyKvStoreOutbox } from './outbox'; | ||
|
||
vi.mock('../kv-helpers', () => ({ | ||
addToList: vi.fn(), | ||
})); | ||
|
||
describe('FedifyKvStoreOutbox', () => { | ||
describe('add', () => { | ||
it('should add an activity to the outbox', async () => { | ||
const mockKvStore = {} as KvStore; | ||
const mockActivityId = new URL( | ||
'https://example.com/activity/abc-123', | ||
); | ||
const mockActivity = { | ||
id: mockActivityId, | ||
} as Activity; | ||
|
||
const outbox = new FedifyKvStoreOutbox(mockKvStore); | ||
|
||
await outbox.add(mockActivity); | ||
|
||
expect(kvHelpers.addToList).toHaveBeenCalledWith( | ||
mockKvStore, | ||
['outbox'], | ||
mockActivityId.href, | ||
); | ||
}); | ||
|
||
it('should throw an error if the activity has no ID', async () => { | ||
const mockKvStore = {} as KvStore; | ||
const mockActivity = { | ||
id: null, | ||
} as Activity; | ||
|
||
const outbox = new FedifyKvStoreOutbox(mockKvStore); | ||
|
||
await expect(outbox.add(mockActivity)).rejects.toThrow( | ||
'Activity can not be added to outbox without an ID', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { Object as FedifyObject } from '@fedify/fedify'; | ||
|
||
import type { FedifyRequestContext } from '../app'; | ||
|
||
/** | ||
* Builds ActivityPub URIs | ||
* | ||
* @template TObject Type of object to build a URI for | ||
*/ | ||
export interface UriBuilder<TObject> { | ||
/** | ||
* Build a URI for an object | ||
* | ||
* @param cls Class of the object to build a URI for | ||
* @param id ID of the object to build a URI for | ||
*/ | ||
buildObjectUri( | ||
cls: { new (props: Partial<TObject>): TObject; typeId: URL }, | ||
id: string, | ||
): URL; | ||
|
||
/** | ||
* Build a URI for an actor's followers collection | ||
* | ||
* @param handle Handle of the actor to build a followers collection URI for | ||
*/ | ||
buildFollowersCollectionUri(handle: string): URL; | ||
} | ||
|
||
/** | ||
* UriBuilder implementation using Fedify's RequestContext | ||
*/ | ||
export class FedifyUriBuilder implements UriBuilder<FedifyObject> { | ||
constructor(private readonly fedifyCtx: FedifyRequestContext) {} | ||
|
||
buildObjectUri( | ||
cls: { new (props: Partial<FedifyObject>): FedifyObject; typeId: URL }, | ||
id: string, | ||
) { | ||
return this.fedifyCtx.getObjectUri(cls, { id }); | ||
} | ||
|
||
buildFollowersCollectionUri(handle: string) { | ||
return this.fedifyCtx.getFollowersUri(handle); | ||
} | ||
} |
Oops, something went wrong.