Skip to content

Commit 4b6e656

Browse files
committed
feature symfony#38522 [Notifier ] Add Discord notifier (mpiot, connorhu)
This PR was merged into the 5.x branch. Discussion ---------- [Notifier ] Add Discord notifier | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | no | Deprecations? | no | Tickets | | License | MIT | Doc PR | symfony/symfony-docs#13558 It finish the PR symfony#36475 Commits ------- b9b20d7 Added Discord bridge notifier embeds and test 14790d8 Add Discord bridge notifier
2 parents 88b158d + b9b20d7 commit 4b6e656

25 files changed

+1061
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
use Symfony\Component\Mime\Header\Headers;
100100
use Symfony\Component\Mime\MimeTypeGuesserInterface;
101101
use Symfony\Component\Mime\MimeTypes;
102+
use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory;
102103
use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory;
103104
use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory;
104105
use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory;
@@ -2222,6 +2223,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
22222223
SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi',
22232224
EsendexTransportFactory::class => 'notifier.transport_factory.esendex',
22242225
SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue',
2226+
DiscordTransportFactory::class => 'notifier.transport_factory.discord',
22252227
];
22262228

22272229
foreach ($classToServices as $class => $service) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory;
1415
use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory;
1516
use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory;
1617
use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory;
@@ -110,6 +111,10 @@
110111
->parent('notifier.transport_factory.abstract')
111112
->tag('texter.transport_factory')
112113

