amorail 0.4.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|