Repository: zulip/zulip

Issue:
Create incoming webhook integration for Github Sponsors
Create an [incoming webhook](https://zulip.com/api/incoming-webhooks-overview) integration for Github Sponsors. The main use case is getting notifications when new sponsors sign up.

Github Sponsors webhooks documentation: https://docs.github.com/en/sponsors/integrating-with-github-sponsors/configuring-webhooks-for-events-in-your-sponsored-account

Now that [Zulip has a Github Sponsors page](https://github.com/sponsors/zulip), it would be great to be able to monitor new sign-ups!


Discussion:
Hello @zulip/server-integrations members, this issue was labeled with the "area: integrations" label, so you may want to check it out!

<!-- areaLabelAddition -->
@zulipbot claim

Welcome to Zulip, @akshat1499! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
@akshat1499  can I take this issue.
yes please @ritikchauhan-01 
@zulipbot claim
Welcome to Zulip, @Avi-sa! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
@zulipbot abandon
@zulipbot claim
Welcome to Zulip, @SirAzako! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
Hello @siddhant141, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot claim
@zulipbot claim
@timabbott @alya 

I'm getting this error and I don't know how to resolve this. 

ERROR: Some URLs are untested!  Here's the list of untested URLs:
   api/v1/external/githubsponsors
Error: Process completed with exit code 1.

I ran the following command : ./tools/create-test-api-docs
This hasn't resolved the error. 

@zulipbot claim
@zulipbot claim
Welcome to Zulip, @siddhant141! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!

@zulipbot claim
Hello @profgrammer, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot claim
Welcome to Zulip, @iamu985! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!

Hello @iamu985, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot claim
Hi @iamu985 , are you still working on this pr? If not can you please close it? I'd like to work on this one.
@akshi2000 There is no need to close existing PRs. (CC  @iamu985)

As noted in the [contributor guide](https://zulip.readthedocs.io/en/latest/overview/contributing.html#common-questions):

> What if there is already a pull request for the issue I want to work on? Start by reviewing the existing work. If you agree with the approach, you can use the existing pull request (PR) as a starting point for your contribution. If you think a different approach is needed, you can post a new PR, with a comment that clearly explains why you decided to start from scratch.


Okay, Thank you @alya, I'll take a look at the existing PR and see what can be done.
Hi @alya, I have submitted a PR. Some feedback on it would be really helpful. Can you please have a look at it? Thank you!
Hello @akshi2000, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!


Hello @alya , Is this issue still been worked upon or it's done.


If an issue is open, then it's not done. However, note the [comment](https://github.com/zulip/zulip/issues/18320#issuecomment-1016765667) above about how to approach an issue with open PRs.
@zulipbot claim
Hello @B1TSH3LL, it looks like you've currently claimed 1 issue in this repository. We encourage new contributors to focus their efforts on at most 1 issue at a time, so please complete your work on your other claimed issues before trying to claim this issue again.

We look forward to your valuable contributions!

@zulipbot claim
Welcome to Zulip, @qiqistyle! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!

@zulipbot claim
@qiqistyle You have been unassigned from this issue because you have not made any updates for over 14 days. Please feel free to reclaim the issue if you decide to pick up again. Thanks!

@zulipbot  claim
Welcome to Zulip, @Fahdmoh01! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!

@zulipbot abandon
@zulipbot  claim 
Welcome to Zulip, @jo-reko! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!

@jo-reko You have been unassigned from this issue because you have not made any updates for over 14 days. Please feel free to reclaim the issue if you decide to pick up again. Thanks!

@zulipbot claim
@drrosa You have been unassigned from this issue because you have not made any updates for over 14 days. Please feel free to reclaim the issue if you decide to pick up again. Thanks!

@zulipbot claim
@drrosa You have been unassigned from this issue because you have not made any updates for over 14 days. Please feel free to reclaim the issue if you decide to pick up again. Thanks!

@zulipbot claim

Resolving patch:
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py
--- a/zerver/lib/integrations.py
+++ b/zerver/lib/integrations.py
@@ -197,6 +197,7 @@ def __init__(
         stream_name: Optional[str] = None,
         legacy: bool = False,
         config_options: Sequence[Tuple[str, str, OptionValidator]] = [],
+        dir_name: Optional[str] = None,
     ) -> None:
         if client_name is None:
             client_name = self.DEFAULT_CLIENT_NAME.format(name=name.title())
@@ -229,9 +230,12 @@ def __init__(
 
         if doc is None:
             doc = self.DEFAULT_DOC_PATH.format(name=name, ext="md")
-
         self.doc = doc
 
+        if dir_name is None:
+            dir_name = self.name
+        self.dir_name = dir_name
+
     @property
     def url_object(self) -> URLResolver:
         assert self.function is not None
@@ -266,7 +270,7 @@ def get_fixture_and_image_paths(
     integration: Integration, screenshot_config: BaseScreenshotConfig
 ) -> Tuple[str, str]:
     if isinstance(integration, WebhookIntegration):
-        fixture_dir = os.path.join("zerver", "webhooks", integration.name, "fixtures")
+        fixture_dir = os.path.join("zerver", "webhooks", integration.dir_name, "fixtures")
     else:
         fixture_dir = os.path.join("zerver", "integration_fixtures", integration.name)
     fixture_path = os.path.join(fixture_dir, screenshot_config.fixture_name)
@@ -401,6 +405,16 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
         function="zerver.webhooks.github.view.api_github_webhook",
         stream_name="github",
     ),
+    WebhookIntegration(
+        "githubsponsors",
+        ["financial"],
+        display_name="GitHub Sponsors",
+        logo="images/integrations/logos/github.svg",
+        dir_name="github",
+        function="zerver.webhooks.github.view.api_github_webhook",
+        doc="github/githubsponsors.md",
+        stream_name="github",
+    ),
     WebhookIntegration("gitlab", ["version-control"], display_name="GitLab"),
     WebhookIntegration("gocd", ["continuous-integration"], display_name="GoCD"),
     WebhookIntegration("gogs", ["version-control"], stream_name="commits"),
@@ -739,6 +753,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
     "gci": [ScreenshotConfig("task_abandoned_by_student.json")],
     "gitea": [ScreenshotConfig("pull_request__merged.json")],
     "github": [ScreenshotConfig("push__1_commit.json")],
+    "githubsponsors": [ScreenshotConfig("created.json")],
     "gitlab": [ScreenshotConfig("push_hook__push_local_branch_without_commits.json")],
     "gocd": [ScreenshotConfig("pipeline.json")],
     "gogs": [ScreenshotConfig("pull_request__opened.json")],
@@ -836,8 +851,9 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
 
 def get_all_event_types_for_integration(integration: Integration) -> Optional[List[str]]:
     integration = INTEGRATIONS[integration.name]
-    if isinstance(integration, WebhookIntegration) and hasattr(
-        integration.function, "_all_event_types"
-    ):
-        return integration.function._all_event_types
+    if isinstance(integration, WebhookIntegration):
+        if integration.name == "githubsponsors":
+            return import_string("zerver.webhooks.github.view.SPONSORS_EVENT_TYPES")
+        if hasattr(integration.function, "_all_event_types"):
+            return integration.function._all_event_types
     return None
diff --git a/zerver/webhooks/github/view.py b/zerver/webhooks/github/view.py
--- a/zerver/webhooks/github/view.py
+++ b/zerver/webhooks/github/view.py
@@ -1,4 +1,5 @@
 import re
+from datetime import datetime, timezone
 from typing import Callable, Dict, Optional
 
 from django.http import HttpRequest, HttpResponse
@@ -637,6 +638,82 @@ def get_ping_body(helper: Helper) -> str:
     return get_setup_webhook_message("GitHub", get_sender_name(payload))
 
 
+def get_cancelled_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name} cancelled their {subscription} subscription."
+    return template.format(
+        user_name=get_sender_name(payload),
+        subscription=get_subscription(payload),
+    ).rstrip()
+
+
+def get_created_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name} subscribed for {subscription}."
+    return template.format(
+        user_name=get_sender_name(payload),
+        subscription=get_subscription(payload),
+    ).rstrip()
+
+
+def get_edited_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name} changed who can see their sponsorship from {prior_privacy_level} to {privacy_level}."
+    return template.format(
+        user_name=get_sender_name(payload),
+        prior_privacy_level=payload["changes"]["privacy_level"]["from"].tame(check_string),
+        privacy_level=payload["sponsorship"]["privacy_level"].tame(check_string),
+    ).rstrip()
+
+
+def get_pending_cancellation_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name}'s {subscription} subscription will be cancelled on {effective_date}."
+    return template.format(
+        user_name=get_sender_name(payload),
+        subscription=get_subscription(payload),
+        effective_date=get_effective_date(payload),
+    ).rstrip()
+
+
+def get_pending_tier_change_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name}'s subscription will change from {prior_subscription} to {subscription} on {effective_date}."
+    return template.format(
+        user_name=get_sender_name(payload),
+        prior_subscription=get_prior_subscription(payload),
+        subscription=get_subscription(payload),
+        effective_date=get_effective_date(payload),
+    ).rstrip()
+
+
+def get_tier_changed_body(helper: Helper) -> str:
+    payload = helper.payload
+    template = "{user_name} changed their subscription from {prior_subscription} to {subscription}."
+    return template.format(
+        user_name=get_sender_name(payload),
+        prior_subscription=get_prior_subscription(payload),
+        subscription=get_subscription(payload),
+    ).rstrip()
+
+
+def get_subscription(payload: WildValue) -> str:
+    return payload["sponsorship"]["tier"]["name"].tame(check_string)
+
+
+def get_effective_date(payload: WildValue) -> str:
+    effective_date = payload["effective_date"].tame(check_string)[:10]
+    return (
+        datetime.strptime(effective_date, "%Y-%m-%d")
+        .replace(tzinfo=timezone.utc)
+        .strftime("%B %d, %Y")
+    )
+
+
+def get_prior_subscription(payload: WildValue) -> str:
+    return payload["changes"]["tier"]["from"]["name"].tame(check_string)
+
+
 def get_repository_name(payload: WildValue) -> str:
     return payload["repository"]["name"].tame(check_string)
 