114+
->set('notifier.transport_factory.discord', DiscordTransportFactory::class)
115+
->parent('notifier.transport_factory.abstract')
116+
->tag('chatter.transport_factory')
117+
113118
->set('notifier.transport_factory.null', NullTransportFactory::class)
114119
->parent('notifier.transport_factory.abstract')
115120
->tag('chatter.transport_factory')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitignore export-ignore
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.2.0
5+
-----
6+
7+
* Added the bridge
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord;
13+
14+
use Symfony\Component\Notifier\Bridge\Discord\Embeds\DiscordEmbedInterface;
15+
use Symfony\Component\Notifier\Exception\LogicException;
16+
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
17+
18+
/**
19+
* @author Karoly Gossler <[email protected]>
20+
*
21+
* @experimental in 5.2
22+
*/
23+
final class DiscordOptions implements MessageOptionsInterface
24+
{
25+
private $options = [];
26+
27+
public function __construct(array $options = [])
28+
{
29+
$this->options = $options;
30+
}
31+
32+
public function toArray(): array
33+
{
34+
return $this->options;
35+
}
36+
37+
public function getRecipientId(): string
38+
{
39+
return '';
40+
}
41+
42+
public function username(string $username): self
43+
{
44+
$this->options['username'] = $username;
45+
46+
return $this;
47+
}
48+
49+
public function avatarUrl(string $avatarUrl): self
50+
{
51+
$this->options['avatar_url'] = $avatarUrl;
52+
53+
return $this;
54+
}
55+
56+
public function tts(bool $tts): self
57+
{
58+
$this->options['tts'] = $tts;
59+
60+
return $this;
61+
}
62+
63+
public function addEmbed(DiscordEmbedInterface $embed): self
64+
{
65+
if (!isset($this->options['embeds'])) {
66+
$this->options['embeds'] = [];
67+
}
68+
69+
if (\count($this->options['embeds']) >= 10) {
70+
throw new LogicException(sprintf('The "%s" only supports max 10 embeds.', __CLASS__));
71+
}
72+
73+
$this->options['embeds'][] = $embed->toArray();
74+
75+
return $this;
76+
}
77+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord;
13+
14+
use Symfony\Component\Notifier\Exception\LogicException;
15+
use Symfony\Component\Notifier\Exception\TransportException;
16+
use Symfony\Component\Notifier\Message\ChatMessage;
17+
use Symfony\Component\Notifier\Message\MessageInterface;
18+
use Symfony\Component\Notifier\Message\SentMessage;
19+
use Symfony\Component\Notifier\Transport\AbstractTransport;
20+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
21+
use Symfony\Contracts\HttpClient\HttpClientInterface;
22+
23+
/**
24+
* @author Mathieu Piot <[email protected]>
25+
*
26+
* @internal
27+
*
28+
* @experimental in 5.2
29+
*/
30+
final class DiscordTransport extends AbstractTransport
31+
{
32+
protected const HOST = 'discord.com';
33+
34+
private $token;
35+
private $webhookId;
36+
37+
public function __construct(string $token, string $webhookId = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
38+
{
39+
$this->token = $token;
40+
$this->webhookId = $webhookId;
41+
$this->client = $client;
42+
43+
parent::__construct($client, $dispatcher);
44+
}
45+
46+
public function __toString(): string
47+
{
48+
return sprintf('discord://%s?webhook_id=%s', $this->getEndpoint(), $this->webhookId);
49+
}
50+
51+
public function supports(MessageInterface $message): bool
52+
{
53+
return $message instanceof ChatMessage;
54+
}
55+
56+
/**
57+
* @see https://discord.com/developers/docs/resources/webhook
58+
*/
59+
protected function doSend(MessageInterface $message): SentMessage
60+
{
61+
if (!$message instanceof ChatMessage) {
62+
throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" (instance of "%s" given).', __CLASS__, ChatMessage::class, get_debug_type($message)));
63+
}
64+
65+
$messageOptions = $message->getOptions();
66+
$options = $messageOptions ? $messageOptions->toArray() : [];
67+
68+
$content = $message->getSubject();
69+
70+
if (\strlen($content) > 2000) {
71+
throw new LogicException(sprintf('The subject length of "%s" transport must be less than 2000 characters.', __CLASS__, ChatMessage::class, get_debug_type($message)));
72+
}
73+
74+
$endpoint = sprintf('https://%s/api/webhooks/%s/%s', $this->getEndpoint(), $this->webhookId, $this->token);
75+
$options['content'] = $content;
76+
$response = $this->client->request('POST', $endpoint, [
77+
'json' => array_filter($options),
78+
]);
79+
80+
if (204 !== $response->getStatusCode()) {
81+
$result = $response->toArray(false);
82+
83+
if (401 === $response->getStatusCode()) {
84+
$originalContent = $message->getSubject();
85+
$errorMessage = $result['message'];
86+
$errorCode = $result['code'];
87+
throw new TransportException(sprintf('Unable to post the Discord message: "%s" (%d: "%s").', $originalContent, $errorCode, $errorMessage), $response);
88+
}
89+
90+
if (400 === $response->getStatusCode()) {
91+
$originalContent = $message->getSubject();
92+
93+
$errorMessage = '';
94+
foreach ($result as $fieldName => $message) {
95+
$message = \is_array($message) ? implode(' ', $message) : $message;
96+
$errorMessage .= $fieldName.': '.$message.' ';
97+
}
98+
99+
$errorMessage = trim($errorMessage);
100+
throw new TransportException(sprintf('Unable to post the Discord message: "%s" (%s).', $originalContent, $errorMessage), $response);
101+
}
102+
103+
throw new TransportException(sprintf('Unable to post the Discord message: "%s" (Status Code: %d).', $message->getSubject(), $response->getStatusCode()), $response);
104+
}
105+
106+
return new SentMessage($message, (string) $this);
107+
}
108+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord;
13+
14+
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
15+
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
16+
use Symfony\Component\Notifier\Transport\Dsn;
17+
use Symfony\Component\Notifier\Transport\TransportInterface;
18+
19+
/**
20+
* @author Mathieu Piot <[email protected]>
21+
*
22+
* @experimental in 5.2
23+
*/
24+
final class DiscordTransportFactory extends AbstractTransportFactory
25+
{
26+
/**
27+
* @return DiscordTransport
28+
*/
29+
public function create(Dsn $dsn): TransportInterface
30+
{
31+
$scheme = $dsn->getScheme();
32+
$token = $this->getUser($dsn);
33+
$webhookId = $dsn->getOption('webhook_id');
34+
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
35+
$port = $dsn->getPort();
36+
37+
if ('discord' === $scheme) {
38+
return (new DiscordTransport($token, $webhookId, $this->client, $this->dispatcher))->setHost($host)->setPort($port);
39+
}
40+
41+
throw new UnsupportedSchemeException($dsn, 'discord', $this->getSupportedSchemes());
42+
}
43+
44+
protected function getSupportedSchemes(): array
45+
{
46+
return ['discord'];
47+
}
48+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord\Embeds;
13+
14+
/**
15+
* @author Karoly Gossler <[email protected]>
16+
*
17+
* @experimental in 5.2
18+
*/
19+
abstract class AbstractDiscordEmbed implements DiscordEmbedInterface
20+
{
21+
protected $options = [];
22+
23+
public function toArray(): array
24+
{
25+
return $this->options;
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord\Embeds;
13+
14+
/**
15+
* @author Karoly Gossler <[email protected]>
16+
*
17+
* @experimental in 5.2
18+
*/
19+
abstract class AbstractDiscordEmbedObject implements DiscordEmbedObjectInterface
20+
{
21+
protected $options = [];
22+
23+
public function toArray(): array
24+
{
25+
return $this->options;
26+
}
27+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Discord\Embeds;
13+
14+
/**
15+
* @author Karoly Gossler <[email protected]>
16+
*
17+
* @experimental in 5.2
18+
*/
19+
final class DiscordAuthorEmbedObject extends AbstractDiscordEmbedObject
20+
{
21+
public function name(string $name): self
22+
{
23+
$this->options['name'] = $name;
24+
25+
return $this;
26+
}
27+
28+
public function url(string $url): self
29+
{
30+
$this->options['url'] = $url;
31+
32+
return $this;
33+
}
34+
35+
public function iconUrl(string $iconUrl): self
36+
{
37+
$this->options['icon_url'] = $iconUrl;
38+
39+
return $this;
40+
}
41+
42+
public function proxyIconUrl(string $proxyIconUrl): self
43+
{
44+
$this->options['proxy_icon_url'] = $proxyIconUrl;
45+
46+
return $this;
47+
}
48+
}

0 commit comments

Comments
 (0)