followupboss_client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.example +12 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +882 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/followupboss_client.gemspec +43 -0
- data/lib/followupboss_client.rb +16 -0
- data/lib/fub_client/action_plan.rb +5 -0
- data/lib/fub_client/appointment.rb +42 -0
- data/lib/fub_client/appointment_outcome.rb +51 -0
- data/lib/fub_client/appointment_type.rb +51 -0
- data/lib/fub_client/call.rb +4 -0
- data/lib/fub_client/client.rb +200 -0
- data/lib/fub_client/compatibility.rb +18 -0
- data/lib/fub_client/configuration.rb +54 -0
- data/lib/fub_client/cookie_client.rb +190 -0
- data/lib/fub_client/custom_field.rb +5 -0
- data/lib/fub_client/deal.rb +41 -0
- data/lib/fub_client/deal_attachment.rb +61 -0
- data/lib/fub_client/deal_custom_field.rb +47 -0
- data/lib/fub_client/em_event.rb +5 -0
- data/lib/fub_client/email_template.rb +5 -0
- data/lib/fub_client/event.rb +8 -0
- data/lib/fub_client/group.rb +58 -0
- data/lib/fub_client/her_patch.rb +101 -0
- data/lib/fub_client/identity.rb +33 -0
- data/lib/fub_client/message.rb +41 -0
- data/lib/fub_client/middleware/authentication.rb +26 -0
- data/lib/fub_client/middleware/cookie_authentication.rb +61 -0
- data/lib/fub_client/middleware/parser.rb +59 -0
- data/lib/fub_client/middleware.rb +8 -0
- data/lib/fub_client/note.rb +4 -0
- data/lib/fub_client/people_relationship.rb +34 -0
- data/lib/fub_client/person.rb +5 -0
- data/lib/fub_client/person_attachment.rb +50 -0
- data/lib/fub_client/pipeline.rb +45 -0
- data/lib/fub_client/property.rb +26 -0
- data/lib/fub_client/rails8_patch.rb +39 -0
- data/lib/fub_client/resource.rb +33 -0
- data/lib/fub_client/shared_inbox.rb +389 -0
- data/lib/fub_client/smart_list.rb +5 -0
- data/lib/fub_client/stage.rb +39 -0
- data/lib/fub_client/task.rb +18 -0
- data/lib/fub_client/team.rb +65 -0
- data/lib/fub_client/team_inbox.rb +65 -0
- data/lib/fub_client/text_message.rb +46 -0
- data/lib/fub_client/text_message_template.rb +49 -0
- data/lib/fub_client/user.rb +4 -0
- data/lib/fub_client/version.rb +3 -0
- data/lib/fub_client/webhook.rb +47 -0
- data/lib/fub_client.rb +61 -0
- data/scripts/test_api.rb +110 -0
- data/scripts/test_shared_inbox.rb +90 -0
- metadata +335 -0
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'fub_client'
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require 'irb'
|
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require 'fub_client/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'followupboss_client'
|
|
7
|
+
spec.version = FubClient::VERSION
|
|
8
|
+
spec.authors = ['Connor Gallopo', 'Kyoto Kopz']
|
|
9
|
+
spec.email = ['connor.gallopo@me.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Enhanced Ruby client for Follow Up Boss API with Rails 8 compatibility'
|
|
12
|
+
spec.description = 'A comprehensive Ruby client for the Follow Up Boss API with Rails 8 compatibility, secure cookie authentication for SharedInbox, and enhanced features for real estate CRM integration.'
|
|
13
|
+
spec.homepage = 'https://github.com/connorgallopo/followupboss_client'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
18
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
|
|
19
|
+
|
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
21
|
+
|
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
23
|
+
spec.bindir = 'exe'
|
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
|
|
27
|
+
spec.add_dependency 'activemodel', '>= 7.1.0', '< 9.0'
|
|
28
|
+
spec.add_dependency 'activesupport', '>= 7.1.0', '< 9.0'
|
|
29
|
+
spec.add_dependency 'facets', '~> 3.1.0'
|
|
30
|
+
spec.add_dependency 'faraday', '>= 1.10.3', '< 3.0'
|
|
31
|
+
spec.add_dependency 'her', '~> 1.1.1'
|
|
32
|
+
spec.add_dependency 'logger'
|
|
33
|
+
spec.add_dependency 'multi_json', '~> 1.15.0'
|
|
34
|
+
spec.add_dependency 'tzinfo', '~> 2.0.6'
|
|
35
|
+
# Development
|
|
36
|
+
spec.add_development_dependency 'bundler', '~> 2.4'
|
|
37
|
+
spec.add_development_dependency 'dotenv', '>= 2.8.1'
|
|
38
|
+
spec.add_development_dependency 'pry', '>= 0.14.2'
|
|
39
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
40
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
41
|
+
spec.add_development_dependency 'vcr', '>= 6.1.0'
|
|
42
|
+
spec.add_development_dependency 'webmock', '>= 3.18.1'
|
|
43
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# FollowUpBoss Client - Enhanced Ruby client for Follow Up Boss API
|
|
2
|
+
#
|
|
3
|
+
# This gem provides Rails 8 compatibility, secure cookie authentication,
|
|
4
|
+
# and comprehensive API coverage for the Follow Up Boss CRM.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# require 'followupboss_client'
|
|
8
|
+
#
|
|
9
|
+
# FubClient.configure do |config|
|
|
10
|
+
# config.api_key = 'your_api_key'
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# people = FubClient::Person.all
|
|
14
|
+
# deals = FubClient::Deal.active
|
|
15
|
+
|
|
16
|
+
require_relative 'fub_client'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module FubClient
|
|
2
|
+
class Appointment < Resource
|
|
3
|
+
collection_path 'appointments'
|
|
4
|
+
root_element :appointment
|
|
5
|
+
include_root_in_json true
|
|
6
|
+
|
|
7
|
+
scope :between, lambda { |start_date, end_date|
|
|
8
|
+
where(startDate: start_date, endDate: end_date)
|
|
9
|
+
}
|
|
10
|
+
scope :for_person, ->(person_id) { where(personId: person_id) }
|
|
11
|
+
scope :by_user, ->(user_id) { where(userId: user_id) }
|
|
12
|
+
scope :by_type, ->(type_id) { where(typeId: type_id) }
|
|
13
|
+
scope :by_outcome, ->(outcome_id) { where(outcomeId: outcome_id) }
|
|
14
|
+
|
|
15
|
+
def self.upcoming
|
|
16
|
+
where(startDate: Time.now.iso8601)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.past
|
|
20
|
+
where(endDate: Time.now.iso8601, order: 'desc')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def person
|
|
24
|
+
return nil unless respond_to?(:person_id) && person_id
|
|
25
|
+
|
|
26
|
+
FubClient::Person.find(person_id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def user
|
|
30
|
+
return nil unless respond_to?(:user_id) && user_id
|
|
31
|
+
|
|
32
|
+
FubClient::User.find(user_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def complete(outcome_id, notes = nil)
|
|
36
|
+
params = { outcomeId: outcome_id }
|
|
37
|
+
params[:notes] = notes if notes
|
|
38
|
+
|
|
39
|
+
self.class.put("#{id}/complete", params)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module FubClient
|
|
2
|
+
class AppointmentOutcome < Resource
|
|
3
|
+
collection_path 'appointmentOutcomes'
|
|
4
|
+
root_element :appointment_outcome
|
|
5
|
+
include_root_in_json true
|
|
6
|
+
|
|
7
|
+
# Convenience method to find appointment outcomes by name (partial match)
|
|
8
|
+
scope :by_name, ->(name) { where(q: name) }
|
|
9
|
+
|
|
10
|
+
# Convenience method to find active appointment outcomes
|
|
11
|
+
def self.active
|
|
12
|
+
where(active: true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Convenience method to find inactive appointment outcomes
|
|
16
|
+
def self.inactive
|
|
17
|
+
where(active: false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get appointments with this outcome
|
|
21
|
+
def appointments
|
|
22
|
+
return [] unless id
|
|
23
|
+
|
|
24
|
+
FubClient::Appointment.by_outcome(id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Update an appointment outcome
|
|
28
|
+
def update(attributes)
|
|
29
|
+
return false unless id
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
self.class.put(id.to_s, attributes)
|
|
33
|
+
true
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delete an appointment outcome
|
|
40
|
+
def delete
|
|
41
|
+
return false unless id
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
self.class.delete(id.to_s)
|
|
45
|
+
true
|
|
46
|
+
rescue StandardError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module FubClient
|
|
2
|
+
class AppointmentType < Resource
|
|
3
|
+
collection_path 'appointmentTypes'
|
|
4
|
+
root_element :appointment_type
|
|
5
|
+
include_root_in_json true
|
|
6
|
+
|
|
7
|
+
# Convenience method to find appointment types by name (partial match)
|
|
8
|
+
scope :by_name, ->(name) { where(q: name) }
|
|
9
|
+
|
|
10
|
+
# Convenience method to find active appointment types
|
|
11
|
+
def self.active
|
|
12
|
+
where(active: true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Convenience method to find inactive appointment types
|
|
16
|
+
def self.inactive
|
|
17
|
+
where(active: false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get appointments with this type
|
|
21
|
+
def appointments
|
|
22
|
+
return [] unless id
|
|
23
|
+
|
|
24
|
+
FubClient::Appointment.by_type(id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Update an appointment type
|
|
28
|
+
def update(attributes)
|
|
29
|
+
return false unless id
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
self.class.put(id.to_s, attributes)
|
|
33
|
+
true
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delete an appointment type
|
|
40
|
+
def delete
|
|
41
|
+
return false unless id
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
self.class.delete(id.to_s)
|
|
45
|
+
true
|
|
46
|
+
rescue StandardError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
module FubClient
|
|
2
|
+
class Client
|
|
3
|
+
API_URL = 'api.followupboss.com'
|
|
4
|
+
WEBAPP_URL = 'app.followupboss.com'
|
|
5
|
+
API_VERSION = 'v1'
|
|
6
|
+
|
|
7
|
+
include Singleton
|
|
8
|
+
|
|
9
|
+
# Allow explicitly setting the instance (for testing)
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_writer :api_key
|
|
15
|
+
attr_accessor :cookies, :subdomain
|
|
16
|
+
attr_reader :her_api
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
init_her_api
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def api_key
|
|
23
|
+
@api_key ||= ENV['FUB_API_KEY']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def api_uri
|
|
27
|
+
return @api_uri if @api_uri
|
|
28
|
+
|
|
29
|
+
@api_uri = if subdomain
|
|
30
|
+
# Use subdomain-specific URL for cookie-based auth
|
|
31
|
+
URI::HTTPS.build(host: "#{subdomain}.followupboss.com", path: "/api/#{API_VERSION}")
|
|
32
|
+
else
|
|
33
|
+
# Use default API URL for API key auth
|
|
34
|
+
URI::HTTPS.build(host: API_URL, path: "/#{API_VERSION}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Login to obtain cookies
|
|
39
|
+
def login(email, password, remember = true)
|
|
40
|
+
# First get CSRF token
|
|
41
|
+
csrf_token = get_csrf_token
|
|
42
|
+
|
|
43
|
+
puts "CSRF Token: #{csrf_token}" if ENV['DEBUG']
|
|
44
|
+
|
|
45
|
+
if csrf_token.nil?
|
|
46
|
+
puts 'Failed to obtain CSRF token, cannot proceed with login' if ENV['DEBUG']
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
conn = Faraday.new(url: "https://#{WEBAPP_URL}") do |f|
|
|
51
|
+
f.request :url_encoded
|
|
52
|
+
f.adapter :net_http
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Format request similar to the curl example
|
|
56
|
+
# Remove quotes from the password if it's a string with quotes (from .env file)
|
|
57
|
+
password_str = password.to_s.gsub(/^'(.*)'$/, '\1')
|
|
58
|
+
|
|
59
|
+
# Check if the password contains special characters that need encoding
|
|
60
|
+
encoded_password = URI.encode_www_form_component(password_str)
|
|
61
|
+
|
|
62
|
+
# Ensure # is properly encoded as %23
|
|
63
|
+
if password_str.include?('#') && !encoded_password.include?('%23')
|
|
64
|
+
puts "WARNING: The # character in password isn't being properly encoded! Manually fixing..." if ENV['DEBUG']
|
|
65
|
+
encoded_password = encoded_password.gsub(/#/, '%23')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Explicitly use the exact raw data format from the curl example, ensuring all special characters are preserved
|
|
69
|
+
raw_data = "start_url=&subdomain=&email=#{URI.encode_www_form_component(email)}&password=#{encoded_password}&remember=&remember=#{remember ? '1' : ''}&csrf_token=#{csrf_token}"
|
|
70
|
+
|
|
71
|
+
puts "Login raw data: #{raw_data}" if ENV['DEBUG']
|
|
72
|
+
|
|
73
|
+
response = conn.post do |req|
|
|
74
|
+
req.url '/login/index'
|
|
75
|
+
|
|
76
|
+
# Add ALL headers exactly as in the curl example
|
|
77
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
78
|
+
req.headers['Accept'] =
|
|
79
|
+
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
|
|
80
|
+
req.headers['Accept-Language'] = 'en-US,en;q=0.9'
|
|
81
|
+
req.headers['Cache-Control'] = 'max-age=0'
|
|
82
|
+
req.headers['User-Agent'] =
|
|
83
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
|
|
84
|
+
req.headers['Origin'] = "https://#{WEBAPP_URL}"
|
|
85
|
+
req.headers['Referer'] = "https://#{WEBAPP_URL}/login"
|
|
86
|
+
req.headers['DNT'] = '1'
|
|
87
|
+
req.headers['Priority'] = 'u=0, i'
|
|
88
|
+
req.headers['Sec-CH-UA'] = '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"'
|
|
89
|
+
req.headers['Sec-CH-UA-Mobile'] = '?0'
|
|
90
|
+
req.headers['Sec-CH-UA-Platform'] = '"Windows"'
|
|
91
|
+
req.headers['Sec-Fetch-Dest'] = 'document'
|
|
92
|
+
req.headers['Sec-Fetch-Mode'] = 'navigate'
|
|
93
|
+
req.headers['Sec-Fetch-Site'] = 'same-origin'
|
|
94
|
+
req.headers['Sec-Fetch-User'] = '?1'
|
|
95
|
+
req.headers['Sec-GPC'] = '1'
|
|
96
|
+
req.headers['Upgrade-Insecure-Requests'] = '1'
|
|
97
|
+
|
|
98
|
+
# Add any cookies that might help
|
|
99
|
+
default_cookies = '_ga=GA1.1.703376757.1744985902; _ga_J70LJ0E97T=GS1.1.1744990639.2.1.1744990766.0.0.0'
|
|
100
|
+
req.headers['Cookie'] = default_cookies
|
|
101
|
+
|
|
102
|
+
req.body = raw_data
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
puts "Login response status: #{response.status}"
|
|
106
|
+
puts "Login response headers: #{response.headers.inspect}"
|
|
107
|
+
puts "Login response body: #{response.body}"
|
|
108
|
+
|
|
109
|
+
# First check for error messages in the response
|
|
110
|
+
if response.body.include?('Oops! Email address or password is not correct')
|
|
111
|
+
puts 'Login failed: Invalid credentials' if ENV['DEBUG']
|
|
112
|
+
return false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if [302, 200].include?(response.status)
|
|
116
|
+
# Extract cookies from response
|
|
117
|
+
cookies = response.headers['set-cookie']
|
|
118
|
+
if cookies
|
|
119
|
+
puts "Extracted cookies: #{cookies}" if ENV['DEBUG']
|
|
120
|
+
@cookies = cookies
|
|
121
|
+
|
|
122
|
+
# Verify we don't have error messages in the response
|
|
123
|
+
return true unless response.body.include?('<div class="message error">')
|
|
124
|
+
|
|
125
|
+
puts 'Login failed: Error message detected in response' if ENV['DEBUG']
|
|
126
|
+
return false
|
|
127
|
+
|
|
128
|
+
elsif ENV['DEBUG']
|
|
129
|
+
puts 'No cookies in response headers'
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
puts "Login failed with status: #{response.status}" if ENV['DEBUG']
|
|
133
|
+
puts "Response body sample: #{response.body[0..200]}" if ENV['DEBUG']
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get CSRF token for login
|
|
140
|
+
def get_csrf_token
|
|
141
|
+
conn = Faraday.new(url: "https://#{WEBAPP_URL}") do |f|
|
|
142
|
+
f.adapter :net_http
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
response = conn.get('/login')
|
|
146
|
+
|
|
147
|
+
# Extract CSRF token from HTML - using the input field pattern found in the response
|
|
148
|
+
if response.body =~ /csrf_token\\" value=\\"([^\\]+)/
|
|
149
|
+
return ::Regexp.last_match(1)
|
|
150
|
+
elsif response.body =~ /name=\\"csrf_token\\" value=\\"([^"]+)/
|
|
151
|
+
return ::Regexp.last_match(1)
|
|
152
|
+
elsif response.body =~ /csrf_token=([^"&]+)/
|
|
153
|
+
return ::Regexp.last_match(1)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# For debugging
|
|
157
|
+
if ENV['DEBUG']
|
|
158
|
+
puts 'Could not find CSRF token in the response. Sample of response body:'
|
|
159
|
+
puts response.body[0..500]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Use cookie authentication?
|
|
166
|
+
def use_cookies?
|
|
167
|
+
!@cookies.nil? && !@cookies.empty?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Reset the HER API connection with current settings
|
|
171
|
+
def reset_her_api
|
|
172
|
+
@api_uri = nil # Clear cached URI to rebuild with current settings
|
|
173
|
+
init_her_api
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def init_her_api
|
|
179
|
+
@her_api = Her::API.new
|
|
180
|
+
@her_api.setup url: api_uri.to_s do |c|
|
|
181
|
+
# Request - use appropriate authentication middleware
|
|
182
|
+
if use_cookies?
|
|
183
|
+
# Let the CookieAuthentication middleware handle all headers
|
|
184
|
+
# to ensure they're consistent with the cookie format
|
|
185
|
+
c.use FubClient::Middleware::CookieAuthentication
|
|
186
|
+
else
|
|
187
|
+
c.use FubClient::Middleware::Authentication
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
c.request :url_encoded
|
|
191
|
+
|
|
192
|
+
# Response
|
|
193
|
+
c.use FubClient::Middleware::Parser
|
|
194
|
+
|
|
195
|
+
# Adapter
|
|
196
|
+
c.adapter :net_http
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Compatibility fixes for Rails 8
|
|
2
|
+
module FubClient
|
|
3
|
+
module Compatibility
|
|
4
|
+
# Fix for Her gem's ActiveSupport::BasicObject usage in Rails 8+
|
|
5
|
+
# ActiveSupport::BasicObject was removed in Rails 8, but Her gem still references it
|
|
6
|
+
def self.patch_her_gem_rails8_compatibility!
|
|
7
|
+
# Only patch if ActiveSupport::BasicObject is not defined (Rails 8+)
|
|
8
|
+
return if ActiveSupport.const_defined?('BasicObject')
|
|
9
|
+
|
|
10
|
+
# Define BasicObject as an alias to ProxyObject for backward compatibility
|
|
11
|
+
ActiveSupport.const_set('BasicObject', ActiveSupport::ProxyObject)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Apply the patch immediately when this file is loaded
|
|
17
|
+
# This must happen before Her gem is loaded
|
|
18
|
+
FubClient::Compatibility.patch_her_gem_rails8_compatibility!
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module FubClient
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :api_key, :subdomain, :gist_url, :encryption_key, :cookie
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@api_key = ENV['FUB_API_KEY']
|
|
7
|
+
@subdomain = ENV['FUB_SUBDOMAIN']
|
|
8
|
+
@gist_url = ENV['FUB_GIST_URL']
|
|
9
|
+
@encryption_key = ENV['FUB_ENCRYPTION_KEY']
|
|
10
|
+
@cookie = ENV['FUB_COOKIE']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid?
|
|
14
|
+
has_api_key_auth? || has_cookie_auth?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def has_api_key_auth?
|
|
18
|
+
!@api_key.nil? && !@api_key.empty?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def has_cookie_auth?
|
|
22
|
+
(!@cookie.nil? && !@cookie.empty?) ||
|
|
23
|
+
(!@gist_url.nil? && !@gist_url.empty? && !@encryption_key.nil? && !@encryption_key.empty?)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def auth_summary
|
|
27
|
+
summary = []
|
|
28
|
+
summary << "API Key: #{has_api_key_auth? ? 'configured' : 'not configured'}"
|
|
29
|
+
summary << "Cookie Auth: #{has_cookie_auth? ? 'configured' : 'not configured'}"
|
|
30
|
+
if has_cookie_auth?
|
|
31
|
+
if @cookie
|
|
32
|
+
summary << " - Direct cookie: #{@cookie.length} chars"
|
|
33
|
+
elsif @gist_url && @encryption_key
|
|
34
|
+
summary << " - GIST URL: #{@gist_url}"
|
|
35
|
+
summary << ' - Encryption key: configured'
|
|
36
|
+
end
|
|
37
|
+
summary << " - Subdomain: #{@subdomain || 'not set'}"
|
|
38
|
+
end
|
|
39
|
+
summary.join("\n")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.configuration
|
|
44
|
+
@configuration ||= Configuration.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.configure
|
|
48
|
+
yield(configuration) if block_given?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.reset_configuration!
|
|
52
|
+
@configuration = Configuration.new
|
|
53
|
+
end
|
|
54
|
+
end
|