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

fix: use webhooks for discord notification #84

Merged
merged 13 commits into from
Nov 6, 2024
18 changes: 9 additions & 9 deletions apps/agent/README.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could add some link referring to Discord docs on how to create your Webhook

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ cp .env.example .env

**Environment Variables**:

| Variable | Description | Required |
| ------------------------------- | ------------------------------------------------------------------------- | -------- |
| `PROTOCOL_PROVIDER_PRIVATE_KEY` | Private key for the Protocol Provider | Yes |
| `PROTOCOL_PROVIDER_L1_RPC_URLS` | Comma-separated URLs for Layer 1 RPC endpoints | Yes |
| `PROTOCOL_PROVIDER_L2_RPC_URLS` | Comma-separated URLs for Layer 2 RPC endpoints | Yes |
| `BLOCK_NUMBER_RPC_URLS_MAP` | JSON map of chain IDs to arrays of RPC URLs for Block Number calculations | Yes |
| `BLOCK_NUMBER_BLOCKMETA_TOKEN` | Bearer token for the Blockmeta service (see notes below on how to obtain) | Yes |
| `EBO_AGENT_CONFIG_FILE_PATH` | Path to the agent YAML configuration file | Yes |
| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications | Yes |
| Variable | Description | Required |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `PROTOCOL_PROVIDER_PRIVATE_KEY` | Private key for the Protocol Provider | Yes |
| `PROTOCOL_PROVIDER_L1_RPC_URLS` | Comma-separated URLs for Layer 1 RPC endpoints | Yes |
| `PROTOCOL_PROVIDER_L2_RPC_URLS` | Comma-separated URLs for Layer 2 RPC endpoints | Yes |
| `BLOCK_NUMBER_RPC_URLS_MAP` | JSON map of chain IDs to arrays of RPC URLs for Block Number calculations | Yes |
| `BLOCK_NUMBER_BLOCKMETA_TOKEN` | Bearer token for the Blockmeta service (see notes below on how to obtain) | Yes |
| `EBO_AGENT_CONFIG_FILE_PATH` | Path to the agent YAML configuration file | Yes |
| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications [Learn how to create a Discord webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | No |

**Notes:**

Expand Down
3 changes: 3 additions & 0 deletions apps/agent/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ processor:
requestModule: "0x1234567890123456789012345678901234567890" # Address of the Request module
responseModule: "0x1234567890123456789012345678901234567890" # Address of the Response module
escalationModule: "0x1234567890123456789012345678901234567890" # Address of the Escalation module

notifier:
discordDefaultAvatarUrl: "https://cryptologos.cc/logos/the-graph-grt-logo.png" # Default avatar URL for Discord notifications
2 changes: 1 addition & 1 deletion packages/automated-dispute/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";

const ConfigSchema = z.object({
DISCORD_WEBHOOK: z.string().min(1),
DISCORD_WEBHOOK: z.string().url().optional(),
});

export const config = ConfigSchema.parse(process.env);
8 changes: 3 additions & 5 deletions packages/automated-dispute/src/exceptions/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ export class ErrorHandler {
this.logger.error(`Error executing custom action: ${actionError}`);
} finally {
if (strategy.shouldNotify) {
const errorMessage = this.notificationService.createErrorMessage(
error,
context,
await this.notificationService.sendError(
"An error occurred in the custom contract",
context,
error,
);

await this.notificationService.send(errorMessage);
}

if (strategy.shouldReenqueue && context.reenqueueEvent) {
Expand Down
33 changes: 28 additions & 5 deletions packages/automated-dispute/src/interfaces/notificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,36 @@ export interface NotificationService {
*/
send(message: IMessage): Promise<void>;

/**
* Sends a notification message and throws an exception if sending fails.
*
* @param {IMessage} message - The message to send.
* @returns {Promise<void>} A promise that resolves when the message is sent.
* @throws {NotificationFailureException} If sending the message fails.
*/
sendOrThrow(message: IMessage): Promise<void>;

/**
* Creates an IMessage from an error.
*
* @param err - The error object of type unknown.
* @param context - Additional context for the error.
* @param defaultMessage - A default message describing the error context.
* @returns An IMessage object ready to be sent via the notifier.
* @param {string} defaultMessage - A default message describing the error context.
* @param {unknown} [context] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {IMessage} An IMessage object ready to be sent via the notifier.
*/
createErrorMessage(defaultMessage: string, context?: unknown, err?: unknown): IMessage;

/**
* Sends an error notification message.
*
* @param {string} defaultMessage - A default message describing the error context.
* @param {Record<string, unknown>} [context] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {Promise<void>} A promise that resolves when the message is sent.
*/
createErrorMessage(err: unknown, context: unknown, defaultMessage: string): IMessage;
sendError(
defaultMessage: string,
context?: Record<string, unknown>,
err?: unknown,
): Promise<void>;
}
91 changes: 62 additions & 29 deletions packages/automated-dispute/src/services/discordNotifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNativeError } from "util/types";
import type { APIEmbed, JSONEncodable } from "discord.js";
import { Logger, stringify } from "@ebo-agent/shared";
import { WebhookClient, WebhookMessageCreateOptions } from "discord.js";

import { NotificationFailureException } from "../exceptions/index.js";
Expand All @@ -14,18 +15,23 @@ export type WebhookMessage = WebhookMessageCreateOptions & {
*/
export class DiscordNotifier implements NotificationService {
private client: WebhookClient;
private defaultAvatarUrl?: string;
private logger: Logger;

/**
* Creates an instance of DiscordNotifier.
*
* @param {string} url - The Discord webhook URL.
* @param {string} [defaultAvatarUrl] - The default avatar URL to use if none is provided.
*/
constructor(url: string) {
constructor(url: string, defaultAvatarUrl?: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's inject logger. And added some minor tweaks, just being consistent with the rest of our codebase:

Suggested change
constructor(url: string, defaultAvatarUrl?: string) {
constructor(
url: string,
private readonly logger: ILogger,
private readonly defaultAvatarUrl?: string
) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.client = new WebhookClient({ url });
this.defaultAvatarUrl = defaultAvatarUrl;
this.logger = Logger.getInstance();
}

/**
* Sends a notification message to Discord.
* Sends a notification message to Discord. Errors are logged but not thrown.
*
* @param {IMessage} message - The message to send.
* @returns {Promise<void>} A promise that resolves when the message is sent.
Expand All @@ -34,7 +40,26 @@ export class DiscordNotifier implements NotificationService {
const webhookMessage = this.buildWebhookMessage(message);
try {
await this.client.send(webhookMessage);
console.error(this.client, this.client);
} catch (error) {
this.logger.error(
`Failed to send Discord notification: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}

/**
* Sends a notification message to Discord. Throws an exception if sending fails.
*
* @param {IMessage} message - The message to send.
* @returns {Promise<void>} A promise that resolves when the message is sent.
* @throws {NotificationFailureException} If sending the message fails.
*/
async sendOrThrow(message: IMessage): Promise<void> {
const webhookMessage = this.buildWebhookMessage(message);
try {
await this.client.send(webhookMessage);
} catch (error) {
throw new NotificationFailureException("An error occurred with the Discord client.");
}
Expand All @@ -50,7 +75,7 @@ export class DiscordNotifier implements NotificationService {
return {
username: message.username || "EBO Agent",
content: message.title,
avatarURL: message.avatarUrl || "https://cryptologos.cc/logos/the-graph-grt-logo.png",
avatarURL: message.avatarUrl || this.defaultAvatarUrl,
embeds:
message.subtitle || message.description
? [this.buildWebhookMessageEmbed(message)]
Expand All @@ -76,42 +101,50 @@ export class DiscordNotifier implements NotificationService {
/**
* Creates an IMessage from an error.
*
* @param err - The error object of type unknown.
* @param context - Additional context for the error.
* @param defaultMessage - A default message describing the error context.
* @returns An IMessage object ready to be sent via the notifier.
* @param {string} defaultMessage - A default message describing the error context.
* @param {unknown} [context={}] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {IMessage} An IMessage object ready to be sent via the notifier.
*/
public createErrorMessage(
err: unknown,
context: Record<string, unknown>,
defaultMessage: string,
context: unknown = {},
err?: unknown,
): IMessage {
const contextObject = typeof context === "object" && context !== null ? context : {};
if (isNativeError(err)) {
return {
title: `**Error:** ${err.name} - ${err.message}`,
description: `**Context:**\n\`\`\`json\n${JSON.stringify(
{
message: defaultMessage,
stack: err.stack,
...context,
},
null,
2,
)}\n\`\`\``,
description: `**Context:**\n\`\`\`json\n${stringify({
...contextObject,
stack: err.stack,
})}\n\`\`\``,
};
} else {
return {
title: `**Error:** Unknown error`,
description: `**Context:**\n\`\`\`json\n${JSON.stringify(
{
message: defaultMessage,
error: String(err),
...context,
},
null,
2,
)}\n\`\`\``,
title: `**Error:** ${defaultMessage}`,
description: `**Context:**\n\`\`\`json\n${stringify({
...contextObject,
error: err ? stringify(err) : undefined,
})}\n\`\`\``,
};
}
}

/**
* Sends an error notification message to Discord.
*
* @param {string} defaultMessage - A default message describing the error context.
* @param {Record<string, unknown>} [context={}] - Additional context for the error.
* @param {unknown} [err] - The error object.
* @returns {Promise<void>} A promise that resolves when the message is sent.
*/
public async sendError(
defaultMessage: string,
context: Record<string, unknown> = {},
err?: unknown,
): Promise<void> {
const errorMessage = this.createErrorMessage(defaultMessage, context, err);
await this.send(errorMessage);
}
}
Loading