diff --git a/.env.dev b/.env.dev index 50d4c55a..db4ed537 100644 --- a/.env.dev +++ b/.env.dev @@ -27,8 +27,7 @@ VONAGE_API_KEY= VONAGE_API_SECRET= VONAGE_TO= VONAGE_FROM= -DISCORD_WEBHOOK_ID= -DISCORD_WEBHOOK_TOKEN= +DISCORD_WEBHOOK_URL= FAST2SMS_API_KEY= FAST2SMS_SENDER_ID= FAST2SMS_MESSAGE_ID= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e166869..b7ec5b28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,8 +43,7 @@ jobs: VONAGE_API_SECRET: ${{ secrets.VONAGE_API_SECRET }} VONAGE_TO: ${{ secrets.VONAGE_TO }} VONAGE_FROM: ${{ secrets.VONAGE_FROM }} - DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }} - DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} FAST2SMS_API_KEY: ${{ secrets.FAST2SMS_API_KEY }} FAST2SMS_SENDER_ID: ${{ secrets.FAST2SMS_SENDER_ID }} FAST2SMS_MESSAGE_ID: ${{ secrets.FAST2SMS_MESSAGE_ID }} diff --git a/docker-compose.yml b/docker-compose.yml index fc7b7cc4..d729e2f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,8 +36,7 @@ services: - VONAGE_API_SECRET - VONAGE_TO - VONAGE_FROM - - DISCORD_WEBHOOK_ID - - DISCORD_WEBHOOK_TOKEN + - DISCORD_WEBHOOK_URL - FAST2SMS_API_KEY - FAST2SMS_SENDER_ID - FAST2SMS_MESSAGE_ID diff --git a/src/Utopia/Messaging/Adapter/Chat/Discord.php b/src/Utopia/Messaging/Adapter/Chat/Discord.php index 19f01f6d..73bf5a82 100644 --- a/src/Utopia/Messaging/Adapter/Chat/Discord.php +++ b/src/Utopia/Messaging/Adapter/Chat/Discord.php @@ -5,21 +5,47 @@ use Utopia\Messaging\Adapter; use Utopia\Messaging\Messages\Discord as DiscordMessage; use Utopia\Messaging\Response; +use InvalidArgumentException; class Discord extends Adapter { protected const NAME = 'Discord'; protected const TYPE = 'chat'; protected const MESSAGE_TYPE = DiscordMessage::class; + protected string $webhookId = ''; /** - * @param string $webhookId Your Discord webhook ID. - * @param string $webhookToken Your Discord webhook token. + * @param string $webhookURL Your Discord webhook URL. + * @throws InvalidArgumentException When webhook URL is invalid */ public function __construct( - private string $webhookId, - private string $webhookToken, + private string $webhookURL ) { + // Validate URL format + if (!filter_var($webhookURL, FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException('Invalid Discord webhook URL format.'); + } + + // Validate URL uses https scheme + $urlParts = parse_url($webhookURL); + if (!isset($urlParts['scheme']) || $urlParts['scheme'] !== 'https') { + throw new InvalidArgumentException('Discord webhook URL must use HTTPS scheme.'); + } + + // Validate host is discord.com + if (!isset($urlParts['host']) || $urlParts['host'] !== 'discord.com') { + throw new InvalidArgumentException('Discord webhook URL must use discord.com as host.'); + } + + // Extract and validate webhook ID + $parts = explode('/webhooks/', $urlParts['path']); + if (count($parts) >= 2) { + $webhookParts = explode('/', $parts[1]); + $this->webhookId = $webhookParts[0]; + } + if (empty($this->webhookId)) { + throw new InvalidArgumentException('Discord webhook ID cannot be empty.'); + } } public function getName(): string @@ -69,7 +95,7 @@ protected function process(DiscordMessage $message): array $response = new Response($this->getType()); $result = $this->request( method: 'POST', - url: "https://discord.com/api/webhooks/{$this->webhookId}/{$this->webhookToken}{$queryString}", + url: "{$this->webhookURL}{$queryString}", headers: [ 'Content-Type: application/json', ], diff --git a/tests/Messaging/Adapter/Chat/DiscordTest.php b/tests/Messaging/Adapter/Chat/DiscordTest.php index 7c7d2789..3147c34a 100644 --- a/tests/Messaging/Adapter/Chat/DiscordTest.php +++ b/tests/Messaging/Adapter/Chat/DiscordTest.php @@ -2,6 +2,7 @@ namespace Utopia\Tests\Adapter\Chat; +use InvalidArgumentException; use Utopia\Messaging\Adapter\Chat\Discord; use Utopia\Messaging\Messages\Discord as DiscordMessage; use Utopia\Tests\Adapter\Base; @@ -10,13 +11,9 @@ class DiscordTest extends Base { public function testSendMessage(): void { - $id = \getenv('DISCORD_WEBHOOK_ID'); - $token = \getenv('DISCORD_WEBHOOK_TOKEN'); + $url = \getenv('DISCORD_WEBHOOK_URL'); - $sender = new Discord( - webhookId: $id, - webhookToken: $token - ); + $sender = new Discord($url); $content = 'Test Content'; @@ -29,4 +26,62 @@ public function testSendMessage(): void $this->assertResponse($result); } + + /** + * @return array> + */ + public static function invalidURLProvider(): array + { + return [ + 'invalid URL format' => ['not-a-url'], + 'invalid scheme (http)' => ['http://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz'], + 'invalid host' => ['https://example.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz'], + 'missing path' => ['https://discord.com'], + 'no webhooks segment' => ['https://discord.com/api/invalid/123456789012345678/token'], + 'missing webhook ID' => ['https://discord.com/api/webhooks//token'], + ]; + } + + /** + * @dataProvider invalidURLProvider + */ + public function testInvalidURLs(string $invalidURL): void + { + $this->expectException(InvalidArgumentException::class); + new Discord($invalidURL); + } + + public function testValidURLVariations(): void + { + // Valid URL format variations + $validURLs = [ + 'with api path' => 'https://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz', + 'without api path' => 'https://discord.com/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz', + 'with trailing slash' => 'https://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz/', + ]; + + foreach ($validURLs as $label => $url) { + try { + $discord = new Discord($url); + // If we get here, the URL was accepted + $this->assertTrue(true, "Valid URL variant '{$label}' was accepted as expected"); + } catch (InvalidArgumentException $e) { + $this->fail("Valid URL variant '{$label}' was rejected: " . $e->getMessage()); + } + } + } + + public function testWebhookIDExtraction(): void + { + // Create a reflection of Discord to access protected properties + $webhookId = '123456789012345678'; + $url = "https://discord.com/api/webhooks/{$webhookId}/abcdefghijklmnopqrstuvwxyz"; + + $discord = new Discord($url); + $reflector = new \ReflectionClass($discord); + $property = $reflector->getProperty('webhookId'); + $property->setAccessible(true); + + $this->assertEquals($webhookId, $property->getValue($discord), 'Webhook ID was not correctly extracted'); + } }