@julanzw/ttoolbox-discordjs-framework 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes/Command.class.d.ts +7 -0
- package/dist/classes/Command.class.js +17 -5
- package/dist/classes/CommandManager.class.d.ts +6 -0
- package/dist/classes/CommandManager.class.js +13 -0
- package/dist/classes/SubcommandGroup.class.d.ts +8 -1
- package/dist/classes/SubcommandGroup.class.js +21 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/testing/mock.d.ts +72 -0
- package/dist/testing/mock.js +197 -0
- package/dist/utils/ErrorReporter.d.ts +54 -0
- package/dist/utils/ErrorReporter.js +95 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChatInputCommandInteraction, Client, SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
|
|
2
2
|
import { PermissionLevel } from '../types/permission.js';
|
|
3
3
|
import { ILogger } from '../types/logger.js';
|
|
4
|
+
import { ErrorReporter } from '../utils/ErrorReporter.js';
|
|
4
5
|
/**
|
|
5
6
|
* Abstract base class for Discord slash commands.
|
|
6
7
|
*
|
|
@@ -35,6 +36,8 @@ export declare abstract class Command {
|
|
|
35
36
|
cooldown?: number;
|
|
36
37
|
/** Logger instance to use inside the command */
|
|
37
38
|
protected logger?: ILogger;
|
|
39
|
+
/** ErrorReporter instance to use inside the command */
|
|
40
|
+
protected errorReporter?: ErrorReporter;
|
|
38
41
|
/**
|
|
39
42
|
* Validates whether the command can be executed in the current context.
|
|
40
43
|
*
|
|
@@ -166,4 +169,8 @@ export declare abstract class Command {
|
|
|
166
169
|
* @param logToConsole - Whether to also log to console
|
|
167
170
|
*/
|
|
168
171
|
protected log(message: string, level: string, scope: string, logToConsole?: boolean): void;
|
|
172
|
+
/**
|
|
173
|
+
* Set the error reporter for this command
|
|
174
|
+
*/
|
|
175
|
+
setErrorReporter(reporter: ErrorReporter): void;
|
|
169
176
|
}
|
|
@@ -85,13 +85,19 @@ export class Command {
|
|
|
85
85
|
const scope = `${commandName}_EXECUTION`;
|
|
86
86
|
try {
|
|
87
87
|
await fn();
|
|
88
|
-
|
|
89
|
-
this.logger?.log(`${commandName} ${subcommandName ? `(${subcommandName}) ` : ``}command executed`, 'info', scope);
|
|
88
|
+
this.logger?.log(`${commandName} command executed`, 'info', scope);
|
|
90
89
|
}
|
|
91
90
|
catch (err) {
|
|
92
|
-
this.logger?.log(
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
this.logger?.log(`An Error occurred: ${err.message ?? err}`, 'error', scope, true);
|
|
92
|
+
if (this.errorReporter) {
|
|
93
|
+
await this.errorReporter.reportError(err, `Command: ${commandName}`, {
|
|
94
|
+
user: interaction.user.tag,
|
|
95
|
+
userId: interaction.user.id,
|
|
96
|
+
guild: interaction.guild?.name,
|
|
97
|
+
guildId: interaction.guildId,
|
|
98
|
+
channel: interaction.channel?.id,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
95
101
|
return await safeReply(interaction, 'An unexpected error occurred.');
|
|
96
102
|
}
|
|
97
103
|
}
|
|
@@ -153,4 +159,10 @@ export class Command {
|
|
|
153
159
|
log(message, level, scope, logToConsole = false) {
|
|
154
160
|
this.logger?.log(message, level, scope, logToConsole);
|
|
155
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Set the error reporter for this command
|
|
164
|
+
*/
|
|
165
|
+
setErrorReporter(reporter) {
|
|
166
|
+
this.errorReporter = reporter;
|
|
167
|
+
}
|
|
156
168
|
}
|
|
@@ -2,9 +2,11 @@ import { ChatInputCommandInteraction, Client, RESTPostAPIChatInputApplicationCom
|
|
|
2
2
|
import { ILogger } from '../types/logger.js';
|
|
3
3
|
import { Command } from './Command.class.js';
|
|
4
4
|
import { SubcommandGroup } from './SubcommandGroup.class.js';
|
|
5
|
+
import { ErrorReporter } from '../utils/ErrorReporter.js';
|
|
5
6
|
export declare class CommandManager {
|
|
6
7
|
private commands;
|
|
7
8
|
protected logger?: ILogger;
|
|
9
|
+
protected errorReporter?: ErrorReporter;
|
|
8
10
|
/**
|
|
9
11
|
* Register a single command or subcommand group
|
|
10
12
|
*/
|
|
@@ -66,4 +68,8 @@ export declare class CommandManager {
|
|
|
66
68
|
*/
|
|
67
69
|
getCommandNames(): string[];
|
|
68
70
|
setLogger(logger: ILogger): this;
|
|
71
|
+
/**
|
|
72
|
+
* Set the error reporter for all commands
|
|
73
|
+
*/
|
|
74
|
+
setErrorReporter(reporter: ErrorReporter): this;
|
|
69
75
|
}
|
|
@@ -10,6 +10,9 @@ export class CommandManager {
|
|
|
10
10
|
if (this.logger) {
|
|
11
11
|
command.setLogger(this.logger);
|
|
12
12
|
}
|
|
13
|
+
if (this.errorReporter) {
|
|
14
|
+
command.setErrorReporter(this.errorReporter);
|
|
15
|
+
}
|
|
13
16
|
this.commands.set(command.name, command);
|
|
14
17
|
return this;
|
|
15
18
|
}
|
|
@@ -146,4 +149,14 @@ export class CommandManager {
|
|
|
146
149
|
}
|
|
147
150
|
return this;
|
|
148
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Set the error reporter for all commands
|
|
154
|
+
*/
|
|
155
|
+
setErrorReporter(reporter) {
|
|
156
|
+
this.errorReporter = reporter;
|
|
157
|
+
for (const command of this.commands.values()) {
|
|
158
|
+
command.setErrorReporter(reporter);
|
|
159
|
+
}
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
149
162
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChatInputCommandInteraction, Client, RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord.js';
|
|
2
2
|
import { ILogger } from '../types/logger.js';
|
|
3
3
|
import { Command } from './Command.class.js';
|
|
4
|
+
import { ErrorReporter } from '../utils/ErrorReporter.js';
|
|
4
5
|
/**
|
|
5
6
|
* Abstract base class for Discord slash command groups with subcommands.
|
|
6
7
|
*
|
|
@@ -32,6 +33,8 @@ export declare abstract class SubcommandGroup {
|
|
|
32
33
|
protected abstract subcommands: Map<string, Command>;
|
|
33
34
|
/** The logger instance used in the subcommand group */
|
|
34
35
|
protected logger?: ILogger;
|
|
36
|
+
/** ErrorReporter instance to use inside the subcommand group */
|
|
37
|
+
private errorReporter?;
|
|
35
38
|
/**
|
|
36
39
|
* Safely executes a function with error handling and logging.
|
|
37
40
|
*
|
|
@@ -39,7 +42,7 @@ export declare abstract class SubcommandGroup {
|
|
|
39
42
|
* and automatically handles errors by logging them and sending a user-friendly
|
|
40
43
|
* error message.
|
|
41
44
|
*
|
|
42
|
-
* @param
|
|
45
|
+
* @param subcommandName - The name of the subcommand
|
|
43
46
|
* @param scope - The logging scope for this execution
|
|
44
47
|
* @param interaction - The command interaction
|
|
45
48
|
* @param fn - The function to execute
|
|
@@ -124,4 +127,8 @@ export declare abstract class SubcommandGroup {
|
|
|
124
127
|
* @param logToConsole - Whether to also log to console
|
|
125
128
|
*/
|
|
126
129
|
protected log(message: string, level: string, scope: string, logToConsole?: boolean): void;
|
|
130
|
+
/**
|
|
131
|
+
* Set the error reporter for this subcommand group.
|
|
132
|
+
*/
|
|
133
|
+
setErrorReporter(reporter: ErrorReporter): this;
|
|
127
134
|
}
|
|
@@ -30,20 +30,26 @@ export class SubcommandGroup {
|
|
|
30
30
|
* and automatically handles errors by logging them and sending a user-friendly
|
|
31
31
|
* error message.
|
|
32
32
|
*
|
|
33
|
-
* @param
|
|
33
|
+
* @param subcommandName - The name of the subcommand
|
|
34
34
|
* @param scope - The logging scope for this execution
|
|
35
35
|
* @param interaction - The command interaction
|
|
36
36
|
* @param fn - The function to execute
|
|
37
37
|
* @private
|
|
38
38
|
*/
|
|
39
|
-
async safeExecute(
|
|
39
|
+
async safeExecute(subcommandName, scope, interaction, fn) {
|
|
40
40
|
try {
|
|
41
41
|
await fn();
|
|
42
|
-
|
|
43
|
-
this.logger?.log(`${commandName} ${subcommandName ? `(${subcommandName}) ` : ``}command executed`, 'info', scope);
|
|
42
|
+
this.logger?.log(`${this.name} (${subcommandName}) command executed`, 'info', scope);
|
|
44
43
|
}
|
|
45
44
|
catch (err) {
|
|
46
45
|
this.logger?.log('An Error occured' + err, 'error', scope, true);
|
|
46
|
+
await this.errorReporter?.reportError(err, `Command: ${subcommandName} in SubcommandGroup: ${this.name}`, {
|
|
47
|
+
user: interaction.user.tag,
|
|
48
|
+
userId: interaction.user.id,
|
|
49
|
+
guild: interaction.guild?.name,
|
|
50
|
+
guildId: interaction.guildId,
|
|
51
|
+
channel: interaction.channel?.id,
|
|
52
|
+
});
|
|
47
53
|
return await safeReply(interaction, 'An unexpected error occurred.');
|
|
48
54
|
}
|
|
49
55
|
}
|
|
@@ -74,7 +80,7 @@ export class SubcommandGroup {
|
|
|
74
80
|
throw new Error(`Unknown subcommand: ${subcommandName}`);
|
|
75
81
|
}
|
|
76
82
|
const scope = `${subcommand.name}_EXECUTION`;
|
|
77
|
-
await this.safeExecute(
|
|
83
|
+
await this.safeExecute(subcommandName, scope, interaction, () => subcommand.execute(interaction, client));
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* Converts the command group to Discord API JSON format for registration.
|
|
@@ -153,4 +159,14 @@ export class SubcommandGroup {
|
|
|
153
159
|
log(message, level, scope, logToConsole = false) {
|
|
154
160
|
this.logger?.log(message, level, scope, logToConsole);
|
|
155
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Set the error reporter for this subcommand group.
|
|
164
|
+
*/
|
|
165
|
+
setErrorReporter(reporter) {
|
|
166
|
+
this.errorReporter = reporter;
|
|
167
|
+
for (const command of this.subcommands.values()) {
|
|
168
|
+
command.setErrorReporter(reporter);
|
|
169
|
+
}
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
156
172
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,4 +15,5 @@ export { safeReply, safeEdit } from './utils/editAndReply.js';
|
|
|
15
15
|
export { formatDuration, formatDateToString, formatDateToYYYYMMDDHHMMSS, formatDateToDDMMYYYY, getDaySuffix, capitalizeFirst, } from './utils/formatting.js';
|
|
16
16
|
export { TIMES_MILISECONDS } from './utils/miliseconds.js';
|
|
17
17
|
export { TToolboxLogger } from './utils/TToolboxLogger.class.js';
|
|
18
|
+
export { ErrorReporter } from './utils/ErrorReporter.js';
|
|
18
19
|
export { InteractionError } from './classes/InteractionError.class.js';
|
package/dist/index.js
CHANGED
|
@@ -13,5 +13,6 @@ export { safeReply, safeEdit } from './utils/editAndReply.js';
|
|
|
13
13
|
export { formatDuration, formatDateToString, formatDateToYYYYMMDDHHMMSS, formatDateToDDMMYYYY, getDaySuffix, capitalizeFirst, } from './utils/formatting.js';
|
|
14
14
|
export { TIMES_MILISECONDS } from './utils/miliseconds.js';
|
|
15
15
|
export { TToolboxLogger } from './utils/TToolboxLogger.class.js';
|
|
16
|
+
export { ErrorReporter } from './utils/ErrorReporter.js';
|
|
16
17
|
// Errors
|
|
17
18
|
export { InteractionError } from './classes/InteractionError.class.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ChatInputCommandInteraction, User, Guild, TextChannel, Client } from 'discord.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for creating a mock interaction
|
|
4
|
+
*/
|
|
5
|
+
export interface MockInteractionOptions {
|
|
6
|
+
commandName?: string;
|
|
7
|
+
user?: Partial<User>;
|
|
8
|
+
guild?: Partial<Guild>;
|
|
9
|
+
guildId?: string | null;
|
|
10
|
+
channelId?: string;
|
|
11
|
+
options?: Record<string, any>;
|
|
12
|
+
replied?: boolean;
|
|
13
|
+
deferred?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a mock ChatInputCommandInteraction for testing.
|
|
17
|
+
*
|
|
18
|
+
* @param overrides - Optional overrides for interaction properties
|
|
19
|
+
* @returns A mocked interaction object
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { describe, it, expect, vi } from 'vitest';
|
|
24
|
+
* import { createMockInteraction } from '@julanzw/ttoolbox-discordjs-framework/testing';
|
|
25
|
+
*
|
|
26
|
+
* describe('PingCommand', () => {
|
|
27
|
+
* it('should reply with pong', async () => {
|
|
28
|
+
* const interaction = createMockInteraction({
|
|
29
|
+
* commandName: 'ping',
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* const command = new PingCommand();
|
|
33
|
+
* await command.execute(interaction, mockClient);
|
|
34
|
+
*
|
|
35
|
+
* expect(interaction.reply).toHaveBeenCalledWith('Pong!');
|
|
36
|
+
* });
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function createMockInteraction(overrides?: MockInteractionOptions): ChatInputCommandInteraction;
|
|
41
|
+
/**
|
|
42
|
+
* Create a mock Discord client for testing.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const client = createMockClient();
|
|
47
|
+
* const command = new PingCommand();
|
|
48
|
+
* await command.execute(mockInteraction, client);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function createMockClient(): Client;
|
|
52
|
+
/**
|
|
53
|
+
* Create a mock user for testing.
|
|
54
|
+
*
|
|
55
|
+
* @param overrides - Optional overrides for user properties
|
|
56
|
+
* @returns A mocked User object
|
|
57
|
+
*/
|
|
58
|
+
export declare function createMockUser(overrides?: Partial<User>): User;
|
|
59
|
+
/**
|
|
60
|
+
* Create a mock guild for testing.
|
|
61
|
+
*
|
|
62
|
+
* @param overrides - Optional overrides for guild properties
|
|
63
|
+
* @returns A mocked Guild object
|
|
64
|
+
*/
|
|
65
|
+
export declare function createMockGuild(overrides?: Partial<Guild>): Guild;
|
|
66
|
+
/**
|
|
67
|
+
* Create a mock text channel for testing.
|
|
68
|
+
*
|
|
69
|
+
* @param overrides - Optional overrides for channel properties
|
|
70
|
+
* @returns A mocked TextChannel object
|
|
71
|
+
*/
|
|
72
|
+
export declare function createMockChannel(overrides?: Partial<TextChannel>): TextChannel;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a mock ChatInputCommandInteraction for testing.
|
|
3
|
+
*
|
|
4
|
+
* @param overrides - Optional overrides for interaction properties
|
|
5
|
+
* @returns A mocked interaction object
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
* import { createMockInteraction } from '@julanzw/ttoolbox-discordjs-framework/testing';
|
|
11
|
+
*
|
|
12
|
+
* describe('PingCommand', () => {
|
|
13
|
+
* it('should reply with pong', async () => {
|
|
14
|
+
* const interaction = createMockInteraction({
|
|
15
|
+
* commandName: 'ping',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const command = new PingCommand();
|
|
19
|
+
* await command.execute(interaction, mockClient);
|
|
20
|
+
*
|
|
21
|
+
* expect(interaction.reply).toHaveBeenCalledWith('Pong!');
|
|
22
|
+
* });
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createMockInteraction(overrides = {}) {
|
|
27
|
+
const mockUser = {
|
|
28
|
+
id: overrides.user?.id || '123456789',
|
|
29
|
+
username: overrides.user?.username || 'testuser',
|
|
30
|
+
discriminator: overrides.user?.discriminator || '0001',
|
|
31
|
+
tag: overrides.user?.tag || 'testuser#0001',
|
|
32
|
+
bot: overrides.user?.bot || false,
|
|
33
|
+
system: overrides.user?.system || false,
|
|
34
|
+
avatar: overrides.user?.avatar || null,
|
|
35
|
+
...overrides.user,
|
|
36
|
+
};
|
|
37
|
+
const mockGuild = overrides.guildId === null ? null : {
|
|
38
|
+
id: overrides.guild?.id || overrides.guildId || '987654321',
|
|
39
|
+
name: overrides.guild?.name || 'Test Guild',
|
|
40
|
+
...overrides.guild,
|
|
41
|
+
};
|
|
42
|
+
const mockChannel = {
|
|
43
|
+
id: overrides.channelId || '111222333',
|
|
44
|
+
type: 0, // TextChannel
|
|
45
|
+
isTextBased: () => true,
|
|
46
|
+
};
|
|
47
|
+
const mockMember = mockGuild ? {
|
|
48
|
+
id: mockUser.id,
|
|
49
|
+
user: mockUser,
|
|
50
|
+
guild: mockGuild,
|
|
51
|
+
roles: {
|
|
52
|
+
cache: new Map(),
|
|
53
|
+
highest: { position: 0 },
|
|
54
|
+
},
|
|
55
|
+
} : null;
|
|
56
|
+
// Create mock options resolver
|
|
57
|
+
const mockOptionsResolver = {
|
|
58
|
+
getString: vi.fn((name, required) => {
|
|
59
|
+
const value = overrides.options?.[name];
|
|
60
|
+
if (required && !value)
|
|
61
|
+
throw new Error(`Required option ${name} is missing`);
|
|
62
|
+
return value || null;
|
|
63
|
+
}),
|
|
64
|
+
getInteger: vi.fn((name, required) => {
|
|
65
|
+
const value = overrides.options?.[name];
|
|
66
|
+
if (required && value === undefined)
|
|
67
|
+
throw new Error(`Required option ${name} is missing`);
|
|
68
|
+
return value !== undefined ? value : null;
|
|
69
|
+
}),
|
|
70
|
+
getNumber: vi.fn((name, required) => {
|
|
71
|
+
const value = overrides.options?.[name];
|
|
72
|
+
if (required && value === undefined)
|
|
73
|
+
throw new Error(`Required option ${name} is missing`);
|
|
74
|
+
return value !== undefined ? value : null;
|
|
75
|
+
}),
|
|
76
|
+
getBoolean: vi.fn((name, required) => {
|
|
77
|
+
const value = overrides.options?.[name];
|
|
78
|
+
if (required && value === undefined)
|
|
79
|
+
throw new Error(`Required option ${name} is missing`);
|
|
80
|
+
return value !== undefined ? value : null;
|
|
81
|
+
}),
|
|
82
|
+
getUser: vi.fn((name) => overrides.options?.[name] || null),
|
|
83
|
+
getChannel: vi.fn((name) => overrides.options?.[name] || null),
|
|
84
|
+
getRole: vi.fn((name) => overrides.options?.[name] || null),
|
|
85
|
+
getMember: vi.fn((name) => overrides.options?.[name] || null),
|
|
86
|
+
getMentionable: vi.fn((name) => overrides.options?.[name] || null),
|
|
87
|
+
getAttachment: vi.fn((name) => overrides.options?.[name] || null),
|
|
88
|
+
getSubcommand: vi.fn((required) => {
|
|
89
|
+
const value = overrides.options?.['_subcommand'];
|
|
90
|
+
if (required && !value)
|
|
91
|
+
throw new Error('Subcommand is required');
|
|
92
|
+
return value || null;
|
|
93
|
+
}),
|
|
94
|
+
data: [],
|
|
95
|
+
};
|
|
96
|
+
const mockInteraction = {
|
|
97
|
+
id: 'mock-interaction-id',
|
|
98
|
+
type: 2, // ApplicationCommandType.ChatInput
|
|
99
|
+
user: mockUser,
|
|
100
|
+
member: mockMember,
|
|
101
|
+
guild: mockGuild,
|
|
102
|
+
guildId: mockGuild?.id || null,
|
|
103
|
+
channel: mockChannel,
|
|
104
|
+
channelId: mockChannel.id,
|
|
105
|
+
commandName: overrides.commandName || 'test',
|
|
106
|
+
options: mockOptionsResolver,
|
|
107
|
+
replied: overrides.replied || false,
|
|
108
|
+
deferred: overrides.deferred || false,
|
|
109
|
+
createdTimestamp: Date.now(),
|
|
110
|
+
// Mock methods
|
|
111
|
+
reply: vi.fn().mockResolvedValue(undefined),
|
|
112
|
+
editReply: vi.fn().mockResolvedValue(undefined),
|
|
113
|
+
followUp: vi.fn().mockResolvedValue(undefined),
|
|
114
|
+
deferReply: vi.fn().mockResolvedValue(undefined),
|
|
115
|
+
deleteReply: vi.fn().mockResolvedValue(undefined),
|
|
116
|
+
fetchReply: vi.fn().mockResolvedValue(undefined),
|
|
117
|
+
};
|
|
118
|
+
return mockInteraction;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a mock Discord client for testing.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const client = createMockClient();
|
|
126
|
+
* const command = new PingCommand();
|
|
127
|
+
* await command.execute(mockInteraction, client);
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function createMockClient() {
|
|
131
|
+
const mockClient = {
|
|
132
|
+
user: {
|
|
133
|
+
id: '999888777',
|
|
134
|
+
tag: 'TestBot#1234',
|
|
135
|
+
username: 'TestBot',
|
|
136
|
+
},
|
|
137
|
+
ws: {
|
|
138
|
+
ping: 42,
|
|
139
|
+
},
|
|
140
|
+
channels: {
|
|
141
|
+
fetch: vi.fn().mockResolvedValue({
|
|
142
|
+
id: '111222333',
|
|
143
|
+
isTextBased: () => true,
|
|
144
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
return mockClient;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a mock user for testing.
|
|
152
|
+
*
|
|
153
|
+
* @param overrides - Optional overrides for user properties
|
|
154
|
+
* @returns A mocked User object
|
|
155
|
+
*/
|
|
156
|
+
export function createMockUser(overrides = {}) {
|
|
157
|
+
return {
|
|
158
|
+
id: overrides.id || '123456789',
|
|
159
|
+
username: overrides.username || 'testuser',
|
|
160
|
+
discriminator: overrides.discriminator || '0001',
|
|
161
|
+
tag: overrides.tag || 'testuser#0001',
|
|
162
|
+
bot: overrides.bot || false,
|
|
163
|
+
system: overrides.system || false,
|
|
164
|
+
avatar: overrides.avatar || null,
|
|
165
|
+
...overrides,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a mock guild for testing.
|
|
170
|
+
*
|
|
171
|
+
* @param overrides - Optional overrides for guild properties
|
|
172
|
+
* @returns A mocked Guild object
|
|
173
|
+
*/
|
|
174
|
+
export function createMockGuild(overrides = {}) {
|
|
175
|
+
return {
|
|
176
|
+
id: overrides.id || '987654321',
|
|
177
|
+
name: overrides.name || 'Test Guild',
|
|
178
|
+
ownerId: '111111111',
|
|
179
|
+
...overrides,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Create a mock text channel for testing.
|
|
184
|
+
*
|
|
185
|
+
* @param overrides - Optional overrides for channel properties
|
|
186
|
+
* @returns A mocked TextChannel object
|
|
187
|
+
*/
|
|
188
|
+
export function createMockChannel(overrides = {}) {
|
|
189
|
+
return {
|
|
190
|
+
id: overrides.id || '111222333',
|
|
191
|
+
type: 0, // TextChannel
|
|
192
|
+
name: 'test-channel',
|
|
193
|
+
isTextBased: () => true,
|
|
194
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
195
|
+
...overrides,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Client } from 'discord.js';
|
|
2
|
+
import type { ILogger } from '../types/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reports errors to a designated Discord channel.
|
|
5
|
+
*
|
|
6
|
+
* Used for monitoring production bots - get notified when errors occur
|
|
7
|
+
* without having to constantly checking logs.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const errorReporter = new ErrorReporter(client, '1234567890', logger);
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* await riskyOperation();
|
|
15
|
+
* } catch (err) {
|
|
16
|
+
* await errorReporter.reportError(err, 'Processing user data');
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class ErrorReporter {
|
|
21
|
+
private client;
|
|
22
|
+
private channelId;
|
|
23
|
+
private logger?;
|
|
24
|
+
constructor(client: Client, channelId: string, logger?: ILogger | undefined);
|
|
25
|
+
/**
|
|
26
|
+
* Report an error to the configured Discord channel.
|
|
27
|
+
*
|
|
28
|
+
* @param error - The error that occurred
|
|
29
|
+
* @param context - Context about where/why the error happened
|
|
30
|
+
* @param additionalInfo - Optional additional information to include
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* await errorReporter.reportError(
|
|
35
|
+
* new Error('Comparison went wrong'),
|
|
36
|
+
* 'data not equal to moreData',
|
|
37
|
+
* { data: '123', moreData: '456' }
|
|
38
|
+
* );
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
reportError(error: Error, context: string, additionalInfo?: Record<string, any>): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Truncate text to a maximum length
|
|
44
|
+
*/
|
|
45
|
+
private truncate;
|
|
46
|
+
/**
|
|
47
|
+
* Update the channel ID for error reporting
|
|
48
|
+
*/
|
|
49
|
+
setChannel(channelId: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current channel ID
|
|
52
|
+
*/
|
|
53
|
+
getChannelId(): string;
|
|
54
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EmbedBuilder } from 'discord.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reports errors to a designated Discord channel.
|
|
4
|
+
*
|
|
5
|
+
* Used for monitoring production bots - get notified when errors occur
|
|
6
|
+
* without having to constantly checking logs.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const errorReporter = new ErrorReporter(client, '1234567890', logger);
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* await riskyOperation();
|
|
14
|
+
* } catch (err) {
|
|
15
|
+
* await errorReporter.reportError(err, 'Processing user data');
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class ErrorReporter {
|
|
20
|
+
constructor(client, channelId, logger) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.channelId = channelId;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Report an error to the configured Discord channel.
|
|
27
|
+
*
|
|
28
|
+
* @param error - The error that occurred
|
|
29
|
+
* @param context - Context about where/why the error happened
|
|
30
|
+
* @param additionalInfo - Optional additional information to include
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* await errorReporter.reportError(
|
|
35
|
+
* new Error('Comparison went wrong'),
|
|
36
|
+
* 'data not equal to moreData',
|
|
37
|
+
* { data: '123', moreData: '456' }
|
|
38
|
+
* );
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
async reportError(error, context, additionalInfo) {
|
|
42
|
+
try {
|
|
43
|
+
const channel = await this.client.channels.fetch(this.channelId);
|
|
44
|
+
if (!channel?.isTextBased()) {
|
|
45
|
+
this.logger?.warn(`Error reporter channel ${this.channelId} is not text-based`, 'error-reporter');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const embed = new EmbedBuilder()
|
|
49
|
+
.setTitle('Error Occurred')
|
|
50
|
+
.setColor(0xed4245)
|
|
51
|
+
.addFields({ name: 'Context', value: context }, {
|
|
52
|
+
name: 'Error',
|
|
53
|
+
value: `\`\`\`${this.truncate(error.message, 1000)}\`\`\``
|
|
54
|
+
})
|
|
55
|
+
.setTimestamp();
|
|
56
|
+
if (error.stack) {
|
|
57
|
+
embed.addFields({
|
|
58
|
+
name: 'Stack Trace',
|
|
59
|
+
value: `\`\`\`${this.truncate(error.stack, 1000)}\`\`\``,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (additionalInfo && Object.keys(additionalInfo).length > 0) {
|
|
63
|
+
embed.addFields({
|
|
64
|
+
name: 'Additional Info',
|
|
65
|
+
value: `\`\`\`json\n${JSON.stringify(additionalInfo, null, 2).slice(0, 1000)}\`\`\``,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
await channel.send({ embeds: [embed] });
|
|
69
|
+
this.logger?.info(`Error reported to channel ${this.channelId}`, 'error-reporter');
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
this.logger?.error(`Failed to report error to Discord: ${err.message}`, 'error-reporter');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Truncate text to a maximum length
|
|
77
|
+
*/
|
|
78
|
+
truncate(text, maxLength) {
|
|
79
|
+
if (text.length <= maxLength)
|
|
80
|
+
return text;
|
|
81
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Update the channel ID for error reporting
|
|
85
|
+
*/
|
|
86
|
+
setChannel(channelId) {
|
|
87
|
+
this.channelId = channelId;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the current channel ID
|
|
91
|
+
*/
|
|
92
|
+
getChannelId() {
|
|
93
|
+
return this.channelId;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/package.json
CHANGED