import os from collections.abc import Callable, Sequence from dataclasses import dataclass, field from typing import Any, TypeAlias from django.contrib.staticfiles.storage import staticfiles_storage from django.http import HttpRequest, HttpResponseBase from django.urls import URLPattern, path from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy from django.views.decorators.csrf import csrf_exempt from django_stubs_ext import StrPromise from zerver.lib.storage import static_path from zerver.lib.validator import check_bool from zerver.lib.webhooks.common import PresetUrlOption, WebhookConfigOption, WebhookUrlOption from zerver.webhooks import fixtureless_integrations """This module declares all of the (documented) integrations available in the Zulip server. The Integration class is used as part of generating the documentation on the /integrations/ page, while the WebhookIntegration class is also used to generate the URLs in `zproject/urls.py` for webhook integrations. To add a new non-webhook integration, add code to the INTEGRATIONS dictionary below. To add a new webhook integration, declare a WebhookIntegration in the WEBHOOK_INTEGRATIONS list below (it will be automatically added to INTEGRATIONS). To add a new integration category, add to either the CATEGORIES or META_CATEGORY dicts below. The META_CATEGORY dict is for categories that do not describe types of tools (e.g., bots or frameworks). Over time, we expect this registry to grow additional convenience features for writing and configuring integrations efficiently. """ OptionValidator: TypeAlias = Callable[[str, str], str | bool | None] META_CATEGORY: dict[str, StrPromise] = { "meta-integration": gettext_lazy("Integration frameworks"), "bots": gettext_lazy("Interactive bots"), } CATEGORIES: dict[str, StrPromise] = { **META_CATEGORY, "video-calling": gettext_lazy("Video calling"), "continuous-integration": gettext_lazy("Continuous integration"), "customer-support": gettext_lazy("Customer support"), "deployment": gettext_lazy("Deployment"), "entertainment": gettext_lazy("Entertainment"), "communication": gettext_lazy("Communication"), "financial": gettext_lazy("Financial"), "hr": gettext_lazy("Human resources"), "marketing": gettext_lazy("Marketing"), "misc": gettext_lazy("Miscellaneous"), "monitoring": gettext_lazy("Monitoring"), "project-management": gettext_lazy("Project management"), "productivity": gettext_lazy("Productivity"), "version-control": gettext_lazy("Version control"), } # Can also be computed from INTEGRATIONS by removing entries from # WEBHOOK_INTEGRATIONS and NO_SCREENSHOT_CONFIG, but defined explicitly to # avoid circular dependency FIXTURELESS_INTEGRATIONS_WITH_SCREENSHOTS: list[str] = [ "asana", "capistrano", "codebase", "discourse", "git", "github-actions", "google-calendar", "jenkins", "jira-plugin", "mastodon", "mercurial", "nagios", "notion", "openshift", "perforce", "puppet", "redmine", "rss", "svn", "trac", ] FIXTURELESS_SCREENSHOT_CONTENT: dict[str, list[fixtureless_integrations.ScreenshotContent]] = { key: [getattr(fixtureless_integrations, key.upper().replace("-", "_"))] for key in FIXTURELESS_INTEGRATIONS_WITH_SCREENSHOTS } class Integration: DEFAULT_LOGO_STATIC_PATH_PNG = "images/integrations/logos/{name}.png" DEFAULT_LOGO_STATIC_PATH_SVG = "images/integrations/logos/{name}.svg" DEFAULT_BOT_AVATAR_PATH = "images/integrations/bot_avatars/{name}.png" DEFAULT_DOC_PATH = "zerver/integrations/{name}.md" def __init__( self, name: str, categories: list[str], client_name: str | None = None, logo: str | None = None, secondary_line_text: str | None = None, display_name: str | None = None, doc: str | None = None, stream_name: str | None = None, legacy: bool = False, config_options: Sequence[WebhookConfigOption] = [], url_options: Sequence[WebhookUrlOption] = [], ) -> None: self.name = name self.client_name = client_name if client_name is not None else name self.secondary_line_text = secondary_line_text self.legacy = legacy self.doc = doc self.url_options = url_options # Note: Currently only incoming webhook type bots use this list for # defining how the bot's BotConfigData should be. Embedded bots follow # a different approach. self.config_options = config_options for category in categories: if category not in CATEGORIES: raise KeyError( # nocoverage "INTEGRATIONS: " + name + " - category '" + category + "' is not a key in CATEGORIES.", ) self.categories = [CATEGORIES[c] for c in categories] self.logo_path = logo if logo is not None else self.get_logo_path() # TODO: Enforce that all integrations have logo_url with an assertion. self.logo_url = self.get_logo_url() if display_name is None: display_name = name.title() self.display_name = display_name if doc is None: doc = self.DEFAULT_DOC_PATH.format(name=self.name) self.doc = doc if stream_name is None: stream_name = self.name self.stream_name = stream_name def is_enabled(self) -> bool: return True def get_logo_path(self) -> str | None: logo_file_path_svg = self.DEFAULT_LOGO_STATIC_PATH_SVG.format(name=self.name) logo_file_path_png = self.DEFAULT_LOGO_STATIC_PATH_PNG.format(name=self.name) if os.path.isfile(static_path(logo_file_path_svg)): return logo_file_path_svg elif os.path.isfile(static_path(logo_file_path_png)): return logo_file_path_png return None def get_bot_avatar_path(self) -> str | None: if self.logo_path is not None: name = os.path.splitext(os.path.basename(self.logo_path))[0] return self.DEFAULT_BOT_AVATAR_PATH.format(name=name) return None def get_logo_url(self) -> str | None: if self.logo_path is not None: return staticfiles_storage.url(self.logo_path) return None def get_translated_categories(self) -> list[str]: return [str(category) for category in self.categories] class BotIntegration(Integration): DEFAULT_LOGO_STATIC_PATH_PNG = "generated/bots/{name}/logo.png" DEFAULT_LOGO_STATIC_PATH_SVG = "generated/bots/{name}/logo.svg" ZULIP_LOGO_STATIC_PATH_PNG = "images/logo/zulip-icon-128x128.png" DEFAULT_DOC_PATH = "{name}/doc.md" def __init__( self, name: str, categories: list[str], logo: str | None = None, secondary_line_text: str | None = None, display_name: str | None = None, doc: str | None = None, ) -> None: super().__init__( name, client_name=name, categories=categories, secondary_line_text=secondary_line_text, ) if logo is None: self.logo_url = self.get_logo_url() if self.logo_url is None: # TODO: Add a test for this by initializing one in a test. logo = staticfiles_storage.url(self.ZULIP_LOGO_STATIC_PATH_PNG) # nocoverage else: self.logo_url = staticfiles_storage.url(logo) if display_name is None: display_name = f"{name.title()} Bot" # nocoverage else: display_name = f"{display_name} Bot" self.display_name = display_name if doc is None: doc = self.DEFAULT_DOC_PATH.format(name=name) self.doc = doc class PythonAPIIntegration(Integration): DEFAULT_DOC_PATH = "{directory_name}/doc.md" def __init__( self, name: str, categories: list[str], client_name: str | None = None, logo: str | None = None, secondary_line_text: str | None = None, display_name: str | None = None, directory_name: str | None = None, doc: str | None = None, stream_name: str | None = None, legacy: bool = False, ) -> None: if directory_name is None: directory_name = name self.directory_name = directory_name # Assign before super(), to use self.directory_name instead of self.name if doc is None: doc = self.DEFAULT_DOC_PATH.format(directory_name=self.directory_name) self.doc = doc super().__init__( name, categories, client_name=client_name, logo=logo, secondary_line_text=secondary_line_text, display_name=display_name, doc=doc, stream_name=stream_name, legacy=legacy, ) class WebhookIntegration(Integration): DEFAULT_FUNCTION_PATH = "zerver.webhooks.{dir_name}.view.api_{dir_name}_webhook" DEFAULT_URL = "api/v1/external/{name}" DEFAULT_CLIENT_NAME = "Zulip{name}Webhook" DEFAULT_DOC_PATH = "{name}/doc.md" def __init__( self, name: str, categories: list[str], client_name: str | None = None, logo: str | None = None, secondary_line_text: str | None = None, function: str | None = None, url: str | None = None, display_name: str | None = None, doc: str | None = None, stream_name: str | None = None, legacy: bool = False, config_options: Sequence[WebhookConfigOption] = [], url_options: Sequence[WebhookUrlOption] = [], dir_name: str | None = None, ) -> None: if client_name is None: client_name = self.DEFAULT_CLIENT_NAME.format(name=name.title()) super().__init__( name, categories, client_name=client_name, logo=logo, secondary_line_text=secondary_line_text, display_name=display_name, stream_name=stream_name, legacy=legacy, config_options=config_options, url_options=url_options, ) if dir_name is None: dir_name = self.name self.dir_name = dir_name if function is None: function = self.DEFAULT_FUNCTION_PATH.format(dir_name=dir_name) self.function_name = function if url is None: url = self.DEFAULT_URL.format(name=name) self.url = url if doc is None: doc = self.DEFAULT_DOC_PATH.format(name=name) self.doc = doc def get_function(self) -> Callable[[HttpRequest], HttpResponseBase]: return import_string(self.function_name) @csrf_exempt def view(self, request: HttpRequest) -> HttpResponseBase: # Lazily load the real view function to improve startup performance. function = self.get_function() assert function.csrf_exempt # type: ignore[attr-defined] # ensure the above @csrf_exempt is justified return function(request) @property def url_object(self) -> URLPattern: return path(self.url, self.view) def split_fixture_path(path: str) -> tuple[str, str]: path, fixture_name = os.path.split(path) fixture_name, _ = os.path.splitext(fixture_name) integration_name = os.path.split(os.path.dirname(path))[-1] return integration_name, fixture_name @dataclass class WebhookScreenshotConfig: fixture_name: str image_name: str = "001.png" image_dir: str | None = None bot_name: str | None = None payload_as_query_param: bool = False payload_param_name: str = "payload" extra_params: dict[str, str] = field(default_factory=dict) use_basic_auth: bool = False custom_headers: dict[str, str] = field(default_factory=dict) @dataclass class FixturelessScreenshotConfig: message: str topic: str channel: str | None = None image_name: str = "001.png" image_dir: str | None = None bot_name: str | None = None def get_fixture_path( integration: WebhookIntegration, screenshot_config: WebhookScreenshotConfig ) -> str: fixture_dir = os.path.join("zerver", "webhooks", integration.dir_name, "fixtures") fixture_path = os.path.join(fixture_dir, screenshot_config.fixture_name) return fixture_path def get_image_path( integration: Integration, screenshot_config: WebhookScreenshotConfig | FixturelessScreenshotConfig, ) -> str: image_dir = screenshot_config.image_dir or integration.name image_name = screenshot_config.image_name image_path = os.path.join("static/images/integrations", image_dir, image_name) return image_path class HubotIntegration(Integration): GIT_URL_TEMPLATE = "https://github.com/hubot-archive/hubot-{}" SECONDARY_LINE_TEXT = "(Hubot script)" DOC_PATH = "zerver/integrations/hubot_common.md" def __init__( self, name: str, categories: list[str], display_name: str | None = None, logo: str | None = None, git_url: str | None = None, legacy: bool = False, ) -> None: if git_url is None: git_url = self.GIT_URL_TEMPLATE.format(name) self.hubot_docs_url = git_url super().__init__( name, categories, logo=logo, secondary_line_text=self.SECONDARY_LINE_TEXT, display_name=display_name, doc=self.DOC_PATH, legacy=legacy, ) class EmbeddedBotIntegration(Integration): """ This class acts as a registry for bots verified as safe and valid such that these are capable of being deployed on the server. """ DEFAULT_CLIENT_NAME = "Zulip{name}EmbeddedBot" def __init__(self, name: str, *args: Any, **kwargs: Any) -> None: assert kwargs.get("client_name") is None kwargs["client_name"] = self.DEFAULT_CLIENT_NAME.format(name=name.title()) super().__init__(name, *args, **kwargs) EMBEDDED_BOTS: list[EmbeddedBotIntegration] = [ EmbeddedBotIntegration("converter", []), EmbeddedBotIntegration("encrypt", []), EmbeddedBotIntegration("helloworld", []), EmbeddedBotIntegration("virtual_fs", []), EmbeddedBotIntegration("giphy", []), EmbeddedBotIntegration("followup", []), ] WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [ WebhookIntegration("airbrake", ["monitoring"]), WebhookIntegration("airbyte", ["monitoring"]), WebhookIntegration( "alertmanager", ["monitoring"], display_name="Prometheus Alertmanager", logo="images/integrations/logos/prometheus.svg", ), WebhookIntegration("ansibletower", ["deployment"], display_name="Ansible Tower"), WebhookIntegration("appfollow", ["customer-support"], display_name="AppFollow"), WebhookIntegration("appveyor", ["continuous-integration"], display_name="AppVeyor"), WebhookIntegration( "azuredevops", ["version-control"], display_name="AzureDevOps", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration("beanstalk", ["version-control"], stream_name="commits"), WebhookIntegration("basecamp", ["project-management"]), WebhookIntegration("beeminder", ["misc"], display_name="Beeminder"), WebhookIntegration( "bitbucket3", ["version-control"], logo="images/integrations/logos/bitbucket.svg", display_name="Bitbucket Server", stream_name="bitbucket", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration( "bitbucket2", ["version-control"], logo="images/integrations/logos/bitbucket.svg", display_name="Bitbucket", stream_name="bitbucket", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration( "bitbucket", ["version-control"], display_name="Bitbucket", secondary_line_text="(Enterprise)", stream_name="commits", legacy=True, ), WebhookIntegration("buildbot", ["continuous-integration"]), WebhookIntegration("canarytoken", ["monitoring"], display_name="Thinkst Canarytokens"), WebhookIntegration("circleci", ["continuous-integration"], display_name="CircleCI"), WebhookIntegration("clubhouse", ["project-management"]), WebhookIntegration("codeship", ["continuous-integration", "deployment"]), WebhookIntegration("crashlytics", ["monitoring"]), WebhookIntegration("dialogflow", ["customer-support"]), WebhookIntegration("delighted", ["customer-support", "marketing"]), WebhookIntegration("dropbox", ["productivity"]), WebhookIntegration("errbit", ["monitoring"]), WebhookIntegration("flock", ["customer-support"]), WebhookIntegration("freshdesk", ["customer-support"]), WebhookIntegration("freshping", ["monitoring"]), WebhookIntegration("freshstatus", ["monitoring", "customer-support"]), WebhookIntegration("front", ["customer-support"]), WebhookIntegration( "gitea", ["version-control"], stream_name="commits", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration( "github", ["version-control"], display_name="GitHub", url_options=[ WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES), WebhookUrlOption.build_preset_config(PresetUrlOption.IGNORE_PRIVATE_REPOSITORIES), ], ), WebhookIntegration( "githubsponsors", ["financial"], display_name="GitHub Sponsors", logo="images/integrations/logos/github.svg", dir_name="github", doc="github/githubsponsors.md", stream_name="github", ), WebhookIntegration( "gitlab", ["version-control"], display_name="GitLab", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration("gocd", ["continuous-integration"], display_name="GoCD"), WebhookIntegration( "gogs", ["version-control"], stream_name="commits", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration("gosquared", ["marketing"], display_name="GoSquared"), WebhookIntegration("grafana", ["monitoring"]), WebhookIntegration("greenhouse", ["hr"]), WebhookIntegration("groove", ["customer-support"]), WebhookIntegration("harbor", ["deployment", "productivity"]), WebhookIntegration("hellosign", ["productivity", "hr"], display_name="HelloSign"), WebhookIntegration("helloworld", ["misc"], display_name="Hello World"), WebhookIntegration("heroku", ["deployment"]), WebhookIntegration("homeassistant", ["misc"], display_name="Home Assistant"), WebhookIntegration("ifttt", ["meta-integration"], display_name="IFTTT"), WebhookIntegration("insping", ["monitoring"]), WebhookIntegration("intercom", ["customer-support"]), # Avoid collision with jira-plugin's doc "jira/doc.md". WebhookIntegration("jira", ["project-management"], doc="jira/jira-doc.md"), WebhookIntegration("jotform", ["misc"]), WebhookIntegration("json", ["misc"], display_name="JSON formatter"), WebhookIntegration("librato", ["monitoring"]), WebhookIntegration("lidarr", ["entertainment"]), WebhookIntegration("linear", ["project-management"]), WebhookIntegration("mention", ["marketing"]), WebhookIntegration("netlify", ["continuous-integration", "deployment"]), WebhookIntegration("newrelic", ["monitoring"], display_name="New Relic"), WebhookIntegration("opencollective", ["financial"], display_name="Open Collective"), WebhookIntegration("openproject", ["project-management"], display_name="OpenProject"), WebhookIntegration("opensearch", ["monitoring"], display_name="OpenSearch"), WebhookIntegration( "opsgenie", ["meta-integration", "monitoring"], url_options=[ WebhookUrlOption( name="eu_region", label="Use Opsgenie's European service region", validator=check_bool, ) ], ), WebhookIntegration("pagerduty", ["monitoring"], display_name="PagerDuty"), WebhookIntegration("papertrail", ["monitoring"]), WebhookIntegration("patreon", ["financial"]), WebhookIntegration("pingdom", ["monitoring"]), WebhookIntegration("pivotal", ["project-management"], display_name="Pivotal Tracker"), WebhookIntegration("radarr", ["entertainment"]), WebhookIntegration("raygun", ["monitoring"]), WebhookIntegration("reviewboard", ["version-control"], display_name="Review Board"), WebhookIntegration( "rhodecode", ["version-control"], display_name="RhodeCode", url_options=[WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES)], ), WebhookIntegration("rundeck", ["deployment"]), WebhookIntegration("semaphore", ["continuous-integration", "deployment"]), WebhookIntegration("sentry", ["monitoring"]), WebhookIntegration( "slack_incoming", ["communication", "meta-integration"], display_name="Slack-compatible webhook", logo="images/integrations/logos/slack.svg", ), WebhookIntegration("slack", ["communication"]), WebhookIntegration("sonarqube", ["continuous-integration"], display_name="SonarQube"), WebhookIntegration("sonarr", ["entertainment"]), WebhookIntegration("splunk", ["monitoring"]), WebhookIntegration("statuspage", ["customer-support"]), WebhookIntegration("stripe", ["financial"]), WebhookIntegration("taiga", ["project-management"]), WebhookIntegration("teamcity", ["continuous-integration"]), WebhookIntegration("thinkst", ["monitoring"]), WebhookIntegration("transifex", ["misc"]), WebhookIntegration("travis", ["continuous-integration"], display_name="Travis CI"), WebhookIntegration("trello", ["project-management"]), WebhookIntegration("updown", ["monitoring"]), WebhookIntegration("uptimerobot", ["monitoring"], display_name="UptimeRobot"), WebhookIntegration("wekan", ["productivity"]), WebhookIntegration("wordpress", ["marketing"], display_name="WordPress"), WebhookIntegration("zapier", ["meta-integration"]), WebhookIntegration("zendesk", ["customer-support"]), WebhookIntegration("zabbix", ["monitoring"]), ] INTEGRATIONS: dict[str, Integration] = { "asana": Integration("asana", ["project-management"]), "big-blue-button": Integration( "big-blue-button", ["video-calling", "communication"], display_name="BigBlueButton" ), "capistrano": Integration("capistrano", ["deployment"], display_name="Capistrano"), "discourse": Integration("discourse", ["communication"]), "email": Integration("email", ["communication"]), "errbot": Integration("errbot", ["meta-integration", "bots"]), "giphy": Integration("giphy", ["misc"], display_name="GIPHY"), "github-actions": Integration( "github-actions", ["continuous-integration"], display_name="GitHub Actions", stream_name="github-actions updates", ), "hubot": Integration("hubot", ["meta-integration", "bots"]), "jenkins": Integration("jenkins", ["continuous-integration"]), "jitsi": Integration("jitsi", ["video-calling", "communication"], display_name="Jitsi Meet"), "mastodon": Integration("mastodon", ["communication"]), "notion": Integration("notion", ["productivity"]), "onyx": Integration("onyx", ["productivity"], logo="images/integrations/logos/onyx.png"), "puppet": Integration("puppet", ["deployment"]), "redmine": Integration("redmine", ["project-management"]), "zoom": Integration("zoom", ["video-calling", "communication"]), } PYTHON_API_INTEGRATIONS: list[PythonAPIIntegration] = [ PythonAPIIntegration("codebase", ["version-control"]), PythonAPIIntegration("git", ["version-control"], stream_name="commits"), PythonAPIIntegration( "google-calendar", ["productivity"], display_name="Google Calendar", directory_name="google" ), PythonAPIIntegration( "irc", ["communication"], display_name="IRC", directory_name="bridge_with_irc" ), PythonAPIIntegration( "jira-plugin", ["project-management"], logo="images/integrations/logos/jira.svg", secondary_line_text="(locally installed)", display_name="Jira", directory_name="jira", stream_name="jira", legacy=True, ), PythonAPIIntegration("matrix", ["communication"], directory_name="bridge_with_matrix"), PythonAPIIntegration( "mercurial", ["version-control"], display_name="Mercurial (hg)", stream_name="commits", directory_name="hg", ), PythonAPIIntegration("nagios", ["monitoring"]), PythonAPIIntegration( "openshift", ["deployment"], display_name="OpenShift", stream_name="deployments" ), PythonAPIIntegration("perforce", ["version-control"]), PythonAPIIntegration("rss", ["communication"], display_name="RSS"), PythonAPIIntegration("svn", ["version-control"], display_name="Subversion"), PythonAPIIntegration("trac", ["project-management"]), PythonAPIIntegration( "twitter", ["customer-support", "marketing"], # _ needed to get around adblock plus logo="images/integrations/logos/twitte_r.svg", ), ] BOT_INTEGRATIONS: list[BotIntegration] = [ BotIntegration("github_detail", ["version-control", "bots"], display_name="GitHub Detail"), BotIntegration( "xkcd", ["bots", "misc"], display_name="xkcd", logo="images/integrations/logos/xkcd.png" ), ] HUBOT_INTEGRATIONS: list[HubotIntegration] = [ HubotIntegration("assembla", ["version-control", "project-management"]), HubotIntegration("bonusly", ["hr"]), HubotIntegration("chartbeat", ["marketing"]), HubotIntegration("darksky", ["misc"], display_name="Dark Sky"), HubotIntegration( "instagram", ["misc"], # _ needed to get around adblock plus logo="images/integrations/logos/instagra_m.svg", ), HubotIntegration("mailchimp", ["communication", "marketing"]), HubotIntegration("google-translate", ["misc"], display_name="Google Translate"), HubotIntegration( "youtube", ["misc"], display_name="YouTube", # _ needed to get around adblock plus logo="images/integrations/logos/youtub_e.svg", ), ] for python_api_integration in PYTHON_API_INTEGRATIONS: INTEGRATIONS[python_api_integration.name] = python_api_integration for hubot_integration in HUBOT_INTEGRATIONS: INTEGRATIONS[hubot_integration.name] = hubot_integration for webhook_integration in WEBHOOK_INTEGRATIONS: INTEGRATIONS[webhook_integration.name] = webhook_integration for bot_integration in BOT_INTEGRATIONS: INTEGRATIONS[bot_integration.name] = bot_integration # Add webhook integrations that don't have automated screenshots here NO_SCREENSHOT_WEBHOOKS = ( # FIXME: fixture's goal.losedate needs to be modified dynamically {"beeminder"} # Meta integrations - Docs won't have a screenshot | {"ifttt", "slack_incoming", "zapier"} ) hubot_integration_names = {integration.name for integration in HUBOT_INTEGRATIONS} # Add fixtureless integrations that don't have automated screenshots here NO_SCREENSHOT_CONFIG = ( # Outgoing integrations - Docs won't have a screenshot {"email", "onyx"} # Video call integrations - Docs won't have a screenshot | {"big-blue-button", "jitsi", "zoom"} # Integrations that require screenshots of message threads - support is yet to be added | { "errbot", "github_detail", "hubot", "irc", # Also requires a screenshot on the Matrix side of the bridge "matrix", "xkcd", } | { # Doc doesn't have a screenshot "giphy", # the integration is planned to be removed "twitter", } | NO_SCREENSHOT_WEBHOOKS | hubot_integration_names ) WEBHOOK_SCREENSHOT_CONFIG: dict[str, list[WebhookScreenshotConfig]] = { "airbrake": [WebhookScreenshotConfig("error_message.json")], "airbyte": [WebhookScreenshotConfig("airbyte_job_payload_success.json")], "alertmanager": [ WebhookScreenshotConfig("alert.json", extra_params={"name": "topic", "desc": "description"}) ], "ansibletower": [WebhookScreenshotConfig("job_successful_multiple_hosts.json")], "appfollow": [WebhookScreenshotConfig("review.json")], "appveyor": [WebhookScreenshotConfig("appveyor_build_success.json")], "azuredevops": [WebhookScreenshotConfig("code_push.json")], "basecamp": [WebhookScreenshotConfig("doc_active.json")], "beanstalk": [ WebhookScreenshotConfig( "git_multiple.json", use_basic_auth=True, payload_as_query_param=True ) ], # 'beeminder': [WebhookScreenshotConfig('derail_worried.json')], "bitbucket": [ WebhookScreenshotConfig( "push.json", "002.png", use_basic_auth=True, payload_as_query_param=True ) ], "bitbucket2": [ WebhookScreenshotConfig( "issue_created.json", "003.png", "bitbucket", bot_name="Bitbucket Bot" ) ], "bitbucket3": [ WebhookScreenshotConfig( "repo_push_update_single_branch.json", "004.png", "bitbucket", bot_name="Bitbucket Server Bot", ) ], "buildbot": [WebhookScreenshotConfig("started.json")], "canarytoken": [WebhookScreenshotConfig("canarytoken_real.json")], "circleci": [WebhookScreenshotConfig("github_job_completed.json")], "clubhouse": [WebhookScreenshotConfig("story_create.json")], "codeship": [WebhookScreenshotConfig("error_build.json")], "crashlytics": [WebhookScreenshotConfig("issue_message.json")], "delighted": [WebhookScreenshotConfig("survey_response_updated_promoter.json")], "dialogflow": [ WebhookScreenshotConfig("weather_app.json", extra_params={"email": "iago@zulip.com"}) ], "dropbox": [WebhookScreenshotConfig("file_updated.json")], "errbit": [WebhookScreenshotConfig("error_message.json")], "flock": [WebhookScreenshotConfig("messages.json")], "freshdesk": [ WebhookScreenshotConfig("ticket_created.json", image_name="004.png", use_basic_auth=True) ], "freshping": [WebhookScreenshotConfig("freshping_check_unreachable.json")], "freshstatus": [WebhookScreenshotConfig("freshstatus_incident_open.json")], "front": [WebhookScreenshotConfig("inbound_message.json")], "gitea": [WebhookScreenshotConfig("pull_request__merged.json")], "github": [WebhookScreenshotConfig("push__1_commit.json")], "githubsponsors": [WebhookScreenshotConfig("created.json")], "gitlab": [WebhookScreenshotConfig("push_hook__push_local_branch_without_commits.json")], "gocd": [WebhookScreenshotConfig("pipeline_with_mixed_job_result.json")], "gogs": [WebhookScreenshotConfig("pull_request__opened.json")], "gosquared": [WebhookScreenshotConfig("traffic_spike.json")], "grafana": [WebhookScreenshotConfig("alert_values_v11.json")], "greenhouse": [WebhookScreenshotConfig("candidate_stage_change.json")], "groove": [WebhookScreenshotConfig("ticket_started.json")], "harbor": [WebhookScreenshotConfig("scanning_completed.json")], "hellosign": [ WebhookScreenshotConfig( "signatures_signed_by_one_signatory.json", payload_as_query_param=True, payload_param_name="json", ) ], "helloworld": [WebhookScreenshotConfig("hello.json")], "heroku": [WebhookScreenshotConfig("deploy.txt")], "homeassistant": [WebhookScreenshotConfig("reqwithtitle.json")], "insping": [WebhookScreenshotConfig("website_state_available.json")], "intercom": [WebhookScreenshotConfig("conversation_admin_replied.json")], "jira": [WebhookScreenshotConfig("created_v1.json")], "jotform": [WebhookScreenshotConfig("screenshot_response.multipart")], "json": [WebhookScreenshotConfig("json_github_push__1_commit.json")], "librato": [ WebhookScreenshotConfig("three_conditions_alert.json", payload_as_query_param=True) ], "lidarr": [WebhookScreenshotConfig("lidarr_album_grabbed.json")], "linear": [WebhookScreenshotConfig("issue_create_complex.json")], "mention": [WebhookScreenshotConfig("webfeeds.json")], "netlify": [WebhookScreenshotConfig("deploy_building.json")], "newrelic": [WebhookScreenshotConfig("incident_activated_new_default_payload.json")], "opencollective": [WebhookScreenshotConfig("one_time_donation.json")], "openproject": [WebhookScreenshotConfig("project_created__without_parent.json")], "opensearch": [WebhookScreenshotConfig("example_template.txt")], "opsgenie": [WebhookScreenshotConfig("addrecipient.json")], "pagerduty": [WebhookScreenshotConfig("trigger_v2.json")], "papertrail": [WebhookScreenshotConfig("short_post.json", payload_as_query_param=True)], "patreon": [WebhookScreenshotConfig("members_pledge_create.json")], "pingdom": [WebhookScreenshotConfig("http_up_to_down.json")], "pivotal": [WebhookScreenshotConfig("v5_type_changed.json")], "radarr": [WebhookScreenshotConfig("radarr_movie_grabbed.json")], "raygun": [WebhookScreenshotConfig("new_error.json")], "reviewboard": [WebhookScreenshotConfig("review_request_published.json")], "rhodecode": [WebhookScreenshotConfig("push.json")], "rundeck": [WebhookScreenshotConfig("start.json")], "semaphore": [WebhookScreenshotConfig("pull_request.json")], "sentry": [WebhookScreenshotConfig("event_for_exception_python.json")], "slack": [WebhookScreenshotConfig("message_with_normal_text.json")], "sonarqube": [WebhookScreenshotConfig("error.json")], "sonarr": [WebhookScreenshotConfig("sonarr_episode_grabbed.json")], "splunk": [WebhookScreenshotConfig("search_one_result.json")], "statuspage": [WebhookScreenshotConfig("incident_created.json")], "stripe": [WebhookScreenshotConfig("charge_succeeded__card.json")], "taiga": [WebhookScreenshotConfig("userstory_changed_status.json")], "teamcity": [WebhookScreenshotConfig("success.json")], "thinkst": [WebhookScreenshotConfig("canary_consolidated_port_scan.json")], "transifex": [ WebhookScreenshotConfig( "", extra_params={ "project": "Zulip Mobile", "language": "en", "resource": "file", "event": "review_completed", "reviewed": "100", }, ) ], "travis": [WebhookScreenshotConfig("build.json", payload_as_query_param=True)], "trello": [WebhookScreenshotConfig("adding_comment_to_card.json")], "updown": [WebhookScreenshotConfig("check_multiple_events.json")], "uptimerobot": [WebhookScreenshotConfig("uptimerobot_monitor_up.json")], "wekan": [WebhookScreenshotConfig("add_comment.json")], "wordpress": [WebhookScreenshotConfig("publish_post.txt", "wordpress_post_created.png")], "zabbix": [WebhookScreenshotConfig("zabbix_alert.json")], "zendesk": [ WebhookScreenshotConfig( "", use_basic_auth=True, extra_params={ "ticket_title": "Hardware Ecosystem Compatibility Inquiry", "ticket_id": "4837", "message": "Hi, I am planning to purchase the X5000 smartphone and want to ensure compatibility with my existing devices - WDX10 wireless earbuds and Z600 smartwatch. Are there any known issues?", }, ) ], } FIXTURELESS_SCREENSHOT_CONFIG: dict[str, list[FixturelessScreenshotConfig]] = {} for integration, screenshots_contents in FIXTURELESS_SCREENSHOT_CONTENT.items(): FIXTURELESS_SCREENSHOT_CONFIG[integration] = [ FixturelessScreenshotConfig(screenshot_content["content"], screenshot_content["topic"]) for screenshot_content in screenshots_contents ] FIXTURELESS_SCREENSHOT_CONFIG_OPTIONAL_FIELDS = { "mercurial": {"image_dir": "hg"}, "jenkins": {"image_name": "004.png"}, "google-calendar": {"image_name": "003.png", "image_dir": "google/calendar"}, } for integration, fields in FIXTURELESS_SCREENSHOT_CONFIG_OPTIONAL_FIELDS.items(): assert integration in FIXTURELESS_SCREENSHOT_CONFIG for field_name, value in fields.items(): # Assume a single screenshot config for each integration setattr(FIXTURELESS_SCREENSHOT_CONFIG[integration][0], field_name, value) DOC_SCREENSHOT_CONFIG: dict[ str, list[WebhookScreenshotConfig] | list[FixturelessScreenshotConfig] ] = { **WEBHOOK_SCREENSHOT_CONFIG, **FIXTURELESS_SCREENSHOT_CONFIG, } def get_all_event_types_for_integration(integration: Integration) -> list[str] | None: integration = INTEGRATIONS[integration.name] if isinstance(integration, WebhookIntegration): if integration.name == "githubsponsors": return import_string("zerver.webhooks.github.view.SPONSORS_EVENT_TYPES") function = integration.get_function() if hasattr(function, "_all_event_types"): return function._all_event_types return None