infopark_webcrm_sdk 1.0.0.rc3
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/LICENSE +840 -0
- data/README.md +113 -0
- data/UPGRADE.md +507 -0
- data/config/ca-bundle.crt +4484 -0
- data/lib/crm/account.rb +17 -0
- data/lib/crm/activity.rb +143 -0
- data/lib/crm/collection.rb +41 -0
- data/lib/crm/contact.rb +121 -0
- data/lib/crm/core/attachment_store.rb +122 -0
- data/lib/crm/core/basic_resource.rb +68 -0
- data/lib/crm/core/configuration.rb +65 -0
- data/lib/crm/core/connection_manager.rb +98 -0
- data/lib/crm/core/item_enumerator.rb +61 -0
- data/lib/crm/core/log_subscriber.rb +41 -0
- data/lib/crm/core/mixins/attribute_provider.rb +135 -0
- data/lib/crm/core/mixins/change_loggable.rb +98 -0
- data/lib/crm/core/mixins/findable.rb +28 -0
- data/lib/crm/core/mixins/inspectable.rb +27 -0
- data/lib/crm/core/mixins/merge_and_deletable.rb +17 -0
- data/lib/crm/core/mixins/modifiable.rb +102 -0
- data/lib/crm/core/mixins/searchable.rb +88 -0
- data/lib/crm/core/mixins.rb +6 -0
- data/lib/crm/core/rest_api.rb +148 -0
- data/lib/crm/core/search_configurator.rb +207 -0
- data/lib/crm/core.rb +6 -0
- data/lib/crm/errors.rb +169 -0
- data/lib/crm/event.rb +17 -0
- data/lib/crm/event_contact.rb +16 -0
- data/lib/crm/mailing.rb +111 -0
- data/lib/crm/template_set.rb +81 -0
- data/lib/crm/type.rb +78 -0
- data/lib/crm.rb +154 -0
- data/lib/infopark_webcrm_sdk.rb +1 -0
- metadata +149 -0
data/lib/crm/account.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Crm
|
2
|
+
# An Infopark WebCRM account is an organizational entity such as a company.
|
3
|
+
# @api public
|
4
|
+
class Account < Core::BasicResource
|
5
|
+
include Core::Mixins::Findable
|
6
|
+
include Core::Mixins::Modifiable
|
7
|
+
include Core::Mixins::ChangeLoggable
|
8
|
+
include Core::Mixins::MergeAndDeletable
|
9
|
+
include Core::Mixins::Searchable
|
10
|
+
include Core::Mixins::Inspectable
|
11
|
+
inspectable :id, :name
|
12
|
+
|
13
|
+
# @!parse extend Core::Mixins::Findable::ClassMethods
|
14
|
+
# @!parse extend Core::Mixins::Modifiable::ClassMethods
|
15
|
+
# @!parse extend Core::Mixins::Searchable::ClassMethods
|
16
|
+
end
|
17
|
+
end
|
data/lib/crm/activity.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
module Crm
|
2
|
+
# An Infopark WebCRM activity is a record of an action or a sequence of actions,
|
3
|
+
# for example a support case.
|
4
|
+
# It can be associated with an {Account account} or a {Contact contact}.
|
5
|
+
#
|
6
|
+
# === Comments
|
7
|
+
# Comments can be read be means of {#comments}.
|
8
|
+
# In order to add a comment, set the following write-only attributes on {.create} or {#update}:
|
9
|
+
# * +comment_notes+ (String) - the comment text.
|
10
|
+
# * +comment_contact_id+ (String) - the contact ID of the comment author (optional).
|
11
|
+
# * +comment_published+ (Boolean) - whether the comment should be visible
|
12
|
+
# to the associated contact of this activity (+item.contact_id+).
|
13
|
+
# Default: +false+.
|
14
|
+
# * +comment_attachments+ (Array<String, #read>) - the comment attachments (optional).
|
15
|
+
# Every array element may either be an attachment ID or an object that implements +#read+
|
16
|
+
# (e.g. an open file). In the latter case, the content will be
|
17
|
+
# uploaded automatically. See {Crm::Core::AttachmentStore} for manually uploading attachments.
|
18
|
+
# @api public
|
19
|
+
class Activity < Core::BasicResource
|
20
|
+
include Core::Mixins::Findable
|
21
|
+
include Core::Mixins::Modifiable
|
22
|
+
include Core::Mixins::ChangeLoggable
|
23
|
+
include Core::Mixins::Searchable
|
24
|
+
include Core::Mixins::Inspectable
|
25
|
+
inspectable :id, :title, :type_id
|
26
|
+
|
27
|
+
# @!parse extend Core::Mixins::Findable::ClassMethods
|
28
|
+
# @!parse extend Core::Mixins::Modifiable::ClassMethods
|
29
|
+
# @!parse extend Core::Mixins::Searchable::ClassMethods
|
30
|
+
|
31
|
+
# Creates a new activity using the given +params+.
|
32
|
+
# See {Core::Mixins::Modifiable::ClassMethods#create Modifiable.create} for details.
|
33
|
+
# @return [self] the created activity.
|
34
|
+
# @api public
|
35
|
+
def self.create(attributes = {})
|
36
|
+
super(filter_attributes(attributes))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Updates the attributes of this activity.
|
40
|
+
# See {Core::Mixins::Modifiable#update Modifiable#update} for details.
|
41
|
+
# @return [self] the updated activity.
|
42
|
+
# @api public
|
43
|
+
def update(attributes = {})
|
44
|
+
super(self.class.filter_attributes(attributes))
|
45
|
+
end
|
46
|
+
|
47
|
+
# +Comment+ represents a comment of an {Activity Activity},
|
48
|
+
# for example a single comment of a support case discussion.
|
49
|
+
# @api public
|
50
|
+
class Comment
|
51
|
+
include Core::Mixins::AttributeProvider
|
52
|
+
|
53
|
+
# +Attachment+ represents an attachment of an {Activity::Comment activity comment}.
|
54
|
+
# @api public
|
55
|
+
class Attachment
|
56
|
+
# Returns the ID of this attachment.
|
57
|
+
# @return [String]
|
58
|
+
# @api public
|
59
|
+
attr_reader :id
|
60
|
+
|
61
|
+
def initialize(id)
|
62
|
+
@id = id
|
63
|
+
end
|
64
|
+
|
65
|
+
# Generates a download URL for this attachment.
|
66
|
+
# Retrieve the attachment data by fetching this URL.
|
67
|
+
# This URL is only valid for a couple of minutes.
|
68
|
+
# Hence, it is recommended to have such URLs generated on demand.
|
69
|
+
# @return [String]
|
70
|
+
# @api public
|
71
|
+
def download_url
|
72
|
+
Crm::Core::AttachmentStore.generate_download_url(id)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(raw_comment)
|
77
|
+
comment = raw_comment.dup
|
78
|
+
comment['attachments'] = raw_comment['attachments'].map{ |attachment_id|
|
79
|
+
Attachment.new(attachment_id)
|
80
|
+
}
|
81
|
+
super(comment)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!attribute [r] attachments
|
85
|
+
# Returns the list of comment {Attachment attachments}.
|
86
|
+
# @return [Array<Attachment>]
|
87
|
+
# @api public
|
88
|
+
|
89
|
+
# @!attribute [r] updated_at
|
90
|
+
# Returns the timestamp of the comment.
|
91
|
+
# @return [Time]
|
92
|
+
# @api public
|
93
|
+
|
94
|
+
# @!attribute [r] updated_by
|
95
|
+
# Returns the login of the API user who created the comment.
|
96
|
+
# @return [String]
|
97
|
+
# @api public
|
98
|
+
|
99
|
+
# @!attribute [r] notes
|
100
|
+
# Returns the comment text.
|
101
|
+
# @return [String]
|
102
|
+
# @api public
|
103
|
+
|
104
|
+
# @!attribute [r] published?
|
105
|
+
# Returns whether the comment is published.
|
106
|
+
# @return [Boolean]
|
107
|
+
# @api public
|
108
|
+
def published?
|
109
|
+
published
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @!attribute [r] comments
|
114
|
+
# Returns the {Comment comments} of this activity.
|
115
|
+
# @return [Array<Comment>]
|
116
|
+
# @api public
|
117
|
+
|
118
|
+
def self.filter_attributes(attributes)
|
119
|
+
attachments = attributes.delete('comment_attachments') ||
|
120
|
+
attributes.delete(:comment_attachments)
|
121
|
+
if attachments
|
122
|
+
attributes['comment_attachments'] = attachments.map do |attachment|
|
123
|
+
if attachment.respond_to?(:read)
|
124
|
+
Core::AttachmentStore.upload(attachment)
|
125
|
+
else
|
126
|
+
attachment
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
attributes
|
131
|
+
end
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
def load_attributes(raw_attributes)
|
136
|
+
attributes = raw_attributes.dup
|
137
|
+
attributes['comments'] = (attributes['comments'] || []).map do |comment_attributes|
|
138
|
+
Comment.new(comment_attributes)
|
139
|
+
end
|
140
|
+
super(attributes)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Crm
|
2
|
+
# An Infopark WebCRM collection is a saved search. To execute such a saved search, call {#compute}.
|
3
|
+
# The results are persisted and can be accessed by means of {#output_items}.
|
4
|
+
# Output items can be {Account accounts}, {Contact contacts}, {Activity activities},
|
5
|
+
# and {Event events}.
|
6
|
+
# @api public
|
7
|
+
class Collection < Core::BasicResource
|
8
|
+
include Core::Mixins::Findable
|
9
|
+
include Core::Mixins::Modifiable
|
10
|
+
include Core::Mixins::ChangeLoggable
|
11
|
+
include Core::Mixins::Searchable
|
12
|
+
include Core::Mixins::Inspectable
|
13
|
+
inspectable :id, :title
|
14
|
+
|
15
|
+
# @!parse extend Core::Mixins::Findable::ClassMethods
|
16
|
+
# @!parse extend Core::Mixins::Modifiable::ClassMethods
|
17
|
+
# @!parse extend Core::Mixins::Searchable::ClassMethods
|
18
|
+
|
19
|
+
# Computes this collection.
|
20
|
+
# @return [self]
|
21
|
+
# @api public
|
22
|
+
def compute
|
23
|
+
load_attributes(Core::RestApi.instance.put("#{path}/compute", {}))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the IDs resulting from the computation.
|
27
|
+
# @return [Array<String>]
|
28
|
+
# @api public
|
29
|
+
def output_ids
|
30
|
+
Core::RestApi.instance.get("#{path}/output_ids")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns an {Core::ItemEnumerator ItemEnumerator}
|
34
|
+
# that provides access to the items of {#output_ids}.
|
35
|
+
# @return [Core::ItemEnumerator]
|
36
|
+
# @api public
|
37
|
+
def output_items
|
38
|
+
Core::ItemEnumerator.new(output_ids)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/crm/contact.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module Crm
|
2
|
+
# An Infopark WebCRM contact represents contact information about a person.
|
3
|
+
# It can be associated with an {Account account}.
|
4
|
+
# @api public
|
5
|
+
class Contact < Core::BasicResource
|
6
|
+
include Core::Mixins::Findable
|
7
|
+
include Core::Mixins::Modifiable
|
8
|
+
include Core::Mixins::ChangeLoggable
|
9
|
+
include Core::Mixins::MergeAndDeletable
|
10
|
+
include Core::Mixins::Searchable
|
11
|
+
include Core::Mixins::Inspectable
|
12
|
+
inspectable :id, :last_name, :first_name, :email
|
13
|
+
|
14
|
+
# @!parse extend Core::Mixins::Findable::ClassMethods
|
15
|
+
# @!parse extend Core::Mixins::Modifiable::ClassMethods
|
16
|
+
# @!parse extend Core::Mixins::Searchable::ClassMethods
|
17
|
+
|
18
|
+
# @!group Authentication and password management
|
19
|
+
|
20
|
+
# Authenticates a contact using their +login+ and +password+.
|
21
|
+
# @example
|
22
|
+
# contact = Crm::Contact.authenticate!('jane@example.org', 'correct')
|
23
|
+
# # => Crm::Contact
|
24
|
+
#
|
25
|
+
# contact.login
|
26
|
+
# # => 'jane@example.org'
|
27
|
+
#
|
28
|
+
# Crm::Contact.authenticate!('jane@example.org', 'wrong')
|
29
|
+
# # => raises AuthenticationFailed
|
30
|
+
# @param login [String] the login of the contact.
|
31
|
+
# @param password [String] the password of the contact.
|
32
|
+
# @return [Contact] the authenticated contact.
|
33
|
+
# @raise [Errors::AuthenticationFailed] if the +login+/+password+ combination is wrong.
|
34
|
+
# @api public
|
35
|
+
def self.authenticate!(login, password)
|
36
|
+
new(Core::RestApi.instance.put("#{path}/authenticate",
|
37
|
+
{'login' => login, 'password' => password}))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Authenticates a contact using their +login+ and +password+.
|
41
|
+
# @example
|
42
|
+
# contact = Crm::Contact.authenticate('jane@example.org', 'correct')
|
43
|
+
# # => Crm::Contact
|
44
|
+
#
|
45
|
+
# contact.login
|
46
|
+
# # => 'jane@example.org'
|
47
|
+
#
|
48
|
+
# Crm::Contact.authenticate('jane@example.org', 'wrong')
|
49
|
+
# # => nil
|
50
|
+
# @param login [String] the login of the contact.
|
51
|
+
# @param password [String] the password of the contact.
|
52
|
+
# @return [Contact, nil] the authenticated contact. +nil+ if authentication failed.
|
53
|
+
# @api public
|
54
|
+
def self.authenticate(login, password)
|
55
|
+
authenticate!(login, password)
|
56
|
+
rescue Errors::AuthenticationFailed
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets the new password.
|
61
|
+
# @param new_password [String] the new password.
|
62
|
+
# @return [self] the updated contact.
|
63
|
+
# @api public
|
64
|
+
def set_password(new_password)
|
65
|
+
load_attributes(Core::RestApi.instance.put("#{path}/set_password",
|
66
|
+
{'password' => new_password}))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Generates a password token.
|
70
|
+
#
|
71
|
+
# Use case: A project sends an e-mail to the contact. The e-mail contains a link to
|
72
|
+
# the project web app. The link contains the param +?token=...+.
|
73
|
+
# The web app retrieves and parses the token
|
74
|
+
# and passes it to {Contact.set_password_by_token}.
|
75
|
+
# @return [String] the generated token.
|
76
|
+
# @api public
|
77
|
+
def generate_password_token
|
78
|
+
Core::RestApi.instance.post("#{path}/generate_password_token", {})['token']
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sets a contact's new password by means of the token. Generate a token by calling
|
82
|
+
# {#send_password_token_email} or {#generate_password_token}.
|
83
|
+
#
|
84
|
+
# Use case: A contact clicks a link (that includes a token) in an e-mail
|
85
|
+
# to get to a password change page.
|
86
|
+
# @param new_password [String] the new password.
|
87
|
+
# @param token [String] the given token.
|
88
|
+
# @return [Contact] the updated contact.
|
89
|
+
# @raise [Errors::ResourceNotFound] if +token+ is invalid.
|
90
|
+
# @api public
|
91
|
+
def self.set_password_by_token(new_password, token)
|
92
|
+
new(Core::RestApi.instance.put("#{path}/set_password_by_token", {
|
93
|
+
'password' => new_password,
|
94
|
+
'token' => token,
|
95
|
+
}))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Clears the contact's password.
|
99
|
+
# @return [self] the updated contact.
|
100
|
+
# @api public
|
101
|
+
def clear_password
|
102
|
+
load_attributes(Core::RestApi.instance.put("#{path}/clear_password", {}))
|
103
|
+
end
|
104
|
+
|
105
|
+
# Sends a password token by e-mail to this contact.
|
106
|
+
#
|
107
|
+
# Put a link to the project web app into the +password_request_email_body+ template.
|
108
|
+
# The link should contain the +?token=...+ parameter, e.g.:
|
109
|
+
#
|
110
|
+
# <tt>https://example.com/user/set_password?token={{password_request_token}}</tt>
|
111
|
+
#
|
112
|
+
# The web app can then pass the token to {Contact.set_password_by_token}.
|
113
|
+
# @return [void]
|
114
|
+
# @api public
|
115
|
+
def send_password_token_email
|
116
|
+
Core::RestApi.instance.post("#{path}/send_password_token_email", {})
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!endgroup
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Crm; module Core
|
2
|
+
# +AttachmentStore+ represents an attachment of an {Activity::Comment activity comment}.
|
3
|
+
#
|
4
|
+
# To upload a file as an attachment, add it to +comment_attachments+. The SDK will automatically
|
5
|
+
# upload the content of the file.
|
6
|
+
#
|
7
|
+
# Note that this method of uploading an attachment using a browser will upload the file twice.
|
8
|
+
# It is first uploaded to the ruby application (e.g. Rails) which then uploads it to AWS S3.
|
9
|
+
#
|
10
|
+
# To upload the attachment directly to AWS S3 (i.e. bypassing the ruby application),
|
11
|
+
# please proceed as follows:
|
12
|
+
#
|
13
|
+
# 1. Request an upload permission
|
14
|
+
# ({Crm::Core::AttachmentStore.generate_upload_permission AttachmentStore.generate_upload_permission}).
|
15
|
+
# The response grants the client permission to upload a file to a given key on AWS S3.
|
16
|
+
# This permission is valid for one hour.
|
17
|
+
# 2. Upload the file to the URL (+permission.url+ or
|
18
|
+
# +permission.uri+), together with the fields (+permission.fields+) as parameters.
|
19
|
+
# AWS S3 itself then verifies the signature of these parameters prior to accepting the upload.
|
20
|
+
# 3. Attach the upload to a new activity comment by setting its +comment_attachments+ attribute
|
21
|
+
# to an array of upload IDs. The client may append filenames to the upload IDs for producing
|
22
|
+
# download URLs with proper filenames later on.
|
23
|
+
#
|
24
|
+
# The format of +comment_attachments+ is
|
25
|
+
# <tt>["upload_id/filename.ext", ...]</tt>,
|
26
|
+
# e.g. <tt>["e13f0d960feeb2b2903bd/screenshot.jpg"]</tt>.
|
27
|
+
# Infopark WebCRM in turn translates these upload IDs to attachment IDs.
|
28
|
+
# Syntactically they look the same. Upload IDs, however, are only temporary,
|
29
|
+
# whereas attachment IDs are permanent. If the client appended a filename to the upload ID,
|
30
|
+
# the attachment ID will contain this filename, too. Otherwise, the attachment ID ends
|
31
|
+
# with <tt>"/file"</tt>. Please note that Infopark WebCRM replaces filename characters other
|
32
|
+
# than <tt>a-zA-Z0-9.+-</tt> with a dash. Multiple dashes will be joined into a single dash.
|
33
|
+
# 4. Later, when downloading the attachment, pass the attachment ID to
|
34
|
+
# {Crm::Core::AttachmentStore.generate_download_url}.
|
35
|
+
# Infopark WebCRM returns a signed AWS S3 URL that remains valid for 5 minutes.
|
36
|
+
# @api public
|
37
|
+
class AttachmentStore
|
38
|
+
# +Permission+ holds all the pieces of information required to upload an {AttachmentStore attachment}.
|
39
|
+
# Generate a permission by calling {AttachmentStore.generate_upload_permission}.
|
40
|
+
# @api public
|
41
|
+
class Permission
|
42
|
+
# Returns the {http://www.ruby-doc.org/stdlib/libdoc/uri/rdoc/URI.html URI}
|
43
|
+
# for uploading the new attachment data.
|
44
|
+
# @return [URI]
|
45
|
+
# @api public
|
46
|
+
attr_reader :uri
|
47
|
+
|
48
|
+
# Returns the URL for uploading the new attachment data.
|
49
|
+
# @return [String]
|
50
|
+
# @api public
|
51
|
+
attr_reader :url
|
52
|
+
|
53
|
+
# Returns a hash of additional request parameters to be sent to the {#url}.
|
54
|
+
# @return [Hash{String => String}]
|
55
|
+
# @api public
|
56
|
+
attr_reader :fields
|
57
|
+
|
58
|
+
# Returns a temporary ID associated with this upload.
|
59
|
+
# Use this ID when setting the +comment_attachments+ attribute of an activity.
|
60
|
+
# @return [String]
|
61
|
+
# @api public
|
62
|
+
attr_reader :upload_id
|
63
|
+
|
64
|
+
def initialize(uri, url, fields, upload_id)
|
65
|
+
@uri, @url, @fields, @upload_id = uri, url, fields, upload_id
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class << self
|
70
|
+
|
71
|
+
# Obtains the permission to upload a file manually.
|
72
|
+
# The permission is valid for a couple of minutes.
|
73
|
+
# Hence, it is recommended to have such permissions generated on demand.
|
74
|
+
# @return [Permission]
|
75
|
+
# @api public
|
76
|
+
def generate_upload_permission
|
77
|
+
perm = Core::RestApi.instance.post("attachment_store/generate_upload_permission", {})
|
78
|
+
uri = resolve_uri(perm["url"])
|
79
|
+
Permission.new(uri, uri.to_s, perm["fields"], perm["upload_id"])
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generates a download URL for the given attachment.
|
83
|
+
# The URL is valid for a couple of minutes.
|
84
|
+
# Hence, it is recommended to have such URLs generated on demand.
|
85
|
+
# @param attachment_id [String] the ID of an attachment.
|
86
|
+
# @return [String]
|
87
|
+
# @api public
|
88
|
+
def generate_download_url(attachment_id)
|
89
|
+
response = Core::RestApi.instance.post("attachment_store/generate_download_url",
|
90
|
+
{'attachment_id' => attachment_id})
|
91
|
+
resolve_uri(response["url"]).to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
# Uploads a file to S3.
|
95
|
+
# @param file [File] the file to be uploaded.
|
96
|
+
# @return [String] the upload ID. Add this ID to the +comment_attachments+ attribute
|
97
|
+
# of an activity.
|
98
|
+
def upload(file)
|
99
|
+
permission = generate_upload_permission
|
100
|
+
|
101
|
+
file_name = File.basename(file.path)
|
102
|
+
upload_io = UploadIO.new(file, 'application/octet-stream', file_name)
|
103
|
+
params = permission.fields.merge(file: upload_io)
|
104
|
+
request = Net::HTTP::Post::Multipart.new(permission.uri, params)
|
105
|
+
|
106
|
+
response = Core::ConnectionManager.new(permission.uri).request(request)
|
107
|
+
|
108
|
+
if response.code.starts_with?('2')
|
109
|
+
[permission.upload_id, file_name].compact.join('/')
|
110
|
+
else
|
111
|
+
raise Errors::ServerError, "File upload failed with code #{response.code}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def resolve_uri(url)
|
118
|
+
Core::RestApi.instance.resolve_uri(url)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end; end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Crm; module Core
|
2
|
+
# +BasicResource+ is the base class of all Infopark WebCRM SDK resources.
|
3
|
+
# @api public
|
4
|
+
class BasicResource
|
5
|
+
include Mixins::AttributeProvider
|
6
|
+
|
7
|
+
def self.base_type
|
8
|
+
name.split(/::/).last
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.resource_name
|
12
|
+
base_type.underscore
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.path
|
16
|
+
resource_name.pluralize
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the ID of this item.
|
20
|
+
# @return [String]
|
21
|
+
# @api public
|
22
|
+
def id
|
23
|
+
self['id']
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
[self.class.path, id].compact.join('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the type object of this item.
|
31
|
+
# @return [Crm::Type]
|
32
|
+
# @api public
|
33
|
+
def type
|
34
|
+
::Crm::Type.find(type_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Reloads the attributes of this item from the remote web service.
|
38
|
+
# @example
|
39
|
+
# contact.locality
|
40
|
+
# # => 'Bergen'
|
41
|
+
#
|
42
|
+
# # Assume this contact has been modified concurrently.
|
43
|
+
#
|
44
|
+
# contact.reload
|
45
|
+
# # => Crm::Contact
|
46
|
+
#
|
47
|
+
# contact.locality
|
48
|
+
# # => 'Oslo'
|
49
|
+
# @return [self] the reloaded item.
|
50
|
+
# @api public
|
51
|
+
def reload
|
52
|
+
load_attributes(RestApi.instance.get(path))
|
53
|
+
end
|
54
|
+
|
55
|
+
def eql?(other)
|
56
|
+
other.equal?(self) || other.instance_of?(self.class) && other.id == id
|
57
|
+
end
|
58
|
+
|
59
|
+
alias_method :==, :eql?
|
60
|
+
delegate :hash, to: :id
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def if_match_header
|
65
|
+
{'If-Match' => self['version']}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end; end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Crm; module Core
|
2
|
+
# +Configuration+ is yielded by {Crm.configure}.
|
3
|
+
# It lets you set the credentials for accessing the API.
|
4
|
+
# The +tenant+, +login+, and +api_key+ attributes must be provided.
|
5
|
+
# @api public
|
6
|
+
class Configuration
|
7
|
+
attr_reader :api_key
|
8
|
+
attr_reader :login
|
9
|
+
attr_reader :tenant
|
10
|
+
|
11
|
+
attr_accessor :endpoint
|
12
|
+
|
13
|
+
# @param value [String]
|
14
|
+
# @return [void]
|
15
|
+
# @api public
|
16
|
+
attr_writer :api_key
|
17
|
+
|
18
|
+
# @param value [String]
|
19
|
+
# @return [void]
|
20
|
+
# @api public
|
21
|
+
attr_writer :login
|
22
|
+
|
23
|
+
# @param value [String]
|
24
|
+
# @return [void]
|
25
|
+
# @api public
|
26
|
+
attr_writer :tenant
|
27
|
+
|
28
|
+
def endpoint_uri
|
29
|
+
if endpoint.present?
|
30
|
+
url = endpoint
|
31
|
+
url = "https://#{url}" unless url.match(/^http/)
|
32
|
+
url += '/' unless url.end_with?('/')
|
33
|
+
URI.parse(url)
|
34
|
+
else
|
35
|
+
URI.parse("https://#{tenant}.crm.infopark.net/api2/")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger
|
40
|
+
Crm::Core::LogSubscriber.logger
|
41
|
+
end
|
42
|
+
|
43
|
+
# The {http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html logger} of the
|
44
|
+
# Infopark WebCRM SDK. It logs request URLs according to the +:info+ level.
|
45
|
+
# Additionally, it logs request and response payloads according to the +:debug+ level.
|
46
|
+
# Password fields are filtered out.
|
47
|
+
# In a Rails environment, the logger defaults to +Rails.logger+. Otherwise, no logger is set.
|
48
|
+
# @param value [Logger]
|
49
|
+
# @return [void]
|
50
|
+
# @api public
|
51
|
+
# @!parse attr_writer :logger
|
52
|
+
|
53
|
+
def logger=(logger)
|
54
|
+
Crm::Core::LogSubscriber.logger = logger
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate!
|
58
|
+
raise "Missing required configuration key: api_key" if api_key.blank?
|
59
|
+
raise "Missing required configuration key: login" if login.blank?
|
60
|
+
if tenant.blank? && endpoint.blank?
|
61
|
+
raise "Missing required configuration key: tenant"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end; end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Crm; module Core
|
2
|
+
class ConnectionManager
|
3
|
+
SOCKET_ERRORS = [
|
4
|
+
EOFError,
|
5
|
+
Errno::ECONNABORTED,
|
6
|
+
Errno::ECONNREFUSED,
|
7
|
+
Errno::ECONNRESET,
|
8
|
+
Errno::EINVAL,
|
9
|
+
Errno::EPIPE,
|
10
|
+
Errno::ETIMEDOUT,
|
11
|
+
IOError,
|
12
|
+
SocketError,
|
13
|
+
Timeout::Error,
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
DEFAULT_TIMEOUT = 10.freeze
|
17
|
+
|
18
|
+
attr_reader :uri
|
19
|
+
attr_reader :ca_file
|
20
|
+
attr_reader :cert_store
|
21
|
+
|
22
|
+
def initialize(uri)
|
23
|
+
@uri = uri
|
24
|
+
@ca_file = File.expand_path('../../../../config/ca-bundle.crt', __FILE__)
|
25
|
+
@cert_store = OpenSSL::X509::Store.new.tap do |store|
|
26
|
+
store.set_default_paths
|
27
|
+
store.add_file(@ca_file)
|
28
|
+
end
|
29
|
+
@connection = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def request(request, timeout=DEFAULT_TIMEOUT)
|
33
|
+
request['User-Agent'] = user_agent
|
34
|
+
ensure_started(timeout)
|
35
|
+
|
36
|
+
begin
|
37
|
+
@connection.request(request)
|
38
|
+
rescue *SOCKET_ERRORS => e
|
39
|
+
ensure_finished
|
40
|
+
raise Errors::NetworkError.new(e.message, e)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def ensure_started(timeout)
|
47
|
+
if @connection && @connection.started?
|
48
|
+
configure_timeout(@connection, timeout)
|
49
|
+
else
|
50
|
+
conn = Net::HTTP.new(uri.host, uri.port)
|
51
|
+
if uri.scheme == 'https'
|
52
|
+
conn.use_ssl = true
|
53
|
+
conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
54
|
+
conn.cert_store = @cert_store
|
55
|
+
end
|
56
|
+
configure_timeout(conn, timeout)
|
57
|
+
retry_twice_on_socket_error do |attempt|
|
58
|
+
ActiveSupport::Notifications.instrument("establish_connection.crm") do |msg|
|
59
|
+
msg[:attempt] = attempt
|
60
|
+
conn.start
|
61
|
+
end
|
62
|
+
end
|
63
|
+
@connection = conn
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def ensure_finished
|
68
|
+
@connection.finish if @connection && @connection.started?
|
69
|
+
@connection = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def retry_twice_on_socket_error
|
73
|
+
attempt = 1
|
74
|
+
begin
|
75
|
+
yield attempt
|
76
|
+
rescue *SOCKET_ERRORS => e
|
77
|
+
raise Errors::NetworkError.new(e.message, e) if attempt > 2
|
78
|
+
attempt += 1
|
79
|
+
retry
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def configure_timeout(connection, timeout)
|
84
|
+
connection.open_timeout = timeout
|
85
|
+
connection.read_timeout = timeout
|
86
|
+
connection.ssl_timeout = timeout
|
87
|
+
end
|
88
|
+
|
89
|
+
def user_agent
|
90
|
+
@user_agent ||= (
|
91
|
+
gem_info = Gem.loaded_specs["infopark_webcrm_sdk"]
|
92
|
+
if gem_info
|
93
|
+
"#{gem_info.name}-#{gem_info.version}"
|
94
|
+
end
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end; end
|