webhook_common: Add a method to build preset WebhookUrlOption.

This adds `WebhookUrlOption.build_preset_config` method which builds
pre-configured WebhookUrlOptions objects. It can be used to abstract
commonly used WebhookUrlOption settings or to construct special
settings that have additional logic and UI in the web-app modal for
generating an incoming webhook URL.

Currently, one such setting is the "branches" url option. This setting
is meant to be used by "versioncontrol" integrations such as GitHub,
Gitea, etc. It adds UI that lets the user to choose which branches of
their repository can trigger notifications. So, we refactor those
integrations to use `build_preset_config` for the "branches" option.

Co-authored-by: Lauryn Menard <lauryn@zulip.com>
This commit is contained in:
PieterCK
2025-04-10 18:39:36 +07:00
committed by Tim Abbott
parent f0a88d51cc
commit 57ff908af9
4 changed files with 79 additions and 12 deletions

View File

@@ -259,6 +259,44 @@ would want to include in a Zulip notification message.
The `config_options` field in the `WebhookIntegration` class is reserved The `config_options` field in the `WebhookIntegration` class is reserved
for this use case. for this use case.
### WebhookUrlOption presets
The `build_preset_config` method creates `WebhookUrlOption` objects with
pre-configured fields. These preset URL options primarily serve two
purposes:
- To construct common `WebhookUrlOption` objects that are used in various
incoming webhook integrations.
- To construct `WebhookUrlOption` objects with special UI in the web-app
for [generating incoming webhook URLs](/help/generate-integration-url).
Using a preset URL option with the `build_preset_config` method:
```python
# zerver/lib/integrations.py
from zerver.lib.webhooks.common import PresetUrlOption, WebhookUrlOption
# -- snip --
WebhookIntegration(
"github",
# -- snip --
url_options=[
WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES),
],
),
```
Currently configured preset URL options:
- **`BRANCHES`**: This preset is intended to be used for [version control
integrations](/integrations/version-control), and adds UI for the user to
configure which branches of a project's repository will trigger Zulip
notification messages. When the user specifies which branches to receive
notifications from, the `branches` parameter will be added to the [generated
integration URL](/help/generate-integration-url). For example, if the user
input `main` and `dev` for the branches of their repository, then
`&branches=main%2Cdev` would be appended to the generated integration URL.
## Step 4: Manually testing the webhook ## Step 4: Manually testing the webhook
For either one of the command line tools, first, you'll need to get an For either one of the command line tools, first, you'll need to get an

View File

