unisender-ruby 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/CONTRIBUTING.md +43 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +140 -0
  6. data/docs/api.md +1063 -0
  7. data/examples/campaign.rb +13 -0
  8. data/examples/check_connection.rb +16 -0
  9. data/examples/events.rb +20 -0
  10. data/examples/export.rb +11 -0
  11. data/examples/import.rb +18 -0
  12. data/examples/offline.rb +26 -0
  13. data/examples/quickstart.rb +6 -0
  14. data/examples/subscribe.rb +12 -0
  15. data/lib/unisender/batches.rb +31 -0
  16. data/lib/unisender/client.rb +59 -0
  17. data/lib/unisender/errors.rb +129 -0
  18. data/lib/unisender/event_payload.rb +63 -0
  19. data/lib/unisender/events.rb +19 -0
  20. data/lib/unisender/form.rb +67 -0
  21. data/lib/unisender/helpers.rb +28 -0
  22. data/lib/unisender/operations/contacts_1.rb +51 -0
  23. data/lib/unisender/operations/contacts_2.rb +44 -0
  24. data/lib/unisender/operations/inputs_1.rb +48 -0
  25. data/lib/unisender/operations/messages_1.rb +58 -0
  26. data/lib/unisender/operations/messages_2.rb +54 -0
  27. data/lib/unisender/operations/messages_3.rb +12 -0
  28. data/lib/unisender/operations/notes_1.rb +38 -0
  29. data/lib/unisender/operations/statistics_1.rb +48 -0
  30. data/lib/unisender/operations/statistics_2.rb +18 -0
  31. data/lib/unisender/operations/templates_1.rb +45 -0
  32. data/lib/unisender/paginated_operations.rb +42 -0
  33. data/lib/unisender/pagination.rb +68 -0
  34. data/lib/unisender/protocol.rb +46 -0
  35. data/lib/unisender/response.rb +64 -0
  36. data/lib/unisender/retry_policy.rb +82 -0
  37. data/lib/unisender/task_waiter.rb +52 -0
  38. data/lib/unisender/transport.rb +53 -0
  39. data/lib/unisender/validation.rb +72 -0
  40. data/lib/unisender/version.rb +5 -0
  41. data/lib/unisender.rb +29 -0
  42. data/sig/operations.rbs +78 -0
  43. data/sig/unisender.rbs +218 -0
  44. metadata +87 -0
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY, UNISENDER_LIST_ID, and UNISENDER_SENDER_EMAIL.
4
+ # Sends a campaign to the list. Use a verified sender and a list of consenting test recipients.
5
+ require "unisender"
6
+
7
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
8
+ message = client.create_email_message(
9
+ sender_name: "Example", sender_email: ENV.fetch("UNISENDER_SENDER_EMAIL"),
10
+ list_id: ENV.fetch("UNISENDER_LIST_ID"), subject: "Product news", body: "<p>Hello</p>"
11
+ )
12
+ campaign = client.create_campaign(message_id: message.result.fetch("message_id"))
13
+ puts campaign.result.fetch("campaign_id")
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY. Reads lists without printing account data.
4
+ require "unisender"
5
+
6
+ api_key = ENV.fetch("UNISENDER_API_KEY", nil)
7
+ abort "Set UNISENDER_API_KEY before running this example." if api_key.nil? || api_key.strip.empty?
8
+
9
+ begin
10
+ client = Unisender::Client.new(api_key:)
11
+ response = client.get_lists
12
+ puts "Connected to UniSender: HTTP #{response.status}, #{response.result.size} list(s)."
13
+ rescue ArgumentError, Unisender::Error => e
14
+ warn e.message
15
+ exit 1
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY and UNISENDER_CONTACT_EMAIL.
4
+ # Records an event and can trigger account automation.
5
+ require "unisender"
6
+
7
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
8
+ begin
9
+ client.track_events_in_batches(events: [
10
+ { name: "purchase", email: ENV.fetch("UNISENDER_CONTACT_EMAIL"), total: 1200,
11
+ currency: "RUB" }
12
+ ]).each { |response| puts "Recorded batch: HTTP #{response.status}" }
13
+ rescue Unisender::BatchError => e
14
+ warn "Review events #{e.failed_range}; completed batches: #{e.completed_responses.size}"
15
+ failure = e.error
16
+ if failure.is_a?(Unisender::EventError) && failure.possibly_partially_accepted?
17
+ warn "Valid events may already be accepted. Inspect failure.details privately before retrying."
18
+ end
19
+ warn failure.message
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY and UNISENDER_LIST_ID. Starts an export job.
4
+ # The returned download URL grants access to contact data; keep it private.
5
+ require "unisender"
6
+
7
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
8
+ job = client.export_contacts(list_id: ENV.fetch("UNISENDER_LIST_ID"))
9
+ completed = client.wait_for_task(task_uuid: job.result.fetch("task_uuid"))
10
+ download_url = completed.result.fetch("file_to_download")
11
+ puts "Export ready at host: #{URI(download_url).host}"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY and UNISENDER_CONTACT_EMAIL. Changes contacts.
4
+ require "unisender"
5
+
6
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
7
+ begin
8
+ client.import_contacts_in_batches(
9
+ field_names: %w[email Name], data: [[ENV.fetch("UNISENDER_CONTACT_EMAIL"), "Example"]]
10
+ ).each { |response| puts "Imported batch: HTTP #{response.status}" }
11
+ rescue Unisender::BatchError => e
12
+ warn "Completed batches: #{e.completed_responses.size}; review rows #{e.failed_range}"
13
+ if e.error.is_a?(Unisender::PartialFailure)
14
+ warn "Some rows in the failed batch were accepted. Inspect error.error.response.result privately."
15
+ end
16
+ warn e.error.message
17
+ # Stop here. Reconcile accepted rows before building a new input; never replay all rows blindly.
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # No credentials or network access required.
4
+ require "unisender"
5
+
6
+ # Records requests and returns one deterministic response without a socket.
7
+ class FakeTransport
8
+ attr_reader :requests
9
+
10
+ def initialize
11
+ @requests = []
12
+ end
13
+
14
+ def post(uri, headers:, body:)
15
+ @requests << { uri:, headers:, body: }
16
+ Unisender::HTTPResponse.new(
17
+ status: 200, headers: { "x-request-id" => "offline-example" },
18
+ body: '{"result":[{"id":42,"title":"Product news"}]}'
19
+ )
20
+ end
21
+ end
22
+
23
+ transport = FakeTransport.new
24
+ client = Unisender::Client.new(api_key: "test-key", transport:)
25
+ client.get_lists.result.each { |list| puts list.fetch("title") }
26
+ puts "Requests: #{transport.requests.size}"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unisender"
4
+
5
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
6
+ client.get_lists.result.each { |list| puts list.fetch("title") }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires UNISENDER_API_KEY, UNISENDER_LIST_ID, and UNISENDER_CONTACT_EMAIL.
4
+ # Changes the account and sends a subscription confirmation email.
5
+ require "unisender"
6
+
7
+ client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
8
+ response = client.subscribe(
9
+ list_ids: [ENV.fetch("UNISENDER_LIST_ID")],
10
+ fields: { email: ENV.fetch("UNISENDER_CONTACT_EMAIL") }
11
+ )
12
+ puts response.result.fetch("person_id")
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Executes one batch at a time; a failure never replays previous batches.
5
+ module Batches
6
+ def self.each(items, size, maximum, &operation)
7
+ raise ArgumentError, "Invalid batch_size" unless size.is_a?(Integer) && size.positive? && size <= maximum
8
+
9
+ Enumerator.new do |output|
10
+ # @type var completed: Array[Response]
11
+ completed = []
12
+ items.each_slice(size).with_index do |batch, index|
13
+ response = execute(batch, index * size, completed, &operation)
14
+ completed << response
15
+ output << response
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.execute(batch, offset, completed)
21
+ response = yield batch
22
+ result = response.result
23
+ raise PartialFailure, response if result.is_a?(Hash) && result.fetch("invalid", 0).to_s.to_i.positive?
24
+
25
+ response
26
+ rescue Error, ArgumentError => e
27
+ raise BatchError.new(completed_responses: completed, failed_range: offset...(offset + batch.size), error: e),
28
+ cause: nil
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Synchronous client for the documented UniSender API.
5
+ class Client
6
+ RESERVED_PARAMETERS = %w[api_key format].freeze
7
+
8
+ def initialize(api_key:, transport: Transport.new, language: :en, retry_policy: RetryPolicy.new)
9
+ unless api_key.is_a?(String) && !api_key.strip.empty? && !api_key.match?(/[\r\n]/)
10
+ raise ArgumentError, "api_key must be a nonempty string without line breaks"
11
+ end
12
+ raise ArgumentError, "language must be en or ru" unless %w[en ru].include?(language.to_s)
13
+
14
+ @api_key = api_key.dup.freeze
15
+ @language = language.to_s.dup.freeze
16
+ @transport = transport
17
+ @retry_policy = retry_policy
18
+ freeze
19
+ end
20
+
21
+ def request(method, params: {})
22
+ validate_request(method, params)
23
+ uri = URI("https://api.unisender.com/#{@language}/api/#{method}")
24
+ headers = { "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8" }
25
+ body = Form.encode(params.merge("api_key" => @api_key, "format" => "json"))
26
+ @retry_policy.call(method) do
27
+ response = @transport.post(uri, headers:, body:)
28
+ Protocol.parse(response, method:)
29
+ end
30
+ rescue Error => e
31
+ raise e.with_operation(method), cause: nil
32
+ end
33
+
34
+ def inspect
35
+ "#<Unisender::Client>"
36
+ end
37
+
38
+ private
39
+
40
+ def validate_request(method, params)
41
+ raise ArgumentError, "Invalid API method name" unless method.match?(%r{\A(?:async/)?[A-Za-z][A-Za-z0-9]*\z})
42
+ return unless params.keys.any? { |key| RESERVED_PARAMETERS.include?(key.to_s) }
43
+
44
+ raise ArgumentError, "api_key and format are managed by the client"
45
+ end
46
+
47
+ # The wire response stays JSON; each named operation declares the documented result type.
48
+ # @type method invoke: (String, params, ?required: Array[String], ?comma: Array[String]) -> Response[json]
49
+ def invoke(method, params, required: [], comma: [])
50
+ Validation.required!(params, required)
51
+ Validation.operation!(method, params)
52
+ comma.each do |name|
53
+ value = params[name.to_sym]
54
+ params[name.to_sym] = Validation.comma_list(value) if value.is_a?(Array)
55
+ end
56
+ request(method, params:)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Base class for client and service failures.
5
+ class Error < StandardError
6
+ OPERATIONS = %w[
7
+ createList deleteList exclude async/exportContacts getContactCount getLists getTotalContactsCount
8
+ importContacts subscribe updateList isContactInLists getContact unsubscribe cancelCampaign
9
+ checkEmail checkSms createCampaign createEmailMessage createSmsMessage deleteMessage
10
+ getActualMessageVersion getWebVersion sendEmail sendSms sendTestEmail updateEmailMessage
11
+ updateOptInEmail getSenderDomainList getCampaignCommonStats async/getCampaignDeliveryStats
12
+ getCampaignStatus getMessages getVisitedLinks listMessages getCampaigns getMessage async/getTaskResult
13
+ createEmailTemplate deleteTemplate getTemplate getTemplates listTemplates updateEmailTemplate
14
+ createField deleteField deleteTag getFields getTags updateField getContactFieldValues
15
+ createSubscriberNote updateSubscriberNote deleteSubscriberNote getSubscriberNote getSubscriberNotes
16
+ track_events
17
+ ].freeze
18
+ private_constant :OPERATIONS
19
+
20
+ attr_reader :operation
21
+
22
+ def with_operation(operation)
23
+ @operation = OPERATIONS.include?(operation) ? operation.dup.freeze : nil
24
+ self
25
+ end
26
+
27
+ def to_s
28
+ "UniSender #{@operation || 'request'} failed: #{super}"
29
+ end
30
+
31
+ def inspect
32
+ "#<#{self.class}>"
33
+ end
34
+ end
35
+
36
+ # Main API failure, including failures returned with HTTP 200.
37
+ class APIError < Error
38
+ # https://www.unisender.com/ru/support/api/common/api-errors/
39
+ SAFE_CODES = %w[
40
+ unspecified invalid_api_key access_denied unknown_method invalid_arg not_enough_money retry_later
41
+ api_call_limit_exceeded_for_api_key api_call_limit_exceeded_for_ip
42
+ ].freeze
43
+ private_constant :SAFE_CODES
44
+
45
+ attr_reader :response, :code, :details
46
+
47
+ def initialize(response)
48
+ @response = response
49
+ @code = response.data["code"]
50
+ @details = response.data["error"]
51
+ super(SAFE_CODES.include?(@code) ? @code.to_s : "API error")
52
+ end
53
+ end
54
+
55
+ # Non-success HTTP response, including redirects.
56
+ class HTTPError < Error
57
+ attr_reader :response
58
+
59
+ def initialize(response)
60
+ @response = response
61
+ super("HTTP #{response.status}")
62
+ end
63
+ end
64
+
65
+ class InvalidResponseError < Error; end
66
+
67
+ # Network or TLS failure with no complete HTTP response.
68
+ class TransportError < Error
69
+ def initialize(message, retryable: false)
70
+ @retryable = retryable
71
+ super(message)
72
+ end
73
+
74
+ def retryable?
75
+ @retryable
76
+ end
77
+ end
78
+
79
+ class TimeoutError < TransportError; end
80
+
81
+ # A failed event request can still have accepted some events (HTTP 400).
82
+ class EventError < HTTPError
83
+ def code
84
+ response.data["code"]
85
+ end
86
+
87
+ def details
88
+ response.data.fetch("details", response.data["error"])
89
+ end
90
+
91
+ def possibly_partially_accepted?
92
+ response.status == 400
93
+ end
94
+ end
95
+
96
+ # Carries the completed batches and the range whose outcome needs inspection.
97
+ class BatchError < Error
98
+ attr_reader :completed_responses, :failed_range, :error
99
+
100
+ def initialize(completed_responses:, failed_range:, error:)
101
+ @completed_responses = completed_responses.dup.freeze
102
+ @failed_range = failed_range.freeze
103
+ @error = error
104
+ super("UniSender batch failed; inspect the completed responses and failed range")
105
+ end
106
+ end
107
+
108
+ class PaginationError < Error; end
109
+
110
+ # The API accepted the import request but rejected some records.
111
+ class PartialFailure < Error
112
+ attr_reader :response
113
+
114
+ def initialize(response)
115
+ @response = response
116
+ super("UniSender rejected part of the batch")
117
+ end
118
+ end
119
+
120
+ # Includes the last response for an unexpected or failed asynchronous task state.
121
+ class TaskError < Error
122
+ attr_reader :response
123
+
124
+ def initialize(response)
125
+ @response = response
126
+ super("UniSender task returned an unexpected state")
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Event Tracking uses JSON with its own batch and payload limits.
5
+ module EventPayload
6
+ def self.encode(events)
7
+ raise ArgumentError, "Expected 1 to 25 events" unless (1..25).cover?(events.size)
8
+
9
+ normalized = events.map { |event| validate(normalize(event, 0)) }
10
+ body = JSON.generate({ "events" => normalized })
11
+ raise ArgumentError, "Event payload exceeds 130 KB" if body.bytesize > 130 * 1024
12
+
13
+ body
14
+ rescue EncodingError, JSON::GeneratorError
15
+ raise ArgumentError, "Events must contain valid JSON values and UTF-8 text", cause: nil
16
+ end
17
+
18
+ def self.validate(event)
19
+ raise ArgumentError, "Each event must be an object" unless event.is_a?(Hash)
20
+ raise ArgumentError, "Each event may have at most 25 fields" if event.size > 25
21
+
22
+ validate_name(event["name"])
23
+ validate_email(event["email"])
24
+ event
25
+ end
26
+
27
+ def self.validate_name(name)
28
+ return if name.is_a?(String) && (1..40).cover?(name.size)
29
+
30
+ raise ArgumentError, "Event name must contain 1 to 40 characters"
31
+ end
32
+
33
+ def self.validate_email(email)
34
+ return if email.is_a?(String) && email.ascii_only? && email.match?(/\A[^\s@]+@[^\s@]+\z/)
35
+
36
+ raise ArgumentError, "Event email must be an ASCII email address"
37
+ end
38
+
39
+ def self.normalize(value, depth)
40
+ raise ArgumentError, "Events are nested too deeply" if depth > 32
41
+
42
+ case value
43
+ when Hash then normalize_hash(value, depth)
44
+ when Array then value.map { |item| normalize(item, depth + 1) }
45
+ when String then value.encode(Encoding::UTF_8)
46
+ when Integer, Float, true, false, nil then value
47
+ else raise ArgumentError, "Unsupported JSON value"
48
+ end
49
+ end
50
+
51
+ def self.normalize_hash(value, depth)
52
+ # @type var result: Hash[String, json]
53
+ result = {}
54
+ value.each do |key, item|
55
+ name = key.to_s.encode(Encoding::UTF_8)
56
+ raise ArgumentError, "Duplicate event field names" if result.key?(name)
57
+
58
+ result[name] = normalize(item, depth + 1)
59
+ end
60
+ result
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # JSON Event Tracking endpoint.
5
+ class Client
6
+ def track_events(events:)
7
+ body = EventPayload.encode(events)
8
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{@api_key}" }
9
+ http = @transport.post(URI("https://event.unisender.com/v1/events/"), headers:, body:)
10
+ response = Response.new(data: Protocol.decode(http), status: http.status, headers: http.headers)
11
+ raise EventError, response, cause: nil unless (200..299).cover?(http.status)
12
+ raise InvalidResponseError, "Missing event success status", cause: nil unless response.data["success"] == true
13
+
14
+ response # : Response[event_accepted]
15
+ rescue Error => e
16
+ raise e.with_operation("track_events"), cause: nil
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # UniSender accepts indexed form fields, not JSON, including for attachments.
5
+ module Form
6
+ def self.encode(params)
7
+ URI.encode_www_form(pairs(params, nil, 0))
8
+ rescue EncodingError
9
+ raise ArgumentError, "Parameters must contain valid UTF-8 text", cause: nil
10
+ end
11
+
12
+ def self.pairs(value, prefix, depth)
13
+ raise ArgumentError, "Parameters are nested too deeply" if depth > 32
14
+
15
+ case value
16
+ when Hash
17
+ hash_pairs(value, prefix, depth)
18
+ when Array
19
+ value.each_with_index.flat_map { |item, index| pairs(item, "#{prefix}[#{index}]", depth + 1) }
20
+ else
21
+ [[prefix.to_s, scalar(value, prefix.to_s)]]
22
+ end
23
+ end
24
+
25
+ def self.hash_pairs(value, prefix, depth)
26
+ keys = value.keys.map(&:to_s)
27
+ raise ArgumentError, "Duplicate parameter names" unless keys.uniq.size == keys.size
28
+
29
+ value.flat_map do |key, item|
30
+ name = key.to_s.encode(Encoding::UTF_8)
31
+ raise ArgumentError, "Parameter names cannot contain brackets" if name.match?(/[\[\]]/)
32
+ next [] if item.nil?
33
+
34
+ pairs(item, prefix ? "#{prefix}[#{name}]" : name, depth + 1)
35
+ end
36
+ end
37
+
38
+ def self.scalar(value, name)
39
+ case value
40
+ when String
41
+ name.start_with?("attachments[") ? value.b : text(value)
42
+ when true then "1"
43
+ when false then "0"
44
+ when nil then ""
45
+ else numeric(value)
46
+ end
47
+ end
48
+
49
+ def self.text(value)
50
+ raise ArgumentError, "Parameters must contain valid UTF-8 text" unless value.valid_encoding?
51
+
52
+ value.encode(Encoding::UTF_8)
53
+ end
54
+
55
+ def self.numeric(value)
56
+ case value
57
+ when Integer then value.to_s
58
+ when Float
59
+ raise ArgumentError, "Numeric parameters must be finite" unless value.finite?
60
+
61
+ value.to_s
62
+ else
63
+ raise ArgumentError, "Unsupported parameter type"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Explicit helpers for multiple requests.
5
+ class Client
6
+ def each_page(method, page_size: 100, max_pages: 1000, **params)
7
+ Pagination.new(self, method, page_size, max_pages, params).each
8
+ end
9
+
10
+ # rubocop:disable-next Metrics/ParameterLists -- Includes the documented import options.
11
+ def import_contacts_in_batches(field_names:, data:, batch_size: 500, overwrite_tags: nil,
12
+ overwrite_lists: nil, telegram_bot_channel_id: nil)
13
+ Batches.each(data, batch_size, 500) do |batch|
14
+ import_contacts(field_names:, data: batch, overwrite_tags:, overwrite_lists:, telegram_bot_channel_id:)
15
+ end
16
+ end
17
+
18
+ def track_events_in_batches(events:, batch_size: 25)
19
+ Batches.each(events, batch_size, 25) { |batch| track_events(events: batch) }
20
+ end
21
+
22
+ def wait_for_task(task_uuid:, interval: 1, timeout: 60, clock: nil, sleeper: nil)
23
+ clock ||= -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
24
+ sleeper ||= ->(delay) { sleep(delay) }
25
+ TaskWaiter.new(self, clock:, sleeper:).call(task_uuid, interval, timeout) # : Response[task_completed]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Contacts operations from the official UniSender API.
5
+ class Client
6
+ # https://www.unisender.com/ru/support/api/contacts/createlist/
7
+ def create_list(title:, before_subscribe_url: nil, after_subscribe_url: nil, sub_user_rights: nil)
8
+ params = { title:, before_subscribe_url:, after_subscribe_url:, sub_user_rights: }
9
+ invoke("createList", params, required: %w[title])
10
+ end
11
+
12
+ # https://www.unisender.com/ru/support/api/contacts/deletelist/
13
+ def delete_list(list_id:)
14
+ params = { list_id: }
15
+ invoke("deleteList", params, required: %w[list_id])
16
+ end
17
+
18
+ # https://www.unisender.com/ru/support/api/contacts/exclude/
19
+ def exclude(contact_type:, contact:, list_ids: nil)
20
+ params = { contact_type:, contact:, list_ids: }
21
+ invoke("exclude", params, required: %w[contact_type contact], comma: %w[list_ids])
22
+ end
23
+
24
+ # https://www.unisender.com/ru/support/api/contacts/exportcontacts/
25
+ # rubocop:disable-next Metrics/ParameterLists -- Mirrors the documented operation.
26
+ def export_contacts(notify_url: nil, list_id: nil, field_names: nil, email: nil, phone: nil, tag: nil,
27
+ email_status: nil, phone_status: nil, contact_status: nil)
28
+ params = { notify_url:, list_id:, field_names:, email:, phone:, tag:, email_status:, phone_status:,
29
+ contact_status: }
30
+ invoke("async/exportContacts", params)
31
+ end
32
+
33
+ # https://www.unisender.com/ru/support/api/contacts/getcontactcount/
34
+ def get_contact_count(list_id:, params:)
35
+ params = { list_id:, params: }
36
+ invoke("getContactCount", params, required: %w[list_id params])
37
+ end
38
+
39
+ # https://www.unisender.com/ru/support/api/contacts/getlists/
40
+ # rubocop:disable-next Naming/AccessorMethodName -- Mirrors the documented operation.
41
+ def get_lists
42
+ invoke("getLists", {})
43
+ end
44
+
45
+ # https://www.unisender.com/ru/support/api/contacts/gettotalcontactscount/
46
+ def get_total_contacts_count(login:)
47
+ params = { login: }
48
+ invoke("getTotalContactsCount", params, required: %w[login])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Contacts operations from the official UniSender API.
5
+ class Client
6
+ # https://www.unisender.com/ru/support/api/contacts/importcontacts/
7
+ def import_contacts(field_names:, data:, overwrite_tags: nil, overwrite_lists: nil,
8
+ telegram_bot_channel_id: nil)
9
+ params = { field_names:, data:, overwrite_tags:, overwrite_lists:, telegram_bot_channel_id: }
10
+ invoke("importContacts", params, required: %w[field_names data])
11
+ end
12
+
13
+ # https://www.unisender.com/ru/support/api/contacts/subscribe/
14
+ def subscribe(list_ids:, fields:, tags: nil, double_optin: nil, overwrite: nil)
15
+ params = { list_ids:, fields:, tags:, double_optin:, overwrite: }
16
+ invoke("subscribe", params, required: %w[list_ids fields], comma: %w[list_ids tags])
17
+ end
18
+
19
+ # https://www.unisender.com/ru/support/api/contacts/updatelist/
20
+ def update_list(list_id:, title:, before_subscribe_url: nil, after_subscribe_url: nil, sub_user_rights: nil)
21
+ params = { list_id:, title:, before_subscribe_url:, after_subscribe_url:, sub_user_rights: }
22
+ invoke("updateList", params, required: %w[list_id title])
23
+ end
24
+
25
+ # https://www.unisender.com/ru/support/api/contacts/iscontactinlists/
26
+ # rubocop:disable-next Naming/PredicatePrefix -- Mirrors the documented operation.
27
+ def is_contact_in_lists(email:, list_ids:, condition:)
28
+ params = { email:, list_ids:, condition: }
29
+ invoke("isContactInLists", params, required: %w[email list_ids condition], comma: %w[list_ids])
30
+ end
31
+
32
+ # https://www.unisender.com/ru/support/api/contacts/getcontact/
33
+ def get_contact(email:, include_lists: nil, include_fields: nil, include_details: nil)
34
+ params = { email:, include_lists:, include_fields:, include_details: }
35
+ invoke("getContact", params, required: %w[email])
36
+ end
37
+
38
+ # https://www.unisender.com/ru/support/api/contacts/unsubscribe/
39
+ def unsubscribe(contact_type:, contact:, list_ids: nil)
40
+ params = { contact_type:, contact:, list_ids: }
41
+ invoke("unsubscribe", params, required: %w[contact_type contact], comma: %w[list_ids])
42
+ end
43
+ end
44
+ end