Repository: PrefectHQ/prefect

Issue:
Ability to add a Discord webhook url while creating a new notification
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

2.x

### Describe the current behavior

Currently there are options for Mattermost, Microsoft Teams, Opsgenei, Pager Duty and Slack webhook to receive notifications. 

### Describe the proposed behavior

User will be able to select "Discord webhook" and paste the webhook url for it to create notification. The user will also be able to select "Run states" and "Notify Type (Optional)".

### Example Use


### Additional context

I am working on this feature and any suggestions / feedback / ideas are welcomed.


Discussion:
Hi @Ishankoradia, welcome to the community and thanks for the issue! Did you want to be assigned to this enhancement?
@serinamarie yes please.Thanks
May I work on this?
Hi. @Ishankoradia I made a PR on this . I am new to open source so if there are any changes needed let me know amd help me walk me through it.
Created the above PR for this. Please review.

Resolving patch:
diff --git a/src/prefect/blocks/notifications.py b/src/prefect/blocks/notifications.py
--- a/src/prefect/blocks/notifications.py
+++ b/src/prefect/blocks/notifications.py
@@ -490,6 +490,99 @@ def block_initialization(self) -> None:
         self._start_apprise_client(url)
 
 
+class DiscordWebhook(AbstractAppriseNotificationBlock):
+    """
+    Enables sending notifications via a provided Discord webhook.
+    See [Apprise notify_Discord docs](https://github.com/caronc/apprise/wiki/Notify_Discord) # noqa
+
+    Examples:
+        Load a saved Discord webhook and send a message:
+        ```python
+        from prefect.blocks.notifications import DiscordWebhook
+
+        discord_webhook_block = DiscordWebhook.load("BLOCK_NAME")
+
+        discord_webhook_block.notify("Hello from Prefect!")
+        ```
+    """
+
+    _description = "Enables sending notifications via a provided Discord webhook."
+    _block_type_name = "Discord Webhook"
+    _block_type_slug = "discord-webhook"
+    _logo_url = "https://images.ctfassets.net/keir3zrx8eg0/64fsff0qm7st33BqViEpqY/e177db0d1ada88a7ee6c9433576b98d5/icons8-discord-new-480.png?h=250"
+    _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.DiscordWebhook"
+
+    webhook_id: SecretStr = Field(
+        default=...,
+        description=(
+            "The first part of 2 tokens provided to you after creating a"
+            " incoming-webhook."
+        ),
+    )
+
+    webhook_token: SecretStr = Field(
+        default=...,
+        description=(
+            "The second part of 2 tokens provided to you after creating a"
+            " incoming-webhook."
+        ),
+    )
+
+    botname: Optional[str] = Field(
+        title="Bot name",
+        default=None,
+        description=(
+            "Identify the name of the bot that should issue the message. If one isn't"
+            " specified then the default is to just use your account (associated with"
+            " the incoming-webhook)."
+        ),
+    )
+
+    tts: bool = Field(
+        default=False,
+        description="Whether to enable Text-To-Speech.",
+    )
+
+    include_image: bool = Field(
+        default=False,
+        description=(
+            "Whether to include an image in-line with the message describing the"
+            " notification type."
+        ),
+    )
+
+    avatar: bool = Field(
+        default=False,
+        description="Whether to override the default discord avatar icon.",
+    )
+
+    avatar_url: Optional[str] = Field(
+        title="Avatar URL",
+        default=False,
+        description=(
+            "Over-ride the default discord avatar icon URL. By default this is not set"
+            " and Apprise chooses the URL dynamically based on the type of message"
+            " (info, success, warning, or error)."
+        ),
+    )
+
+    def block_initialization(self) -> None:
+        from apprise.plugins.NotifyDiscord import NotifyDiscord
+
+        url = SecretStr(
+            NotifyDiscord(
+                webhook_id=self.webhook_id.get_secret_value(),
+                webhook_token=self.webhook_token.get_secret_value(),
+                botname=self.botname,
+                tts=self.tts,
+                include_image=self.include_image,
+                avatar=self.avatar,
+                avatar_url=self.avatar_url,
+            ).url()
+        )
+        self._start_apprise_client(url)
+
+
 class CustomWebhookNotificationBlock(NotificationBlock):
     """
     Enables sending notifications via any custom webhook.


Test patch:
diff --git a/tests/blocks/test_notifications.py b/tests/blocks/test_notifications.py
--- a/tests/blocks/test_notifications.py
+++ b/tests/blocks/test_notifications.py
@@ -12,6 +12,7 @@
     PREFECT_NOTIFY_TYPE_DEFAULT,
     AppriseNotificationBlock,
     CustomWebhookNotificationBlock,
+    DiscordWebhook,
     MattermostWebhook,
     OpsgenieWebhook,
     PagerDutyWebHook,
@@ -170,6 +171,58 @@ def test_is_picklable(self):
         assert isinstance(unpickled, MattermostWebhook)
 
 
+class TestDiscordWebhook:
+    async def test_notify_async(self):
+        with patch("apprise.Apprise", autospec=True) as AppriseMock:
+            reload_modules()
+
+            apprise_instance_mock = AppriseMock.return_value
+            apprise_instance_mock.async_notify = AsyncMock()
+
+            discord_block = DiscordWebhook(
+                webhook_id="123456",
+                webhook_token="abc123EFG",
+            )
+            await discord_block.notify("test")
+
+            AppriseMock.assert_called_once()
+            apprise_instance_mock.add.assert_called_once_with(
+                f"discord://{discord_block.webhook_id.get_secret_value()}/{discord_block.webhook_token.get_secret_value()}/"
+                "?tts=no&avatar=no&footer=no&footer_logo=yes&image=no&fields=yes&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes"
+            )
+            apprise_instance_mock.async_notify.assert_awaited_once_with(
+                body="test", title=None, notify_type=PREFECT_NOTIFY_TYPE_DEFAULT
+            )
+
+    def test_notify_sync(self):
+        with patch("apprise.Apprise", autospec=True) as AppriseMock:
+            reload_modules()
+
+            apprise_instance_mock = AppriseMock.return_value
+            apprise_instance_mock.async_notify = AsyncMock()
+
+            discord_block = DiscordWebhook(
+                webhook_id="123456", webhook_token="abc123EFG"
+            )
+            discord_block.notify("test")
+
+            AppriseMock.assert_called_once()
+            apprise_instance_mock.add.assert_called_once_with(
+                f"discord://{discord_block.webhook_id.get_secret_value()}/{discord_block.webhook_token.get_secret_value()}/"
+                "?tts=no&avatar=no&footer=no&footer_logo=yes&image=no&fields=yes&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes"
+            )
+            apprise_instance_mock.async_notify.assert_called_once_with(
+                body="test", title=None, notify_type=PREFECT_NOTIFY_TYPE_DEFAULT
+            )
+
+    def test_is_picklable(self):
+        reload_modules()
+        block = DiscordWebhook(webhook_id="123456", webhook_token="abc123EFG")
+        pickled = cloudpickle.dumps(block)
+        unpickled = cloudpickle.loads(pickled)
+        assert isinstance(unpickled, DiscordWebhook)
+
+
 class TestOpsgenieWebhook:
     API_KEY = "api_key"