/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
zerver/webhooks/gitlab/view.py
894 строки
34 KB
Niloth P
webhooks: Update remaining webhooks to support empty URL param "topic".
25 мар 2026, 21:31
25 мар 2026, 21:31
c859006
Код
Авторство
О чём код?
import re from datetime import datetime from typing import Protocol from django.http import HttpRequest, HttpResponse from pydantic import Json from zerver.decorator import webhook_view from zerver.lib.exceptions import UnsupportedWebhookEventTypeError from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS from zerver.lib.markdown.fenced_code import get_unused_fence from zerver.lib.mention import silent_mention_syntax_for_user from zerver.lib.partial import partial from zerver.lib.response import json_success from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_int, check_none_or, check_string from zerver.lib.webhooks.common import ( OptionalUserSpecifiedTopicStr, check_send_webhook_message, get_event_header, guess_zulip_user_from_external_account, ) from zerver.lib.webhooks.git import ( CONTENT_MESSAGE_TEMPLATE, EMPTY_SHA, RELEASE_MESSAGE_TEMPLATE_WITHOUT_USER_NAME, RELEASE_MESSAGE_TEMPLATE_WITHOUT_USER_NAME_WITHOUT_URL, TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE, get_commits_comment_action_message, get_issue_event_message, get_pull_request_event_message, get_push_commits_event_message, get_push_tag_event_message, get_remove_branch_event_message, is_branch_name_notifiable, ) from zerver.models import Realm, UserProfile TOPIC_WITH_DESIGN_INFO_TEMPLATE = "{repo} / {type} {design_name}" DESIGN_COMMENT_MESSAGE_TEMPLATE = ( "{user_name} {action} on design [{design_name}]({design_url}):\n{content_message}" ) DESIGN_COMMENT_MESSAGE_TEMPLATE_WITHOUT_REFERENCE = "{user_name} {action}:\n{content_message}" FEATURE_FLAG_MESSAGE_TEMPLATE = "{user} {action} the feature flag [{name}]({url})." ACCESS_TOKEN_EXPIRY_MESSAGE_TEMPLATE = "The access token [{name}]({url}) will expire on {date}." EMOJI_MESSAGE_TEMPLATE = "{user_name} {action} the emoji :{emoji_text}:{suffix}." def fixture_to_headers(fixture_name: str) -> dict[str, str]: if fixture_name.startswith("build"): return {} # Since there are 2 possible event types. # Map "push_hook__push_commits_more_than_limit.json" into GitLab's # HTTP event title "Push Hook". return {"HTTP_X_GITLAB_EVENT": fixture_name.split("__", 1)[0].replace("_", " ").title()} def get_push_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: after = payload.get("after") if after: stringified_after = after.tame(check_string) if stringified_after == EMPTY_SHA: return get_remove_branch_event_body(payload, realm) return get_normal_push_event_body(payload, realm) def get_normal_push_event_body(payload: WildValue, realm: Realm) -> str: compare_url = "{}/-/compare/{}...{}".format( get_project_homepage(payload), payload["before"].tame(check_string), payload["after"].tame(check_string), ) commits = [ { "name": commit["author"]["name"].tame(check_string), "sha": commit["id"].tame(check_string), "message": commit["message"].tame(check_string), "url": commit["url"].tame(check_string), } for commit in payload["commits"] ] return get_push_commits_event_message( get_user_name(payload, realm), compare_url, get_branch_name(payload), commits, ) def get_remove_branch_event_body(payload: WildValue, realm: Realm) -> str: return get_remove_branch_event_message( get_user_name(payload, realm), get_branch_name(payload), ) def get_tag_push_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: return get_push_tag_event_message( get_user_name(payload, realm), get_tag_name(payload), action="pushed" if payload.get("checkout_sha") else "removed", ) def get_issue_created_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: description = payload["object_attributes"].get("description") # Filter out multiline hidden comments if description: stringified_description = description.tame(check_string) stringified_description = re.sub( r"<!--.*?-->", "", stringified_description, count=0, flags=re.DOTALL ) stringified_description = stringified_description.rstrip() else: stringified_description = None return get_issue_event_message( user_name=get_user_name(payload, realm), action="created", url=get_object_url(payload), number=payload["object_attributes"]["iid"].tame(check_int), message=stringified_description, assignees=replace_assignees_username_with_name(get_assignees(payload)), title=payload["object_attributes"]["title"].tame(check_string) if include_title else None, ) def get_issue_event_body(action: str, payload: WildValue, include_title: bool, realm: Realm) -> str: return get_issue_event_message( user_name=get_user_name(payload, realm), action=action, url=get_object_url(payload), number=payload["object_attributes"]["iid"].tame(check_int), title=payload["object_attributes"]["title"].tame(check_string) if include_title else None, ) def get_merge_request_updated_event_body( payload: WildValue, include_title: bool, realm: Realm ) -> str: if payload["object_attributes"].get("oldrev"): return get_merge_request_event_body( "added commit(s) to", payload, include_title=include_title, realm=realm, ) return get_merge_request_open_or_updated_body( "updated", payload, include_title=include_title, realm=realm, ) def get_merge_request_event_body( action: str, payload: WildValue, include_title: bool, realm: Realm ) -> str: pull_request = payload["object_attributes"] target_branch = None base_branch = None if action == "merged": target_branch = pull_request["source_branch"].tame(check_string) base_branch = pull_request["target_branch"].tame(check_string) return get_pull_request_event_message( user_name=get_user_name(payload, realm), action=action, url=pull_request["url"].tame(check_string), number=pull_request["iid"].tame(check_int), target_branch=target_branch, base_branch=base_branch, type="MR", title=payload["object_attributes"]["title"].tame(check_string) if include_title else None, ) def get_merge_request_open_or_updated_body( action: str, payload: WildValue, include_title: bool, realm: Realm, ) -> str: pull_request = payload["object_attributes"] return get_pull_request_event_message( user_name=get_user_name(payload, realm), action=action, url=pull_request["url"].tame(check_string), number=pull_request["iid"].tame(check_int), target_branch=( pull_request["source_branch"].tame(check_string) if action == "created" else None ), base_branch=( pull_request["target_branch"].tame(check_string) if action == "created" else None ), message=pull_request["description"].tame(check_none_or(check_string)), assignees=replace_assignees_username_with_name(get_assignees(payload)), type="MR", title=payload["object_attributes"]["title"].tame(check_string) if include_title else None, ) def get_assignees(payload: WildValue) -> list[WildValue] | WildValue: assignee_details = payload.get("assignees") if not assignee_details: single_assignee_details = payload.get("assignee") if not single_assignee_details: transformed_assignee_details = [] else: transformed_assignee_details = [single_assignee_details] return transformed_assignee_details return assignee_details def replace_assignees_username_with_name( assignees: list[WildValue] | WildValue, ) -> list[dict[str, str]]: """Replace the username of each assignee with their (full) name. This is a hack-like adaptor so that when assignees are passed to `get_pull_request_event_message` we can use the assignee's name and not their username (for more consistency). """ formatted_assignees = [] for assignee in assignees: formatted_assignee = {} formatted_assignee["username"] = assignee["name"].tame(check_string) formatted_assignees.append(formatted_assignee) return formatted_assignees def parse_design_comment(comment: WildValue, repository_url: str) -> tuple[str, str, str]: note_id = comment["id"].tame(check_int) # As there is no issue field in the payloads related to designs, # we need to parse the issue number from the new_path field. design_path = comment["position"]["new_path"].tame(check_string) # Sample design_path: "designs/issue-1/Screenshot_20250302_230445.png" _, issue_subpath, design_name = design_path.split("/") issue_number = issue_subpath.split("-")[-1] design_url = f"{repository_url}/-/issues/{issue_number}/designs/{design_name}" comment_url = f"{design_url}#note_{note_id}" return comment_url, design_url, design_name def get_commented_commit_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: comment = payload["object_attributes"] action = "[commented]({})".format(comment["url"].tame(check_string)) return get_commits_comment_action_message( get_user_name(payload, realm), action, payload["commit"]["url"].tame(check_string), payload["commit"]["id"].tame(check_string), comment["note"].tame(check_string), ) def get_commented_merge_request_event_body( payload: WildValue, include_title: bool, realm: Realm ) -> str: comment = payload["object_attributes"] comment_url = comment["url"].tame(check_string) action = f"[commented]({comment_url}) on" if include_title else f"[commented]({comment_url})" url = payload["merge_request"]["url"].tame(check_string) return get_pull_request_event_message( user_name=get_user_name(payload, realm), action=action, url=url, number=payload["merge_request"]["iid"].tame(check_int), message=comment["note"].tame(check_string), type="MR", title=payload["merge_request"]["title"].tame(check_string), include_topic_reference=include_title, ) def get_commented_issue_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: comment = payload["object_attributes"] comment_url = comment["url"].tame(check_string) action = f"[commented]({comment_url}) on" if include_title else f"[commented]({comment_url})" url = payload["issue"]["url"].tame(check_string) return get_pull_request_event_message( user_name=get_user_name(payload, realm), action=action, url=url, number=payload["issue"]["iid"].tame(check_int), message=comment["note"].tame(check_string), type="issue", title=payload["issue"]["title"].tame(check_string), include_topic_reference=include_title, ) def get_commented_design_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: comment = payload["object_attributes"] repository_url = payload["repository"]["homepage"].tame(check_string) comment_url, design_url, design_name = parse_design_comment(comment, repository_url) action = f"[commented]({comment_url})" message = comment["note"].tame(check_string) content_message = CONTENT_MESSAGE_TEMPLATE.format( message=message, fence=get_unused_fence(message) ) message_template = ( DESIGN_COMMENT_MESSAGE_TEMPLATE if include_title else DESIGN_COMMENT_MESSAGE_TEMPLATE_WITHOUT_REFERENCE ) return message_template.format( user_name=get_user_name(payload, realm), action=action, design_name=design_name, design_url=design_url, content_message=content_message, ) def get_commented_snippet_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: comment = payload["object_attributes"] comment_url = comment["url"].tame(check_string) action = f"[commented]({comment_url}) on" if include_title else f"[commented]({comment_url})" # Snippet URL is only available in GitLab 16.1+ if "url" in payload["snippet"]: url = payload["snippet"]["url"].tame(check_string) else: url = "{}/-/snippets/{}".format( payload["project"]["web_url"].tame(check_string), payload["snippet"]["id"].tame(check_int), ) return get_pull_request_event_message( user_name=get_user_name(payload, realm), action=action, url=url, number=payload["snippet"]["id"].tame(check_int), message=comment["note"].tame(check_string), type="snippet", title=payload["snippet"]["title"].tame(check_string), include_topic_reference=include_title, ) def get_wiki_page_event_body( action: str, payload: WildValue, include_title: bool, realm: Realm ) -> str: return '{} {} [wiki page "{}"]({}).'.format( get_user_name(payload, realm), action, payload["object_attributes"]["title"].tame(check_string), payload["object_attributes"]["url"].tame(check_string), ) def get_build_hook_event_body( payload: WildValue, include_title: bool, realm: Realm | None = None ) -> str: build_status = payload["build_status"].tame(check_string) if build_status == "created": action = "was created" elif build_status == "running": action = "started" else: action = f"changed status to {build_status}" return "Build {} from {} stage {}.".format( payload["build_name"].tame(check_string), payload["build_stage"].tame(check_string), action, ) def get_test_event_body(payload: WildValue, include_title: bool, realm: Realm | None = None) -> str: return f"Webhook for **{get_repo_name(payload)}** has been configured successfully! :tada:" def get_pipeline_event_body( payload: WildValue, include_title: bool, realm: Realm | None = None ) -> str: pipeline_status = payload["object_attributes"]["status"].tame(check_string) if pipeline_status == "pending": action = "was created" elif pipeline_status == "running": action = "started" else: action = f"changed status to {pipeline_status}" project_homepage = get_project_homepage(payload) pipeline_url = "{}/-/pipelines/{}".format( project_homepage, payload["object_attributes"]["id"].tame(check_int), ) builds_status = "" for build in payload["builds"]: build_url = "{}/-/jobs/{}".format( project_homepage, build["id"].tame(check_int), ) artifact_filename = build.get("artifacts_file", {}).get("filename", None) if artifact_filename: artifact_download_url = f"{build_url}/artifacts/download" artifact_browse_url = f"{build_url}/artifacts/browse" artifact_string = f" * built artifact: *{artifact_filename.tame(check_string)}* [[Browse]({artifact_browse_url})|[Download]({artifact_download_url})]\n" else: artifact_string = "" builds_status += "* [{}]({}) - {}\n{}".format( build["name"].tame(check_string), build_url, build["status"].tame(check_string), artifact_string, ) return "[Pipeline ({})]({}) {} with build(s):\n{}.".format( payload["object_attributes"]["id"].tame(check_int), pipeline_url, action, builds_status[:-1], ) def get_release_event_body( payload: WildValue, include_title: bool, realm: Realm | None = None ) -> str: action = payload["action"].tame(check_string) name = payload["name"].tame(check_string) tag = payload["tag"].tame(check_string) message_action = f"{action}d" if action == "delete": body = RELEASE_MESSAGE_TEMPLATE_WITHOUT_USER_NAME_WITHOUT_URL.format( release_name=name, tagname=tag, action=message_action ) else: url = payload["url"].tame(check_string) body = RELEASE_MESSAGE_TEMPLATE_WITHOUT_USER_NAME.format( release_name=name, url=url, tagname=tag, action=message_action ) if "description" in payload: description = payload["description"].tame(check_string) body += CONTENT_MESSAGE_TEMPLATE.format( message=description, fence=get_unused_fence(description) ) return body def get_feature_flag_event_body( payload: WildValue, include_title: bool, realm: Realm, ) -> str: repo_url = payload["project"]["web_url"].tame(check_string) feature_flag = payload["object_attributes"] action = "activated" if feature_flag["active"] else "deactivated" return FEATURE_FLAG_MESSAGE_TEMPLATE.format( user=get_user_name(payload, realm), action=action, name=feature_flag["name"].tame(check_string), url=f"{repo_url}/-/feature_flags", ) def get_access_token_page_url(payload: WildValue) -> str: """ Generate the URL for the access tokens based on whether it's for a group or a project. """ if "group" in payload: group_path = payload["group"]["group_path"].tame(check_string) return f"https://gitlab.com/groups/{group_path}/-/settings/access_tokens" project_url = payload["project"]["web_url"].tame(check_string) return f"{project_url}/-/settings/access_tokens" def get_resource_access_token_expiry_event_body( payload: WildValue, include_title: bool, realm: Realm | None = None ) -> str: access_token = payload["object_attributes"] expiry_date = access_token["expires_at"].tame(check_string) formatted_date = datetime.fromisoformat(expiry_date).strftime("%b %d, %Y") return ACCESS_TOKEN_EXPIRY_MESSAGE_TEMPLATE.format( name=access_token["name"].tame(check_string), url=get_access_token_page_url(payload), date=formatted_date, ) def get_deployment_event_body(payload: WildValue, include_title: bool, realm: Realm) -> str: user_text = f"[{get_user_name(payload, realm)}]({payload['user_url'].tame(check_string)})" deployment_status = payload["status"].tame(check_string) deployable_url = payload.get("deployable_url", "").tame(check_string) deployment_text = f"[deployment]({deployable_url})" if deployable_url else "deployment" commit_title = payload["commit_title"].tame(check_string) commit_url = payload["commit_url"].tame(check_string) commit_sha = commit_url.split("/")[-1][:7] deployment_event_body_map = { "running": f"{user_text} started a new {deployment_text}:\n> [{commit_sha}]({commit_url}) {commit_title}", "success": f"The {deployment_text} was successful.", "failed": f"The {deployment_text} failed.", "canceled": f"The {deployment_text} was canceled.", } return deployment_event_body_map[deployment_status] def get_emoji_event_transformed_type(payload: WildValue, type: str) -> str: if type == "MergeRequest": return "MR" elif type == "Note": event_type = payload["note"]["noteable_type"].tame(check_string) return get_emoji_event_transformed_type(payload, event_type) elif type == "DesignManagement::Design": return "design" return type.lower() def get_emoji_event_subtype_message(type: str) -> str: return "a comment" if type == "Note" else "" def get_emoji_event_url_id(payload: WildValue, type: str) -> tuple[str, str]: if type == "design": comment_url, _, design_name = parse_design_comment( payload["note"], payload["project"]["web_url"].tame(check_string) ) return comment_url, design_name url = payload["object_attributes"]["awarded_on_url"].tame(check_string) # Extract the last numeric ID in the URL path before any '#' fragment. # Example: # https://gitlab.com/abc/def/issues/123 → "123" # https://gitlab.com/abc/def/-/merge_requests/456#note_789 → "456" clean_url = url.split("#")[0] match = re.search(r"/(\d+)(?:/)?$", clean_url) assert match is not None return url, match.group(1) def get_emoji_event_topic_title( payload: WildValue, awardable_type: str, type: str, use_merge_request_title: bool ) -> str: """ The payload format depends on the event target type (MR / issue / snippet). Get topic from the corresponding payload section's title. """ event_type = "" if type == "MR": event_type = "merge_request" if use_merge_request_title else "" elif type == "issue": event_type = "issue" if awardable_type == "Note" else "work_item" elif type == "snippet": event_type = "project_snippet" if event_type: return payload[event_type]["title"].tame(check_string) return "" def get_emoji_event_number_sign(type: str) -> str: return "" if type == "design" else "#" def get_emoji_event_body(action: str, payload: WildValue, include_title: bool, realm: Realm) -> str: transformed_action = {"award": "added", "revoke": "removed"}.get(action, "reacted") preposition = {"award": "to", "revoke": "from"}.get(action, "to") emoji = payload["object_attributes"] awardable_type = emoji["awardable_type"].tame(check_string) transformed_type = get_emoji_event_transformed_type(payload, awardable_type) url, id = get_emoji_event_url_id(payload, transformed_type) subtype = get_emoji_event_subtype_message(awardable_type) number_sign = get_emoji_event_number_sign(transformed_type) suffix = "" if include_title or awardable_type == "Note": target = "" if awardable_type == "Note" else f"{transformed_type} {number_sign}{id}" suffix = f" {preposition} [{subtype}{target}]({url})" return EMOJI_MESSAGE_TEMPLATE.format( user_name=get_user_name(payload, realm), action=transformed_action, emoji_text=emoji["name"].tame(check_string), suffix=suffix, ) def get_repo_name(payload: WildValue) -> str: if "project" in payload: return payload["project"]["name"].tame(check_string) if "repository" in payload: # Job Hook payloads don't have a `project` section, # but the repository name is accessible from the `repository` # section. return payload["repository"]["name"].tame(check_string) # Group-level events (e.g. group access token expiry) have neither # `project` nor `repository`, but have a `group` section. return payload["group"]["group_name"].tame(check_string) def get_user_mention(realm: Realm, username: str, fallback_name: str) -> str: external_account_field_name = str(DEFAULT_EXTERNAL_ACCOUNTS["gitlab"].name) zulip_user = guess_zulip_user_from_external_account( realm, username, external_account_field_name, external_username_case_insensitive=True ) if zulip_user is not None: return silent_mention_syntax_for_user(zulip_user) return fallback_name def get_user_name(payload: WildValue, realm: Realm) -> str: # Push and tag events don't have a `user` section. user_data = payload["user"] if "user" in payload else payload username = user_data["username" if "user" in payload else "user_username"].tame(check_string) full_name = user_data["name" if "user" in payload else "user_name"].tame(check_string) return get_user_mention(realm, username, full_name) def get_project_homepage(payload: WildValue) -> str: if "project" in payload: return payload["project"]["web_url"].tame(check_string) if "repository" in payload: return payload["repository"]["homepage"].tame(check_string) # Group-level events have a group path instead of a project URL. group_path = payload["group"]["full_path"].tame(check_string) return f"https://gitlab.com/groups/{group_path}" def get_branch_name(payload: WildValue) -> str: return payload["ref"].tame(check_string).replace("refs/heads/", "") def get_tag_name(payload: WildValue) -> str: return payload["ref"].tame(check_string).replace("refs/tags/", "") def get_object_url(payload: WildValue) -> str: return payload["object_attributes"]["url"].tame(check_string) def skip_previews(event: str) -> bool: # Add event names to this array for which previews should be skipped. return event in [ # Design events link to images, but the images cannot be # accessed without authentication, so trying to preview them # doesn't work. "Note Hook DesignManagement::Design", "Confidential Note Hook DesignManagement::Design", "Emoji Hook award", "Emoji Hook revoke", ] class EventFunction(Protocol): def __call__(self, payload: WildValue, include_title: bool, realm: Realm) -> str: ... EVENT_FUNCTION_MAPPER: dict[str, EventFunction] = { "Push Hook": get_push_event_body, "Tag Push Hook": get_tag_push_event_body, "Test Hook": get_test_event_body, "Issue Hook open": get_issue_created_event_body, "Issue Hook close": partial(get_issue_event_body, "closed"), "Issue Hook reopen": partial(get_issue_event_body, "reopened"), "Issue Hook update": partial(get_issue_event_body, "updated"), "Confidential Issue Hook open": get_issue_created_event_body, "Confidential Issue Hook close": partial(get_issue_event_body, "closed"), "Confidential Issue Hook reopen": partial(get_issue_event_body, "reopened"), "Confidential Issue Hook update": partial(get_issue_event_body, "updated"), "Note Hook Commit": get_commented_commit_event_body, "Note Hook MergeRequest": get_commented_merge_request_event_body, "Note Hook Issue": get_commented_issue_event_body, "Confidential Note Hook Issue": get_commented_issue_event_body, "Note Hook DesignManagement::Design": get_commented_design_event_body, "Confidential Note Hook DesignManagement::Design": get_commented_design_event_body, "Note Hook Snippet": get_commented_snippet_event_body, "Merge Request Hook approved": partial(get_merge_request_event_body, "approved"), "Merge Request Hook unapproved": partial(get_merge_request_event_body, "unapproved"), # approval and unapproval events are triggered only if there's more than one required approver # ref: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8742 "Merge Request Hook approval": partial(get_merge_request_event_body, "approval"), "Merge Request Hook unapproval": partial(get_merge_request_event_body, "unapproval"), "Merge Request Hook open": partial(get_merge_request_open_or_updated_body, "created"), "Merge Request Hook update": get_merge_request_updated_event_body, "Merge Request Hook merge": partial(get_merge_request_event_body, "merged"), "Merge Request Hook close": partial(get_merge_request_event_body, "closed"), "Merge Request Hook reopen": partial(get_merge_request_event_body, "reopened"), "Wiki Page Hook create": partial(get_wiki_page_event_body, "created"), "Wiki Page Hook update": partial(get_wiki_page_event_body, "updated"), "Job Hook": get_build_hook_event_body, "Build Hook": get_build_hook_event_body, "Pipeline Hook": get_pipeline_event_body, "Release Hook": get_release_event_body, "Feature Flag Hook": get_feature_flag_event_body, "Resource Access Token Hook": get_resource_access_token_expiry_event_body, "Deployment Hook": get_deployment_event_body, "Emoji Hook award": partial(get_emoji_event_body, "award"), "Emoji Hook revoke": partial(get_emoji_event_body, "revoke"), } ALL_EVENT_TYPES = list(EVENT_FUNCTION_MAPPER.keys()) @webhook_view("GitLab", all_event_types=ALL_EVENT_TYPES) @typed_endpoint def api_gitlab_webhook( request: HttpRequest, user_profile: UserProfile, *, payload: JsonBodyPayload[WildValue], branches: str | None = None, use_merge_request_title: Json[bool] = True, user_specified_topic: OptionalUserSpecifiedTopicStr = None, ignore_private_projects: Json[bool] = False, ) -> HttpResponse: event = get_event(request, payload, branches) # Ignore events from private projects if the URL option is set if ( "project" in payload and payload["project"]["visibility_level"].tame(check_int) == 0 and ignore_private_projects ): return json_success(request) if event is not None: event_body_function = get_body_based_on_event(event) body = event_body_function( payload, include_title=user_specified_topic is not None, realm=user_profile.realm, ) # Add a link to the project if a custom topic is set if user_specified_topic is not None: project_url = f"[{get_repo_name(payload)}]({get_project_homepage(payload)})" body = f"[{project_url}] {body}" topic_name = get_topic_based_on_event(event, payload, use_merge_request_title) check_send_webhook_message( request, user_profile, topic_name, body, event, no_previews=skip_previews(event) ) return json_success(request) def get_body_based_on_event(event: str) -> EventFunction: return EVENT_FUNCTION_MAPPER[event] def get_topic_based_on_event(event: str, payload: WildValue, use_merge_request_title: bool) -> str: if event == "Push Hook": return f"{get_repo_name(payload)} / {get_branch_name(payload)}" elif event in ("Job Hook", "Build Hook"): return "{} / {}".format( payload["repository"]["name"].tame(check_string), get_branch_name(payload) ) elif event == "Pipeline Hook": return "{} / {}".format( get_repo_name(payload), payload["object_attributes"]["ref"].tame(check_string).replace("refs/heads/", ""), ) elif event.startswith("Merge Request Hook"): return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="MR", id=payload["object_attributes"]["iid"].tame(check_int), title=( payload["object_attributes"]["title"].tame(check_string) if use_merge_request_title else "" ), ) elif event.startswith(("Issue Hook", "Confidential Issue Hook")): return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="issue", id=payload["object_attributes"]["iid"].tame(check_int), title=payload["object_attributes"]["title"].tame(check_string), ) elif event in ("Note Hook Issue", "Confidential Note Hook Issue"): return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="issue", id=payload["issue"]["iid"].tame(check_int), title=payload["issue"]["title"].tame(check_string), ) elif event in ( "Note Hook DesignManagement::Design", "Confidential Note Hook DesignManagement::Design", ): design_path = payload["object_attributes"]["position"]["new_path"].tame(check_string) design_name = design_path.split("/")[-1] return TOPIC_WITH_DESIGN_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="design", design_name=design_name, ) elif event == "Note Hook MergeRequest": return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="MR", id=payload["merge_request"]["iid"].tame(check_int), title=( payload["merge_request"]["title"].tame(check_string) if use_merge_request_title else "" ), ) elif event == "Note Hook Snippet": return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="snippet", id=payload["snippet"]["id"].tame(check_int), title=payload["snippet"]["title"].tame(check_string), ) elif event == "Resource Access Token Hook" and payload.get("group"): return payload["group"]["group_name"].tame(check_string) elif event == "Deployment Hook": return "{} / {}".format( get_repo_name(payload), payload["environment"].tame(check_string), ) elif event.startswith("Emoji Hook"): awardable_type = payload["object_attributes"]["awardable_type"].tame(check_string) transformed_type = get_emoji_event_transformed_type(payload, awardable_type) id = get_emoji_event_url_id(payload, transformed_type)[1] if transformed_type == "design": return TOPIC_WITH_DESIGN_INFO_TEMPLATE.format( repo=get_repo_name(payload), type="design", design_name=id ) return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format( repo=get_repo_name(payload), type=transformed_type, id=id, title=get_emoji_event_topic_title( payload, awardable_type, transformed_type, use_merge_request_title ), ) return get_repo_name(payload) def get_event(request: HttpRequest, payload: WildValue, branches: str | None) -> str | None: event = get_event_header(request, "X-GitLab-Event", "GitLab") if event == "System Hook": # Convert the event name to a GitLab event title if "event_name" in payload: event_name = payload["event_name"].tame(check_string) else: event_name = payload["object_kind"].tame(check_string) event = event_name.split("__")[0].replace("_", " ").title() event = f"{event} Hook" if event in ["Confidential Issue Hook", "Issue Hook", "Merge Request Hook", "Wiki Page Hook"]: action = payload["object_attributes"].get("action", "open").tame(check_string) event = f"{event} {action}" elif event in ["Confidential Note Hook", "Note Hook"]: action = payload["object_attributes"]["noteable_type"].tame(check_string) event = f"{event} {action}" elif event == "Push Hook" and branches is not None: branch = get_branch_name(payload) if not is_branch_name_notifiable(branch, branches): return None elif event == "Emoji Hook": action = payload["event_type"].tame(check_string) event = f"{event} {action}" if event in EVENT_FUNCTION_MAPPER: return event raise UnsupportedWebhookEventTypeError(event)