Skip to content

Commit

Permalink
feat: plugin system (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyTseng authored Mar 23, 2024
1 parent d818d4c commit 62e2156
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 449 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/client-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ClientContext {
* @param options Client context options
*/
constructor(
private readonly client: Client,
readonly client: Client,
options: ClientContextOptions = {},
) {
this.id = randomUUID();
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/interfaces/common.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export type Signature = string;
export type Tag = string[];

export type SubscriptionId = string;

export type KeysOfUnion<T> = T extends T ? keyof T : never;
2 changes: 1 addition & 1 deletion packages/common/src/interfaces/event.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Event {
sig: Signature;
}

export type EventHandleResult = {
export type HandleEventResult = {
success: boolean;
message?: string;
noReplyNeeded?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,5 @@
import { Event, LogLevel, Logger, MessageType } from '@nostr-relay/common';

/**
* Options for NostrRelay
*/
export type NostrRelayOptions = {
/**
* Domain name of the Nostr Relay server. If not set, NIP-42 is not enabled.
* More info: https://github.com/nostr-protocol/nips/blob/master/42.md
*/
domain?: string;
/**
* Logger to use. `Default: ConsoleLoggerService`
*/
logger?: Logger;
/**
* The minimum log level to log. `Default: LogLevel.INFO`
*/
logLevel?: LogLevel;
createdAtUpperLimit?: number;
createdAtLowerLimit?: number;
/**
* Allowed minimum PoW difficulty for events.` Default: 0`
*/
minPowDifficulty?: number;
/**
* Maximum number of subscriptions per client. `Default: 20`
*/
maxSubscriptionsPerClient?: number;
/**
* TTL for filter result cache in milliseconds. `Default: 1000`
*/
filterResultCacheTtl?: number;
/**
* TTL for event handling result cache in milliseconds. `Default: 600000`
*/
eventHandlingResultCacheTtl?: number;
};
import { MessageType } from '../constants';
import { Event } from './event.interface';

/**
* Result of handling REQ message
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from './common.interface';
export * from './event-repository.interface';
export * from './event.interface';
export * from './filter.interface';
export * from './handle-result.interface';
export * from './logger.interface';
export * from './message.interface';
export * from './nostr-relay-options.interface';
export * from './plugin.interface';
39 changes: 39 additions & 0 deletions packages/common/src/interfaces/nostr-relay-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { LogLevel } from '../constants';
import { Logger } from './logger.interface';

/**
* Options for NostrRelay
*/
export type NostrRelayOptions = {
/**
* Domain name of the Nostr Relay server. If not set, NIP-42 is not enabled.
* More info: https://github.com/nostr-protocol/nips/blob/master/42.md
*/
domain?: string;
/**
* Logger to use. `Default: ConsoleLoggerService`
*/
logger?: Logger;
/**
* The minimum log level to log. `Default: LogLevel.INFO`
*/
logLevel?: LogLevel;
createdAtUpperLimit?: number;
createdAtLowerLimit?: number;
/**
* Allowed minimum PoW difficulty for events.` Default: 0`
*/
minPowDifficulty?: number;
/**
* Maximum number of subscriptions per client. `Default: 20`
*/
maxSubscriptionsPerClient?: number;
/**
* TTL for filter result cache in milliseconds. `Default: 1000`
*/
filterResultCacheTtl?: number;
/**
* TTL for event handling result cache in milliseconds. `Default: 600000`
*/
eventHandlingResultCacheTtl?: number;
};
44 changes: 12 additions & 32 deletions packages/common/src/interfaces/plugin.interface.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,22 @@
import { ClientContext } from '../client-context';
import { Event, EventHandleResult } from './event.interface';
import { Event } from './event.interface';
import { HandleMessageResult } from './handle-result.interface';
import { IncomingMessage } from './message.interface';

export type NostrRelayPlugin =
| BeforeEventHandle
| AfterEventHandle
| BeforeEventBroadcast
| AfterEventBroadcast;
export type NostrRelayPlugin = HandleMessageMiddleware | BroadcastMiddleware;

export type BeforeHookResult<T = {}> =
| { canContinue: true }
| ({ canContinue: false } & T);

export type BeforeEventHandleResult = BeforeHookResult<{
result: EventHandleResult;
}>;

export interface BeforeEventHandle {
beforeEventHandle(
export interface HandleMessageMiddleware {
handleMessage(
ctx: ClientContext,
event: Event,
): Promise<BeforeEventHandleResult> | BeforeEventHandleResult;
message: IncomingMessage,
next: () => Promise<HandleMessageResult>,
): Promise<HandleMessageResult> | HandleMessageResult;
}

export interface AfterEventHandle {
afterEventHandle(
export interface BroadcastMiddleware {
broadcast(
ctx: ClientContext,
event: Event,
handleResult: EventHandleResult,
next: () => Promise<void>,
): Promise<void> | void;
}

export interface BeforeEventBroadcast {
beforeEventBroadcast(
ctx: ClientContext,
event: Event,
): Promise<BeforeHookResult> | BeforeHookResult;
}

export interface AfterEventBroadcast {
afterEventBroadcast(ctx: ClientContext, event: Event): Promise<void> | void;
}
24 changes: 0 additions & 24 deletions packages/core/__test__/nostr-relay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,13 @@ describe('NostrRelay', () => {
'',
];

const mockPlugin = {
beforeEventHandle: jest.fn().mockReturnValue({ canContinue: true }),
afterEventHandle: jest.fn().mockReturnValue(handleResult),
};
nostrRelay.register(mockPlugin);
const mockHandleEvent = jest
.spyOn(nostrRelay['eventService'], 'handleEvent')
.mockResolvedValue(handleResult);
const ctx = nostrRelay['getClientContext'](client);

await nostrRelay.handleEventMessage(client, event);

expect(mockPlugin.beforeEventHandle).toHaveBeenCalledWith(ctx, event);
expect(mockPlugin.afterEventHandle).toHaveBeenCalledWith(
ctx,
event,
handleResult,
);
expect(mockHandleEvent).toHaveBeenCalledWith(ctx, event);
expect(client.send).toHaveBeenCalledWith(JSON.stringify(outgoingMessage));
});
Expand Down Expand Up @@ -156,19 +145,6 @@ describe('NostrRelay', () => {
expect(client.send).toHaveBeenNthCalledWith(1, outgoingMessageStr);
expect(client.send).toHaveBeenNthCalledWith(2, outgoingMessageStr);
});

it('should not handle event due to plugin prevention', async () => {
jest
.spyOn(nostrRelay['pluginManagerService'], 'callBeforeEventHandleHooks')
.mockResolvedValue({ canContinue: false, result: {} as any });
const mockHandleEvent = jest
.spyOn(nostrRelay['eventService'], 'handleEvent')
.mockResolvedValue({ success: true });

await nostrRelay.handleEventMessage(client, { id: 'eventId' } as Event);

expect(mockHandleEvent).not.toHaveBeenCalled();
});
});

describe('req', () => {
Expand Down
33 changes: 1 addition & 32 deletions packages/core/__test__/services/event.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,45 +131,14 @@ describe('eventService', () => {
});

it('should handle ephemeral event successfully', async () => {
const mockBeforeEventBroadcast = jest
.spyOn(
eventService['pluginManagerService'],
'callBeforeEventBroadcastHooks',
)
.mockResolvedValue({ canContinue: true });
const mockAfterEventBroadcast = jest
.spyOn(
eventService['pluginManagerService'],
'callAfterEventBroadcastHooks',
)
.mockImplementation();
jest.spyOn(EventUtils, 'validate').mockReturnValue(undefined);

const event = { id: 'a', kind: EventKind.EPHEMERAL_FIRST } as Event;
expect(await eventService.handleEvent(ctx, event)).toEqual({
noReplyNeeded: true,
success: true,
});
expect(mockBeforeEventBroadcast).toHaveBeenCalledWith(ctx, event);
expect(mockAfterEventBroadcast).toHaveBeenCalledWith(ctx, event);
expect(subscriptionService.broadcast).toHaveBeenCalledWith(event);
});

it('should not broadcast due to plugin prevention', async () => {
jest
.spyOn(
eventService['pluginManagerService'],
'callBeforeEventBroadcastHooks',
)
.mockResolvedValue({ canContinue: false });
jest.spyOn(EventUtils, 'validate').mockReturnValue(undefined);

const event = { id: 'a', kind: EventKind.EPHEMERAL_FIRST } as Event;
expect(await eventService.handleEvent(ctx, event)).toEqual({
noReplyNeeded: true,
success: true,
});
expect(subscriptionService.broadcast).not.toHaveBeenCalled();
expect(subscriptionService.broadcast).toHaveBeenCalledWith(event);
});

it('should handle regular event successfully', async () => {
Expand Down
Loading

0 comments on commit 62e2156

Please sign in to comment.