mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:39:43 +08:00
Previously enabling the Patreon plugin and clicking its sidebar link gave you an "Error loading route" whenever the Patreon connection was not configured. This change moves the plugin off the legacy admin route pattern and adds an empty state linking to its settings so an unconfigured Patreon explains why instead of failing. Before: <img width="700" alt="image" src="https://github.com/user-attachments/assets/90cc16c5-e3be-4b4a-8e59-87ce8c27875d" /> After: <img width="900" alt="image" src="https://github.com/user-attachments/assets/5865d330-1020-4b05-aff9-799a51e62c13" />
206 lines
6.6 KiB
Ruby
Vendored
206 lines
6.6 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
# name: discourse-patreon
|
|
# about: Enables Patreon Social Login, and synchronization between Discourse Groups and Patreon rewards.
|
|
# meta_topic_id: 44366
|
|
# version: 2.0
|
|
# author: Rafael dos Santos Silva <xfalcox@gmail.com>
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-patreon
|
|
|
|
require "omniauth-oauth2"
|
|
|
|
enabled_site_setting :patreon_enabled
|
|
|
|
register_asset "stylesheets/patreon.scss"
|
|
|
|
register_svg_icon "fab-patreon"
|
|
register_svg_icon "patreon-new"
|
|
|
|
# Site setting validators must be loaded before initialize
|
|
require_relative "lib/validators/patreon_login_enabled_validator"
|
|
|
|
module ::Patreon
|
|
PLUGIN_NAME = "discourse-patreon"
|
|
end
|
|
|
|
require_relative "lib/discourse_patreon/engine"
|
|
|
|
after_initialize do
|
|
require_dependency "admin_constraint"
|
|
|
|
require_relative "lib/api_version/v1"
|
|
require_relative "lib/api_version/v2"
|
|
require_relative "lib/api"
|
|
require_relative "app/controllers/patreon/patreon_admin_controller"
|
|
require_relative "app/controllers/patreon/patreon_webhook_controller"
|
|
require_relative "app/jobs/regular/sync_patron_groups"
|
|
require_relative "app/jobs/scheduled/patreon_sync_patrons_to_groups"
|
|
require_relative "app/jobs/scheduled/patreon_update_tokens"
|
|
require_relative "app/services/problem_check/access_token_invalid"
|
|
require_relative "app/services/problem_check/patreon_api_v1_deprecated"
|
|
require_relative "lib/seed"
|
|
require_relative "lib/campaign"
|
|
require_relative "lib/pledge"
|
|
require_relative "lib/patron"
|
|
require_relative "lib/tokens"
|
|
|
|
Discourse::Application.routes.prepend { mount Patreon::Engine, at: "/patreon" }
|
|
|
|
add_admin_route "patreon.title", "discourse-patreon", use_new_show_route: true
|
|
|
|
Discourse::Application.routes.append do
|
|
get "/admin/plugins/discourse-patreon/filters" => "admin/plugins#index",
|
|
:constraints => AdminConstraint.new
|
|
get "/admin/plugins/patreon/list" => "patreon/patreon_admin#list",
|
|
:constraints => AdminConstraint.new
|
|
get "/u/:username/patreon_email" => "patreon/patreon_admin#email",
|
|
:constraints => {
|
|
username: RouteFormat.username,
|
|
}
|
|
end
|
|
|
|
on(:user_created) do |user|
|
|
filters = PluginStore.get(Patreon::PLUGIN_NAME, "filters")
|
|
patreon_id = Patreon::Patron.all.key(user.email)
|
|
|
|
if filters.present? && patreon_id.present?
|
|
begin
|
|
reward_id =
|
|
Patreon::RewardUser.all.except("0").detect { |_k, v| v.include? patreon_id }&.first
|
|
|
|
group_ids = filters.select { |_k, v| v.include?(reward_id) || v.include?("0") }.keys
|
|
|
|
Group.where(id: group_ids).each { |group| group.add user }
|
|
|
|
Patreon::Patron.update_local_user(user, patreon_id, true)
|
|
rescue => e
|
|
Rails.logger.warn(
|
|
"Patreon group membership callback failed for new user #{id} with error: #{e}.\n\n #{e.backtrace.join("\n")}",
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
Patreon::USER_DETAIL_FIELDS.each do |attribute|
|
|
add_to_serializer(
|
|
:admin_detailed_user,
|
|
:"patreon_#{attribute}",
|
|
include_condition: -> do
|
|
Patreon::Patron.attr(attribute, object).present? &&
|
|
(attribute != "amount_cents" || scope.is_admin?)
|
|
end,
|
|
) { Patreon::Patron.attr(attribute, object) }
|
|
end
|
|
|
|
add_to_serializer(
|
|
:admin_detailed_user,
|
|
:patreon_email_exists,
|
|
include_condition: -> { Patreon::Patron.attr("email", object).present? },
|
|
) { true }
|
|
|
|
add_to_serializer(:current_user, :show_donation_prompt?) do
|
|
Patreon.show_donation_prompt_to_user?(object)
|
|
end
|
|
|
|
register_problem_check ProblemCheck::AccessTokenInvalid
|
|
register_problem_check ProblemCheck::PatreonApiV1Deprecated
|
|
end
|
|
|
|
# Authentication with Patreon
|
|
class ::OmniAuth::Strategies::Patreon < ::OmniAuth::Strategies::OAuth2
|
|
option :name, "patreon"
|
|
|
|
option :client_options,
|
|
site: "https://www.patreon.com",
|
|
authorize_url: "https://www.patreon.com/oauth2/authorize",
|
|
token_url: "https://api.patreon.com/oauth2/token"
|
|
|
|
option :authorize_params, response_type: "code"
|
|
|
|
def custom_build_access_token
|
|
verifier = request.params["code"]
|
|
client.auth_code.get_token(verifier, redirect_uri: options.redirect_uri)
|
|
end
|
|
|
|
alias_method :build_access_token, :custom_build_access_token
|
|
|
|
uid { raw_info["data"]["id"].to_s }
|
|
|
|
info do
|
|
{
|
|
email: raw_info["data"]["attributes"]["email"],
|
|
email_verified: raw_info["data"]["attributes"]["is_email_verified"],
|
|
name: raw_info["data"]["attributes"]["full_name"],
|
|
}
|
|
end
|
|
|
|
extra { { raw_info: raw_info } }
|
|
|
|
def raw_info
|
|
@raw_info ||=
|
|
begin
|
|
response =
|
|
client.request(
|
|
:get,
|
|
Patreon::ApiVersion.current.oauth_identity_url,
|
|
headers: {
|
|
"Authorization" => "Bearer #{access_token.token}",
|
|
},
|
|
parse: :json,
|
|
)
|
|
response.parsed
|
|
end
|
|
end
|
|
|
|
def callback_url
|
|
full_host + script_name + callback_path
|
|
end
|
|
end
|
|
|
|
class Auth::PatreonAuthenticator < Auth::ManagedAuthenticator
|
|
def name
|
|
"patreon"
|
|
end
|
|
|
|
def register_middleware(omniauth)
|
|
omniauth.provider :patreon,
|
|
setup:
|
|
lambda { |env|
|
|
strategy = env["omniauth.strategy"]
|
|
adapter = Patreon::ApiVersion.current
|
|
strategy.options[:client_id] = SiteSetting.patreon_client_id
|
|
strategy.options[:client_secret] = SiteSetting.patreon_client_secret
|
|
strategy.options[
|
|
:redirect_uri
|
|
] = "#{Discourse.base_url}/auth/patreon/callback"
|
|
strategy.options[
|
|
:provider_ignores_state
|
|
] = SiteSetting.patreon_login_ignore_state
|
|
strategy.options[:client_options][:token_url] = adapter.oauth_token_url
|
|
strategy.options[:authorize_params] = adapter.oauth_authorize_params
|
|
}
|
|
end
|
|
|
|
def after_authenticate(auth_token, existing_account: nil)
|
|
result = super
|
|
|
|
user = result.user
|
|
discourse_username = SiteSetting.patreon_creator_discourse_username
|
|
if discourse_username.present? && user && user.username == discourse_username
|
|
SiteSetting.patreon_creator_access_token = auth_token.credentials["token"]
|
|
SiteSetting.patreon_creator_refresh_token = auth_token.credentials["refresh_token"]
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def enabled?
|
|
SiteSetting.patreon_login_enabled
|
|
end
|
|
|
|
def primary_email_verified?(auth_token)
|
|
auth_token[:info][:email_verified]
|
|
end
|
|
end
|
|
|
|
auth_provider authenticator: Auth::PatreonAuthenticator.new, icon: "patreon-new"
|