Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: plugin system #17

Merged
merged 5 commits into from
Mar 23, 2024
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
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
Loading