integrations: Create incoming webhook for Patreon.

Creates an incoming webhook integration for Patreon. The main
use case is getting notifications when new patrons sign up.

Fixes #18321.

Co-authored-by: Hari Prashant Bhimaraju <haripb01@gmail.com>
Co-authored-by: Sudipto Mondal <sudipto.mondal1997@gmail.com>
This commit is contained in:
David Rosa 2024-01-17 13:56:18 -08:00 committed by Tim Abbott
parent 26536a20ed
commit 1e4f5c6433
15 changed files with 1254 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -441,6 +441,7 @@ WEBHOOK_INTEGRATIONS: List[WebhookIntegration] = [
WebhookIntegration("opsgenie", ["meta-integration", "monitoring"]), WebhookIntegration("opsgenie", ["meta-integration", "monitoring"]),
WebhookIntegration("pagerduty", ["monitoring"], display_name="PagerDuty"), WebhookIntegration("pagerduty", ["monitoring"], display_name="PagerDuty"),
WebhookIntegration("papertrail", ["monitoring"]), WebhookIntegration("papertrail", ["monitoring"]),
WebhookIntegration("patreon", ["financial"], display_name="Patreon"),
WebhookIntegration("pingdom", ["monitoring"]), WebhookIntegration("pingdom", ["monitoring"]),
WebhookIntegration("pivotal", ["project-management"], display_name="Pivotal Tracker"), WebhookIntegration("pivotal", ["project-management"], display_name="Pivotal Tracker"),
WebhookIntegration("radarr", ["entertainment"], display_name="Radarr"), WebhookIntegration("radarr", ["entertainment"], display_name="Radarr"),
@ -777,6 +778,7 @@ DOC_SCREENSHOT_CONFIG: Dict[str, List[BaseScreenshotConfig]] = {
"opsgenie": [ScreenshotConfig("addrecipient.json", image_name="000.png")], "opsgenie": [ScreenshotConfig("addrecipient.json", image_name="000.png")],
"pagerduty": [ScreenshotConfig("trigger_v2.json")], "pagerduty": [ScreenshotConfig("trigger_v2.json")],
"papertrail": [ScreenshotConfig("short_post.json", payload_as_query_param=True)], "papertrail": [ScreenshotConfig("short_post.json", payload_as_query_param=True)],
"patreon": [ScreenshotConfig("members_pledge_create.json")],
"pingdom": [ScreenshotConfig("http_up_to_down.json", image_name="002.png")], "pingdom": [ScreenshotConfig("http_up_to_down.json", image_name="002.png")],
"pivotal": [ScreenshotConfig("v5_type_changed.json")], "pivotal": [ScreenshotConfig("v5_type_changed.json")],
"radarr": [ScreenshotConfig("radarr_movie_grabbed.json")], "radarr": [ScreenshotConfig("radarr_movie_grabbed.json")],

View File

View File

@ -0,0 +1,22 @@
Get Patreon notifications in Zulip!
1. {!create-stream.md!}
1. {!create-an-incoming-webhook.md!}
1. {!generate-integration-url.md!}
You can refer to Patreons's documentation for
[triggers](https://docs.patreon.com/#triggers-v2) and
[webhook events](https://docs.patreon.com/#webhooks).
1. Login to your Patreon developer account, navigate to your Patreon Portal, and
click on [My Webhooks](https://www.patreon.com/portal/registration/register-webhooks).
1. Paste the URL constructed above in the webhook URL field, and click the
**plus** (**+**) button. Enable the events you would like to receive
notifications for, and click **Send Test**.
{!congrats.md!}
![](/static/images/integrations/patreon/001.png)

View File

@ -0,0 +1,104 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 0,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": null,
"last_charge_status": null,
"lifetime_support_cents": 0,
"next_charge_date": null,
"note": "",
"patron_status": null,
"pledge_cadence": null,
"pledge_relationship_start": "2023-12-29T02:59:46.036+00:00",
"will_pay_amount_cents": 0
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": []
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 0,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,144 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 0,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": null,
"last_charge_status": null,
"lifetime_support_cents": 0,
"next_charge_date": null,
"note": "",
"patron_status": null,
"pledge_cadence": null,
"pledge_relationship_start": "2023-12-29T02:38:24.943+00:00",
"will_pay_amount_cents": 0
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": [
{
"id": "21926168",
"type": "tier"
}
]
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 0,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
},
{
"attributes": {
"amount_cents": 0,
"created_at": "2023-12-29T01:11:16.871+00:00",
"description": "",
"discord_role_ids": null,
"edited_at": "2023-12-29T01:11:16.871+00:00",
"image_url": null,
"patron_count": 0,
"post_count": 0,
"published": true,
"published_at": "2023-12-29T01:11:16.871+00:00",
"remaining": null,
"requires_shipping": false,
"title": "Free",
"unpublished_at": null,
"url": "/checkout/11539233?rid=21926168",
"user_limit": null
},
"id": "21926168",
"type": "tier"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,117 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 500,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": "2023-12-29T05:26:19.000+00:00",
"last_charge_status": "Paid",
"lifetime_support_cents": 0,
"next_charge_date": "2024-01-29T00:00:00.000+00:00",
"note": "",
"patron_status": "active_patron",
"pledge_cadence": 1,
"pledge_relationship_start": "2023-12-29T05:26:17.634+00:00",
"will_pay_amount_cents": 500
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": []
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 5,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,144 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 500,
"email": "drrosa@duck.com",
"full_name": "David",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": null,
"last_charge_status": null,
"lifetime_support_cents": 0,
"next_charge_date": "2024-02-01T08:00:00.000+00:00",
"note": "",
"patron_status": "active_patron",
"pledge_cadence": 1,
"pledge_relationship_start": "2024-01-10T22:37:02.731+00:00",
"will_pay_amount_cents": 500
},
"id": "359a4862-bc4e-496f-8559-82a0a596ed72",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11579710",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11579710"
}
},
"currently_entitled_tiers": {
"data": [
{
"id": "21992054",
"type": "tier"
}
]
},
"user": {
"data": {
"id": "111828554",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111828554"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2024-01-06T02:15:31.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11579710/4d4cde3afc3e4caf900f1237f7be1347/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1707004800&token-hash=HVyUX_Jm6Qp4Zy_1uSzi2M8BOV9xkPMNwUHvDu9Mwis%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11579710/4d4cde3afc3e4caf900f1237f7be1347/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1707004800&token-hash=HVyUX_Jm6Qp4Zy_1uSzi2M8BOV9xkPMNwUHvDu9Mwis%3D",
"is_charged_immediately": false,
"is_monthly": false,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 0,
"pay_per_name": "pull request",
"pledge_url": "/checkout/Test2979",
"published_at": "2024-01-10T22:06:53.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/Test2979",
"vanity": "Test2979"
},
"id": "11579710",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T01:09:55.000+00:00",
"first_name": "David",
"full_name": "David",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111828554/1543482da60f49dda649625e7bdf4e15/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=8SZgxLW0nhdUhKDxEaur_X-rYnZI5Gcu-9m2lC1F18Y%3D",
"is_creator": true,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111828554/1543482da60f49dda649625e7bdf4e15/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=8SZgxLW0nhdUhKDxEaur_X-rYnZI5Gcu-9m2lC1F18Y%3D",
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "111828554",
"type": "user"
},
{
"attributes": {
"amount_cents": 500,
"created_at": "2024-01-06T02:16:24.443+00:00",
"description": "Access to exclusive content and more",
"discord_role_ids": null,
"edited_at": "2024-01-06T02:16:24.443+00:00",
"image_url": null,
"patron_count": 1,
"post_count": 0,
"published": true,
"published_at": "2024-01-06T02:16:24.443+00:00",
"remaining": null,
"requires_shipping": false,
"title": "",
"unpublished_at": null,
"url": "/checkout/Test2979?rid=21992054",
"user_limit": null
},
"id": "21992054",
"type": "tier"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/359a4862-bc4e-496f-8559-82a0a596ed72"
}
}

