From 8667644da577b4bf3d344c4994c8bac9beb57e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Fri, 16 Aug 2024 23:23:51 +0200 Subject: [PATCH] feat: start webhook support --- composer.json | 8 + composer.lock | 547 +++++++++++++++++- config/packages/notifier.yaml | 8 + config/services.yaml | 2 + migrations/Version20240816185909.php | 32 + package.json | 4 + src/Config/TriggerAction.php | 1 + src/Config/WebhookScheme.php | 38 ++ src/Controller/WatchListController.php | 32 + src/Entity/WatchList.php | 18 + .../ProcessDomainTriggerHandler.php | 109 ++-- .../ProcessWatchListTriggerHandler.php | 33 +- src/Notifier/DomainOrderErrorNotification.php | 44 ++ src/Notifier/DomainOrderNotification.php | 49 ++ .../DomainUpdateErrorNotification.php | 44 ++ src/Notifier/DomainUpdateNotification.php | 46 ++ src/Notifier/TestChatNotification.php | 19 + symfony.lock | 72 +++ yarn.lock | 38 +- 19 files changed, 1050 insertions(+), 94 deletions(-) create mode 100644 migrations/Version20240816185909.php create mode 100644 src/Config/WebhookScheme.php create mode 100644 src/Notifier/DomainOrderErrorNotification.php create mode 100644 src/Notifier/DomainOrderNotification.php create mode 100644 src/Notifier/DomainUpdateErrorNotification.php create mode 100644 src/Notifier/DomainUpdateNotification.php create mode 100644 src/Notifier/TestChatNotification.php diff --git a/composer.json b/composer.json index 930771c..e3b4491 100644 --- a/composer.json +++ b/composer.json @@ -41,16 +41,20 @@ "symfony/asset": "7.1.*", "symfony/asset-mapper": "7.1.*", "symfony/console": "7.1.*", + "symfony/discord-notifier": "7.1.*", "symfony/doctrine-messenger": "7.1.*", "symfony/dotenv": "7.1.*", "symfony/expression-language": "7.1.*", "symfony/flex": "^2", "symfony/form": "7.1.*", "symfony/framework-bundle": "7.1.*", + "symfony/google-chat-notifier": "7.1.*", "symfony/http-client": "7.1.*", "symfony/intl": "7.1.*", "symfony/lock": "7.1.*", "symfony/mailer": "7.1.*", + "symfony/mattermost-notifier": "7.1.*", + "symfony/microsoft-teams-notifier": "7.1.*", "symfony/mime": "7.1.*", "symfony/monolog-bundle": "^3.0", "symfony/notifier": "7.1.*", @@ -58,12 +62,15 @@ "symfony/property-access": "7.1.*", "symfony/property-info": "7.1.*", "symfony/rate-limiter": "7.1.*", + "symfony/rocket-chat-notifier": "7.1.*", "symfony/runtime": "7.1.*", "symfony/scheduler": "7.1.*", "symfony/security-bundle": "7.1.*", "symfony/serializer": "7.1.*", + "symfony/slack-notifier": "7.1.*", "symfony/stimulus-bundle": "^2.18", "symfony/string": "7.1.*", + "symfony/telegram-notifier": "7.1.*", "symfony/translation": "7.1.*", "symfony/twig-bundle": "7.1.*", "symfony/uid": "7.1.*", @@ -72,6 +79,7 @@ "symfony/web-link": "7.1.*", "symfony/webpack-encore-bundle": "^2.1", "symfony/yaml": "7.1.*", + "symfony/zulip-notifier": "7.1.*", "symfonycasts/verify-email-bundle": "*", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" diff --git a/composer.lock b/composer.lock index 46447c6..657297b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bab584811b8175e404608e6738549f52", + "content-hash": "f64fa606b60efd34dccdee3abcdad8b2", "packages": [ { "name": "api-platform/core", @@ -4325,6 +4325,74 @@ ], "time": "2024-04-18T09:32:20+00:00" }, + { + "name": "symfony/discord-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/discord-notifier.git", + "reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/discord-notifier/zipball/f3d8368ca5ff80c1268a851f925e1f0c07997a8e", + "reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Discord\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Discord Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "discord", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/discord-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/doctrine-bridge", "version": "v7.1.2", @@ -5313,6 +5381,75 @@ ], "time": "2024-06-28T08:00:31+00:00" }, + { + "name": "symfony/google-chat-notifier", + "version": "v7.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/google-chat-notifier.git", + "reference": "1e92b6c89b2182ba26554861dc261c530c98000f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/google-chat-notifier/zipball/1e92b6c89b2182ba26554861dc261c530c98000f", + "reference": "1e92b6c89b2182ba26554861dc261c530c98000f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\GoogleChat\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Google Chat Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "Google-Chat", + "chat", + "google", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/google-chat-notifier/tree/v7.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-25T19:55:06+00:00" + }, { "name": "symfony/http-client", "version": "v7.1.2", @@ -5920,6 +6057,73 @@ ], "time": "2024-06-28T08:00:31+00:00" }, + { + "name": "symfony/mattermost-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mattermost-notifier.git", + "reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mattermost-notifier/zipball/c5ff6774682ab3504a77bbe01f8c1275b4bf48e9", + "reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Mattermost\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuele Panzeri", + "email": "thepanz@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mattermost Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "Mattermost", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/mattermost-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/messenger", "version": "v7.1.2", @@ -6006,6 +6210,78 @@ ], "time": "2024-06-28T08:00:31+00:00" }, + { + "name": "symfony/microsoft-teams-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/microsoft-teams-notifier.git", + "reference": "546b0368928b5849d08728b7daf5d22a07a052b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/microsoft-teams-notifier/zipball/546b0368928b5849d08728b7daf5d22a07a052b3", + "reference": "546b0368928b5849d08728b7daf5d22a07a052b3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\MicrosoftTeams\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Edouard Lescot", + "email": "edouard.lescot@gmail.com" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Microsoft Teams Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "chat", + "microsoft-teams", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/microsoft-teams-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/mime", "version": "v7.1.2", @@ -7319,6 +7595,73 @@ ], "time": "2024-05-31T14:57:53+00:00" }, + { + "name": "symfony/rocket-chat-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/rocket-chat-notifier.git", + "reference": "b17bff59107b51753e3e347c5194dc304019daf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rocket-chat-notifier/zipball/b17bff59107b51753e3e347c5194dc304019daf7", + "reference": "b17bff59107b51753e3e347c5194dc304019daf7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\RocketChat\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen Spee", + "homepage": "https://github.com/Jeroeny" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony RocketChat Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "rocketchat" + ], + "support": { + "source": "https://github.com/symfony/rocket-chat-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/routing", "version": "v7.1.1", @@ -8087,6 +8430,73 @@ ], "time": "2024-04-18T09:32:20+00:00" }, + { + "name": "symfony/slack-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/slack-notifier.git", + "reference": "452a17e3935192e6a9a5b16f0443911d67e456af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/slack-notifier/zipball/452a17e3935192e6a9a5b16f0443911d67e456af", + "reference": "452a17e3935192e6a9a5b16f0443911d67e456af", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Slack\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Slack Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "slack" + ], + "support": { + "source": "https://github.com/symfony/slack-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/stimulus-bundle", "version": "v2.18.1", @@ -8305,6 +8715,74 @@ ], "time": "2024-06-28T09:27:18+00:00" }, + { + "name": "symfony/telegram-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/telegram-notifier.git", + "reference": "521e77470d5b07306c1001c2d1d1bc88474a8035" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/telegram-notifier/zipball/521e77470d5b07306c1001c2d1d1bc88474a8035", + "reference": "521e77470d5b07306c1001c2d1d1bc88474a8035", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Telegram\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Telegram Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "telegram" + ], + "support": { + "source": "https://github.com/symfony/telegram-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/translation", "version": "v7.1.1", @@ -9404,6 +9882,73 @@ ], "time": "2024-05-31T14:57:53+00:00" }, + { + "name": "symfony/zulip-notifier", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/zulip-notifier.git", + "reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/zulip-notifier/zipball/48b3e1ac791d8eac7ee268108865b36de3be5ed2", + "reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Zulip\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mohammad Emran Hasan", + "email": "phpfour@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Zulip Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "zulip" + ], + "support": { + "source": "https://github.com/symfony/zulip-notifier/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfonycasts/verify-email-bundle", "version": "v1.17.0", diff --git a/config/packages/notifier.yaml b/config/packages/notifier.yaml index d02f986..ee4dc19 100644 --- a/config/packages/notifier.yaml +++ b/config/packages/notifier.yaml @@ -1,6 +1,14 @@ framework: notifier: chatter_transports: + zulip: '%env(ZULIP_DSN)%' + telegram: '%env(TELEGRAM_DSN)%' + slack: '%env(SLACK_DSN)%' + rocketchat: '%env(ROCKETCHAT_DSN)%' + microsoftteams: '%env(MICROSOFT_TEAMS_DSN)%' + mattermost: '%env(MATTERMOST_DSN)%' + googlechat: '%env(GOOGLE_CHAT_DSN)%' + discord: '%env(DISCORD_DSN)%' texter_transports: channel_policy: # use chat/slack, chat/telegram, sms/twilio or sms/nexmo diff --git a/config/services.yaml b/config/services.yaml index bc8ea08..b69bfc5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,6 +14,8 @@ parameters: limit_max_watchlist_domains: '%env(int:LIMIT_MAX_WATCHLIST_DOMAINS)%' services: + Symfony\Component\Messenger\Transport\TransportFactoryInterface: '@messenger.transport_factory' + # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. diff --git a/migrations/Version20240816185909.php b/migrations/Version20240816185909.php new file mode 100644 index 0000000..aa873a0 --- /dev/null +++ b/migrations/Version20240816185909.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE watch_list ADD webhook_dsn TEXT DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN watch_list.webhook_dsn IS \'(DC2Type:simple_array)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE watch_list DROP webhook_dsn'); + } +} diff --git a/package.json b/package.json index acae3d1..7af1b8c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "@babel/preset-env": "^7.16.0", "@babel/preset-react": "^7.24.7", "@fontsource/noto-color-emoji": "^5.0.27", + "@hotwired/stimulus": "^3.0.0", + "@hotwired/turbo": "^7.1.1 || ^8.0", + "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets", "@symfony/webpack-encore": "^4.0.0", "@types/axios": "^0.14.0", "@types/dagre": "^0.7.52", diff --git a/src/Config/TriggerAction.php b/src/Config/TriggerAction.php index a3271c3..af582d5 100644 --- a/src/Config/TriggerAction.php +++ b/src/Config/TriggerAction.php @@ -5,4 +5,5 @@ namespace App\Config; enum TriggerAction: string { case SendEmail = 'email'; + case SendChat = 'chat'; } diff --git a/src/Config/WebhookScheme.php b/src/Config/WebhookScheme.php new file mode 100644 index 0000000..16c54a9 --- /dev/null +++ b/src/Config/WebhookScheme.php @@ -0,0 +1,38 @@ + DiscordTransportFactory::class, + WebhookScheme::GOOGLE_CHAT => GoogleChatTransportFactory::class, + WebhookScheme::MATTERMOST => MattermostTransportFactory::class, + WebhookScheme::MICROSOFT_TEAMS => MicrosoftTeamsTransportFactory::class, + WebhookScheme::ROCKET_CHAT => RocketChatTransportFactory::class, + WebhookScheme::SLACK => SlackTransportFactory::class, + WebhookScheme::TELEGRAM => TelegramTransportFactory::class, + WebhookScheme::ZULIP => ZulipTransportFactory::class + }; + } +} diff --git a/src/Controller/WatchListController.php b/src/Controller/WatchListController.php index da869c4..add187d 100644 --- a/src/Controller/WatchListController.php +++ b/src/Controller/WatchListController.php @@ -2,11 +2,13 @@ namespace App\Controller; +use App\Config\WebhookScheme; use App\Entity\Domain; use App\Entity\DomainEntity; use App\Entity\DomainEvent; use App\Entity\User; use App\Entity\WatchList; +use App\Notifier\TestChatNotification; use App\Repository\WatchListRepository; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; @@ -32,6 +34,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\SerializerInterface; @@ -65,6 +70,23 @@ class WatchListController extends AbstractController $user = $this->getUser(); $watchList->setUser($user); + if (null !== $watchList->getWebhookDsn()) { + foreach ($watchList->getWebhookDsn() as $dsnString) { + $dsn = new Dsn($dsnString); + + $scheme = $dsn->getScheme(); + $webhookScheme = WebhookScheme::tryFrom($scheme); + + if (null === $webhookScheme) { + throw new BadRequestHttpException("The DSN scheme ($scheme) is not supported"); + } + $transportFactoryClass = $webhookScheme->getChatTransportFactory(); + /** @var AbstractTransportFactory $transportFactory */ + $transportFactory = new $transportFactoryClass(); + $transportFactory->create($dsn)->send((new TestChatNotification())->asChatMessage()); + } + } + /* * In the limited version, we do not want a user to be able to register the same domain more than once in their watchlists. * This policy guarantees the equal probability of obtaining a domain name if it is requested by several users. @@ -151,6 +173,16 @@ class WatchListController extends AbstractController $user = $this->getUser(); $watchList->setUser($user); + if (null !== $watchList->getWebhookDsn()) { + foreach ($watchList->getWebhookDsn() as $dsnString) { + $scheme = (new Dsn($dsnString))->getScheme(); + + if (null === WebhookScheme::tryFrom($scheme)) { + throw new BadRequestHttpException("The DSN scheme ($scheme) is not supported"); + } + } + } + if ($this->getParameter('limited_features')) { if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) { $this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [ diff --git a/src/Entity/WatchList.php b/src/Entity/WatchList.php index 1e129db..c08fa5b 100644 --- a/src/Entity/WatchList.php +++ b/src/Entity/WatchList.php @@ -12,6 +12,7 @@ use App\Controller\WatchListController; use App\Repository\WatchListRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\SerializedName; @@ -118,6 +119,11 @@ class WatchList #[Groups(['watchlist:list', 'watchlist:item'])] private ?\DateTimeImmutable $createdAt = null; + #[SerializedName('dsn')] + #[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)] + #[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])] + private ?array $webhookDsn = null; + public function __construct() { $this->token = Uuid::v4(); @@ -237,4 +243,16 @@ class WatchList return $this; } + + public function getWebhookDsn(): ?array + { + return $this->webhookDsn; + } + + public function setWebhookDsn(?array $webhookDsn): static + { + $this->webhookDsn = $webhookDsn; + + return $this; + } } diff --git a/src/MessageHandler/ProcessDomainTriggerHandler.php b/src/MessageHandler/ProcessDomainTriggerHandler.php index 6c85a31..069c04b 100644 --- a/src/MessageHandler/ProcessDomainTriggerHandler.php +++ b/src/MessageHandler/ProcessDomainTriggerHandler.php @@ -4,43 +4,50 @@ namespace App\MessageHandler; use App\Config\Connector\ConnectorInterface; use App\Config\TriggerAction; -use App\Entity\Connector; +use App\Config\WebhookScheme; use App\Entity\Domain; use App\Entity\DomainEvent; -use App\Entity\User; use App\Entity\WatchList; use App\Entity\WatchListTrigger; use App\Message\ProcessDomainTrigger; +use App\Notifier\DomainOrderErrorNotification; +use App\Notifier\DomainOrderNotification; +use App\Notifier\DomainUpdateNotification; use App\Repository\DomainRepository; use App\Repository\WatchListRepository; use Psr\Log\LoggerInterface; -use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\Email; +use Symfony\Component\Notifier\Recipient\Recipient; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsMessageHandler] final readonly class ProcessDomainTriggerHandler { + private Address $sender; + public function __construct( - private string $mailerSenderEmail, - private string $mailerSenderName, - private MailerInterface $mailer, + string $mailerSenderEmail, + string $mailerSenderName, private WatchListRepository $watchListRepository, private DomainRepository $domainRepository, private KernelInterface $kernel, private LoggerInterface $logger, - private HttpClientInterface $client + private HttpClientInterface $client, + private MailerInterface $mailer ) { + $this->sender = new Address($mailerSenderEmail, $mailerSenderName); } /** * @throws TransportExceptionInterface * @throws \Exception + * @throws ExceptionInterface */ public function __invoke(ProcessDomainTrigger $message): void { @@ -70,12 +77,16 @@ final readonly class ProcessDomainTriggerHandler $connectorProvider->orderDomain($domain, $this->kernel->isDebug()); - $this->sendEmailDomainOrdered($domain, $connector, $watchList->getUser()); + $email = (new DomainOrderNotification($this->sender, $domain, $connector)) + ->asEmailMessage(new Recipient($watchList->getUser()->getEmail())); + $this->mailer->send($email->getMessage()); } catch (\Throwable) { $this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [ 'username' => $watchList->getUser()->getUserIdentifier(), ]); - $this->sendEmailDomainOrderError($domain, $watchList->getUser()); + $email = (new DomainOrderErrorNotification($this->sender, $domain)) + ->asEmailMessage(new Recipient($watchList->getUser()->getEmail())); + $this->mailer->send($email->getMessage()); } } @@ -91,67 +102,29 @@ final readonly class ProcessDomainTriggerHandler 'ldhName' => $message->ldhName, 'username' => $watchList->getUser()->getUserIdentifier(), ]); + + $recipient = new Recipient($watchList->getUser()->getEmail()); + $notification = new DomainUpdateNotification($this->sender, $event); + if (TriggerAction::SendEmail == $watchListTrigger->getAction()) { - $this->sendEmailDomainUpdated($event, $watchList->getUser()); + $this->mailer->send($notification->asEmailMessage($recipient)->getMessage()); + } elseif (TriggerAction::SendChat == $watchListTrigger->getAction()) { + if (null !== $watchList->getWebhookDsn()) { + foreach ($watchList->getWebhookDsn() as $dsnString) { + $dsn = new \Symfony\Component\Notifier\Transport\Dsn($dsnString); + + $scheme = $dsn->getScheme(); + $webhookScheme = WebhookScheme::tryFrom($scheme); + if (null !== $webhookScheme) { + $transportFactoryClass = $webhookScheme->getChatTransportFactory(); + /** @var AbstractTransportFactory $transportFactory */ + $transportFactory = new $transportFactoryClass(); + $transportFactory->create($dsn)->send($notification->asChatMessage()); + } + } + } } } } } - - /** - * @throws TransportExceptionInterface - */ - private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void - { - $email = (new TemplatedEmail()) - ->from(new Address($this->mailerSenderEmail, $this->mailerSenderName)) - ->to($user->getEmail()) - ->priority(Email::PRIORITY_HIGHEST) - ->subject('A domain name has been ordered') - ->htmlTemplate('emails/success/domain_ordered.html.twig') - ->locale('en') - ->context([ - 'domain' => $domain, - 'provider' => $connector->getProvider()->value, - ]); - - $this->mailer->send($email); - } - - /** - * @throws TransportExceptionInterface - */ - private function sendEmailDomainOrderError(Domain $domain, User $user): void - { - $email = (new TemplatedEmail()) - ->from(new Address($this->mailerSenderEmail, $this->mailerSenderName)) - ->to($user->getEmail()) - ->subject('An error occurred while ordering a domain name') - ->htmlTemplate('emails/errors/domain_order.html.twig') - ->locale('en') - ->context([ - 'domain' => $domain, - ]); - - $this->mailer->send($email); - } - - /** - * @throws TransportExceptionInterface - */ - private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void - { - $email = (new TemplatedEmail()) - ->from(new Address($this->mailerSenderEmail, $this->mailerSenderName)) - ->to($user->getEmail()) - ->priority(Email::PRIORITY_HIGHEST) - ->subject('A domain name has been changed') - ->htmlTemplate('emails/success/domain_updated.html.twig') - ->locale('en') - ->context([ - 'event' => $domainEvent, - ]); - - $this->mailer->send($email); - } } diff --git a/src/MessageHandler/ProcessWatchListTriggerHandler.php b/src/MessageHandler/ProcessWatchListTriggerHandler.php index c43d9c5..c5c77fb 100644 --- a/src/MessageHandler/ProcessWatchListTriggerHandler.php +++ b/src/MessageHandler/ProcessWatchListTriggerHandler.php @@ -3,33 +3,36 @@ namespace App\MessageHandler; use App\Entity\Domain; -use App\Entity\User; use App\Entity\WatchList; use App\Message\ProcessDomainTrigger; use App\Message\ProcessWatchListTrigger; +use App\Notifier\DomainUpdateErrorNotification; use App\Repository\WatchListRepository; use App\Service\RDAPService; use Psr\Log\LoggerInterface; -use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Mime\Address; +use Symfony\Component\Notifier\Recipient\Recipient; #[AsMessageHandler] final readonly class ProcessWatchListTriggerHandler { + private Address $sender; + public function __construct( private RDAPService $RDAPService, private MailerInterface $mailer, - private string $mailerSenderEmail, - private string $mailerSenderName, + string $mailerSenderEmail, + string $mailerSenderName, private MessageBusInterface $bus, private WatchListRepository $watchListRepository, private LoggerInterface $logger ) { + $this->sender = new Address($mailerSenderEmail, $mailerSenderName); } /** @@ -63,28 +66,12 @@ final readonly class ProcessWatchListTriggerHandler 'username' => $watchList->getUser()->getUserIdentifier(), 'error' => $e, ]); - $this->sendEmailDomainUpdateError($domain, $watchList->getUser()); + $email = (new DomainUpdateErrorNotification($this->sender, $domain)) + ->asEmailMessage(new Recipient($watchList->getUser()->getEmail())); + $this->mailer->send($email->getMessage()); } $this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt)); } } - - /** - * @throws TransportExceptionInterface - */ - private function sendEmailDomainUpdateError(Domain $domain, User $user): void - { - $email = (new TemplatedEmail()) - ->from(new Address($this->mailerSenderEmail, $this->mailerSenderName)) - ->to($user->getEmail()) - ->subject('An error occurred while updating a domain name') - ->htmlTemplate('emails/errors/domain_update.html.twig') - ->locale('en') - ->context([ - 'domain' => $domain, - ]); - - $this->mailer->send($email); - } } diff --git a/src/Notifier/DomainOrderErrorNotification.php b/src/Notifier/DomainOrderErrorNotification.php new file mode 100644 index 0000000..8174486 --- /dev/null +++ b/src/Notifier/DomainOrderErrorNotification.php @@ -0,0 +1,44 @@ +subject('Error: Domain Order'); + + return ChatMessage::fromNotification($this); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage + { + return new EmailMessage((new TemplatedEmail()) + ->from($this->sender) + ->to($recipient->getEmail()) + ->subject('An error occurred while ordering a domain name') + ->htmlTemplate('emails/errors/domain_order.html.twig') + ->locale('en') + ->context([ + 'domain' => $this->domain, + ])); + } +} diff --git a/src/Notifier/DomainOrderNotification.php b/src/Notifier/DomainOrderNotification.php new file mode 100644 index 0000000..9131dc4 --- /dev/null +++ b/src/Notifier/DomainOrderNotification.php @@ -0,0 +1,49 @@ +subject('Domain Ordered'); + + return ChatMessage::fromNotification($this); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage + { + return new EmailMessage((new TemplatedEmail()) + ->from($this->sender) + ->to($recipient->getEmail()) + ->priority(Email::PRIORITY_HIGHEST) + ->subject('A domain name has been ordered') + ->htmlTemplate('emails/success/domain_ordered.html.twig') + ->locale('en') + ->context([ + 'domain' => $this->domain, + 'provider' => $this->connector->getProvider()->value, + ])); + } +} diff --git a/src/Notifier/DomainUpdateErrorNotification.php b/src/Notifier/DomainUpdateErrorNotification.php new file mode 100644 index 0000000..063a27d --- /dev/null +++ b/src/Notifier/DomainUpdateErrorNotification.php @@ -0,0 +1,44 @@ +subject('Error: Domain Update'); + + return ChatMessage::fromNotification($this); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage + { + return new EmailMessage((new TemplatedEmail()) + ->from($this->sender) + ->to($recipient->getEmail()) + ->subject('An error occurred while updating a domain name') + ->htmlTemplate('emails/errors/domain_update.html.twig') + ->locale('en') + ->context([ + 'domain' => $this->domain, + ])); + } +} diff --git a/src/Notifier/DomainUpdateNotification.php b/src/Notifier/DomainUpdateNotification.php new file mode 100644 index 0000000..49cfe36 --- /dev/null +++ b/src/Notifier/DomainUpdateNotification.php @@ -0,0 +1,46 @@ +subject('Domain Updated'); + + return ChatMessage::fromNotification($this); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage + { + return new EmailMessage((new TemplatedEmail()) + ->from($this->sender) + ->to($recipient->getEmail()) + ->priority(Email::PRIORITY_HIGHEST) + ->subject('A domain name has been changed') + ->htmlTemplate('emails/success/domain_updated.html.twig') + ->locale('en') + ->context([ + 'event' => $this->domainEvent, + ])); + } +} diff --git a/src/Notifier/TestChatNotification.php b/src/Notifier/TestChatNotification.php new file mode 100644 index 0000000..ca3f434 --- /dev/null +++ b/src/Notifier/TestChatNotification.php @@ -0,0 +1,19 @@ +subject('Test notification'); + $this->content('This is a test message. If you can read me, this Webhook is configured correctly'); + + return ChatMessage::fromNotification($this); + } +} diff --git a/symfony.lock b/symfony.lock index e266829..83aa0f0 100644 --- a/symfony.lock +++ b/symfony.lock @@ -153,6 +153,15 @@ "config/packages/debug.yaml" ] }, + "symfony/discord-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.2", + "ref": "b97655f9a2fb8fc04d9f4081e0b5599d897b7f6e" + } + }, "symfony/flex": { "version": "2.4", "recipe": { @@ -184,6 +193,15 @@ "src/Kernel.php" ] }, + "symfony/google-chat-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "5954a3403bf1cdc557e2b71d3854cc81ba7d37cb" + } + }, "symfony/lock": { "version": "7.1", "recipe": { @@ -217,6 +235,15 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/mattermost-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.1", + "ref": "60df16a0ff39e942f6579e624d5630fc6b4e0cc1" + } + }, "symfony/messenger": { "version": "7.1", "recipe": { @@ -229,6 +256,15 @@ "config/packages/messenger.yaml" ] }, + "symfony/microsoft-teams-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "e4e1704f0b11573aaededc00640492c915de0bbe" + } + }, "symfony/monolog-bundle": { "version": "3.10", "recipe": { @@ -268,6 +304,15 @@ "tests/bootstrap.php" ] }, + "symfony/rocket-chat-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.1", + "ref": "5c91d24b503de5cc0d0eb880fc95afa1bef8b6f4" + } + }, "symfony/routing": { "version": "7.1", "recipe": { @@ -294,6 +339,15 @@ "config/routes/security.yaml" ] }, + "symfony/slack-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.2", + "ref": "8fb9603326990013efbe6d71c2dd78ada316808a" + } + }, "symfony/stimulus-bundle": { "version": "2.18", "recipe": { @@ -308,6 +362,15 @@ "assets/controllers/hello_controller.js" ] }, + "symfony/telegram-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.0", + "ref": "6cecb59a0e96c9e1cee469f2b82fa920101a68e8" + } + }, "symfony/translation": { "version": "7.1", "recipe": { @@ -387,6 +450,15 @@ "webpack.config.js" ] }, + "symfony/zulip-notifier": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.2", + "ref": "f420901c554baf7cde79a2a0bbf9b37ab1a650aa" + } + }, "symfonycasts/verify-email-bundle": { "version": "v1.17.0" }, diff --git a/yarn.lock b/yarn.lock index 5a203cf..597413d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1504,6 +1504,21 @@ resolved "https://registry.yarnpkg.com/@fontsource/noto-color-emoji/-/noto-color-emoji-5.0.27.tgz#61e40657bea980553bde8fd2d566104bd2859ad6" integrity sha512-gsIMN5o8qoRLrA+XyDNKKnMNpTbwpNbINisJqirLOLXl83arOV2sHRnNOkVht2gzUcImUxEUBGektp56G3Vj9w== +"@hotwired/stimulus-webpack-helpers@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd" + integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ== + +"@hotwired/stimulus@^3.0.0": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608" + integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== + +"@hotwired/turbo@^7.1.1 || ^8.0": + version "8.0.5" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.5.tgz#abae6dad018a891e4286e87fa0959217e3866d5a" + integrity sha512-TdZDA7fxVQ2ZycygvpnzjGPmFq4sO/E2QVg+2em/sJ3YTSsIWVEis8HmWlumz+c9DjWcUkcCuB+muF08TInpAQ== + "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -1670,6 +1685,20 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@symfony/stimulus-bridge@^3.2.0": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@symfony/stimulus-bridge/-/stimulus-bridge-3.2.2.tgz#afc1918f82d78cb2b6e299285c54094aa7f53696" + integrity sha512-kIaUEGPXW7g14zsHkIvQWw8cmfCdXSqsEQx18fuHPBb+R0h8nYPyY+e9uVtTuHlE2wHwAjrJoc6YBBK4a7CpKA== + dependencies: + "@hotwired/stimulus-webpack-helpers" "^1.0.1" + "@types/webpack-env" "^1.16.4" + acorn "^8.0.5" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +"@symfony/ux-turbo@file:vendor/symfony/ux-turbo/assets": + version "0.1.0" + "@symfony/webpack-encore@^4.0.0": version "4.6.1" resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-4.6.1.tgz#a3ced0baf1b02feb6d1a564906aff0e479b07259" @@ -1993,6 +2022,11 @@ dependencies: "@types/node" "*" +"@types/webpack-env@^1.16.4": + version "1.18.5" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.5.tgz#eccda0b04fe024bed505881e2e532f9c119169bf" + integrity sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA== + "@types/ws@^8.5.5": version "8.5.11" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508" @@ -2202,7 +2236,7 @@ acorn-import-attributes@^1.9.5: resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== -acorn@^8.7.1, acorn@^8.8.2: +acorn@^8.0.5, acorn@^8.7.1, acorn@^8.8.2: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -6020,7 +6054,7 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@^3.1.1, schema-utils@^3.2.0: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==