@@ -33,6 +33,10 @@ const url_option_schema = z.object({
const url_options_schema = z.array(url_option_schema); const url_options_schema = z.array(url_option_schema);
const PresetUrlOption = {
BRANCHES: "branches",
};
export function show_generate_integration_url_modal(api_key: string): void { export function show_generate_integration_url_modal(api_key: string): void {
const default_url_message = $t_html({defaultMessage: "Integration URL will appear here."}); const default_url_message = $t_html({defaultMessage: "Integration URL will appear here."});
const streams = stream_data.subscribed_subs(); const streams = stream_data.subscribed_subs();
@@ -114,7 +118,7 @@ export function show_generate_integration_url_modal(api_key: string): void {
for (const option of validated_config) { for (const option of validated_config) {
let $config_element: JQuery; let $config_element: JQuery;
if (option.key === "branches") { if (option.key === PresetUrlOption.BRANCHES) {
const filter_branches_html = const filter_branches_html =
render_generate_integration_url_filter_branches_modal(); render_generate_integration_url_filter_branches_modal();
$config_element = $(filter_branches_html); $config_element = $(filter_branches_html);
@@ -238,7 +242,7 @@ export function show_generate_integration_url_modal(api_key: string): void {
for (const option of url_options) { for (const option of url_options) {
let $input_element; let $input_element;
if ( if (
option.key === "branches" && option.key === PresetUrlOption.BRANCHES &&
!$("#integration-url-all-branches").prop("checked") !$("#integration-url-all-branches").prop("checked")
) { ) {
const $pill_container = $( const $pill_container = $(

View File

@@ -12,8 +12,8 @@ from django.views.decorators.csrf import csrf_exempt
from django_stubs_ext import StrPromise from django_stubs_ext import StrPromise
from zerver.lib.storage import static_path from zerver.lib.storage import static_path
from zerver.lib.validator import check_bool, check_string from zerver.lib.validator import check_bool
from zerver.lib.webhooks.common import WebhookConfigOption, WebhookUrlOption from zerver.lib.webhooks.common import PresetUrlOption, WebhookConfigOption, WebhookUrlOption
"""This module declares all of the (documented) integrations available """This module declares all of the (documented) integrations available
in the Zulip server. The Integration class is used as part of in the Zulip server. The Integration class is used as part of
@@ -414,7 +414,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
"azuredevops", "azuredevops",
["version-control"], ["version-control"],
display_name="AzureDevOps", display_name="AzureDevOps",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration("beanstalk", ["version-control"], stream_name="commits"), WebhookIntegration("beanstalk", ["version-control"], stream_name="commits"),
WebhookIntegration("basecamp", ["project-management"]), WebhookIntegration("basecamp", ["project-management"]),
@@ -425,7 +425,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
logo="images/integrations/logos/bitbucket.svg", logo="images/integrations/logos/bitbucket.svg",
display_name="Bitbucket Server", display_name="Bitbucket Server",
stream_name="bitbucket", stream_name="bitbucket",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration( WebhookIntegration(
"bitbucket2", "bitbucket2",
@@ -433,7 +433,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
logo="images/integrations/logos/bitbucket.svg", logo="images/integrations/logos/bitbucket.svg",
display_name="Bitbucket", display_name="Bitbucket",
stream_name="bitbucket", stream_name="bitbucket",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration( WebhookIntegration(
"bitbucket", "bitbucket",
@@ -462,7 +462,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
"gitea", "gitea",
["version-control"], ["version-control"],
stream_name="commits", stream_name="commits",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration( WebhookIntegration(
"github", "github",
@@ -471,7 +471,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
function="zerver.webhooks.github.view.api_github_webhook", function="zerver.webhooks.github.view.api_github_webhook",
stream_name="github", stream_name="github",
url_options=[ url_options=[
WebhookUrlOption(name="branches", label="", validator=check_string), WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES),
WebhookUrlOption( WebhookUrlOption(
name="ignore_private_repositories", name="ignore_private_repositories",
label="Exclude notifications from private repositories", label="Exclude notifications from private repositories",
@@ -493,14 +493,14 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
"gitlab", "gitlab",
["version-control"], ["version-control"],
display_name="GitLab", display_name="GitLab",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration("gocd", ["continuous-integration"], display_name="GoCD"), WebhookIntegration("gocd", ["continuous-integration"], display_name="GoCD"),
WebhookIntegration( WebhookIntegration(
"gogs", "gogs",
["version-control"], ["version-control"],
stream_name="commits", stream_name="commits",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration("gosquared", ["marketing"], display_name="GoSquared"), WebhookIntegration("gosquared", ["marketing"], display_name="GoSquared"),
WebhookIntegration("grafana", ["monitoring"]), WebhookIntegration("grafana", ["monitoring"]),
@@ -555,7 +555,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
"rhodecode", "rhodecode",
["version-control"], ["version-control"],
display_name="RhodeCode", display_name="RhodeCode",
url_options=[WebhookUrlOption(name="branches", label="", validator=check_string)], url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)],
), ),
WebhookIntegration("rundeck", ["deployment"]), WebhookIntegration("rundeck", ["deployment"]),
WebhookIntegration("semaphore", ["continuous-integration", "deployment"]), WebhookIntegration("semaphore", ["continuous-integration", "deployment"]),

View File

@@ -5,6 +5,7 @@ import importlib
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Annotated, Any, TypeAlias from typing import Annotated, Any, TypeAlias
from urllib.parse import unquote from urllib.parse import unquote
@@ -31,6 +32,7 @@ from zerver.lib.request import RequestNotes
from zerver.lib.send_email import FromAddress from zerver.lib.send_email import FromAddress
from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.timestamp import timestamp_to_datetime
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
from zerver.lib.validator import check_string
from zerver.models import UserProfile from zerver.models import UserProfile
MISSING_EVENT_HEADER_MESSAGE = """\ MISSING_EVENT_HEADER_MESSAGE = """\
@@ -54,6 +56,10 @@ SETUP_MESSAGE_USER_PART = " by {user_name}"
OptionalUserSpecifiedTopicStr: TypeAlias = Annotated[str | None, ApiParamConfig("topic")] OptionalUserSpecifiedTopicStr: TypeAlias = Annotated[str | None, ApiParamConfig("topic")]
class PresetUrlOption(str, Enum):
BRANCHES = "branches"
@dataclass @dataclass
class WebhookConfigOption: class WebhookConfigOption:
name: str name: str
@@ -67,6 +73,25 @@ class WebhookUrlOption:
label: str label: str
validator: Callable[[str, str], str | bool | None] validator: Callable[[str, str], str | bool | None]
@classmethod
def build_preset_config(cls, config: PresetUrlOption) -> "WebhookUrlOption":
"""
This creates a pre-configured WebhookUrlOption object to be used
in various incoming webhook integrations.
See https://zulip.com/api/incoming-webhooks-walkthrough#webhookurloption-presets
for more details on this system and what each option does.
"""
match config:
case PresetUrlOption.BRANCHES:
return cls(
name=config.value,
label="",
validator=check_string,
)
raise AssertionError(_("Unknown 'PresetUrlOption': {config}").format(config=config))
def get_setup_webhook_message(integration: str, user_name: str | None = None) -> str: def get_setup_webhook_message(integration: str, user_name: str | None = None) -> str:
content = SETUP_MESSAGE_TEMPLATE.format(integration=integration) content = SETUP_MESSAGE_TEMPLATE.format(integration=integration)