From 0b742d0e3995d62ea00318ef5f43f9b8c953691e Mon Sep 17 00:00:00 2001 From: ArkHost <150489165+ARKHOST21@users.noreply.github.com> Date: Sat, 31 May 2025 00:44:18 +0200 Subject: [PATCH] Add files via upload --- modules/notifications/Discord/Discord.php | 274 ++++++++++++++++++ modules/notifications/Discord/lib/Embed.php | 96 ++++++ modules/notifications/Discord/lib/Field.php | 39 +++ modules/notifications/Discord/lib/Message.php | 57 ++++ modules/notifications/Discord/logo.png | Bin 0 -> 17046 bytes modules/notifications/Discord/whmcs.json | 28 ++ 6 files changed, 494 insertions(+) create mode 100644 modules/notifications/Discord/Discord.php create mode 100644 modules/notifications/Discord/lib/Embed.php create mode 100644 modules/notifications/Discord/lib/Field.php create mode 100644 modules/notifications/Discord/lib/Message.php create mode 100644 modules/notifications/Discord/logo.png create mode 100644 modules/notifications/Discord/whmcs.json diff --git a/modules/notifications/Discord/Discord.php b/modules/notifications/Discord/Discord.php new file mode 100644 index 0000000..a404062 --- /dev/null +++ b/modules/notifications/Discord/Discord.php @@ -0,0 +1,274 @@ +setDisplayName('Discord') + ->setLogoFileName('logo.png'); + } + + /** + * Settings required for module configuration + * + * @return array + */ + public function settings() + { + return [ + 'webhookURL' => [ + 'FriendlyName' => 'Discord Webhook URL', + 'Type' => 'text', + 'Description' => 'The Discord Webhook URL that notifications should be sent to.', + ], + 'companyName' => [ + 'FriendlyName' => 'Company Name (Optional)', + 'Type' => 'text', + 'Description' => 'This will be the name of the user that sends the message in the Discord channel.', + 'Placeholder' => Setting::getValue('CompanyName'), + ], + 'discordColor' => [ + 'FriendlyName' => 'Message Colour (Optional)', + 'Type' => 'text', + 'Description' => 'This is the side line colour for the message. The color code format within this script is standard hex. Exclude the beginning # character if one is present.', + 'Placeholder' => '73CB0B', + ], + 'discordGroupID' => [ + 'FriendlyName' => 'Notification Role ID (Optional)', + 'Type' => 'text', + 'Description' => 'If you\'d like to have a specific group pinged on each message, please place the ID here. An example of a group ID is: 343029528563548162 (Only include the numerical ID and no other formatting)', + ], + 'discordWebHookAvatar' => [ + 'FriendlyName' => 'Webhook Avatar Image (Optional)', + 'Type' => 'text', + 'Description' => 'Your desired Webhook Avatar. Please make sure you enter a direct link to the image (E.G. https://example.com/avatar.png )', + ], + ]; + } + + /** + * Validate settings for notification module + * + * This method will be invoked prior to saving any settings via the UI. + * + * @param array $settings Settings present in configuration modal + * + * @return boolean + * @throws Exception webhookURL value is missing + */ + public function testConnection($settings): bool + { + $webhookURL = $settings['webhookURL'] ?? ''; + + if (empty($webhookURL)) { + throw new Exception("You must provide a webhook URL"); + } + + // Validate webhook URL format + if (!filter_var($webhookURL, FILTER_VALIDATE_URL) || + !preg_match('/^https:\/\/discord(app)?\.com\/api\/webhooks\//', $webhookURL)) { + throw new Exception("Invalid Discord webhook URL format"); + } + + return true; + } + + /** + * The individual customisable settings for a notification. + * + * @return array + */ + public function notificationSettings() + { + return [ + 'message' => [ + 'FriendlyName' => 'Customise Message (Optional)', + 'Type' => 'text', + 'Description' => 'Allows you to customise the primary display message shown in the notification.', + ], + 'webhookURL' => [ + 'FriendlyName' => 'Discord Webhook URL (Optional)', + 'Type' => 'text', + 'Description' => 'Override the Discord Webhook URL for this notification to send to a different channel.', + ], + 'discordColor' => [ + 'FriendlyName' => 'Message Colour (Optional)', + 'Type' => 'text', + 'Description' => 'Override the side line colour for the message. The color code format within this script is standard hex. Exclude the beginning # character if one is present.', + 'Placeholder' => '73CB0B', + ], + 'discordGroupID' => [ + 'FriendlyName' => 'Notification Role ID (Optional)', + 'Type' => 'text', + 'Description' => 'If you\'d like to have a specific group pinged for this message type, please place the ID here (this is in addition to the module setting). An example of a group ID is: 343029528563548162 (Only include the numerical ID and no other formatting)', + ], + ]; + } + + /** + * The option values available for a 'dynamic' Type notification setting + */ + public function getDynamicField($fieldName, $settings) + { + return []; + } + + /** + * Deliver notification + * + * This method is invoked when rule criteria are met. + * + * @param NotificationInterface $notification A notification to send + * @param array $moduleSettings Configured settings of the notification module + * @param array $notificationSettings Configured notification settings set by the triggered rule + * + */ + public function sendNotification(NotificationInterface $notification, $moduleSettings, $notificationSettings) + { + // Determine message body + $messageBody = $notification->getMessage(); + if (!empty($notificationSettings["message"])) { + $messageBody = $notificationSettings["message"]; + } + + // Determine webhook URL + if (!empty($notificationSettings["webhookURL"])) { + $webhookURL = $notificationSettings["webhookURL"]; + } else { + $webhookURL = $moduleSettings["webhookURL"]; + } + + // Determine color + if (!empty($notificationSettings["discordColor"])) { + $discordColor = hexdec($notificationSettings["discordColor"]); + } elseif (!empty($moduleSettings["discordColor"])) { + $discordColor = hexdec($moduleSettings["discordColor"]); + } else { + $discordColor = 7588619; // Default blue color + } + + // Create embed + $embed = (new Embed()) + ->title(\WHMCS\Input\Sanitize::decode($notification->getTitle())) + ->url($notification->getUrl()) + ->description($messageBody) + ->timestamp(date(\DateTime::ISO8601)) + ->color($discordColor) + ->footer("WHMCS Notification"); + + // Add notification attributes as fields + foreach ($notification->getAttributes() as $attribute) { + $value = $attribute->getValue(); + if ($attribute->getUrl()) { + $value = "[" . $value . "](" . $attribute->getUrl() . ")"; + } + $embed->addField((new Field()) + ->name($attribute->getLabel()) + ->value($value)); + } + + // Prepare role mentions + $discordGroupID = ''; + $discordGroupIDNotificationSpecific = ''; + + if (!empty($moduleSettings["discordGroupID"])) { + $discordGroupID = "<@&" . $moduleSettings["discordGroupID"] . ">"; + } + if (!empty($notificationSettings["discordGroupID"])) { + $discordGroupIDNotificationSpecific = "<@&" . $notificationSettings["discordGroupID"] . ">"; + } + + // Prepare other settings + $companyName = $moduleSettings["companyName"] ?? ''; + $discordWebHookAvatar = $moduleSettings["discordWebHookAvatar"] ?? ''; + + // Create message + $message = (new Message()) + ->content($discordGroupID . $discordGroupIDNotificationSpecific) + ->username($companyName) + ->avatarUrl($discordWebHookAvatar) + ->embed($embed); + + // Send the notification + $this->call($moduleSettings, $webhookURL, $message->toArray()); + } + + /** + * Send HTTP request to Discord webhook + * + * @param array $settings Module settings + * @param string $url Webhook URL + * @param array $postdata Data to send + * @param bool $throwOnError Whether to throw exception on error + * @return mixed + * @throws Exception + */ + protected function call(array $settings, $url, array $postdata = array(), $throwOnError = true) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json' + )); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postdata)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, 'WHMCS Discord Notification Module'); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + // Handle cURL errors + if ($response === false || !empty($curlError)) { + logModuleCall('discord', 'Notification Sending Failed - cURL Error', $postdata, $curlError, null); + if ($throwOnError) { + throw new Exception("cURL Error: " . $curlError); + } + return false; + } + + $decoded = json_decode($response, true); + + // Discord webhook returns 204 on success + if ($httpCode == 204) { + logModuleCall('discord', 'Notification Successfully Sent', $postdata, $response, $decoded); + } else { + logModuleCall('discord', 'Notification Sending Failed', $postdata, $response, $decoded); + if ($throwOnError) { + $errorMessage = "HTTP Error " . $httpCode; + if (isset($decoded['message'])) { + $errorMessage .= ": " . $decoded['message']; + } + throw new Exception($errorMessage); + } + } + + return $decoded; + } +} diff --git a/modules/notifications/Discord/lib/Embed.php b/modules/notifications/Discord/lib/Embed.php new file mode 100644 index 0000000..0eb7e1d --- /dev/null +++ b/modules/notifications/Discord/lib/Embed.php @@ -0,0 +1,96 @@ +title = trim($title); + return $this; + } + + public function url($url) + { + $this->url = trim($url); + return $this; + } + + public function description($description) + { + $this->description = trim($description); + return $this; + } + + public function timestamp($timestamp) + { + $this->timestamp = trim($timestamp); + return $this; + } + + public function color($color) + { + $this->color = $color; + return $this; + } + + public function footer($footertext, $icon = "") + { + $this->footer["text"] = trim($footertext); + if (!empty($icon)) { + $this->footer["icon_url"] = trim($icon); + } + return $this; + } + + public function addField(Field $field) + { + if ($this->fields === null) { + $this->fields = array(); + } + $this->fields[] = $field; + return $this; + } + + public function toArray() + { + $embed = []; + + if (!empty($this->title)) { + $embed["title"] = $this->title; + } + if (!empty($this->url)) { + $embed["url"] = $this->url; + } + if (!empty($this->description)) { + $embed["description"] = $this->description; + } + if (!empty($this->timestamp)) { + $embed["timestamp"] = $this->timestamp; + } + if (!empty($this->color)) { + $embed["color"] = $this->color; + } + if (!empty($this->footer)) { + $embed["footer"] = $this->footer; + } + + if (!empty($this->fields)) { + $embed["fields"] = array_map(function (Field $field) { + return $field->toArray(); + }, $this->fields); + } + + return $embed; + } +} + +?> \ No newline at end of file diff --git a/modules/notifications/Discord/lib/Field.php b/modules/notifications/Discord/lib/Field.php new file mode 100644 index 0000000..01286ee --- /dev/null +++ b/modules/notifications/Discord/lib/Field.php @@ -0,0 +1,39 @@ +name = trim($name); + return $this; + } + + public function value($value) + { + $this->value = trim($value); + return $this; + } + + public function inline($inline) + { + $this->inline = (bool) $inline; + return $this; + } + + public function toArray() + { + return [ + 'name' => $this->name, + 'value' => $this->value, + 'inline' => $this->inline + ]; + } +} + +?> \ No newline at end of file diff --git a/modules/notifications/Discord/lib/Message.php b/modules/notifications/Discord/lib/Message.php new file mode 100644 index 0000000..01fea44 --- /dev/null +++ b/modules/notifications/Discord/lib/Message.php @@ -0,0 +1,57 @@ +content = trim($content); + return $this; + } + + public function username($username) + { + $this->username = trim($username); + return $this; + } + + public function avatarUrl($avatarUrl) + { + $this->avatarUrl = trim($avatarUrl); + return $this; + } + + public function embed($embed) + { + $this->embeds[] = $embed; + return $this; + } + + public function toArray() + { + $message = array(); + + if (!empty($this->content)) { + $message["content"] = $this->content; + } + if (!empty($this->avatarUrl)) { + $message["avatar_url"] = $this->avatarUrl; + } + if (!empty($this->username)) { + $message["username"] = $this->username; + } + if (!empty($this->embeds)) { + $message["embeds"] = array_map(function($embed) { + return $embed instanceof Embed ? $embed->toArray() : $embed; + }, $this->embeds); + } + + return $message; + } +} \ No newline at end of file diff --git a/modules/notifications/Discord/logo.png b/modules/notifications/Discord/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1d1d999d7259fb1212dc22689902ed234a5e4a GIT binary patch literal 17046 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