Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
3 changes: 1 addition & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 31 additions & 5 deletions src/Utopia/Messaging/Adapter/Chat/Discord.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
],
Expand Down
67 changes: 61 additions & 6 deletions tests/Messaging/Adapter/Chat/DiscordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';

Expand All @@ -29,4 +26,62 @@ public function testSendMessage(): void

$this->assertResponse($result);
}

/**
* @return array<array<string>>
*/
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');
}
}
Loading