View File

@ -0,0 +1,117 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 0,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": "2023-12-29T05:26:19.000+00:00",
"last_charge_status": "Deleted",
"lifetime_support_cents": 0,
"next_charge_date": "2024-01-29T00:00:00.000+00:00",
"note": "",
"patron_status": "former_patron",
"pledge_cadence": 1,
"pledge_relationship_start": "2023-12-29T05:26:17.634+00:00",
"will_pay_amount_cents": 0
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": []
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 4,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,139 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 1000,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": false,
"last_charge_date": "2023-12-29T05:26:19.000+00:00",
"last_charge_status": "Paid",
"lifetime_support_cents": 0,
"next_charge_date": "2024-01-29T00:00:00.000+00:00",
"note": "",
"patron_status": "active_patron",
"pledge_cadence": 1,
"pledge_relationship_start": "2023-12-29T05:26:17.634+00:00",
"will_pay_amount_cents": 1000
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": []
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 5,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
},
{
"attributes": {
"amount_cents": 0,
"created_at": "2023-12-29T01:11:16.871+00:00",
"description": "",
"discord_role_ids": null,
"edited_at": "2023-12-29T01:11:16.871+00:00",
"image_url": null,
"patron_count": 5,
"post_count": 0,
"published": true,
"published_at": "2023-12-29T01:11:16.871+00:00",
"remaining": null,
"requires_shipping": false,
"title": "Free",
"unpublished_at": null,
"url": "/checkout/11539233?rid=21926168",
"user_limit": null
},
"id": "21926168",
"type": "tier"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,170 @@
{
"data": {
"attributes": {
"campaign_lifetime_support_cents": 0,
"currently_entitled_amount_cents": 100,
"email": "kopi@pangea.com",
"full_name": "Kopi",
"is_follower": false,
"is_free_trial": true,
"last_charge_date": null,
"last_charge_status": null,
"lifetime_support_cents": 0,
"next_charge_date": null,
"note": "",
"patron_status": "active_patron",
"pledge_cadence": null,
"pledge_relationship_start": "2023-12-29T03:40:28.000+00:00",
"will_pay_amount_cents": 0
},
"id": "38c264ba-0612-4bb9-bc33-f9755cc68bb0",
"relationships": {
"address": {
"data": null
},
"campaign": {
"data": {
"id": "11539233",
"type": "campaign"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/campaigns/11539233"
}
},
"currently_entitled_tiers": {
"data": [
{
"id": "21926235",
"type": "tier"
},
{
"id": "21926168",
"type": "tier"
}
]
},
"user": {
"data": {
"id": "111836593",
"type": "user"
},
"links": {
"related": "https://www.patreon.com/api/oauth2/v2/user/111836593"
}
}
},
"type": "member"
},
"included": [
{
"attributes": {
"created_at": "2023-12-29T01:11:16.000+00:00",
"creation_name": "",
"discord_server_id": null,
"google_analytics_id": null,
"has_rss": false,
"has_sent_rss_notify": false,
"image_small_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/campaign/11539233/e1a083b0bf3a44d399523f2f08e59d9c/eyJ3IjoxOTIwLCJ3ZSI6MX0%3D/1.png?token-time=1704326400&token-hash=Qb6nRUM96w1o5QgxEpHevoF7TZ5LjFVPAxpQBOLKHas%3D",
"is_charged_immediately": true,
"is_monthly": true,
"is_nsfw": false,
"main_video_embed": null,
"main_video_url": null,
"one_liner": null,
"patron_count": 0,
"pay_per_name": "month",
"pledge_url": "/checkout/11539233",
"published_at": "2023-12-29T01:29:54.000+00:00",
"rss_artwork_url": null,
"rss_feed_title": null,
"summary": null,
"thanks_embed": null,
"thanks_msg": null,
"thanks_video_url": null,
"url": "https://www.patreon.com/user?u=111828554",
"vanity": null
},
"id": "11539233",
"type": "campaign"
},
{
"attributes": {
"about": null,
"created": "2023-12-29T02:38:24.000+00:00",
"first_name": "Kopi",
"full_name": "Kopi",
"hide_pledges": true,
"image_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"is_creator": false,
"last_name": "",
"like_count": 0,
"social_connections": {
"discord": null,
"facebook": null,
"google": null,
"instagram": null,
"reddit": null,
"spotify": null,
"spotify_open_access": null,
"twitch": null,
"twitter": null,
"vimeo": null,
"youtube": null
},
"thumb_url": "https://c10.patreonusercontent.com/4/patreon-media/p/user/111836593/7e238b05a88548e2ac1afed4e0f95b74/eyJ3IjoyMDB9/1.png?token-time=2145916800&token-hash=NI0mVHbv2rxc8cQ1qGsRCOFHPbHe4Le_0L1OLKZJsYo%3D",
"url": "https://www.patreon.com/user?u=111836593",
"vanity": null
},
"id": "111836593",
"type": "user"
},
{
"attributes": {
"amount_cents": 100,
"created_at": "2023-12-29T01:22:12.268+00:00",
"description": "Access to exclusive content and more",
"discord_role_ids": null,
"edited_at": "2023-12-29T01:23:06.534+00:00",
"image_url": null,
"patron_count": 1,
"post_count": 0,
"published": true,
"published_at": "2023-12-29T01:22:12.268+00:00",
"remaining": null,
"requires_shipping": false,
"title": "Paid",
"unpublished_at": null,
"url": "/checkout/11539233?rid=21926235",
"user_limit": null
},
"id": "21926235",
"type": "tier"
},
{
"attributes": {
"amount_cents": 0,
"created_at": "2023-12-29T01:11:16.871+00:00",
"description": "",
"discord_role_ids": null,
"edited_at": "2023-12-29T01:11:16.871+00:00",
"image_url": null,
"patron_count": 0,
"post_count": 0,
"published": true,
"published_at": "2023-12-29T01:11:16.871+00:00",
"remaining": null,
"requires_shipping": false,
"title": "Free",
"unpublished_at": null,
"url": "/checkout/11539233?rid=21926168",
"user_limit": null
},
"id": "21926168",
"type": "tier"
}
],
"links": {
"self": "https://www.patreon.com/api/oauth2/v2/members/38c264ba-0612-4bb9-bc33-f9755cc68bb0"
}
}

View File

@ -0,0 +1,105 @@
from unittest.mock import patch
import orjson
from zerver.lib.test_classes import WebhookTestCase
EXPECTED_TOPIC = "membership notifications"
IGNORED_EVENTS = [
"pledges:create",
"pledges:update",
"pledges:delete",
]
class PatreonHookTests(WebhookTestCase):
STREAM_NAME = "Patreon"
URL_TEMPLATE = "/api/v1/external/patreon?&api_key={api_key}&stream={stream}"
WEBHOOK_DIR_NAME = "patreon"
def test_patreon_members_create(self) -> None:
expected_message = "Kopi has joined as a member!"
self.check_webhook(
"members_create",
EXPECTED_TOPIC,
expected_message,
)
def test_patreon_members_update(self) -> None:
expected_message = "Kopi's membership has been updated to active patron."
self.check_webhook(
"members_update",
EXPECTED_TOPIC,
expected_message,
)
def test_patreon_members_delete(self) -> None:
expected_message = "Kopi's membership has ended."
self.check_webhook(
"members_delete",
EXPECTED_TOPIC,
expected_message,
)
def test_patreon_members_pledge_create(self) -> None:
expected_message = "Kopi has pledged $5.00 per month. :tada:\nTotal number of patrons: 5"
self.check_webhook(
"members_pledge_create",
EXPECTED_TOPIC,
expected_message,
)
def test_patreon_members_pledge_update(self) -> None:
expected_message = "Kopi has updated their pledge to $10.00 per month. :gear:"
self.check_webhook(
"members_pledge_update",
EXPECTED_TOPIC,
expected_message,
)
def test_patreon_members_pledge_delete(self) -> None:
expected_message = (
"Kopi's pledge has been cancelled. :cross_mark:\nTotal number of patrons: 4"
)
self.check_webhook(
"members_pledge_delete",
EXPECTED_TOPIC,
expected_message,
)
def test_ignored_events(self) -> None:
# The payload for these events never gets looked at in the
# webhook itself; it only needs to be valid JSON.
payload = "{}"
for event in IGNORED_EVENTS:
self.verify_post_is_ignored(payload, event)
def test_ignored_payloads(self) -> None:
payload = orjson.loads(self.get_body("members_create"))
payload["data"]["attributes"]["last_charge_status"] = "Declined"
payload["data"]["attributes"]["patron_status"] = "declined_patron"
event_types = [
"members:create",
"members:update",
"members:delete",
"members:pledge:create",
"members:pledge:update",
"members:pledge:delete",
]
for event in event_types:
self.verify_post_is_ignored(orjson.dumps(payload).decode(), event)
def verify_post_is_ignored(self, payload: str, http_x_patreon_event: str) -> None:
with patch("zerver.webhooks.patreon.view.check_send_webhook_message") as m:
result = self.client_post(
self.url,
payload,
HTTP_X_PATREON_EVENT=http_x_patreon_event,
content_type="application/json",
)
if http_x_patreon_event in IGNORED_EVENTS:
self.assertFalse(m.called)
self.assert_json_success(result)

View File

@ -0,0 +1,190 @@
from typing import Callable, Dict, Optional
from django.http import HttpRequest, HttpResponse
from zerver.decorator import webhook_view
from zerver.lib.exceptions import UnsupportedWebhookEventTypeError
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 (
check_send_webhook_message,
validate_extract_webhook_http_header,
)
from zerver.models import UserProfile
# The events for this integration contain the ":" character, which is not appropriate in a
# filename and requires us to deviate from the common `get_http_headers_from_filename` method
# from zerver.lib.webhooks.common.
def get_custom_http_headers_from_filename(http_header_key: str) -> Callable[[str], Dict[str, str]]:
def fixture_to_headers(filename: str) -> Dict[str, str]:
event_type = filename.replace("_", ":")
return {http_header_key: event_type}
return fixture_to_headers
fixture_to_headers = get_custom_http_headers_from_filename("HTTP_X_PATREON_EVENT")
def get_members_create_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
patron_status = get_patron_status(payload)
# null values indicate the member has never pledged
if last_charge_status is None and patron_status is None:
template = "{user_name} has joined as a member!"
return template.format(
user_name=get_user_name(payload),
).rstrip()
return None
def get_members_update_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
patron_status = get_patron_status(payload)
if last_charge_status in ("Paid", None) and patron_status in ("active_patron", "former_patron"):
template = "{user_name}'s membership has been updated to {patron_status}."
return template.format(
user_name=get_user_name(payload),
patron_status=str(patron_status).replace("_", " "),
).rstrip()
return None
def get_members_delete_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
patron_status = get_patron_status(payload)
# null value indicates the member has never pledged
if last_charge_status in ("Paid", None) and patron_status != "declined_patron":
template = "{user_name}'s membership has ended."
return template.format(
user_name=get_user_name(payload),
).rstrip()
return None
def get_members_pledge_create_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
pledge_amount = get_pledge_amount(payload)
# The only successful charge status is "Paid". null if not yet charged.
if last_charge_status in ("Paid", None) and pledge_amount > 0:
template = "{user_name} has pledged ${pledge_amount:.2f} per {pay_per_name}. :tada:\nTotal number of patrons: {patron_count}"
return template.format(
user_name=get_user_name(payload),
pledge_amount=pledge_amount,
pay_per_name=get_pay_per_name(payload),
patron_count=get_patron_count(payload),
).rstrip()
return None
def get_members_pledge_update_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
pledge_amount = get_pledge_amount(payload)
# The only successful charge status is "Paid". null if not yet charged.
if last_charge_status in ("Paid", None) and pledge_amount > 0:
template = "{user_name} has updated their pledge to ${pledge_amount:.2f} per {pay_per_name}. :gear:"
return template.format(
user_name=get_user_name(payload),
pledge_amount=pledge_amount,
pay_per_name=get_pay_per_name(payload),
).rstrip()
return None
def get_members_pledge_delete_body(payload: WildValue) -> Optional[str]:
last_charge_status = get_last_charge_status(payload)
if last_charge_status in ("Paid", "Deleted", None):
template = "{user_name}'s pledge has been cancelled. :cross_mark:\nTotal number of patrons: {patron_count}"
return template.format(
user_name=get_user_name(payload),
patron_count=get_patron_count(payload),
).rstrip()
return None
def get_last_charge_status(payload: WildValue) -> Optional[str]:
return payload["data"]["attributes"]["last_charge_status"].tame(check_none_or(check_string))
def get_patron_status(payload: WildValue) -> Optional[str]:
return payload["data"]["attributes"]["patron_status"].tame(check_none_or(check_string))
def get_user_name(payload: WildValue) -> str:
return payload["data"]["attributes"]["full_name"].tame(check_string)
def get_pledge_amount(payload: WildValue) -> float:
return payload["data"]["attributes"]["currently_entitled_amount_cents"].tame(check_int) / 100
def get_patron_count(payload: WildValue) -> int:
return payload["included"][0]["attributes"]["patron_count"].tame(check_int)
def get_pay_per_name(payload: WildValue) -> str:
return payload["included"][0]["attributes"]["pay_per_name"].tame(check_string)
EVENT_FUNCTION_MAPPER: Dict[str, Callable[[WildValue], Optional[str]]] = {
"members:create": get_members_create_body,
"members:update": get_members_update_body,
"members:delete": get_members_delete_body,
"members:pledge:create": get_members_pledge_create_body,
"members:pledge:update": get_members_pledge_update_body,
"members:pledge:delete": get_members_pledge_delete_body,
}
# deprecated events
IGNORED_EVENTS = [
"pledges:create",
"pledges:update",
"pledges:delete",
]
ALL_EVENT_TYPES = list(EVENT_FUNCTION_MAPPER.keys())
@webhook_view("Patreon", all_event_types=ALL_EVENT_TYPES)
@typed_endpoint
def api_patreon_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
payload: JsonBodyPayload[WildValue],
) -> HttpResponse:
header_event = validate_extract_webhook_http_header(request, "X-Patreon-Event", "Patreon")
event_name = get_zulip_event_name(header_event, payload)
if event_name is None:
# See IGNORED_EVENTS.
return json_success(request)
topic = "membership notifications"
body_function = EVENT_FUNCTION_MAPPER[event_name]
body = body_function(payload)
if body is None:
# None for payloads that are valid,
# but where we intentionally do not send a message.
return json_success(request)
check_send_webhook_message(request, user_profile, topic, body, event_name)
return json_success(request)
def get_zulip_event_name(
header_event: str,
payload: WildValue,
) -> Optional[str]:
"""
Usually, we return an event name that is a key in EVENT_FUNCTION_MAPPER.
We return None for an event that we know we don't want to handle.
"""
if header_event in EVENT_FUNCTION_MAPPER:
return header_event
elif header_event in IGNORED_EVENTS:
return None
raise UnsupportedWebhookEventTypeError(header_event)