mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 03:30:38 +08:00
## Summary - **Nil patron data crash**: Patreon V1 API can return pledge entries where `relationships.patron.data` is null (deleted/deactivated accounts). This caused `extract` to raise `NoMethodError`, preventing `save!` from completing — leaving the pledge store stuck at stale data (exactly 100 pledges from a previous sync). Fixed with `dig` for safe nil traversal, skipping entries with missing IDs. - **Double base URL**: Pagination `next` links from Patreon are absolute URLs, but `Api.get` was prepending the base URL again (`https://api.patreon.comhttps://api.patreon.com/...`). Fixed by detecting absolute URIs and using them directly. - **Non-incremental saves**: All pages were collected before a single `save!`. If any page failed, nothing was persisted. Now each page is saved incrementally — first page clears stale data, subsequent pages append. Also handles the edge case where the API returns empty data by explicitly clearing the store so ex-patrons don't keep group access.
169 lines
5 KiB
Ruby
169 lines
5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Patreon
|
|
module ApiVersion
|
|
module V1
|
|
BASE_URL = "https://api.patreon.com"
|
|
|
|
def self.campaign_data_url
|
|
"/oauth2/api/current_user/campaigns?include=rewards,creator,goals,pledges&page[count]=100"
|
|
end
|
|
|
|
def self.api_base_url
|
|
BASE_URL
|
|
end
|
|
|
|
def self.token_base_url
|
|
BASE_URL
|
|
end
|
|
|
|
def self.token_path
|
|
"/oauth2/token"
|
|
end
|
|
|
|
def self.oauth_token_url
|
|
"#{BASE_URL}/oauth2/token"
|
|
end
|
|
|
|
def self.oauth_authorize_params
|
|
{ response_type: "code" }
|
|
end
|
|
|
|
def self.oauth_identity_url
|
|
"#{BASE_URL}/oauth2/api/current_user"
|
|
end
|
|
|
|
def self.parse_campaigns(response)
|
|
rewards = {}
|
|
campaign_rewards = []
|
|
pledge_uris = []
|
|
|
|
response["data"].each do |campaign|
|
|
uri = campaign["relationships"]["pledges"]["links"]["first"]
|
|
pledge_uris << uri.sub("page%5Bcount%5D=20", "page%5Bcount%5D=100")
|
|
|
|
campaign["relationships"]["rewards"]["data"].each do |entry|
|
|
campaign_rewards << entry["id"]
|
|
end
|
|
end
|
|
|
|
response["included"].each do |entry|
|
|
id = entry["id"]
|
|
if entry["type"] == "reward" && campaign_rewards.include?(id)
|
|
rewards[id] = entry["attributes"]
|
|
rewards[id]["id"] = id
|
|
end
|
|
end
|
|
|
|
{ rewards: rewards, pledge_uris: pledge_uris }
|
|
end
|
|
|
|
def self.pull_pledges!(campaign_data)
|
|
uris = campaign_data[:pledge_uris].dup
|
|
|
|
if uris.blank?
|
|
Patreon::Pledge.save!([], false, adapter: self)
|
|
return
|
|
end
|
|
|
|
is_first_page = true
|
|
|
|
uris.each do |uri|
|
|
pledge_data = Patreon::Api.get(uri)
|
|
|
|
if pledge_data["links"] && pledge_data["links"]["next"]
|
|
next_page_uri = pledge_data["links"]["next"]
|
|
uris << next_page_uri if next_page_uri.present?
|
|
end
|
|
|
|
if pledge_data.present?
|
|
Patreon::Pledge.save!([pledge_data], !is_first_page, adapter: self)
|
|
is_first_page = false
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.extract(pledge_data)
|
|
pledges, declines, reward_users, users = {}, {}, {}, {}
|
|
|
|
if pledge_data && pledge_data["data"].present?
|
|
pledge_data["data"] = [pledge_data["data"]] unless pledge_data["data"].kind_of?(Array)
|
|
|
|
pledge_data["data"].each do |entry|
|
|
if entry["type"] == "pledge"
|
|
patron_id = entry.dig("relationships", "patron", "data", "id")
|
|
next if patron_id.nil?
|
|
|
|
attrs = entry["attributes"]
|
|
|
|
unless entry["relationships"]["reward"]["data"].nil?
|
|
(reward_users[entry["relationships"]["reward"]["data"]["id"]] ||= []) << patron_id
|
|
end
|
|
pledges[patron_id] = attrs["amount_cents"]
|
|
declines[patron_id] = attrs["declined_since"] if attrs["declined_since"].present?
|
|
elsif entry["type"] == "member"
|
|
patron_id = entry.dig("relationships", "user", "data", "id")
|
|
next if patron_id.nil?
|
|
|
|
attrs = entry["attributes"]
|
|
|
|
currently_entitled_tiers = entry["relationships"]["currently_entitled_tiers"] || {}
|
|
(currently_entitled_tiers["data"] || []).each do |tier|
|
|
(reward_users[tier["id"]] ||= []) << patron_id
|
|
end
|
|
pledges[patron_id] = attrs["pledge_amount_cents"]
|
|
declines[patron_id] = attrs["last_charge_date"] if attrs["last_charge_status"] ==
|
|
"Declined"
|
|
end
|
|
end
|
|
|
|
pledge_data["included"]&.each do |entry|
|
|
if entry["type"] == "user" && entry["attributes"]["email"].present?
|
|
users[entry["id"]] = entry["attributes"]["email"].downcase
|
|
end
|
|
end
|
|
end
|
|
|
|
[pledges, declines, reward_users, users]
|
|
end
|
|
|
|
def self.delete_pledge_data(entry, reward_users)
|
|
rel = entry["relationships"]
|
|
|
|
if entry["type"] == "pledge"
|
|
patron_id = rel.dig("patron", "data", "id")
|
|
return if patron_id.nil?
|
|
|
|
reward_id = rel.dig("reward", "data", "id")
|
|
reward_users[reward_id].reject! { |i| i == patron_id } if reward_id.present?
|
|
elsif entry["type"] == "member"
|
|
patron_id = rel.dig("user", "data", "id")
|
|
return if patron_id.nil?
|
|
|
|
(rel.dig("currently_entitled_tiers", "data") || []).each do |tier|
|
|
(reward_users[tier["id"]] || []).reject! { |i| i == patron_id }
|
|
end
|
|
end
|
|
|
|
patron_id
|
|
end
|
|
|
|
def self.get_patreon_id(data)
|
|
entry = data["data"]
|
|
key = entry["type"] == "member" ? "user" : "patron"
|
|
entry.dig("relationships", key, "data", "id")
|
|
end
|
|
|
|
def self.webhook_triggers
|
|
%w[
|
|
pledges:create
|
|
pledges:update
|
|
pledges:delete
|
|
members:pledge:create
|
|
members:pledge:update
|
|
members:pledge:delete
|
|
]
|
|
end
|
|
end
|
|
end
|
|
end
|