snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View File

@@ -0,0 +1,32 @@
# Zulip Patreon integration
Get Patreon notifications in Zulip!
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
1. Login to your Patreon developer account, navigate to your Patreon
Portal, and click on **My Webhooks**.
1. Paste the URL generated above in the webhook URL field, and click the
**plus** (**+**) button. Enable the [events](#filtering-incoming-events)
you would like to receive notifications for, and click **Send Test**.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/patreon/001.png)
{!event-filtering-additional-feature.md!}
### Related documentation
- [Patreon webhook events](https://docs.patreon.com/#webhooks)
- [Patreon webhook triggers](https://docs.patreon.com/#triggers-v2)
{!webhooks-url-specification.md!}

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,101 @@
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):
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,187 @@
from collections.abc import Callable
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, get_event_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 `default_fixture_to_headers` 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) -> str | None:
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) -> str | None:
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) -> str | None:
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) -> str | None:
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) -> str | None:
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) -> str | None:
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) -> str | None:
return payload["data"]["attributes"]["last_charge_status"].tame(check_none_or(check_string))
def get_patron_status(payload: WildValue) -> str | None:
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], str | None]] = {
"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 = get_event_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,
) -> str | None:
"""
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)