paypal_permissions 0.0.3 → 0.0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +37 -0
- data/lib/active_merchant/billing/gateways/paypal_permissions.rb +101 -1
- data/lib/generators/active_record/paypal_permissions_generator.rb +37 -12
- data/lib/generators/paypal_permissions/install_generator.rb +9 -9
- data/lib/generators/paypal_permissions/orm_helpers.rb +2 -2
- data/lib/generators/paypal_permissions/paypal_permissions_generator.rb +8 -2
- data/lib/paypal_permissions/version.rb +1 -1
- metadata +19 -19
- data/README +0 -1
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# PayPal Permissions
|
2
|
+
|
3
|
+
## Assumptions
|
4
|
+
|
5
|
+
You have the ActiveMerchant gem installed.
|
6
|
+
|
7
|
+
## Caveats
|
8
|
+
|
9
|
+
Earlier this morning, this README file contained the following content:
|
10
|
+
|
11
|
+
> Nothing to see here yet. A work in progress.
|
12
|
+
|
13
|
+
This code is currently released only to facilitate my very own testing. But apparently, PayPal directed at least one Ruby developer here, so I'm trying to catch up with demand. :)
|
14
|
+
|
15
|
+
Bear in mind that the resource generator creates a number of fields in the database migration that are undoubtedly superfluous in some cases, and perhaps inadequate in others. If you're looking for a full audit trail by way of database entries for every permissions interaction, you may have to rely on your logs, not on the database. Even so, the generated schema should get the job done.
|
16
|
+
|
17
|
+
## Install
|
18
|
+
|
19
|
+
`rails generate paypal_permissions:install`
|
20
|
+
|
21
|
+
Running the install generator will:
|
22
|
+
|
23
|
+
- update your config/environments/{development,test,production}.rb files. You must edit these files with your PayPal credentials.
|
24
|
+
- create a currently useless config/initializers/paypal_permissions.rb initializer.
|
25
|
+
- create a currently useless config/locales/paypal_permissions.en.yml local file.
|
26
|
+
|
27
|
+
|
28
|
+
## Optionally generate resources
|
29
|
+
|
30
|
+
`rails generate paypal_permissions <new or existing resource name>`
|
31
|
+
|
32
|
+
This generator will:
|
33
|
+
|
34
|
+
- create a migration which updates the table for an existing model or creates a new table along with a new model. ActiveRecord is the only supported orm.
|
35
|
+
- create a controller.
|
36
|
+
- insert routes into config/routes.rb
|
37
|
+
|
@@ -48,6 +48,23 @@ module ActiveMerchant #:nodoc:
|
|
48
48
|
test? ? URLS[:test][:request_permissions] : URLS[:live][:request_permissions]
|
49
49
|
end
|
50
50
|
|
51
|
+
public
|
52
|
+
def get_access_token(request_token, request_token_verifier)
|
53
|
+
query_string = build_get_access_token_query_string request_token, request_token_verifier
|
54
|
+
nvp_response = ssl_get "#{get_access_token_url}?#{query_string}", @options[:get_access_token_headers]
|
55
|
+
if nvp_response =~ /error\(\d+\)/
|
56
|
+
puts "request: #{get_access_token_url}?#{query_string}\n"
|
57
|
+
puts "nvp_response: #{nvp_response}\n"
|
58
|
+
end
|
59
|
+
response = parse_get_access_token_nvp(nvp_response)
|
60
|
+
end
|
61
|
+
|
62
|
+
public
|
63
|
+
def redirect_user_to_paypal_url token
|
64
|
+
template = test? ? URLS[:test][:redirect_user_to_paypal] : URLS[:live][:redirect_user_to_paypal]
|
65
|
+
template % token
|
66
|
+
end
|
67
|
+
|
51
68
|
public
|
52
69
|
def get_access_token_url
|
53
70
|
test? ? URLS[:test][:get_access_token] : URLS[:live][:get_access_token]
|
@@ -62,11 +79,13 @@ module ActiveMerchant #:nodoc:
|
|
62
79
|
URLS = {
|
63
80
|
:test => {
|
64
81
|
:request_permissions => 'https://svcs.sandbox.paypal.com/Permissions/RequestPermissions',
|
82
|
+
:redirect_user_to_paypal => 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_grant-permission&request_token=%s',
|
65
83
|
:get_access_token => 'https://svcs.sandbox.paypal.com/Permissions/GetAccessToken',
|
66
84
|
:get_permissions => 'https://svcs.sandbox.paypal.com/Permissions/GetPermissions',
|
67
85
|
},
|
68
86
|
:live => {
|
69
87
|
:request_permissions => 'https://svcs.paypal.com/Permissions/RequestPermissions',
|
88
|
+
:redirect_user_to_paypal => 'https://www.paypal.com/cgi-bin/webscr?cmd=_grant-permission&request_token=%s',
|
70
89
|
:get_access_token => 'https://svcs.paypal.com/Permissions/GetAccessToken',
|
71
90
|
:get_permissions => 'https://svcs.sandbox.paypal.com/Permissions/GetPermissions',
|
72
91
|
}
|
@@ -90,11 +109,18 @@ module ActiveMerchant #:nodoc:
|
|
90
109
|
scopes.collect{ |s| "scope=#{URI.encode(s.to_s.strip.upcase)}" }.join("&")
|
91
110
|
end
|
92
111
|
|
112
|
+
private
|
113
|
+
def build_get_access_token_query_string(request_token, verifier)
|
114
|
+
"requestEnvelope.errorLanguage=en_US&token=#{request_token}&verifier=#{verifier}"
|
115
|
+
end
|
116
|
+
|
117
|
+
=begin
|
93
118
|
private
|
94
119
|
def setup_request_permission
|
95
120
|
callback
|
96
121
|
scope
|
97
122
|
end
|
123
|
+
=end
|
98
124
|
|
99
125
|
private
|
100
126
|
def parse_request_permissions_nvp(nvp)
|
@@ -168,6 +194,80 @@ module ActiveMerchant #:nodoc:
|
|
168
194
|
response
|
169
195
|
end
|
170
196
|
|
197
|
+
private
|
198
|
+
def parse_get_access_token_nvp(nvp)
|
199
|
+
response = {
|
200
|
+
:errors => [
|
201
|
+
],
|
202
|
+
}
|
203
|
+
pairs = nvp.split "&"
|
204
|
+
pairs.each do |pair|
|
205
|
+
n,v = pair.split "="
|
206
|
+
n = CGI.unescape n
|
207
|
+
v = CGI.unescape v
|
208
|
+
case n
|
209
|
+
when "responseEnvelope.timestamp"
|
210
|
+
response[:timestamp] = v
|
211
|
+
when "responseEnvelope.ack"
|
212
|
+
response[:ack] = v
|
213
|
+
=begin
|
214
|
+
# Client should implement these with logging...
|
215
|
+
case v
|
216
|
+
when "Success"
|
217
|
+
when "Failure"
|
218
|
+
when "Warning"
|
219
|
+
when "SuccessWithWarning"
|
220
|
+
when "FailureWithWarning"
|
221
|
+
end
|
222
|
+
=end
|
223
|
+
when "responseEnvelope.correlationId"
|
224
|
+
response[:correlation_id] = v
|
225
|
+
when "responseEnvelope.build"
|
226
|
+
# do nothing
|
227
|
+
when "token"
|
228
|
+
response[:token] = v
|
229
|
+
when "tokenSecret"
|
230
|
+
response[:tokenSecret] = v
|
231
|
+
when /^error\((?<error_idx>\d+)\)/
|
232
|
+
error_idx = error_idx.to_i
|
233
|
+
if response[:errors].length <= error_idx
|
234
|
+
response[:errors] << { :parameters => [] }
|
235
|
+
raise if response[:errors].length <= error_idx
|
236
|
+
end
|
237
|
+
case n
|
238
|
+
when /^error\(\d+\)\.errorId$/
|
239
|
+
response[:errors][error_idx][:error_id] = v
|
240
|
+
=begin
|
241
|
+
# Client should implement these with logging. PayPal doesn't distinguish
|
242
|
+
# between errors which can be corrected by the user and errors which need
|
243
|
+
# to be corrected by a developer or merchant, say, in configuration.
|
244
|
+
# case v
|
245
|
+
# when "520002"
|
246
|
+
# when
|
247
|
+
=end
|
248
|
+
when /^error\(\d+\)\.domain$/
|
249
|
+
response[:errors][error_idx][:domain] = v
|
250
|
+
when /^error\(\d+\)\.subdomain$/
|
251
|
+
response[:errors][error_idx][:subdomain] = v
|
252
|
+
when /^error\(\d+\)\.severity$/
|
253
|
+
response[:errors][error_idx][:severity] = v
|
254
|
+
when /^error\(\d+\)\.category$/
|
255
|
+
response[:errors][error_idx][:category] = v
|
256
|
+
when /^error\(\d+\)\.message$/
|
257
|
+
response[:errors][error_idx][:message] = v
|
258
|
+
when /^error\(\d+\)\.parameter\((?<parameter_idx>\d+)\)$/
|
259
|
+
parameter_idx = parameter_idx.to_i
|
260
|
+
if response[:errors][error_idx][:parameters].length <= parameter_idx
|
261
|
+
response[:errors][error_idx][:parameters] << {}
|
262
|
+
raise if response[:errors][error_idx][:parameters].length <= parameter_idx
|
263
|
+
end
|
264
|
+
response[:errors][error_idx][:parameters][parameter_idx] = v
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
response
|
269
|
+
end
|
270
|
+
|
171
271
|
private
|
172
272
|
def authentication_header url
|
173
273
|
timestamp = Time.now.to_i
|
@@ -189,7 +289,7 @@ module ActiveMerchant #:nodoc:
|
|
189
289
|
})
|
190
290
|
sorted_params = Hash[params.sort]
|
191
291
|
sorted_query_string = sorted_params.to_query
|
192
|
-
data = [ "POST", url, sorted_query_string ].join("&") # ? "https://api.sandbox.paypal.com/nvp"
|
292
|
+
data = [ "POST", url, sorted_query_string ].join("&") # ? "https://api-3t.sandbox.paypal.com/nvp"
|
193
293
|
digest = OpenSSL::Digest::Digest.new('sha1')
|
194
294
|
OpenSSL::HMAC.digest(digest, key, data)
|
195
295
|
enc = Base64.encode64('Send reinforcements') # encode per RFC 2045 (not 4648
|
@@ -17,32 +17,57 @@ module ActiveRecord
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
|
20
|
+
def generate_paypal_permissions_model
|
21
|
+
invoke "active_record:model", [ name ], :migration => false unless model_exists? && behavior == :invoke
|
22
22
|
end
|
23
23
|
|
24
24
|
def inject_paypal_permissions_content
|
25
25
|
inject_into_class(model_path, class_name, model_contents + <<ACCESSIBLE_FIELDS) if model_exists?
|
26
|
-
attr_accessible :
|
26
|
+
attr_accessible :request_permissions_ack, :request_permissions_correlation_id, :request_permissions_request_token,
|
27
|
+
:request_permissions_verifier, :request_permissions_envelope_timestamp,
|
28
|
+
:request_permissions_errors, :request_permissions_raw_response,
|
29
|
+
:request_permissions_callback_ack, :request_permissions_callback_correlation_id, :request_permissions_callback_request_token,
|
30
|
+
:request_permissions_callback_verifier, :request_permissions_callback_envelope_timestamp,
|
31
|
+
:request_permissions_callback_errors, :request_permissions_callback_raw_response,
|
32
|
+
:get_access_token_ack, :get_access_token_correlation_id, :get_access_token_access_token,
|
33
|
+
:get_access_token_verifier, :get_access_token_envelope_timestamp,
|
34
|
+
:get_access_token_errors, :get_access_token_raw_response
|
27
35
|
ACCESSIBLE_FIELDS
|
28
36
|
end
|
29
37
|
|
30
38
|
def migration_data
|
31
39
|
<<MIGRATION_FIELDS
|
32
|
-
#
|
33
|
-
t.string :
|
34
|
-
t.string :
|
35
|
-
t.string :
|
36
|
-
t.
|
37
|
-
t.
|
38
|
-
t.text :
|
39
|
-
|
40
|
+
# RequestPermissions response fields
|
41
|
+
t.string :request_permissions_ack
|
42
|
+
t.string :request_permissions_correlation_id
|
43
|
+
t.string :request_permissions_request_token
|
44
|
+
t.datetime :request_permissions_envelope_timestamp
|
45
|
+
t.text :request_permissions_errors
|
46
|
+
t.text :request_permissions_raw_response
|
47
|
+
|
48
|
+
# RequestPermissions callback fields
|
49
|
+
t.string :request_permissions_callback_ack
|
50
|
+
t.string :request_permissions_callback_correlation_id
|
51
|
+
t.string :request_permissions_callback_verifier
|
52
|
+
t.datetime :request_permissions_callback_envelope_timestamp
|
53
|
+
t.text :request_permissions_callback_errors
|
54
|
+
t.text :request_permissions_callback_raw_response
|
55
|
+
|
56
|
+
# GetAccessToken response fields
|
57
|
+
t.string :get_access_token_ack
|
58
|
+
t.string :get_access_token_correlation_id
|
59
|
+
t.string :get_access_token_access_token
|
60
|
+
t.string :get_access_token_verifier
|
61
|
+
t.datetime :get_access_token_envelope_timestamp
|
62
|
+
t.text :get_access_token_errors
|
63
|
+
t.text :get_access_token_raw_response
|
40
64
|
MIGRATION_FIELDS
|
41
65
|
end
|
42
66
|
|
43
67
|
def indexes
|
44
68
|
<<INDEXES
|
45
|
-
add_index :#{table_name}, :
|
69
|
+
add_index :#{table_name}, :request_permissions_request_token
|
70
|
+
add_index :#{table_name}, :get_access_token_access_token
|
46
71
|
INDEXES
|
47
72
|
end
|
48
73
|
end
|
@@ -3,7 +3,7 @@ module PaypalPermissions
|
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
4
4
|
source_root File.expand_path("../../templates", __FILE__)
|
5
5
|
|
6
|
-
desc "Creates a PaypalPermissions initializer
|
6
|
+
desc "Creates a PaypalPermissions initializer."
|
7
7
|
class_option :orm
|
8
8
|
|
9
9
|
def update_configuration
|
@@ -11,10 +11,10 @@ module PaypalPermissions
|
|
11
11
|
#{Rails.application.class.name.split('::').first}::Application.configure do
|
12
12
|
config.after_initialize do
|
13
13
|
permissions_options = {
|
14
|
-
:login =>
|
15
|
-
:password =>
|
16
|
-
:signature =>
|
17
|
-
:app_id =>
|
14
|
+
:login => 'TODO: your PayPal sandbox caller login',
|
15
|
+
:password => 'TODO: your PayPal sandbox caller password',
|
16
|
+
:signature => 'TODO: your PayPal sandbox caller signature',
|
17
|
+
:app_id => 'APP-80W284485P519543T', # This is the app_id for all PayPal Permissions Service sandbox test apps
|
18
18
|
}
|
19
19
|
::PAYPAL_PERMISSIONS_GATEWAY = ActiveMerchant::Billing::PaypalPermissionsGateway.new(permissions_options)
|
20
20
|
end
|
@@ -25,10 +25,10 @@ end
|
|
25
25
|
#{Rails.application.class.name.split('::').first}::Application.configure do
|
26
26
|
config.after_initialize do
|
27
27
|
permissions_options = {
|
28
|
-
:login =>
|
29
|
-
:password =>
|
30
|
-
:signature =>
|
31
|
-
:app_id =>
|
28
|
+
:login => 'TODO: your PayPal live caller login',
|
29
|
+
:password => 'TODO: your PayPal live caller password',
|
30
|
+
:signature => 'TODO: your PayPal live caller signature',
|
31
|
+
:app_id => 'TODO: your PayPal live app id',
|
32
32
|
}
|
33
33
|
::PAYPAL_PERMISSIONS_GATEWAY = ActiveMerchant::Billing::PaypalPermissionsGateway.new(permissions_options)
|
34
34
|
end
|
@@ -3,7 +3,7 @@ module PaypalPermissions
|
|
3
3
|
module OrmHelpers
|
4
4
|
def model_contents
|
5
5
|
<<-CONTENT
|
6
|
-
validates :
|
6
|
+
# validates :request_permissions_ack, :presence => true
|
7
7
|
CONTENT
|
8
8
|
end
|
9
9
|
|
@@ -12,7 +12,7 @@ CONTENT
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def migration_exists?(table_name)
|
15
|
-
Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+
|
15
|
+
Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_paypal_permissions_to_#{table_name}.rb$/).first
|
16
16
|
end
|
17
17
|
|
18
18
|
def migration_path
|
@@ -1,17 +1,23 @@
|
|
1
1
|
module PaypalPermissions
|
2
2
|
module Generators
|
3
3
|
class PaypalPermissionsGenerator < Rails::Generators::NamedBase
|
4
|
-
namespace "paypal_permissions"
|
5
4
|
source_root File.expand_path("../templates", __FILE__)
|
6
5
|
|
7
6
|
desc "Generates a paypal_permissions resource with NAME along with a database migration."
|
8
7
|
|
9
8
|
hook_for :orm
|
10
9
|
|
10
|
+
def generate_controller
|
11
|
+
generate "controller", plural_name if behavior == :invoke
|
12
|
+
end
|
13
|
+
|
11
14
|
class_option :routes, :desc => "Generate routes", :type => :boolean, :default => true
|
12
15
|
|
13
16
|
def insert_paypal_permissions_routes
|
14
|
-
|
17
|
+
if options.routes?
|
18
|
+
route "match '#{plural_name}/request_permissions_callback' => '#{plural_name}#request_permissions_callback', :via => [ :post ], :as => :#{plural_name}_request_permissions_callback_url"
|
19
|
+
route "resources :#{plural_name}"
|
20
|
+
end
|
15
21
|
end
|
16
22
|
end
|
17
23
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paypal_permissions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.3
|
4
|
+
version: 0.0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-03-06 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
16
|
-
requirement: &
|
16
|
+
requirement: &2160369400 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2160369400
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: vcr
|
27
|
-
requirement: &
|
27
|
+
requirement: &2160368560 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '1.11'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2160368560
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: railties
|
38
|
-
requirement: &
|
38
|
+
requirement: &2160367380 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '3.0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2160367380
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: activesupport
|
49
|
-
requirement: &
|
49
|
+
requirement: &2160366740 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '3.0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2160366740
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: activemerchant
|
60
|
-
requirement: &
|
60
|
+
requirement: &2160366180 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :runtime
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *2160366180
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &2160365220 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '2.6'
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *2160365220
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: ammeter
|
82
|
-
requirement: &
|
82
|
+
requirement: &2160364660 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :runtime
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *2160364660
|
91
91
|
description: ! '"A gem to support PayPal Permissions API for Rails applications using
|
92
92
|
ActiveMerchant."'
|
93
93
|
email:
|
@@ -98,7 +98,7 @@ extra_rdoc_files: []
|
|
98
98
|
files:
|
99
99
|
- .gitignore
|
100
100
|
- Gemfile
|
101
|
-
- README
|
101
|
+
- README.md
|
102
102
|
- Rakefile
|
103
103
|
- config/locales/en.yml
|
104
104
|
- lib/active_merchant.rb
|
@@ -134,7 +134,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
134
|
version: '0'
|
135
135
|
segments:
|
136
136
|
- 0
|
137
|
-
hash:
|
137
|
+
hash: -1249773084249501105
|
138
138
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
139
|
none: false
|
140
140
|
requirements:
|
@@ -143,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
143
|
version: '0'
|
144
144
|
segments:
|
145
145
|
- 0
|
146
|
-
hash:
|
146
|
+
hash: -1249773084249501105
|
147
147
|
requirements: []
|
148
148
|
rubyforge_project: paypal_permissions
|
149
149
|
rubygems_version: 1.8.10
|
data/README
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Nothing to see here yet. A work in progress.
|