commit 0b742d0e3995d62ea00318ef5f43f9b8c953691e Author: ArkHost <150489165+ARKHOST21@users.noreply.github.com> Date: Sat May 31 00:44:18 2025 +0200 Add files via upload 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 0000000..9a1d1d9 Binary files /dev/null and b/modules/notifications/Discord/logo.png differ diff --git a/modules/notifications/Discord/whmcs.json b/modules/notifications/Discord/whmcs.json new file mode 100644 index 0000000..abc825c --- /dev/null +++ b/modules/notifications/Discord/whmcs.json @@ -0,0 +1,28 @@ +{ + "schema": "1.0", + "type": "whmcs-notifications", + "name": "discord", + "license": "proprietary", + "category": "notifications", + "description": { + "name": "Discord", + "tagline": "Send notifications to your Discord Server.", + "long": "Get real-time notifications directly via Discord.\n\nCreate powerful event driven notification rules that help keep you and your team in the loop using a wide range of event triggers and conditional criteria.", + "features": [ + "Event driven notifications", + "Direct into a Discord Channel" + ] + }, + "logo": { + "filename": "logo.png" + }, + "support": { + "homepage": "https://docs.whmcs.com/Notification_Modules" + }, + "authors": [ + { + "name": "WHMCS Development Team", + "homepage": "https://www.whmcs.com/" + } + ] +}