amorail 0.4.0 → 0.7.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 +5 -5
- data/.github/workflows/rspec.yml +23 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +19 -0
- data/README.md +49 -8
- data/RELEASING.md +43 -0
- data/amorail.gemspec +5 -3
- data/lib/amorail.rb +27 -6
- data/lib/amorail/access_token.rb +44 -0
- data/lib/amorail/client.rb +84 -23
- data/lib/amorail/config.rb +14 -8
- data/lib/amorail/entities/company.rb +2 -0
- data/lib/amorail/entities/contact.rb +3 -0
- data/lib/amorail/entities/contact_link.rb +2 -0
- data/lib/amorail/entities/elementable.rb +4 -2
- data/lib/amorail/entities/lead.rb +3 -0
- data/lib/amorail/entities/leadable.rb +3 -0
- data/lib/amorail/entities/note.rb +2 -0
- data/lib/amorail/entities/task.rb +2 -0
- data/lib/amorail/entities/webhook.rb +44 -0
- data/lib/amorail/entity.rb +17 -3
- data/lib/amorail/entity/finders.rb +5 -2
- data/lib/amorail/entity/params.rb +3 -2
- data/lib/amorail/entity/persistence.rb +5 -0
- data/lib/amorail/exceptions.rb +2 -0
- data/lib/amorail/property.rb +7 -3
- data/lib/amorail/railtie.rb +3 -1
- data/lib/amorail/store_adapters.rb +15 -0
- data/lib/amorail/store_adapters/abstract_store_adapter.rb +23 -0
- data/lib/amorail/store_adapters/memory_store_adapter.rb +50 -0
- data/lib/amorail/store_adapters/redis_store_adapter.rb +83 -0
- data/lib/amorail/version.rb +3 -1
- data/lib/tasks/amorail.rake +2 -0
- data/spec/access_token_spec.rb +59 -0
- data/spec/client_spec.rb +36 -24
- data/spec/company_spec.rb +2 -0
- data/spec/contact_link_spec.rb +2 -0
- data/spec/contact_spec.rb +2 -0
- data/spec/entity_spec.rb +2 -0
- data/spec/fixtures/amorail_test.yml +5 -3
- data/spec/fixtures/authorize.json +6 -0
- data/spec/fixtures/webhooks/list.json +24 -0
- data/spec/fixtures/webhooks/subscribe.json +17 -0
- data/spec/fixtures/webhooks/unsubscribe.json +17 -0
- data/spec/helpers/webmock_helpers.rb +80 -13
- data/spec/lead_spec.rb +2 -0
- data/spec/my_contact_spec.rb +2 -0
- data/spec/note_spec.rb +2 -0
- data/spec/property_spec.rb +2 -0
- data/spec/spec_helper.rb +4 -2
- data/spec/store_adapters/memory_store_adapter_spec.rb +56 -0
- data/spec/store_adapters/redis_store_adapter_spec.rb +67 -0
- data/spec/support/elementable_example.rb +2 -0
- data/spec/support/entity_class_example.rb +2 -0
- data/spec/support/leadable_example.rb +2 -0
- data/spec/support/my_contact.rb +2 -0
- data/spec/support/my_entity.rb +2 -0
- data/spec/task_spec.rb +2 -0
- data/spec/webhook_spec.rb +61 -0
- metadata +48 -17
- data/.travis.yml +0 -9
data/lib/amorail/config.rb
CHANGED
@@ -1,17 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'anyway'
|
2
4
|
|
3
5
|
module Amorail
|
4
6
|
# Amorail config contains:
|
5
|
-
# - usermail ("user@gmail.com")
|
6
|
-
# - api_key ("13601bbac84727df")
|
7
7
|
# - api_endpoint ("http://you_company.amocrm.com")
|
8
8
|
# - api_path (default: "/private/api/v2/json/")
|
9
|
-
# - auth_url (default: "/
|
9
|
+
# - auth_url (default: "/oauth2/access_token")
|
10
10
|
class Config < Anyway::Config
|
11
|
-
attr_config :
|
12
|
-
:
|
13
|
-
:
|
14
|
-
|
15
|
-
|
11
|
+
attr_config :api_endpoint,
|
12
|
+
:client_id,
|
13
|
+
:client_secret,
|
14
|
+
:code,
|
15
|
+
:redirect_uri,
|
16
|
+
:redis_url,
|
17
|
+
api_path: '/private/api/v2/json/',
|
18
|
+
auth_url: '/oauth2/access_token',
|
19
|
+
redis_host: '127.0.0.1',
|
20
|
+
redis_port: '6379',
|
21
|
+
redis_db_name: '0'
|
16
22
|
end
|
17
23
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail
|
2
4
|
# Provides common functionallity for entities
|
3
5
|
# that can be attached to another objects.
|
@@ -6,9 +8,9 @@ module Amorail
|
|
6
8
|
|
7
9
|
ELEMENT_TYPES = {
|
8
10
|
contact: 1,
|
9
|
-
lead:
|
11
|
+
lead: 2,
|
10
12
|
company: 3,
|
11
|
-
task:
|
13
|
+
task: 4
|
12
14
|
}.freeze
|
13
15
|
|
14
16
|
included do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail
|
2
4
|
# AmoCRM lead entity
|
3
5
|
class Lead < Amorail::Entity
|
@@ -15,6 +17,7 @@ module Amorail
|
|
15
17
|
# Return list of associated contacts
|
16
18
|
def contacts
|
17
19
|
fail NotPersisted if id.nil?
|
20
|
+
|
18
21
|
@contacts ||=
|
19
22
|
begin
|
20
23
|
links = Amorail::ContactLink.find_by_leads(id)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail
|
2
4
|
# Lead associations
|
3
5
|
module Leadable
|
@@ -22,6 +24,7 @@ module Amorail
|
|
22
24
|
# Return all linked leads
|
23
25
|
def leads
|
24
26
|
return [] if linked_leads_id.empty?
|
27
|
+
|
25
28
|
@leads ||= Amorail::Lead.find_all(linked_leads_id)
|
26
29
|
end
|
27
30
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Amorail
|
4
|
+
# AmoCRM webhook entity
|
5
|
+
class Webhook < Entity
|
6
|
+
amo_names 'webhooks'
|
7
|
+
|
8
|
+
amo_field :id, :url, :events, :disabled
|
9
|
+
|
10
|
+
def self.list
|
11
|
+
response = client.safe_request(:get, remote_url('list'))
|
12
|
+
|
13
|
+
return [] if response.body.blank?
|
14
|
+
|
15
|
+
response.body['response'].fetch(amo_response_name, []).map do |attributes|
|
16
|
+
new.reload_model(attributes)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.subscribe(webhooks)
|
21
|
+
perform_webhooks_request('subscribe', webhooks) do |data|
|
22
|
+
data.map { |attrs| new.reload_model(attrs) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.unsubscribe(webhooks)
|
27
|
+
perform_webhooks_request('unsubscribe', webhooks)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.perform_webhooks_request(action, webhooks, &block)
|
31
|
+
response = client.safe_request(
|
32
|
+
:post,
|
33
|
+
remote_url(action),
|
34
|
+
request: { webhooks: { action => webhooks } }
|
35
|
+
)
|
36
|
+
|
37
|
+
return response unless block
|
38
|
+
|
39
|
+
block.call(response.body['response'].dig(amo_response_name, 'subscribe'))
|
40
|
+
end
|
41
|
+
|
42
|
+
private_class_method :perform_webhooks_request
|
43
|
+
end
|
44
|
+
end
|
data/lib/amorail/entity.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_model'
|
2
4
|
|
3
5
|
module Amorail
|
@@ -32,7 +34,7 @@ module Amorail
|
|
32
34
|
|
33
35
|
def amo_property(name, options = {})
|
34
36
|
properties[name] = options
|
35
|
-
attr_accessor(name)
|
37
|
+
attr_accessor(options.fetch(:method_name, name))
|
36
38
|
end
|
37
39
|
|
38
40
|
def attributes
|
@@ -77,6 +79,7 @@ module Amorail
|
|
77
79
|
attrs.each do |k, v|
|
78
80
|
action = "#{k}="
|
79
81
|
next unless respond_to?(action)
|
82
|
+
|
80
83
|
send(action, v)
|
81
84
|
end
|
82
85
|
self
|
@@ -84,15 +87,26 @@ module Amorail
|
|
84
87
|
|
85
88
|
def merge_custom_fields(fields)
|
86
89
|
return if fields.nil?
|
90
|
+
|
87
91
|
fields.each do |f|
|
88
|
-
fname = f
|
92
|
+
fname = custom_field_name(f)
|
89
93
|
next if fname.nil?
|
94
|
+
|
90
95
|
fname = "#{fname.downcase}="
|
91
96
|
fval = f.fetch('values').first.fetch('value')
|
92
97
|
send(fname, fval) if respond_to?(fname)
|
93
98
|
end
|
94
99
|
end
|
95
100
|
|
101
|
+
def custom_field_name(field)
|
102
|
+
fname = field['code'] || field['name']
|
103
|
+
return if fname.nil?
|
104
|
+
|
105
|
+
fname = self.class.properties
|
106
|
+
.fetch(fname.downcase, {})[:method_name] || fname
|
107
|
+
fname
|
108
|
+
end
|
109
|
+
|
96
110
|
# call safe method <safe_request>. safe_request call authorize
|
97
111
|
# if current session undefined or expires.
|
98
112
|
def push(method)
|
@@ -119,7 +133,7 @@ module Amorail
|
|
119
133
|
)
|
120
134
|
reload_model(data)
|
121
135
|
rescue InvalidRecord
|
122
|
-
|
136
|
+
false
|
123
137
|
end
|
124
138
|
end
|
125
139
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail # :nodoc: all
|
2
4
|
class Entity
|
3
5
|
class << self
|
@@ -11,6 +13,7 @@ module Amorail # :nodoc: all
|
|
11
13
|
def find!(id)
|
12
14
|
rec = find(id)
|
13
15
|
fail RecordNotFound unless rec
|
16
|
+
|
14
17
|
rec
|
15
18
|
end
|
16
19
|
|
@@ -32,8 +35,8 @@ module Amorail # :nodoc: all
|
|
32
35
|
|
33
36
|
# Find AMO entities by query
|
34
37
|
# Returns array of matching entities.
|
35
|
-
def find_by_query(
|
36
|
-
where(query:
|
38
|
+
def find_by_query(query)
|
39
|
+
where(query: query)
|
37
40
|
end
|
38
41
|
|
39
42
|
private
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/core_ext/hash/indifferent_access"
|
2
4
|
|
3
5
|
module Amorail # :nodoc: all
|
@@ -19,10 +21,9 @@ module Amorail # :nodoc: all
|
|
19
21
|
props = properties.send(self.class.amo_name)
|
20
22
|
|
21
23
|
custom_fields = []
|
22
|
-
|
23
24
|
self.class.properties.each do |k, v|
|
24
25
|
prop_id = props.send(k).id
|
25
|
-
prop_val = { value: send(k) }.merge(v)
|
26
|
+
prop_val = { value: send(v.fetch(:method_name, k)) }.merge(v)
|
26
27
|
custom_fields << { id: prop_id, values: [prop_val] }
|
27
28
|
end
|
28
29
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail # :nodoc: all
|
2
4
|
class Entity
|
3
5
|
class InvalidRecord < ::Amorail::Error; end
|
@@ -13,6 +15,7 @@ module Amorail # :nodoc: all
|
|
13
15
|
|
14
16
|
def save
|
15
17
|
return false unless valid?
|
18
|
+
|
16
19
|
new_record? ? push('add') : push('update')
|
17
20
|
end
|
18
21
|
|
@@ -22,6 +25,7 @@ module Amorail # :nodoc: all
|
|
22
25
|
|
23
26
|
def update(attrs = {})
|
24
27
|
return false if new_record?
|
28
|
+
|
25
29
|
merge_params(attrs)
|
26
30
|
push('update')
|
27
31
|
end
|
@@ -32,6 +36,7 @@ module Amorail # :nodoc: all
|
|
32
36
|
|
33
37
|
def reload
|
34
38
|
fail NotPersisted if id.nil?
|
39
|
+
|
35
40
|
load_record(id)
|
36
41
|
end
|
37
42
|
|
data/lib/amorail/exceptions.rb
CHANGED
data/lib/amorail/property.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail
|
2
4
|
# Return hash key as method call
|
3
5
|
module MethodMissing
|
4
6
|
def method_missing(method_sym, *arguments, &block)
|
5
|
-
if data.key?(method_sym.to_s)
|
6
|
-
data.fetch(method_sym.to_s)
|
7
|
+
if data.key?(method_sym.to_s.downcase)
|
8
|
+
data.fetch(method_sym.to_s.downcase)
|
7
9
|
else
|
8
10
|
super
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
12
14
|
def respond_to_missing?(method_sym, *args)
|
13
|
-
args.size.zero? && data.key?(method_sym.to_s)
|
15
|
+
args.size.zero? && data.key?(method_sym.to_s.downcase)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
@@ -26,6 +28,7 @@ module Amorail
|
|
26
28
|
data['custom_fields'].fetch(source_name, []).each do |contact|
|
27
29
|
identifier = contact['code'].presence || contact['name'].presence
|
28
30
|
next if identifier.nil?
|
31
|
+
|
29
32
|
hash[identifier.downcase] = PropertyItem.new(contact)
|
30
33
|
end
|
31
34
|
new hash
|
@@ -118,6 +121,7 @@ module Amorail
|
|
118
121
|
prop_item = PropertyItem.new(tt)
|
119
122
|
identifier = tt['code'].presence || tt['name'].presence
|
120
123
|
next if identifier.nil?
|
124
|
+
|
121
125
|
hash[identifier.downcase] = prop_item
|
122
126
|
hash[identifier] = prop_item
|
123
127
|
end
|
data/lib/amorail/railtie.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Amorail
|
2
4
|
# Add amorail rake tasks
|
3
5
|
class Railtie < Rails::Railtie
|
4
6
|
rake_tasks do
|
5
|
-
load File.expand_path('
|
7
|
+
load File.expand_path('../tasks/amorail.rake', __dir__)
|
6
8
|
end
|
7
9
|
end
|
8
10
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'amorail/store_adapters/abstract_store_adapter'
|
4
|
+
require 'amorail/store_adapters/redis_store_adapter'
|
5
|
+
require 'amorail/store_adapters/memory_store_adapter'
|
6
|
+
|
7
|
+
module Amorail
|
8
|
+
module StoreAdapters
|
9
|
+
def self.build_by_name(adapter, options = nil)
|
10
|
+
camelized_adapter = adapter.to_s.split('_').map(&:capitalize).join
|
11
|
+
adapter_class_name = "#{camelized_adapter}StoreAdapter"
|
12
|
+
StoreAdapters.const_get(adapter_class_name).new(**(options || {}))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Amorail
|
4
|
+
module StoreAdapters
|
5
|
+
class AbstractStoreAdapter
|
6
|
+
def fetch_access(_secret)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def persist_access(_secret, _token, _refresh_token, _expiration)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_refresh(_secret, _token, _refresh_token, _expiration)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def access_expired?(_key)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Amorail
|
4
|
+
module StoreAdapters
|
5
|
+
class MemoryStoreAdapter < AbstractStoreAdapter
|
6
|
+
attr_reader :storage
|
7
|
+
|
8
|
+
def initialize(**options)
|
9
|
+
raise ArgumentError, 'Memory store doesn\'t support any options' if options.any?
|
10
|
+
@storage = Hash.new { |hh, kk| hh[kk] = {} }
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch_access(secret)
|
14
|
+
value_if_not_expired(secret)
|
15
|
+
end
|
16
|
+
|
17
|
+
def persist_access(secret, token, refresh_token, expiration)
|
18
|
+
access_token = { token: token, refresh_token: refresh_token, expiration: expiration }
|
19
|
+
storage.store(secret, access_token)
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_access(secret, token, refresh_token, expiration)
|
23
|
+
update_access_fields(
|
24
|
+
secret,
|
25
|
+
token: token,
|
26
|
+
refresh_token: refresh_token,
|
27
|
+
expiration: expiration
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def access_expired?(key)
|
32
|
+
storage[key][:expiration] && Time.now.to_i >= storage[key][:expiration]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def value_if_not_expired(key)
|
38
|
+
if !access_expired?(key)
|
39
|
+
storage[key]
|
40
|
+
else
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_access_fields(key, fields)
|
46
|
+
storage[key].merge!(fields)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|