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.
@@ -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
@@ -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
@@ -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