mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
ksdjkldsfld
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import type { DependencyConfiguration } from '../../types/ioc';
|
import type { DependencyConfiguration } from '../../types/ioc';
|
||||||
import { Container } from './container';
|
import { Container } from './container';
|
||||||
import * as __Services from '../structures/default-services';
|
import * as __Services from '../structures/default-services';
|
||||||
import { UnpackedDependencies } from '../../types/utility';
|
|
||||||
import type { Logging } from '../interfaces';
|
import type { Logging } from '../interfaces';
|
||||||
import { __add_container, __init_container, __swap_container, useContainerRaw } from './global';
|
import { __add_container, __init_container, __swap_container, useContainerRaw } from './global';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
@@ -14,7 +13,7 @@ export function disposeAll(logger: Logging|undefined) {
|
|||||||
|
|
||||||
|
|
||||||
type Insertable =
|
type Insertable =
|
||||||
| ((container: UnpackedDependencies) => object)
|
| ((container: Dependencies) => object)
|
||||||
| object
|
| object
|
||||||
const dependencyBuilder = (container: Container, excluded: string[] ) => {
|
const dependencyBuilder = (container: Container, excluded: string[] ) => {
|
||||||
return {
|
return {
|
||||||
@@ -26,7 +25,8 @@ const dependencyBuilder = (container: Container, excluded: string[] ) => {
|
|||||||
if(typeof v !== 'function') {
|
if(typeof v !== 'function') {
|
||||||
container.addSingleton(key, v)
|
container.addSingleton(key, v)
|
||||||
} else {
|
} else {
|
||||||
container.addWiredSingleton(key, (cntr) => v(cntr as UnpackedDependencies))
|
//@ts-ignore
|
||||||
|
container.addWiredSingleton(key, (cntr) => v(cntr))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +53,6 @@ const dependencyBuilder = (container: Container, excluded: string[] ) => {
|
|||||||
type ValidDependencyConfig =
|
type ValidDependencyConfig =
|
||||||
| ((c: ReturnType<typeof dependencyBuilder>) => any)
|
| ((c: ReturnType<typeof dependencyBuilder>) => any)
|
||||||
| DependencyConfiguration;
|
| DependencyConfiguration;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the user's conf, check for any excluded/included dependency keys.
|
* Given the user's conf, check for any excluded/included dependency keys.
|
||||||
@@ -79,13 +78,14 @@ async function composeRoot(
|
|||||||
|
|
||||||
if (!hasLogger) {
|
if (!hasLogger) {
|
||||||
container.get<Logging>('@sern/logger')
|
container.get<Logging>('@sern/logger')
|
||||||
?.info({ message: 'All dependencies loaded successfully.' });
|
?.info({ message: 'All dependencies loaded successfully.' });
|
||||||
}
|
}
|
||||||
container.ready();
|
await container.ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeDependencies (conf: ValidDependencyConfig) {
|
export async function makeDependencies (conf: ValidDependencyConfig) {
|
||||||
await __init_container({ autowire: false });
|
await __init_container({ autowire: false });
|
||||||
|
|
||||||
if(typeof conf === 'function') {
|
if(typeof conf === 'function') {
|
||||||
const excluded: string[] = [];
|
const excluded: string[] = [];
|
||||||
conf(dependencyBuilder(useContainerRaw(), excluded));
|
conf(dependencyBuilder(useContainerRaw(), excluded));
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ interface PluginExecutable {
|
|||||||
* Calls any plugin with {args}.
|
* Calls any plugin with {args}.
|
||||||
* @param args if an array, its spread and plugin called.
|
* @param args if an array, its spread and plugin called.
|
||||||
*/
|
*/
|
||||||
export function callPlugin(args: unknown): OperatorFunction<PluginExecutable, VoidResult>
|
export function callPlugin(plugin: PluginExecutable, args: unknown)
|
||||||
{
|
{
|
||||||
return concatMap(async plugin => {
|
if (Array.isArray(args)) {
|
||||||
if (Array.isArray(args)) {
|
return plugin.execute(...args);
|
||||||
return plugin.execute(...args);
|
}
|
||||||
}
|
return plugin.execute(args);
|
||||||
return plugin.execute(args);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const arrayifySource = <T>(src: T) =>
|
export const arrayifySource = <T>(src: T) =>
|
||||||
@@ -52,7 +50,7 @@ export const arrayifySource = <T>(src: T) =>
|
|||||||
* Checks if the stream of results is all ok.
|
* Checks if the stream of results is all ok.
|
||||||
*/
|
*/
|
||||||
export const everyPluginOk: OperatorFunction<VoidResult, boolean> =
|
export const everyPluginOk: OperatorFunction<VoidResult, boolean> =
|
||||||
pipe(every(result => result.isOk()),
|
pipe(every(result => result.isOk()), //this shortcircuits
|
||||||
defaultIfEmpty(true));
|
defaultIfEmpty(true));
|
||||||
|
|
||||||
export const sharedEventStream = <T>(e: Emitter, eventName: string) =>
|
export const sharedEventStream = <T>(e: Emitter, eventName: string) =>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class Cron extends EventEmitter {
|
|||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if(!cron.validate(module.pattern)) {
|
if(!cron.validate(module.pattern)) {
|
||||||
throw Error("Invalid cron expression while adding")
|
throw Error("Invalid cron expression while adding " + module.name)
|
||||||
}
|
}
|
||||||
this.modules.set(module.name!, module as CronEventCommand);
|
this.modules.set(module.name!, module as CronEventCommand);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { Err, Ok, Result } from 'ts-results-es';
|
|||||||
import type { Awaitable, UnpackedDependencies, VoidResult } from '../types/utility';
|
import type { Awaitable, UnpackedDependencies, VoidResult } from '../types/utility';
|
||||||
import type { ControlPlugin } from '../types/core-plugin';
|
import type { ControlPlugin } from '../types/core-plugin';
|
||||||
import type { CommandModule, Module, Processed } from '../types/core-modules';
|
import type { CommandModule, Module, Processed } from '../types/core-modules';
|
||||||
import { EventEmitter } from 'node:events';
|
|
||||||
import * as assert from 'node:assert';
|
import * as assert from 'node:assert';
|
||||||
import { Context } from '../core/structures/context';
|
import { Context } from '../core/structures/context';
|
||||||
import { CommandType } from '../core/structures/enums'
|
import { CommandType } from '../core/structures/enums'
|
||||||
@@ -28,7 +27,6 @@ import type { Args } from '../types/utility';
|
|||||||
import { inspect } from 'node:util'
|
import { inspect } from 'node:util'
|
||||||
import { disposeAll } from '../core/ioc/base';
|
import { disposeAll } from '../core/ioc/base';
|
||||||
import { arrayifySource, callPlugin, everyPluginOk, filterMapTo, handleError } from '../core/operators';
|
import { arrayifySource, callPlugin, everyPluginOk, filterMapTo, handleError } from '../core/operators';
|
||||||
|
|
||||||
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
|
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
|
||||||
|
|
||||||
function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
|
function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
|
||||||
@@ -41,13 +39,17 @@ function intoPayload(module: Module) {
|
|||||||
return pipe(map(arrayifySource),
|
return pipe(map(arrayifySource),
|
||||||
map(args => ({ module, args })));
|
map(args => ({ module, args })));
|
||||||
}
|
}
|
||||||
|
|
||||||
const createResult = createResultResolver<
|
const createResult = createResultResolver<
|
||||||
Processed<Module>,
|
Processed<Module>,
|
||||||
{ module: Processed<Module>; args: unknown[] },
|
{ module: Processed<Module>; args: unknown[] },
|
||||||
unknown[]
|
unknown[]
|
||||||
>({
|
>({
|
||||||
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
|
createStream: async function* ({ module, args }) {
|
||||||
|
for(const plugin of module.onEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
//from(module.onEvent).pipe(callPlugin(args))
|
||||||
|
},
|
||||||
onNext: ({ args }) => args,
|
onNext: ({ args }) => args,
|
||||||
});
|
});
|
||||||
/**
|
/**
|
||||||
@@ -56,10 +58,10 @@ const createResult = createResultResolver<
|
|||||||
* @param source
|
* @param source
|
||||||
*/
|
*/
|
||||||
export function eventDispatcher(module: Module, source: unknown) {
|
export function eventDispatcher(module: Module, source: unknown) {
|
||||||
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
|
assert.ok(source && typeof source === 'object', `${source} cannot be constructed into an event listener`);
|
||||||
|
|
||||||
const execute: OperatorFunction<unknown[], unknown> =
|
const execute: OperatorFunction<unknown[], unknown> =
|
||||||
concatMap(async args => module.execute(...args));
|
concatMap(async args => module.execute(...args));
|
||||||
|
//@ts-ignore
|
||||||
return fromEvent(source, module.name!)
|
return fromEvent(source, module.name!)
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
.pipe(intoPayload(module),
|
.pipe(intoPayload(module),
|
||||||
@@ -67,25 +69,24 @@ export function eventDispatcher(module: Module, source: unknown) {
|
|||||||
execute);
|
execute);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDispatcher(payload: { module: Processed<CommandModule>; event: BaseInteraction; }) {
|
export function createDispatcher({ module, event }: { module: Processed<CommandModule>; event: BaseInteraction; }) {
|
||||||
assert.ok(CommandType.Text !== payload.module.type,
|
assert.ok(CommandType.Text !== module.type,
|
||||||
SernError.MismatchEvent + 'Found text command in interaction stream');
|
SernError.MismatchEvent + 'Found text command in interaction stream');
|
||||||
switch (payload.module.type) {
|
if(isAutocomplete(event)) {
|
||||||
|
assert.ok(module.type === CommandType.Slash
|
||||||
|
|| module.type === CommandType.Both);
|
||||||
|
const option = treeSearch(event, module.options);
|
||||||
|
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(module));
|
||||||
|
const { command } = option;
|
||||||
|
return { module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
||||||
|
args: [event] };
|
||||||
|
}
|
||||||
|
switch (module.type) {
|
||||||
case CommandType.Slash:
|
case CommandType.Slash:
|
||||||
case CommandType.Both: {
|
case CommandType.Both: {
|
||||||
if (isAutocomplete(payload.event)) {
|
return { module, args: contextArgs(event) };
|
||||||
const option = treeSearch(payload.event, payload.module.options);
|
|
||||||
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(payload.module));
|
|
||||||
const { command } = option;
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
|
||||||
args: [payload.event],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { module: payload.module, args: contextArgs(payload.event) };
|
|
||||||
}
|
}
|
||||||
default: return { module: payload.module, args: [payload.event] };
|
default: return { module, args: [event] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createGenericHandler<Source, Narrowed extends Source, Output>(
|
function createGenericHandler<Source, Narrowed extends Source, Output>(
|
||||||
@@ -94,8 +95,8 @@ function createGenericHandler<Source, Narrowed extends Source, Output>(
|
|||||||
) {
|
) {
|
||||||
return (pred: (i: Source) => i is Narrowed) =>
|
return (pred: (i: Source) => i is Narrowed) =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
filter(pred),
|
filter(pred), // only handle this stream if it passes pred
|
||||||
concatMap(makeModule));
|
concatMap(makeModule)); // create a payload, preparing to execute
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,7 +122,7 @@ export function fmt(msg: string, prefix: string): string[] {
|
|||||||
*/
|
*/
|
||||||
export function createInteractionHandler<T extends Interaction>(
|
export function createInteractionHandler<T extends Interaction>(
|
||||||
source: Observable<Interaction>,
|
source: Observable<Interaction>,
|
||||||
mg: Map<string, Module>, //TODO
|
mg: Map<string, Module>,
|
||||||
) {
|
) {
|
||||||
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
|
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
|
||||||
source,
|
source,
|
||||||
@@ -135,7 +136,6 @@ export function createInteractionHandler<T extends Interaction>(
|
|||||||
return Err.EMPTY;
|
return Err.EMPTY;
|
||||||
}
|
}
|
||||||
const [ path ] = fullPaths;
|
const [ path ] = fullPaths;
|
||||||
//@ts-ignore TODO fixme
|
|
||||||
return Ok(createDispatcher({ module: path as Processed<CommandModule>, event }));
|
return Ok(createDispatcher({ module: path as Processed<CommandModule>, event }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,12 +147,9 @@ export function createMessageHandler(
|
|||||||
) {
|
) {
|
||||||
return createGenericHandler(source, async event => {
|
return createGenericHandler(source, async event => {
|
||||||
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
|
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
|
||||||
let fullPath = mg.get(`${prefix}_T`);
|
let fullPath = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`);
|
||||||
if(!fullPath) {
|
if(!fullPath) {
|
||||||
fullPath = mg.get(`${prefix}_B`);
|
return Err('Possibly undefined behavior: could not find a static id to resolve');
|
||||||
if(!fullPath) {
|
|
||||||
return Err('Possibly undefined behavior: could not find a static id to resolve');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Ok({ args: contextArgs(event, rest), module: fullPath as Processed<CommandModule> })
|
return Ok({ args: contextArgs(event, rest), module: fullPath as Processed<CommandModule> })
|
||||||
});
|
});
|
||||||
@@ -173,12 +170,13 @@ interface ExecutePayload {
|
|||||||
* @param module the module that will be executed with task
|
* @param module the module that will be executed with task
|
||||||
* @param task the deferred execution which will be called
|
* @param task the deferred execution which will be called
|
||||||
*/
|
*/
|
||||||
export function executeModule(
|
export async function executeModule(
|
||||||
emitter: Emitter,
|
emitter: Emitter,
|
||||||
logger: Logging|undefined,
|
logger: Logging|undefined,
|
||||||
errHandler: ErrorHandling,
|
errHandler: ErrorHandling,
|
||||||
{ module, task, args }: ExecutePayload,
|
{ module, task, args }: ExecutePayload,
|
||||||
) {
|
) {
|
||||||
|
const wrappedTask = await Result.wrapAsync(async () => task());
|
||||||
return of(module).pipe(
|
return of(module).pipe(
|
||||||
//converting the task into a promise so rxjs can resolve the Awaitable properly
|
//converting the task into a promise so rxjs can resolve the Awaitable properly
|
||||||
concatMap(() => Result.wrapAsync(async () => task())),
|
concatMap(() => Result.wrapAsync(async () => task())),
|
||||||
@@ -208,11 +206,11 @@ export function createResultResolver<
|
|||||||
>(config: {
|
>(config: {
|
||||||
onStop?: (module: T) => unknown;
|
onStop?: (module: T) => unknown;
|
||||||
onNext: (args: Args) => Output;
|
onNext: (args: Args) => Output;
|
||||||
createStream: (args: Args) => Observable<VoidResult>;
|
createStream: (args: Args) => AsyncGenerator<VoidResult>;
|
||||||
}) {
|
}) {
|
||||||
return (args: Args) => {
|
return (args: Args) => {
|
||||||
const task$ = config.createStream(args);
|
const task = config.createStream(args);
|
||||||
return task$.pipe(
|
return from(task).pipe(
|
||||||
tap(result => {
|
tap(result => {
|
||||||
result.isErr() && config.onStop?.(args.module);
|
result.isErr() && config.onStop?.(args.module);
|
||||||
}),
|
}),
|
||||||
@@ -225,9 +223,7 @@ export function createResultResolver<
|
|||||||
* Creates an executable task ( execute the command ) if all control plugins are successful
|
* Creates an executable task ( execute the command ) if all control plugins are successful
|
||||||
* @param onStop emits a failure response to the SernEmitter
|
* @param onStop emits a failure response to the SernEmitter
|
||||||
*/
|
*/
|
||||||
export function makeModuleExecutor<
|
export function makeModuleExecutor< M extends Processed<Module>, Args extends { module: M; args: unknown[]; }>
|
||||||
M extends Processed<Module>,
|
|
||||||
Args extends { module: M; args: unknown[]; }>
|
|
||||||
(onStop: (m: M) => unknown) {
|
(onStop: (m: M) => unknown) {
|
||||||
const onNext = ({ args, module }: Args) => ({
|
const onNext = ({ args, module }: Args) => ({
|
||||||
task: () => module.execute(...args),
|
task: () => module.execute(...args),
|
||||||
@@ -235,10 +231,17 @@ export function makeModuleExecutor<
|
|||||||
args
|
args
|
||||||
});
|
});
|
||||||
return createResultResolver({
|
return createResultResolver({
|
||||||
onStop,
|
onStop,
|
||||||
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
|
createStream: async function* ({ args, module }) {
|
||||||
onNext,
|
for(const plugin of module.onEvent) {
|
||||||
})
|
const result = await callPlugin(plugin, args);
|
||||||
|
if(result.isErr()) {
|
||||||
|
return result.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNext,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleCrash = ({ "@sern/errors": err,
|
export const handleCrash = ({ "@sern/errors": err,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ function hasPrefix(prefix: string, content: string) {
|
|||||||
return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0);
|
return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function message({"@sern/emitter": emitter, '@sern/errors':err,
|
export default function message(
|
||||||
|
{"@sern/emitter": emitter, '@sern/errors':err,
|
||||||
'@sern/logger': log, '@sern/client': client,
|
'@sern/logger': log, '@sern/client': client,
|
||||||
'@sern/modules': commands}: UnpackedDependencies,
|
'@sern/modules': commands}: UnpackedDependencies,
|
||||||
defaultPrefix: string | undefined) {
|
defaultPrefix: string | undefined) {
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ const parseConfig = async (conf: Promise<PresenceResult>) => {
|
|||||||
const src$ = typeof repeat === 'number'
|
const src$ = typeof repeat === 'number'
|
||||||
? interval(repeat)
|
? interval(repeat)
|
||||||
: fromEvent(...repeat);
|
: fromEvent(...repeat);
|
||||||
return src$
|
return src$.pipe(scan(onRepeat, s),
|
||||||
.pipe(scan(onRepeat, s),
|
startWith(s));
|
||||||
startWith(s));
|
|
||||||
}
|
}
|
||||||
return of(s).pipe(take(1));
|
return of(s).pipe(take(1));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { Module } from '../types/core-modules';
|
|||||||
import { UnpackedDependencies } from '../types/utility';
|
import { UnpackedDependencies } from '../types/utility';
|
||||||
|
|
||||||
export default async function(dir: string, deps : UnpackedDependencies) {
|
export default async function(dir: string, deps : UnpackedDependencies) {
|
||||||
const { "@sern/client": client,
|
const { '@sern/client': client,
|
||||||
'@sern/logger': log,
|
'@sern/logger': log,
|
||||||
'@sern/emitter': sEmitter,
|
'@sern/emitter': sEmitter,
|
||||||
'@sern/modules': commands} = deps;
|
'@sern/modules': commands } = deps;
|
||||||
log?.info({ message: "Waiting on discord client to be ready..." })
|
log?.info({ message: "Waiting on discord client to be ready..." })
|
||||||
await once(client, "ready");
|
await once(client, "ready");
|
||||||
log?.info({ message: "Client signaled ready, registering modules" });
|
log?.info({ message: "Client signaled ready, registering modules" });
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const intoDispatcher = (deps: UnpackedDependencies) =>
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const cron = deps['@sern/cron'];
|
const cron = deps['@sern/cron'];
|
||||||
cron.addCronModule(module);
|
cron.addCronModule(module);
|
||||||
return eventDispatcher(module, cron)
|
return eventDispatcher(module, cron);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { Module } from './core-modules';
|
|||||||
|
|
||||||
|
|
||||||
export interface CoreDependencies {
|
export interface CoreDependencies {
|
||||||
'@sern/client': () => Client;
|
'@sern/client': Client;
|
||||||
'@sern/emitter': () => Contracts.Emitter;
|
'@sern/emitter': Contracts.Emitter;
|
||||||
'@sern/errors': () => Contracts.ErrorHandling;
|
'@sern/errors': Contracts.ErrorHandling;
|
||||||
'@sern/logger'?: () => Contracts.Logging;
|
'@sern/logger'?: Contracts.Logging;
|
||||||
'@sern/modules': () => Map<string, Module>
|
'@sern/modules': Map<string, Module>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
|
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ function createRandomModule(): Processed<Module> {
|
|||||||
min: CommandType.Text,
|
min: CommandType.Text,
|
||||||
max: CommandType.ChannelSelect,
|
max: CommandType.ChannelSelect,
|
||||||
}),
|
}),
|
||||||
|
meta: { id:"", absPath: faker.system.directoryPath() },
|
||||||
description: faker.string.alpha(),
|
description: faker.string.alpha(),
|
||||||
name: faker.string.alpha(),
|
name: faker.string.alpha(),
|
||||||
onEvent: [],
|
onEvent: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user