@@ -728,6 +805,8 @@ def get_topic_based_on_type(payload: WildValue, event: str) -> str:
             number=payload["discussion"]["number"].tame(check_int),
             title=payload["discussion"]["title"].tame(check_string),
         )
+    elif event in SPONSORS_EVENT_TYPES:
+        return "sponsors"
 
     return get_repository_name(payload)
 
@@ -770,8 +849,23 @@ def get_topic_based_on_type(payload: WildValue, event: str) -> str:
     "team": get_team_body,
     "team_add": get_add_team_body,
     "watch": get_watch_body,
+    "cancelled": get_cancelled_body,
+    "created": get_created_body,
+    "edited": get_edited_body,
+    "pending_cancellation": get_pending_cancellation_body,
+    "pending_tier_change": get_pending_tier_change_body,
+    "tier_changed": get_tier_changed_body,
 }
 
+SPONSORS_EVENT_TYPES = [
+    "cancelled",
+    "created",
+    "edited",
+    "pending_cancellation",
+    "pending_tier_change",
+    "tier_changed",
+]
+
 IGNORED_EVENTS = [
     "check_suite",
     "label",


Test patch:
diff --git a/zerver/webhooks/github/tests.py b/zerver/webhooks/github/tests.py
--- a/zerver/webhooks/github/tests.py
+++ b/zerver/webhooks/github/tests.py
@@ -13,6 +13,7 @@
 TOPIC_BRANCH = "public-repo / changes"
 TOPIC_WIKI = "public-repo / wiki pages"
 TOPIC_DISCUSSION = "testing-gh discussion #20: Lets discuss"
+TOPIC_SPONSORS = "sponsors"
 
 
 class GitHubWebhookTest(WebhookTestCase):
@@ -591,3 +592,59 @@ def test_discussion_comment_msg_with_custom_topic_in_url(self) -> None:
     def test_discussion_comment_edited_msg(self) -> None:
         expected_message = "sbansal1999 edited a [comment](https://github.com/sbansal1999/testing-gh/discussions/20#discussioncomment-6332416) on [discussion #20](https://github.com/sbansal1999/testing-gh/discussions/20):\n\n~~~ quote\nsome random comment edited\n~~~"
         self.check_webhook("discussion_comment__edited", TOPIC_DISCUSSION, expected_message)
+
+
+class GitHubSponsorsHookTests(WebhookTestCase):
+    STREAM_NAME = "github"
+    URL_TEMPLATE = "/api/v1/external/githubsponsors?stream={stream}&api_key={api_key}"
+    WEBHOOK_DIR_NAME = "github"
+
+    def test_cancelled_message(self) -> None:
+        expected_message = "monalisa cancelled their $5 a month subscription."
+        self.check_webhook(
+            "cancelled",
+            TOPIC_SPONSORS,
+            expected_message,
+        )
+
+    def test_created_message(self) -> None:
+        expected_message = "monalisa subscribed for $5 a month."
+        self.check_webhook(
+            "created",
+            TOPIC_SPONSORS,
+            expected_message,
+        )
+
+    def test_pending_cancellation_message(self) -> None:
+        expected_message = (
+            "monalisa's $5 a month subscription will be cancelled on January 05, 2020."
+        )
+        self.check_webhook(
+            "pending_cancellation",
+            TOPIC_SPONSORS,
+            expected_message,
+        )
+
+    def test_pending_tier_change_message(self) -> None:
+        expected_message = "monalisa's subscription will change from $10 a month to $5 a month on December 30, 2019."
+        self.check_webhook(
+            "pending_tier_change",
+            TOPIC_SPONSORS,
+            expected_message,
+        )
+
+    def test_tier_changed_message(self) -> None:
+        expected_message = "monalisa changed their subscription from $10 a month to $5 a month."
+        self.check_webhook(
+            "tier_changed",
+            TOPIC_SPONSORS,
+            expected_message,
+        )
+
+    def test_edited_message(self) -> None:
+        expected_message = "monalisa changed who can see their sponsorship from public to private."
+        self.check_webhook(
+            "edited",
+            TOPIC_SPONSORS,
+            expected_message,
+        )