Skip to content

Commit

Permalink
refactor(server): notifications have been rebuilt
Browse files Browse the repository at this point in the history
notifications now make proper use of the entity system to validate metadata types for each notification type
  • Loading branch information
ADRFranklin committed May 14, 2024
1 parent 3e9dec5 commit fd0ad99
Show file tree
Hide file tree
Showing 87 changed files with 45,986 additions and 2,812 deletions.
12 changes: 7 additions & 5 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
"depcheck": "depcheck",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint --fix . --ext .ts",
"generate:client-tmdb": "npx @hey-api/openapi-ts -i \"https://developer.themoviedb.org/openapi/64542913e1f86100738e227f\" -o \"./src/infra/tmdb/client\" -c axios --name TheMovieDatabase --schemas true --services true --useOptions",
"generate:client-plex": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/LukeHagar/plex-api-spec/main/plex-media-server-spec-dereferenced.yaml\" -o \"./src/infra/plex\" -c axios --name PlexClient --schemas true --services true --useOptions",
"generate:client-plextv": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/LukeHagar/plex-api-spec/main/plex-tv-spec-dereferenced.yaml\" -o \"./src/infra/plextv\" -c axios --name PlexTvClient --schemas true --services true --useOptions",
"generate:client-radarr": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/Radarr/Radarr/develop/src/Radarr.Api.V3/openapi.json\" -o \"./src/infra/servarr/radarr\" -c axios --name RadarrV3Client --schemas true --services true --useOptions",
"generate:client-sonarr": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/Sonarr/Sonarr/develop/src/Sonarr.Api.V3/openapi.json\" -o \"./src/infra/servarr/sonarr\" -c axios --name SonarrV3Client --schemas true --services true --useOptions"
"generate:client-tmdb": "npx @hey-api/openapi-ts -i \"https://developer.themoviedb.org/openapi/64542913e1f86100738e227f\" -o \"./src/infrastructure/tmdb/client\" -c axios --name TheMovieDatabase --schemas true --services true --useOptions",
"generate:client-plex": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/LukeHagar/plex-api-spec/main/plex-media-server-spec-dereferenced.yaml\" -o \"./src/infrastructure/plex\" -c axios --name PlexClient --schemas true --services true --useOptions",
"generate:client-plextv": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/LukeHagar/plex-api-spec/main/plex-tv-spec-dereferenced.yaml\" -o \"./src/infrastructure/plextv\" -c axios --name PlexTvClient --schemas true --services true --useOptions",
"generate:client-radarr": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/Radarr/Radarr/develop/src/Radarr.Api.V3/openapi.json\" -o \"./src/infrastructure/servarr/radarr\" -c axios --name RadarrV3Client --schemas true --services true --useOptions",
"generate:client-sonarr": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/Sonarr/Sonarr/develop/src/Sonarr.Api.V3/openapi.json\" -o \"./src/infrastructure/servarr/sonarr\" -c axios --name SonarrV3Client --schemas true --services true --useOptions",
"generate:client-discord": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json\" -o \"./src/infrastructure/discord\" -c axios --name DiscordAPIClient --schemas true --services true --useOptions",
"generate:client-telegram": "npx @hey-api/openapi-ts -i \"https://raw.githubusercontent.com/sys-001/telegram-bot-api-versions/main/files/openapi/json/v700.json\" -o \"./src/infrastructure/telegram\" -c axios --name TelegramAPIClient --schemas true --services true --useOptions"
},
"author": "",
"license": "MIT",
Expand Down
35 changes: 35 additions & 0 deletions server/src/infrastructure/discord/DiscordAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { Interceptors } from './core/OpenAPI';
import { AxiosHttpRequest } from './core/AxiosHttpRequest';

import { DefaultService } from './services.gen';

type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;

export class DiscordAPIClient {

public readonly default: DefaultService;

public readonly request: BaseHttpRequest;

constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) {
this.request = new HttpRequest({
BASE: config?.BASE ?? 'https://discord.com/api/v10',
VERSION: config?.VERSION ?? '10',
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? 'include',
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
interceptors: {
request: config?.interceptors?.request ?? new Interceptors(),
response: config?.interceptors?.response ?? new Interceptors(),
},
});

this.default = new DefaultService(this.request);
}
}
21 changes: 21 additions & 0 deletions server/src/infrastructure/discord/core/ApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';

export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;

constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);

this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
13 changes: 13 additions & 0 deletions server/src/infrastructure/discord/core/ApiRequestOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly cookies?: Record<string, unknown>;
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
7 changes: 7 additions & 0 deletions server/src/infrastructure/discord/core/ApiResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};
22 changes: 22 additions & 0 deletions server/src/infrastructure/discord/core/AxiosHttpRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import { BaseHttpRequest } from './BaseHttpRequest';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
import { request as __request } from './request';

export class AxiosHttpRequest extends BaseHttpRequest {

constructor(config: OpenAPIConfig) {
super(config);
}

/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return __request(this.config, options);
}
}
10 changes: 10 additions & 0 deletions server/src/infrastructure/discord/core/BaseHttpRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';

export abstract class BaseHttpRequest {

constructor(public readonly config: OpenAPIConfig) {}

public abstract request<T>(options: ApiRequestOptions): CancelablePromise<T>;
}
126 changes: 126 additions & 0 deletions server/src/infrastructure/discord/core/CancelablePromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}

public get isCancelled(): boolean {
return true;
}
}

export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;

(cancelHandler: () => void): void;
}

export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
readonly cancelHandlers: (() => void)[];
readonly promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;

constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel
) => void
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this.cancelHandlers = [];
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;

const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isResolved = true;
if (this._resolve) this._resolve(value);
};

const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isRejected = true;
if (this._reject) this._reject(reason);
};

const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this.cancelHandlers.push(cancelHandler);
};

Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this._isResolved,
});

Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this._isRejected,
});

Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this._isCancelled,
});

return executor(onResolve, onReject, onCancel as OnCancel);
});
}

get [Symbol.toStringTag]() {
return "Cancellable Promise";
}

public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}

public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}

public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}

public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isCancelled = true;
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError('Request aborted'));
}

public get isCancelled(): boolean {
return this._isCancelled;
}
}
57 changes: 57 additions & 0 deletions server/src/infrastructure/discord/core/OpenAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { ApiRequestOptions } from './ApiRequestOptions';

type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

export class Interceptors<T> {
_fns: Middleware<T>[];

constructor() {
this._fns = [];
}

eject(fn: Middleware<T>) {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}

use(fn: Middleware<T>) {
this._fns = [...this._fns, fn];
}
}

export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
request: Interceptors<AxiosRequestConfig>;
response: Interceptors<AxiosResponse>;
};
};

export const OpenAPI: OpenAPIConfig = {
BASE: 'https://discord.com/api/v10',
CREDENTIALS: 'include',
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: '10',
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
};
Loading

0 comments on commit fd0ad99

Please sign in to comment.