mailerlite-ruby 2.0.0 → 2.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.
- checksums.yaml +4 -4
- data/.envrc +2 -0
- data/.github/workflows/main.yml +1 -1
- data/.github/workflows/publish_gem.yml +1 -1
- data/.gitignore +2 -1
- data/CHANGELOG.md +17 -0
- data/README.md +574 -0
- data/flake.lock +27 -0
- data/flake.nix +25 -0
- data/lefthook.yml +14 -0
- data/lib/mailerlite/api_response.rb +24 -0
- data/lib/mailerlite/automations/automation_emails.rb +57 -0
- data/lib/mailerlite/automations/automation_reports.rb +114 -0
- data/lib/mailerlite/automations/automation_steps.rb +105 -0
- data/lib/mailerlite/automations/automation_templates.rb +34 -0
- data/lib/mailerlite/automations/automation_triggers.rb +62 -0
- data/lib/mailerlite/automations/automations.rb +177 -8
- data/lib/mailerlite/brand_styles/brand_styles.rb +24 -0
- data/lib/mailerlite/campaigns/campaigns.rb +33 -1
- data/lib/mailerlite/ecommerce/ecommerce.rb +316 -0
- data/lib/mailerlite/emails/emails.rb +306 -0
- data/lib/mailerlite/errors.rb +88 -0
- data/lib/mailerlite/files/files.rb +45 -0
- data/lib/mailerlite/groups/groups.rb +16 -8
- data/lib/mailerlite/params.rb +58 -0
- data/lib/mailerlite/query_params.rb +33 -0
- data/lib/mailerlite/request.rb +24 -0
- data/lib/mailerlite/segments/segments.rb +10 -10
- data/lib/mailerlite/subscribers/subscribers.rb +63 -8
- data/lib/mailerlite/version.rb +1 -1
- data/lib/mailerlite.rb +14 -0
- data/spec/api_response_rspec.rb +140 -0
- data/spec/automation_emails_rspec.rb +40 -0
- data/spec/automation_reports_rspec.rb +92 -0
- data/spec/automation_steps_rspec.rb +76 -0
- data/spec/automation_templates_rspec.rb +24 -0
- data/spec/automation_triggers_rspec.rb +48 -0
- data/spec/automations_rspec.rb +143 -0
- data/spec/brand_styles_rspec.rb +49 -0
- data/spec/campaigns_rspec.rb +42 -0
- data/spec/ecommerce_rspec.rb +141 -0
- data/spec/emails_rspec.rb +692 -0
- data/spec/files_rspec.rb +90 -0
- data/spec/fixtures/brand_styles.json +50 -0
- data/spec/fixtures/campaign_simple_editor.json +24 -0
- data/spec/fixtures/email.json +81 -0
- data/spec/fixtures/email_block.json +12 -0
- data/spec/fixtures/email_block_created.json +12 -0
- data/spec/fixtures/email_block_created_rejected.json +15 -0
- data/spec/fixtures/email_block_moved.json +12 -0
- data/spec/fixtures/email_block_updated.json +9 -0
- data/spec/fixtures/email_block_updated_rejected.json +13 -0
- data/spec/fixtures/email_block_validation_invalid.json +5 -0
- data/spec/fixtures/email_etag.json +1 -0
- data/spec/fixtures/email_html.json +1 -0
- data/spec/fixtures/email_settings.json +111 -0
- data/spec/fixtures/email_settings_validation.json +9 -0
- data/spec/fixtures/email_template.json +1 -0
- data/spec/fixtures/email_version.json +1 -0
- data/spec/fixtures/email_versions.json +26 -0
- data/spec/fixtures/files.json +44 -0
- data/spec/groups_rspec.rb +21 -0
- data/spec/params_rspec.rb +126 -0
- data/spec/request_rspec.rb +139 -0
- data/spec/segments_rspec.rb +39 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/subscribers_rspec.rb +57 -0
- data/spec/support/json_fixture.rb +27 -0
- data/spec/support/request_stub.rb +25 -0
- metadata +54 -7
- /data/{renovate.json → .github/renovate.json} +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailerLite
|
|
4
|
+
# This is a class for the emails built with the simple editor.
|
|
5
|
+
#
|
|
6
|
+
# To create such an email, create a campaign with `editor: MailerLite::Campaigns::EDITOR_SIMPLE` in the email.
|
|
7
|
+
# The ID of the email is in `data.emails[0].id` of the response.
|
|
8
|
+
#
|
|
9
|
+
# IDs are Strings of the digits 0-9. You can also give them as Integers. An empty ID raises
|
|
10
|
+
# `MailerLite::EmptyIdError`, and an ID with other characters raises `MailerLite::InvalidIdError`.
|
|
11
|
+
#
|
|
12
|
+
# Each method returns the `HTTP::Response` with the `MailerLite::ApiResponse` methods. The methods do not raise
|
|
13
|
+
# for a 4xx or 5xx status: use `response.api_error`. The SDK checks only the IDs in the URL path, and the settings
|
|
14
|
+
# Hash of `update_settings`. It sends all other values as you give them, and the API checks them.
|
|
15
|
+
#
|
|
16
|
+
# Each write needs the etag from the last response for the email, and returns a new etag. The etag covers the
|
|
17
|
+
# whole email, so send the writes to one email one after another. If the email changed after you read it, the
|
|
18
|
+
# API returns 409 and `response.api_error.code` is `VERSION_CONFLICT`. Then `response.api_error.etag` is the
|
|
19
|
+
# current etag.
|
|
20
|
+
#
|
|
21
|
+
# Idempotency keys: if a write fails with a network error, you do not know if the API did the write. Give an
|
|
22
|
+
# `idempotency_key` to the write, and send it again with the same key and the same arguments. If the first request
|
|
23
|
+
# was a success, the API returns the stored response and does not do the write two times. Then
|
|
24
|
+
# `response.idempotent_replay?` is true. Use a new key for each write. The API keeps a key for 24 hours.
|
|
25
|
+
# The key must be 1 to 255 printable ASCII characters. For a key that is not valid, the API returns 400.
|
|
26
|
+
class Emails
|
|
27
|
+
# The error code of a write with an etag that is not the current etag of the email.
|
|
28
|
+
VERSION_CONFLICT = 'version_conflict'
|
|
29
|
+
|
|
30
|
+
# Block types. The API can also return types that are not in this list.
|
|
31
|
+
module BlockType
|
|
32
|
+
BLOCKQUOTE = 'blockquote'
|
|
33
|
+
BULLETED_LIST = 'bulleted_list'
|
|
34
|
+
BUTTON = 'button'
|
|
35
|
+
CODE = 'code'
|
|
36
|
+
DIVIDER = 'divider'
|
|
37
|
+
HEADING = 'heading'
|
|
38
|
+
IMAGE = 'image'
|
|
39
|
+
NUMBERED_LIST = 'numbered_list'
|
|
40
|
+
PARAGRAPH = 'paragraph'
|
|
41
|
+
SOCIAL_ICON = 'social_icon'
|
|
42
|
+
# Only reads return this type. You cannot insert a block of this type.
|
|
43
|
+
TITLE = 'title'
|
|
44
|
+
VIDEO = 'video'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Positions of a block, for `placement`. `BEFORE` and `AFTER` also need a `block_id`.
|
|
48
|
+
module BlockPosition
|
|
49
|
+
AT_START = 'start'
|
|
50
|
+
AT_END = 'end'
|
|
51
|
+
BEFORE = 'before'
|
|
52
|
+
AFTER = 'after'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Where the palette of an email comes from, in `settings[:brand][:mode]`.
|
|
56
|
+
module BrandMode
|
|
57
|
+
ACCOUNT = 'account'
|
|
58
|
+
TEMPLATE = 'template'
|
|
59
|
+
CUSTOM = 'custom'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader :client
|
|
63
|
+
|
|
64
|
+
# Inits the `Emails` class with the specified `client`.
|
|
65
|
+
#
|
|
66
|
+
# @param client [MailerLite::Client] the `Client` instance to use
|
|
67
|
+
def initialize(client: MailerLite::Client.new)
|
|
68
|
+
@client = client
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns the email with its settings and blocks, and the etag of the email.
|
|
72
|
+
#
|
|
73
|
+
# @param email_id [String, Integer] the ID of the email
|
|
74
|
+
# @return [HTTP::Response] the response from the API
|
|
75
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
76
|
+
def get(email_id)
|
|
77
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
78
|
+
Request.call(client, :get, "/emails/#{email_id}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the HTML that the email sends, and the etag of the email.
|
|
82
|
+
# The API can return a temporary 503 error. Then try again later.
|
|
83
|
+
#
|
|
84
|
+
# @param email_id [String, Integer] the ID of the email
|
|
85
|
+
# @return [HTTP::Response] the response from the API
|
|
86
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
87
|
+
def get_html(email_id)
|
|
88
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
89
|
+
Request.call(client, :get, "/emails/#{email_id}/html")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the block and its place in the email, and the etag of the email.
|
|
93
|
+
# `placement` has the IDs of the blocks `after` and `before` it, or nil.
|
|
94
|
+
#
|
|
95
|
+
# @param email_id [String, Integer] the ID of the email
|
|
96
|
+
# @param block_id [String, Integer] the ID of the block
|
|
97
|
+
# @return [HTTP::Response] the response from the API
|
|
98
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
99
|
+
def get_block(email_id:, block_id:)
|
|
100
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
101
|
+
block_id = Params.path_id(block_id, 'block_id')
|
|
102
|
+
Request.call(client, :get, "/emails/#{email_id}/blocks/#{block_id}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Inserts a block in the email. Returns the new block and the new etag of the email.
|
|
106
|
+
#
|
|
107
|
+
# The API drops the attributes and the `mobile` keys that the block type does not declare, and the write still
|
|
108
|
+
# succeeds. Then the response has `rejected` with a `message` and the `keys` that the API dropped, for example
|
|
109
|
+
# `editor_content.attrs.onclick`. When the API drops nothing, the response has no `rejected`.
|
|
110
|
+
#
|
|
111
|
+
# @param email_id [String, Integer] the ID of the email
|
|
112
|
+
# @param etag [String] the etag from the last response for the email
|
|
113
|
+
# @param type [String] the block type, see `BlockType`. You cannot insert `title`.
|
|
114
|
+
# @param editor_content [Hash] the content of the block. It can be empty. Its keys are camelCase, for example `textAlign`.
|
|
115
|
+
# @param placement [Hash, nil] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`.
|
|
116
|
+
# Without it, the API puts the block at the end of the email.
|
|
117
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
118
|
+
# documentation
|
|
119
|
+
# @return [HTTP::Response] the response from the API
|
|
120
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
121
|
+
def create_block(email_id:, etag:, type:, editor_content:, placement: nil, idempotency_key: nil)
|
|
122
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
123
|
+
body = block_body(type, editor_content, placement).merge('etag' => etag)
|
|
124
|
+
Request.call(client, :post, "/emails/#{email_id}/blocks", json: body, idempotency_key: idempotency_key)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Updates a block of the email. The new content replaces the whole content of the block: the API removes the
|
|
128
|
+
# keys that you do not send. Returns the block and the new etag of the email.
|
|
129
|
+
#
|
|
130
|
+
# The API drops the attributes and the `mobile` keys that the block type does not declare, and the write still
|
|
131
|
+
# succeeds. Then the response has `rejected` with a `message` and the `keys` that the API dropped, for example
|
|
132
|
+
# `editor_content.mobile.onclick`. When the API drops nothing, the response has no `rejected`.
|
|
133
|
+
#
|
|
134
|
+
# @param email_id [String, Integer] the ID of the email
|
|
135
|
+
# @param block_id [String, Integer] the ID of the block
|
|
136
|
+
# @param etag [String] the etag from the last response for the email
|
|
137
|
+
# @param editor_content [Hash] the new content of the block. Its keys are camelCase, for example `textAlign`.
|
|
138
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
139
|
+
# documentation
|
|
140
|
+
# @return [HTTP::Response] the response from the API
|
|
141
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
142
|
+
def update_block(email_id:, block_id:, etag:, editor_content:, idempotency_key: nil)
|
|
143
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
144
|
+
block_id = Params.path_id(block_id, 'block_id')
|
|
145
|
+
body = { 'editor_content' => editor_content, 'etag' => etag }
|
|
146
|
+
Request.call(client, :patch, "/emails/#{email_id}/blocks/#{block_id}", json: body, idempotency_key: idempotency_key)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Deletes a block of the email, and the blocks in it. The response has only the new etag of the email,
|
|
150
|
+
# so the method returns the response and not true.
|
|
151
|
+
#
|
|
152
|
+
# @param email_id [String, Integer] the ID of the email
|
|
153
|
+
# @param block_id [String, Integer] the ID of the block
|
|
154
|
+
# @param etag [String] the etag from the last response for the email
|
|
155
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
156
|
+
# documentation
|
|
157
|
+
# @return [HTTP::Response] the response from the API
|
|
158
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
159
|
+
def delete_block(email_id:, block_id:, etag:, idempotency_key: nil)
|
|
160
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
161
|
+
block_id = Params.path_id(block_id, 'block_id')
|
|
162
|
+
Request.call(client, :delete, "/emails/#{email_id}/blocks/#{block_id}", json: { 'etag' => etag }, idempotency_key: idempotency_key)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Moves a block of the email. Returns the block, its new place in the email, and the new etag of the email.
|
|
166
|
+
#
|
|
167
|
+
# @param email_id [String, Integer] the ID of the email
|
|
168
|
+
# @param block_id [String, Integer] the ID of the block to move
|
|
169
|
+
# @param etag [String] the etag from the last response for the email
|
|
170
|
+
# @param placement [Hash] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`
|
|
171
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
172
|
+
# documentation
|
|
173
|
+
# @return [HTTP::Response] the response from the API
|
|
174
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
175
|
+
def move_block(email_id:, block_id:, etag:, placement:, idempotency_key: nil)
|
|
176
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
177
|
+
block_id = Params.path_id(block_id, 'block_id')
|
|
178
|
+
body = { 'placement' => placement, 'etag' => etag }
|
|
179
|
+
Request.call(client, :post, "/emails/#{email_id}/blocks/#{block_id}/move", json: body, idempotency_key: idempotency_key)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Validates a block before you insert it. It needs no email and no etag. An invalid block is not an error:
|
|
183
|
+
# the response has `valid` set to false and the `errors` for each field.
|
|
184
|
+
#
|
|
185
|
+
# If a valid block has attributes or `mobile` keys that the block type does not declare, the response has
|
|
186
|
+
# `rejected` with a `message` and these `keys`. `create_block` and `update_block` drop these keys.
|
|
187
|
+
# When `valid` is false, the response has no `rejected`.
|
|
188
|
+
#
|
|
189
|
+
# @param type [String] the block type, see `BlockType`
|
|
190
|
+
# @param editor_content [Hash] the content of the block. Its keys are camelCase, for example `textAlign`.
|
|
191
|
+
# @param placement [Hash, nil] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`
|
|
192
|
+
# @return [HTTP::Response] the response from the API, with `valid`, `errors`, `schema` and, in some cases, `rejected`
|
|
193
|
+
def validate_block(type:, editor_content:, placement: nil)
|
|
194
|
+
Request.call(client, :post, '/emails/blocks/validate', json: block_body(type, editor_content, placement))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Updates the design settings of the email. Returns the settings and the new etag of the email.
|
|
198
|
+
#
|
|
199
|
+
# Send only the keys to change, in the shape of `settings` in the response of `get`. The groups are `brand`,
|
|
200
|
+
# `layout`, `typography`, `button` and `footer`. The SDK sends exactly the keys of the Hash:
|
|
201
|
+
# - a key that is not in the Hash does not change;
|
|
202
|
+
# - an empty String unsets a brand colour, and the email then uses the account value;
|
|
203
|
+
# - an empty editor document, `{ editor_content: {} }` or `{ editor_content: { content: [] } }`, unsets a footer text;
|
|
204
|
+
# - other colours, sizes, `button.border_radius` and `brand.style` cannot be empty: an empty String gives a `422` error;
|
|
205
|
+
# - any other value is the new value. Send style values as Strings, for example '16'.
|
|
206
|
+
#
|
|
207
|
+
# The API stores each value that you send, also a value that the email inherits from the account.
|
|
208
|
+
# `footer.social_links` replaces the whole list: keep the `link_id` of each link to keep its click history.
|
|
209
|
+
# To apply a preset palette, send its `identifier` from `BrandStyles#get` in `brand.style`.
|
|
210
|
+
# The response can have `rejected` when the API removed unsafe values.
|
|
211
|
+
#
|
|
212
|
+
# @param email_id [String, Integer] the ID of the email
|
|
213
|
+
# @param etag [String] the etag from the last response for the email
|
|
214
|
+
# @param settings [Hash] the settings to change, without `etag`
|
|
215
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
216
|
+
# documentation
|
|
217
|
+
# @return [HTTP::Response] the response from the API
|
|
218
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
219
|
+
# @raise [ArgumentError] if the settings contain an `etag` key
|
|
220
|
+
# @raise [TypeError] if the settings are not a Hash
|
|
221
|
+
def update_settings(email_id:, etag:, settings:, idempotency_key: nil)
|
|
222
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
223
|
+
body = Params.settings(settings).merge('etag' => etag)
|
|
224
|
+
Request.call(client, :patch, "/emails/#{email_id}/settings", json: body, idempotency_key: idempotency_key)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Validates a change to the design settings of the email with the rules of `update_settings`, and saves nothing.
|
|
228
|
+
# It needs no etag. Invalid settings are not an error: the response has `valid` set to false and the `errors`
|
|
229
|
+
# for each field. The response can also have `rejected`.
|
|
230
|
+
#
|
|
231
|
+
# @param email_id [String, Integer] the ID of the email
|
|
232
|
+
# @param settings [Hash] the settings that you want to send to `update_settings`, without `etag`
|
|
233
|
+
# @return [HTTP::Response] the response from the API, with `valid`, `errors` and `schema`
|
|
234
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
235
|
+
def validate_settings(email_id:, settings:)
|
|
236
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
237
|
+
Request.call(client, :post, "/emails/#{email_id}/settings/validate", json: settings)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Saves the email as a new template with the name. The API cannot delete templates.
|
|
241
|
+
#
|
|
242
|
+
# @param email_id [String, Integer] the ID of the email
|
|
243
|
+
# @param name [String] the name of the template
|
|
244
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
245
|
+
# documentation
|
|
246
|
+
# @return [HTTP::Response] the response from the API
|
|
247
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
248
|
+
def create_template(email_id:, name:, idempotency_key: nil)
|
|
249
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
250
|
+
Request.call(client, :post, "/emails/#{email_id}/templates", json: { 'name' => name }, idempotency_key: idempotency_key)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns the saved versions of the email, newest first. `current` marks the version that the email has now.
|
|
254
|
+
#
|
|
255
|
+
# @param email_id [String, Integer] the ID of the email
|
|
256
|
+
# @param page [Integer] the page number of the results to return
|
|
257
|
+
# @param limit [Integer] the maximum number of versions to return, from 1 to 100
|
|
258
|
+
# @return [HTTP::Response] the response from the API
|
|
259
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
260
|
+
def list_versions(email_id, page: nil, limit: nil)
|
|
261
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
262
|
+
Request.call(client, :get, "/emails/#{email_id}/versions", query: Params.query(page: page, limit: limit))
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Saves the content and the settings of the email as a new version. If nothing changed after the last version,
|
|
266
|
+
# the API saves no new version and returns the last version. The status code (`response.code`) is 201 for a new
|
|
267
|
+
# version and 200 for the last version. Both are a success.
|
|
268
|
+
#
|
|
269
|
+
# @param email_id [String, Integer] the ID of the email
|
|
270
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
271
|
+
# documentation
|
|
272
|
+
# @return [HTTP::Response] the response from the API
|
|
273
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
|
|
274
|
+
def create_version(email_id, idempotency_key: nil)
|
|
275
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
276
|
+
Request.call(client, :post, "/emails/#{email_id}/versions", idempotency_key: idempotency_key)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Restores the email to a version. Returns the email in the shape of `get`, with the new etag. Before the
|
|
280
|
+
# restore, the API saves the current state as a version, so you can undo the restore.
|
|
281
|
+
#
|
|
282
|
+
# @param email_id [String, Integer] the ID of the email
|
|
283
|
+
# @param version_id [String, Integer] the ID of the version
|
|
284
|
+
# @param etag [String] the etag from the last response for the email
|
|
285
|
+
# @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
|
|
286
|
+
# documentation
|
|
287
|
+
# @return [HTTP::Response] the response from the API
|
|
288
|
+
# @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
|
|
289
|
+
def restore_version(email_id:, version_id:, etag:, idempotency_key: nil)
|
|
290
|
+
email_id = Params.path_id(email_id, 'email_id')
|
|
291
|
+
version_id = Params.path_id(version_id, 'version_id')
|
|
292
|
+
Request.call(client, :post, "/emails/#{email_id}/versions/#{version_id}/restore", json: { 'etag' => etag }, idempotency_key: idempotency_key)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def block_body(type, editor_content, placement)
|
|
298
|
+
body = {
|
|
299
|
+
'type' => type,
|
|
300
|
+
'editor_content' => editor_content
|
|
301
|
+
}
|
|
302
|
+
body['placement'] = placement unless placement.nil?
|
|
303
|
+
body
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module MailerLite
|
|
6
|
+
# The base class of the errors of the SDK.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# An error response (4xx or 5xx) from the API.
|
|
10
|
+
#
|
|
11
|
+
# The SDK does not raise it. Get it with `response.api_error` and raise it if you want to.
|
|
12
|
+
# It is not an `HTTP::Error`, so code that retries on network errors does not also retry an API error.
|
|
13
|
+
# It keeps only plain values, so `Marshal.dump` works.
|
|
14
|
+
#
|
|
15
|
+
# Note: `HTTP::Response#code` is the numeric status. `ApiError#code` is the String code of the API,
|
|
16
|
+
# for example `"version_conflict"`. The numeric status is in `ApiError#status`.
|
|
17
|
+
class ApiError < Error
|
|
18
|
+
# @return [Integer] the HTTP status code, for example 409
|
|
19
|
+
attr_reader :status
|
|
20
|
+
# @return [String, nil] the error code of the API, for example "version_conflict"
|
|
21
|
+
attr_reader :code
|
|
22
|
+
# @return [String, nil] the current etag of the email, for a 409 version conflict
|
|
23
|
+
attr_reader :etag
|
|
24
|
+
# @return [Hash, nil] the error messages for each field, for some 422 responses
|
|
25
|
+
attr_reader :errors
|
|
26
|
+
# @return [String, nil] the message of the API
|
|
27
|
+
attr_reader :api_message
|
|
28
|
+
# @return [String] the response body as it is
|
|
29
|
+
attr_reader :body
|
|
30
|
+
# @return [Hash] the response headers
|
|
31
|
+
attr_reader :headers
|
|
32
|
+
|
|
33
|
+
# Makes an error from a response. It works for the responses of all methods of the SDK.
|
|
34
|
+
# A body that is not a JSON object gives nil fields.
|
|
35
|
+
#
|
|
36
|
+
# @param response [HTTP::Response] the response
|
|
37
|
+
# @return [MailerLite::ApiError] the error
|
|
38
|
+
def self.from_response(response)
|
|
39
|
+
body = response.body.to_s
|
|
40
|
+
data = parse(body)
|
|
41
|
+
new(status: response.code, reason: response.reason, body: body, headers: response.headers.to_h,
|
|
42
|
+
code: data['code'], etag: data['etag'], errors: data['errors'], api_message: data['message'])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.parse(body)
|
|
46
|
+
data = JSON.parse(body)
|
|
47
|
+
data.is_a?(Hash) ? data : {}
|
|
48
|
+
rescue JSON::ParserError, EncodingError
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
private_class_method :parse
|
|
52
|
+
|
|
53
|
+
# @param status [Integer] the HTTP status code
|
|
54
|
+
# @param reason [String, nil] the reason phrase of the status
|
|
55
|
+
# @param code [String, nil] the error code of the API
|
|
56
|
+
# @param etag [String, nil] the current etag of the email
|
|
57
|
+
# @param errors [Hash, nil] the error messages for each field
|
|
58
|
+
# @param api_message [String, nil] the message of the API
|
|
59
|
+
# @param body [String] the response body
|
|
60
|
+
# @param headers [Hash] the response headers
|
|
61
|
+
def initialize(status:, reason: nil, code: nil, etag: nil, errors: nil, api_message: nil, body: '', headers: {})
|
|
62
|
+
@status = status
|
|
63
|
+
@code = code
|
|
64
|
+
@etag = etag
|
|
65
|
+
@errors = errors
|
|
66
|
+
@api_message = api_message
|
|
67
|
+
@body = body
|
|
68
|
+
@headers = headers
|
|
69
|
+
super(build_message(reason))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# For example "409 Conflict (version_conflict): The email was changed."
|
|
75
|
+
def build_message(reason)
|
|
76
|
+
text = [status, reason].compact.join(' ')
|
|
77
|
+
text += " (#{code})" if code
|
|
78
|
+
text += ": #{api_message}" if api_message
|
|
79
|
+
text
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An ID for the URL path is empty. The SDK sends no request.
|
|
84
|
+
class EmptyIdError < ArgumentError; end
|
|
85
|
+
|
|
86
|
+
# An ID for the URL path does not contain only the digits 0-9. The SDK sends no request.
|
|
87
|
+
class InvalidIdError < ArgumentError; end
|
|
88
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailerLite
|
|
4
|
+
# This is a class for the files of the media library from MailerLite API.
|
|
5
|
+
class Files
|
|
6
|
+
# File types for `filter: { type: ... }`. The API can also return types that are not in this list.
|
|
7
|
+
module FileType
|
|
8
|
+
IMAGE = 'image'
|
|
9
|
+
PDF = 'pdf'
|
|
10
|
+
EPUB = 'epub'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Sort values. The values that start with `-` sort in descending order.
|
|
14
|
+
module FileSort
|
|
15
|
+
CREATED_AT = 'created_at'
|
|
16
|
+
CREATED_AT_DESC = '-created_at'
|
|
17
|
+
NAME = 'name'
|
|
18
|
+
NAME_DESC = '-name'
|
|
19
|
+
SIZE = 'size'
|
|
20
|
+
SIZE_DESC = '-size'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :client
|
|
24
|
+
|
|
25
|
+
# Inits the `Files` class with the specified `client`.
|
|
26
|
+
#
|
|
27
|
+
# @param client [MailerLite::Client] the `Client` instance to use
|
|
28
|
+
def initialize(client: MailerLite::Client.new)
|
|
29
|
+
@client = client
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the files of the media library. Use their `url` in image blocks.
|
|
33
|
+
# `width` and `height` are nil for files that are not images. The SDK does not send nil values.
|
|
34
|
+
#
|
|
35
|
+
# @param filter [Hash, nil] `name` (a part of the file name) and `type` (see `FileType`)
|
|
36
|
+
# @param sort [String, nil] see `FileSort`. The default is `-created_at`.
|
|
37
|
+
# @param page [Integer, nil] the page number of the results to return
|
|
38
|
+
# @param limit [Integer, nil] the maximum number of files to return, from 1 to 100
|
|
39
|
+
# @return [HTTP::Response] the response from the API, with the `MailerLite::ApiResponse` methods
|
|
40
|
+
# @raise [TypeError] if the filter is not a Hash
|
|
41
|
+
def list(filter: nil, sort: nil, page: nil, limit: nil)
|
|
42
|
+
Request.call(client, :get, '/files', query: Params.query(filter: filter, sort: sort, page: page, limit: limit))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -52,15 +52,23 @@ module MailerLite
|
|
|
52
52
|
# @param group_id [Integer] The id of existing group belonging to the account
|
|
53
53
|
# @param filter[:status] [String] Must be one of the possible statuses: active, unsubscribed, unconfirmed, bounced or junk. Defaults to active.
|
|
54
54
|
# @param limit [Integer] the maximum number of subscribers to return
|
|
55
|
-
# @param
|
|
55
|
+
# @param cursor [String] the cursor for the page of results to return
|
|
56
|
+
# @param include [String] the relations to embed, only groups is supported
|
|
56
57
|
# @return [HTTP::Response] the response from the API
|
|
57
|
-
def get_subscribers(group_id:, filter: {}, limit: nil, cursor: nil, sort: nil)
|
|
58
|
-
params =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
def get_subscribers(group_id:, filter: {}, limit: nil, cursor: nil, sort: nil, include: nil)
|
|
59
|
+
params = QueryParams.filter(filter, %i[status]).merge('limit' => limit, 'sort' => sort, 'cursor' => cursor, 'include' => include)
|
|
60
|
+
client.http.get(QueryParams.uri("#{MAILERLITE_API_URL}/groups/#{group_id}/subscribers", params))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Imports subscribers into the specified group.
|
|
64
|
+
# @param group_id [Integer] The id of existing group belonging to the account
|
|
65
|
+
# @param subscribers [Array] the subscribers to import, each a hash with email, and optional name and fields
|
|
66
|
+
# @param resubscribe [Boolean] reactivate unsubscribed subscribers, bounced and junk subscribers are not reactivated
|
|
67
|
+
# @param trigger_automations [Boolean] start the automations the import qualifies for
|
|
68
|
+
# @return [HTTP::Response] the response from the API
|
|
69
|
+
def import_subscribers(group_id:, subscribers:, resubscribe: nil, trigger_automations: nil)
|
|
70
|
+
params = { 'subscribers' => subscribers, 'resubscribe' => resubscribe, 'trigger_automations' => trigger_automations }
|
|
71
|
+
client.http.post("#{MAILERLITE_API_URL}/groups/#{group_id}/import-subscribers", json: params.compact)
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
# Assign Subscriber to the specified group.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailerLite
|
|
4
|
+
# Checks and helpers for the request parameters of the resources.
|
|
5
|
+
# The SDK checks only the IDs in the URL path and the values that its code reads. The API checks all other values.
|
|
6
|
+
# Each check raises before the request, so the SDK sends no request when a value is not valid.
|
|
7
|
+
module Params
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Use \A and \z: `$` and \Z also match before a final new line.
|
|
11
|
+
# Use [0-9]: [[:digit:]] also matches digits that are not ASCII.
|
|
12
|
+
ID_PATTERN = /\A[0-9]+\z/
|
|
13
|
+
|
|
14
|
+
def path_id(value, name)
|
|
15
|
+
raise EmptyIdError, "`#{name}` must not be empty." if value.nil? || value == ''
|
|
16
|
+
|
|
17
|
+
raise TypeError, "`#{name}` type is not valid. Expected String or Integer, got #{value.class}." unless value.is_a?(String) || value.is_a?(Integer)
|
|
18
|
+
|
|
19
|
+
id = value.to_s
|
|
20
|
+
# match? raises for a String with invalid bytes, so check the encoding first.
|
|
21
|
+
raise InvalidIdError, "`#{name}` must contain only the digits 0-9, got #{id.inspect}." unless id.valid_encoding? && id.ascii_only? && id.match?(ID_PATTERN)
|
|
22
|
+
|
|
23
|
+
id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def require_hash(value, name)
|
|
27
|
+
raise TypeError, "`#{name}` type is not valid. Expected Hash, got #{value.class}." unless value.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def settings(settings)
|
|
33
|
+
require_hash(settings, 'settings')
|
|
34
|
+
# The body has the settings and the etag at the same level. An etag key in the settings can hide the etag argument.
|
|
35
|
+
raise ArgumentError, '`settings` must not contain `etag`. Use the `etag` argument.' if settings.key?(:etag) || settings.key?('etag')
|
|
36
|
+
|
|
37
|
+
settings
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# It drops nil values and keeps empty Strings.
|
|
41
|
+
def query(filter: nil, **params)
|
|
42
|
+
query = {}
|
|
43
|
+
unless filter.nil?
|
|
44
|
+
require_hash(filter, 'filter').each do |key, value|
|
|
45
|
+
query["filter[#{key}]"] = value unless value.nil?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
params.each { |key, value| query[key.to_s] = value unless value.nil? }
|
|
49
|
+
query
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def idempotency_headers(key)
|
|
53
|
+
return {} if key.nil?
|
|
54
|
+
|
|
55
|
+
{ 'Idempotency-Key' => key }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module MailerLite
|
|
6
|
+
# Encodes values for the query string of the MailerLite API.
|
|
7
|
+
module QueryParams
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def boolean(value)
|
|
11
|
+
return if value.nil?
|
|
12
|
+
|
|
13
|
+
value ? 1 : 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def filter(filter, keys)
|
|
17
|
+
filter.each_with_object({}) do |(key, value), params|
|
|
18
|
+
params["filter[#{key}]"] = value.is_a?(Array) ? value.join(',') : value if keys.include?(key)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def report(start_date:, end_date:, shop_id:, excel:)
|
|
23
|
+
{ 'start_date' => start_date, 'end_date' => end_date, 'shop_id' => shop_id, 'excel' => boolean(excel) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def uri(url, params)
|
|
27
|
+
uri = URI(url)
|
|
28
|
+
query = URI.encode_www_form(params.compact)
|
|
29
|
+
uri.query = query unless query.empty?
|
|
30
|
+
uri
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module MailerLite
|
|
6
|
+
# Sends the requests of `Emails`, `BrandStyles` and `Files`.
|
|
7
|
+
module Request
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# It does not raise for a 4xx or 5xx status.
|
|
11
|
+
def call(client, method, path, json: nil, query: nil, idempotency_key: nil)
|
|
12
|
+
headers = Params.idempotency_headers(idempotency_key)
|
|
13
|
+
# Read the constant here, not when the file loads.
|
|
14
|
+
uri = URI("#{MAILERLITE_API_URL}#{path}")
|
|
15
|
+
uri.query = URI.encode_www_form(query) if query && !query.empty?
|
|
16
|
+
options = {}
|
|
17
|
+
# `body: nil.to_json` sends "null", so add `json:` only when there is a body.
|
|
18
|
+
options[:json] = json unless json.nil?
|
|
19
|
+
http = client.http
|
|
20
|
+
http = http.headers(headers) unless headers.empty?
|
|
21
|
+
http.request(method, uri, options).extend(ApiResponse)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -16,22 +16,20 @@ module MailerLite
|
|
|
16
16
|
#
|
|
17
17
|
# @param limit [Integer] the maximum number of Segments to return
|
|
18
18
|
# @param page [Integer] the page number of the results to return
|
|
19
|
+
# @param sort [String] the field to sort by, prefix with - for descending order
|
|
19
20
|
# @return [HTTP::Response] the response from the API
|
|
20
|
-
def list(limit: nil, page: nil)
|
|
21
|
-
|
|
22
|
-
params['limit'] = limit if limit
|
|
23
|
-
params['page'] = page if page
|
|
24
|
-
|
|
25
|
-
client.http.get("#{MAILERLITE_API_URL}/segments", json: params.compact)
|
|
21
|
+
def list(limit: nil, page: nil, sort: nil)
|
|
22
|
+
client.http.get(QueryParams.uri("#{MAILERLITE_API_URL}/segments", { 'limit' => limit, 'page' => page, 'sort' => sort }))
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
# Update the specified Segment
|
|
29
26
|
#
|
|
30
27
|
# @param segment_id [String] the ID of the Segments to update
|
|
31
28
|
# @param name [String] the name to update
|
|
29
|
+
# @param filter [Array] the segment rules
|
|
32
30
|
# @return [HTTP::Response] the response from the API
|
|
33
|
-
def update(segment_id:, name:)
|
|
34
|
-
params = { 'name' => name }
|
|
31
|
+
def update(segment_id:, name:, filter: nil)
|
|
32
|
+
params = { 'name' => name, 'filter' => filter }
|
|
35
33
|
client.http.put("#{MAILERLITE_API_URL}/segments/#{segment_id}", json: params.compact)
|
|
36
34
|
end
|
|
37
35
|
|
|
@@ -39,12 +37,14 @@ module MailerLite
|
|
|
39
37
|
# @param segment_id [Integer] The id of existing Segment belonging to the account
|
|
40
38
|
# @param filter[status] [String] Must be one of the possible statuses: active, unsubscribed, unconfirmed, bounced or junk. Defaults to active.
|
|
41
39
|
# @param limit [Integer] the maximum number of subscribers to return
|
|
42
|
-
# @param
|
|
40
|
+
# @param cursor [String] the cursor for the page of results to return, available in meta.next_cursor
|
|
41
|
+
# @param after [Integer] deprecated, the API ignores it, use cursor
|
|
43
42
|
# @return [HTTP::Response] the response from the API
|
|
44
|
-
def get_subscribers(segment_id:, filter: {}, limit: nil, after: nil)
|
|
43
|
+
def get_subscribers(segment_id:, filter: {}, limit: nil, after: nil, cursor: nil)
|
|
45
44
|
params = {}
|
|
46
45
|
params['filter[status]'] = filter[:status] if filter.key?(:status)
|
|
47
46
|
params['limit'] = limit if limit
|
|
47
|
+
params['cursor'] = cursor if cursor
|
|
48
48
|
params['after'] = after if after
|
|
49
49
|
uri = URI("#{MAILERLITE_API_URL}/segments/#{segment_id}/subscribers")
|
|
50
50
|
uri.query = URI.encode_www_form(params.compact)
|