sevk 1.0.0 → 1.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/LICENSE +21 -0
- data/README.md +6 -6
- data/lib/sevk/client.rb +140 -63
- data/lib/sevk/error.rb +38 -5
- data/lib/sevk/markup/document_parser.rb +83 -0
- data/lib/sevk/markup/html_document.rb +82 -0
- data/lib/sevk/markup/number_formatting.rb +40 -0
- data/lib/sevk/markup/render_limits.rb +52 -0
- data/lib/sevk/markup/renderer.rb +232 -729
- data/lib/sevk/markup/style_helpers.rb +131 -0
- data/lib/sevk/markup/syntax_highlighting.rb +227 -0
- data/lib/sevk/markup/tag_scanner.rb +112 -0
- data/lib/sevk/markup/template_engine.rb +247 -0
- data/lib/sevk/resources/activity.rb +23 -0
- data/lib/sevk/resources/audiences.rb +22 -11
- data/lib/sevk/resources/base.rb +15 -3
- data/lib/sevk/resources/broadcasts.rb +56 -12
- data/lib/sevk/resources/contacts.rb +32 -17
- data/lib/sevk/resources/domains.rb +22 -8
- data/lib/sevk/resources/emails.rb +85 -34
- data/lib/sevk/resources/inbound.rb +123 -0
- data/lib/sevk/resources/outbound.rb +55 -0
- data/lib/sevk/resources/segments.rb +34 -20
- data/lib/sevk/resources/subscriptions.rb +18 -11
- data/lib/sevk/resources/templates.rb +20 -8
- data/lib/sevk/resources/topics.rb +31 -20
- data/lib/sevk/resources/usage.rb +15 -0
- data/lib/sevk/resources/webhooks.rb +21 -7
- data/lib/sevk/version.rb +1 -1
- data/lib/sevk/webhook_verifier.rb +91 -0
- data/lib/sevk.rb +24 -238
- metadata +21 -94
- data/Rakefile +0 -42
- data/lib/sevk/resources/events.rb +0 -18
- data/sevk.gemspec +0 -41
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Markup
|
|
5
|
+
module TemplateEngine
|
|
6
|
+
MAX_IF_PASSES = 2000
|
|
7
|
+
MAX_EACH_ITEMS = 1000
|
|
8
|
+
MAX_EACH_OUTPUT = 2_000_000
|
|
9
|
+
MAX_TEMPLATE_WORK = 20_000_000
|
|
10
|
+
OUTPUT_EXCEEDED = 'Email block exceeds the maximum template output size'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def truthy?(val)
|
|
15
|
+
return false if val.nil?
|
|
16
|
+
return false if val == false
|
|
17
|
+
return false if val.is_a?(Numeric) && val.zero?
|
|
18
|
+
return false if val.is_a?(String) && val.empty?
|
|
19
|
+
return false if val.is_a?(Array) && val.empty?
|
|
20
|
+
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def evaluate_condition(expr, config)
|
|
25
|
+
trimmed = expr.strip
|
|
26
|
+
|
|
27
|
+
return trimmed.split('||').any? { |part| evaluate_condition(part, config) } if trimmed.include?('||')
|
|
28
|
+
|
|
29
|
+
return trimmed.split('&&').all? { |part| evaluate_condition(part, config) } if trimmed.include?('&&')
|
|
30
|
+
|
|
31
|
+
if (eq_match = trimmed.match(/^(\w+)\s*==\s*"([^"]*)"$/))
|
|
32
|
+
return template_text(config[eq_match[1]]) == eq_match[2]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if (neq_match = trimmed.match(/^(\w+)\s*!=\s*"([^"]*)"$/))
|
|
36
|
+
return template_text(config[neq_match[1]]) != neq_match[2]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
truthy?(config[trimmed])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_template(template, config)
|
|
43
|
+
@template_rendered = 0
|
|
44
|
+
result = render_conditionals(template, config)
|
|
45
|
+
result = render_loops(result, config)
|
|
46
|
+
|
|
47
|
+
result = result.gsub(/\{%(\w+)\s*\?\?([^%]+)%\}/) do
|
|
48
|
+
value = config[Regexp.last_match(1)]
|
|
49
|
+
charge_template_output(value.nil? || value == '' ? Regexp.last_match(2).strip : template_text(value))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result.gsub(/\{%(\w+)%\}/) { charge_template_output(template_text(config[Regexp.last_match(1)])) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def charge_template_output(value)
|
|
56
|
+
raise ArgumentError, OUTPUT_EXCEEDED if @template_rendered + value.length > MAX_EACH_OUTPUT
|
|
57
|
+
|
|
58
|
+
@template_rendered += value.length
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_conditionals(template, config)
|
|
63
|
+
result = template
|
|
64
|
+
pattern = %r{\{%#if\s+([^%]+)%\}((?:(?!\{%#if\s)[\s\S])*?)\{%/if%\}}
|
|
65
|
+
work = 0
|
|
66
|
+
|
|
67
|
+
MAX_IF_PASSES.times do
|
|
68
|
+
work += result.length
|
|
69
|
+
raise ArgumentError, RenderLimits::TOO_COMPLEX if work > MAX_TEMPLATE_WORK
|
|
70
|
+
|
|
71
|
+
replaced = result.sub(pattern) do
|
|
72
|
+
condition = Regexp.last_match(1)
|
|
73
|
+
body = Regexp.last_match(2)
|
|
74
|
+
else_index = body.index('{%else%}')
|
|
75
|
+
if else_index.nil?
|
|
76
|
+
evaluate_condition(condition, config) ? body : ''
|
|
77
|
+
else
|
|
78
|
+
evaluate_condition(condition, config) ? body[0...else_index] : body[(else_index + 8)..]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
break if replaced == result
|
|
82
|
+
|
|
83
|
+
result = replaced
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_loops(template, config)
|
|
90
|
+
return template unless template.include?('{%/each%}')
|
|
91
|
+
|
|
92
|
+
output = +''
|
|
93
|
+
from = 0
|
|
94
|
+
work = 0
|
|
95
|
+
|
|
96
|
+
while from <= template.length
|
|
97
|
+
open = template.match(/\{%#each\s+(\w+)(?:\s+as\s+(\w+))?%\}/, from)
|
|
98
|
+
if open.nil?
|
|
99
|
+
output << template[from..]
|
|
100
|
+
break
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
body_start = open.end(0)
|
|
104
|
+
close_index = template.index('{%/each%}', body_start)
|
|
105
|
+
if close_index.nil?
|
|
106
|
+
output << template[from..]
|
|
107
|
+
break
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
output << template[from...open.begin(0)]
|
|
111
|
+
items = config[open[1]]
|
|
112
|
+
|
|
113
|
+
if items.is_a?(Array) && !items.empty?
|
|
114
|
+
if items.length > MAX_EACH_ITEMS
|
|
115
|
+
raise ArgumentError, "Email block exceeds the maximum of #{MAX_EACH_ITEMS} loop items"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias_name = open[2] || 'this'
|
|
119
|
+
body = template[body_start...close_index]
|
|
120
|
+
property_pattern = /\{%#{Regexp.escape(alias_name)}\.(\w+)%\}/
|
|
121
|
+
|
|
122
|
+
items.each do |item|
|
|
123
|
+
work += body.length + 16
|
|
124
|
+
raise ArgumentError, RenderLimits::TOO_COMPLEX if work > MAX_TEMPLATE_WORK
|
|
125
|
+
|
|
126
|
+
output << render_loop_item(body, item, config, property_pattern)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
from = close_index + 9
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
output
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def item_property(item, name)
|
|
137
|
+
return item[name] if item.is_a?(Hash)
|
|
138
|
+
return nil unless name == 'length'
|
|
139
|
+
|
|
140
|
+
case item
|
|
141
|
+
when String then item.encode('UTF-16LE').bytesize / 2
|
|
142
|
+
when Array then item.length
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render_loop_item(body, item, config, property_pattern)
|
|
147
|
+
item_out = 0
|
|
148
|
+
charge = lambda do |value|
|
|
149
|
+
raise ArgumentError, OUTPUT_EXCEEDED if @template_rendered + item_out + value.length > MAX_EACH_OUTPUT
|
|
150
|
+
|
|
151
|
+
item_out += value.length
|
|
152
|
+
value
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
rendered = body.gsub(property_pattern) { charge.call(template_text(item_property(item, Regexp.last_match(1)))) }
|
|
156
|
+
rendered = rendered.gsub(/\{%(\w+)%\}/) { charge.call(template_text(config[Regexp.last_match(1)])) }
|
|
157
|
+
@template_rendered += rendered.length
|
|
158
|
+
raise ArgumentError, OUTPUT_EXCEEDED if @template_rendered > MAX_EACH_OUTPUT
|
|
159
|
+
|
|
160
|
+
rendered
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def template_text(value)
|
|
164
|
+
case value
|
|
165
|
+
when nil then ''
|
|
166
|
+
when true then 'true'
|
|
167
|
+
when false then 'false'
|
|
168
|
+
when Integer, Float then js_number(value.to_f)
|
|
169
|
+
when Hash then '[object Object]'
|
|
170
|
+
when Array then value.map { |item| item.is_a?(Hash) ? '[object Object]' : template_text(item) }.join(',')
|
|
171
|
+
else value.to_s
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def process_block_tag(attrs, inner = '')
|
|
176
|
+
template = inner.strip.empty? ? (attrs['template'] || '') : inner.strip
|
|
177
|
+
return '' if template.empty?
|
|
178
|
+
|
|
179
|
+
config_str = (attrs['config'] || '{}').gsub(/&(?:amp|quot|apos|lt|gt|#39|#x27|#34|#x22);/i) do |entity|
|
|
180
|
+
{
|
|
181
|
+
'&' => '&', '"' => '"', '"' => '"', '"' => '"',
|
|
182
|
+
''' => "'", ''' => "'", ''' => "'", '<' => '<', '>' => '>'
|
|
183
|
+
}.fetch(entity.downcase)
|
|
184
|
+
end
|
|
185
|
+
config = begin
|
|
186
|
+
JSON.parse(config_str)
|
|
187
|
+
rescue JSON::ParserError
|
|
188
|
+
begin
|
|
189
|
+
JSON.parse(config_str.gsub("'", '"'))
|
|
190
|
+
rescue JSON::ParserError
|
|
191
|
+
{}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
render_template(template, config.is_a?(Hash) ? config : {})
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def parse_padding(style)
|
|
198
|
+
pt = pr = pb = pl = 0
|
|
199
|
+
if style['padding']
|
|
200
|
+
parts = style['padding'].split
|
|
201
|
+
case parts.length
|
|
202
|
+
when 1
|
|
203
|
+
val = parse_px(parts[0])
|
|
204
|
+
pt = pr = pb = pl = val
|
|
205
|
+
when 2
|
|
206
|
+
vertical = parse_px(parts[0])
|
|
207
|
+
horizontal = parse_px(parts[1])
|
|
208
|
+
pt = vertical
|
|
209
|
+
pr = horizontal
|
|
210
|
+
pb = vertical
|
|
211
|
+
pl = horizontal
|
|
212
|
+
when 3
|
|
213
|
+
pt = parse_px(parts[0])
|
|
214
|
+
pr = parse_px(parts[1])
|
|
215
|
+
pb = parse_px(parts[2])
|
|
216
|
+
pl = parse_px(parts[1])
|
|
217
|
+
when 4
|
|
218
|
+
pt = parse_px(parts[0])
|
|
219
|
+
pr = parse_px(parts[1])
|
|
220
|
+
pb = parse_px(parts[2])
|
|
221
|
+
pl = parse_px(parts[3])
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
pt = parse_px(style['padding-top']) if style['padding-top']
|
|
225
|
+
pr = parse_px(style['padding-right']) if style['padding-right']
|
|
226
|
+
pb = parse_px(style['padding-bottom']) if style['padding-bottom']
|
|
227
|
+
pl = parse_px(style['padding-left']) if style['padding-left']
|
|
228
|
+
[pt, pr, pb, pl]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def parse_px(value)
|
|
232
|
+
value.to_s[/\A\s*[+-]?\d+/].to_i
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def px_to_pt(pixels)
|
|
236
|
+
((pixels * 3) + 2) / 4
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def compute_font_width_and_space_count(expected_width)
|
|
240
|
+
return [0, 0] unless expected_width.positive?
|
|
241
|
+
|
|
242
|
+
space_count = (expected_width / 10.0).ceil.clamp(1, 1000)
|
|
243
|
+
[expected_width.to_f / space_count / 2.0, space_count]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Activity < Base
|
|
8
|
+
# Lists project activity with optional pagination and filtering.
|
|
9
|
+
def list(page: nil, limit: nil, after: nil, before: nil, category: nil, type: nil, days: nil, search: nil)
|
|
10
|
+
type = type.join(',') if type.is_a?(Array)
|
|
11
|
+
params = { page: page, limit: limit, after: after, before: before, category: category, type: type, days: days,
|
|
12
|
+
search: search }
|
|
13
|
+
|
|
14
|
+
client.get("/activity#{build_query_string(params)}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns aggregate counts and rates for recent project activity.
|
|
18
|
+
def stats
|
|
19
|
+
client.get('/activity/stats')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,46 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Audiences < Base
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Lists audiences with optional pagination and search.
|
|
9
|
+
def list(page: nil, limit: nil, after: nil, before: nil, search: nil)
|
|
10
|
+
params = { page: page, limit: limit, after: after, before: before, search: search }
|
|
11
|
+
|
|
10
12
|
client.get("/audiences#{build_query_string(params)}")
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Retrieves an audience by id.
|
|
13
16
|
def get(id)
|
|
14
17
|
client.get("/audiences/#{id}")
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
# Creates an audience for organizing contacts, topics, and segments.
|
|
21
|
+
def create(name:, description: nil, visibility: nil)
|
|
18
22
|
body = { name: name }
|
|
19
|
-
body[:description] = description
|
|
20
|
-
body[:
|
|
21
|
-
|
|
23
|
+
body[:description] = description unless description.nil?
|
|
24
|
+
body[:visibility] = visibility unless visibility.nil?
|
|
25
|
+
|
|
26
|
+
client.post('/audiences', body)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
# Updates an audience while preserving omitted fields.
|
|
30
|
+
def update(id, name: nil, description: nil, visibility: nil)
|
|
25
31
|
body = {}
|
|
26
|
-
body[:name] = name
|
|
27
|
-
body[:description] = description
|
|
28
|
-
body[:
|
|
32
|
+
body[:name] = name unless name.nil?
|
|
33
|
+
body[:description] = description unless description.nil?
|
|
34
|
+
body[:visibility] = visibility unless visibility.nil?
|
|
35
|
+
|
|
29
36
|
client.put("/audiences/#{id}", body)
|
|
30
37
|
end
|
|
31
38
|
|
|
39
|
+
# Deletes an audience without deleting its contacts.
|
|
32
40
|
def delete(id)
|
|
33
41
|
client.delete("/audiences/#{id}")
|
|
34
42
|
end
|
|
35
43
|
|
|
44
|
+
# Adds contacts to an audience, ignoring memberships that already exist.
|
|
36
45
|
def add_contacts(audience_id, contact_ids)
|
|
37
46
|
client.post("/audiences/#{audience_id}/contacts", { contactIds: contact_ids })
|
|
38
47
|
end
|
|
39
48
|
|
|
49
|
+
# Lists contacts attached to an audience with optional filtering.
|
|
40
50
|
def list_contacts(audience_id, params = {})
|
|
41
51
|
client.get("/audiences/#{audience_id}/contacts#{build_query_string(params)}")
|
|
42
52
|
end
|
|
43
53
|
|
|
54
|
+
# Removes a contact from the audience without deleting the contact.
|
|
44
55
|
def remove_contact(audience_id, contact_id)
|
|
45
56
|
client.delete("/audiences/#{audience_id}/contacts/#{contact_id}")
|
|
46
57
|
end
|
data/lib/sevk/resources/base.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
3
5
|
module Sevk
|
|
4
6
|
module Resources
|
|
5
7
|
class Base
|
|
@@ -12,10 +14,20 @@ module Sevk
|
|
|
12
14
|
private
|
|
13
15
|
|
|
14
16
|
def build_query_string(params)
|
|
15
|
-
return
|
|
17
|
+
return '' if params.nil? || params.empty?
|
|
18
|
+
|
|
19
|
+
compacted = wire_keys(params).compact
|
|
20
|
+
return '' if compacted.empty?
|
|
21
|
+
|
|
22
|
+
"?#{URI.encode_www_form(compacted)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def wire_keys(params)
|
|
26
|
+
params.transform_keys { |key| wire_key(key) }
|
|
27
|
+
end
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
def wire_key(key)
|
|
30
|
+
key.to_s.gsub(/_([a-z0-9])/) { Regexp.last_match(1).upcase }.to_sym
|
|
19
31
|
end
|
|
20
32
|
end
|
|
21
33
|
end
|
|
@@ -1,61 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Broadcasts < Base
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Lists broadcasts with optional pagination, search, and status filtering.
|
|
9
|
+
def list(page: nil, limit: nil, after: nil, before: nil, search: nil, status: nil)
|
|
10
|
+
params = { page: page, limit: limit, after: after, before: before, search: search, status: status }
|
|
11
|
+
|
|
10
12
|
client.get("/broadcasts#{build_query_string(params)}")
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Retrieves a broadcast by id and includes its current delivery state.
|
|
13
16
|
def get(id)
|
|
14
17
|
client.get("/broadcasts/#{id}")
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
# Creates a draft or scheduled broadcast for a target group.
|
|
17
21
|
def create(params)
|
|
18
|
-
client.post(
|
|
22
|
+
client.post('/broadcasts', broadcast_body(params))
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
# Updates a broadcast while preserving omitted fields. The response includes
|
|
26
|
+
# variants and abTest when this mutation touched experiment state.
|
|
21
27
|
def update(id, params)
|
|
22
|
-
client.put("/broadcasts/#{id}", params)
|
|
28
|
+
client.put("/broadcasts/#{id}", broadcast_body(params))
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# Deletes a broadcast that has not been sent.
|
|
25
32
|
def delete(id)
|
|
26
33
|
client.delete("/broadcasts/#{id}")
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Queues a prepared broadcast for delivery.
|
|
29
37
|
def send(id)
|
|
30
38
|
client.post("/broadcasts/#{id}/send")
|
|
31
39
|
end
|
|
32
40
|
|
|
41
|
+
# Cancels a broadcast when its state still allows cancellation.
|
|
33
42
|
def cancel(id)
|
|
34
43
|
client.post("/broadcasts/#{id}/cancel")
|
|
35
44
|
end
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
# Sends the broadcast to test recipients without starting the campaign.
|
|
47
|
+
def test(id, params, options: nil)
|
|
48
|
+
client.post("/broadcasts/#{id}/test", wire_keys(params), headers: idempotency_headers(options))
|
|
39
49
|
end
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
# Returns aggregate delivery and engagement analytics for a broadcast.
|
|
52
|
+
def analytics(id)
|
|
42
53
|
client.get("/broadcasts/#{id}/analytics")
|
|
43
54
|
end
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
# Returns live scheduling and delivery progress for a broadcast.
|
|
57
|
+
def status(id)
|
|
46
58
|
client.get("/broadcasts/#{id}/status")
|
|
47
59
|
end
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
# Lists email deliveries created for a broadcast.
|
|
62
|
+
def emails(id, params = {})
|
|
50
63
|
client.get("/broadcasts/#{id}/emails#{build_query_string(params)}")
|
|
51
64
|
end
|
|
52
65
|
|
|
66
|
+
# Estimates delivery cost without sending the broadcast.
|
|
53
67
|
def estimate_cost(id)
|
|
54
68
|
client.get("/broadcasts/#{id}/estimate-cost")
|
|
55
69
|
end
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
# Resolves the subscribed recipient count for a broadcast target.
|
|
72
|
+
def estimate_recipients(params)
|
|
73
|
+
client.post('/broadcasts/estimate-recipients', wire_keys(params))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Lists broadcasts that are currently scheduled or processing.
|
|
77
|
+
def active
|
|
78
|
+
client.get('/broadcasts/active')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def broadcast_body(params)
|
|
84
|
+
body = wire_keys(params)
|
|
85
|
+
|
|
86
|
+
if body[:variants].is_a?(Array)
|
|
87
|
+
body[:variants] = body[:variants].map { |variant| variant.is_a?(Hash) ? wire_keys(variant) : variant }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
body[:abTest] = wire_keys(body[:abTest]) if body[:abTest].is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
body
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def idempotency_headers(options)
|
|
96
|
+
return nil unless options
|
|
97
|
+
|
|
98
|
+
return nil unless options.key?(:idempotency_key) || options.key?('idempotency_key')
|
|
99
|
+
|
|
100
|
+
key = options.key?(:idempotency_key) ? options[:idempotency_key] : options['idempotency_key']
|
|
101
|
+
|
|
102
|
+
{ 'Idempotency-Key' => key.to_s }
|
|
59
103
|
end
|
|
60
104
|
end
|
|
61
105
|
end
|
|
@@ -1,51 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Contacts < Base
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
UNSET = Object.new
|
|
9
|
+
private_constant :UNSET
|
|
10
|
+
|
|
11
|
+
# Lists contacts with optional pagination, search, and subscription filters.
|
|
12
|
+
def list(page: nil, limit: nil, after: nil, before: nil, search: nil, exclude_audience: nil, subscribed: nil)
|
|
13
|
+
params = { page: page, limit: limit, after: after, before: before, search: search,
|
|
14
|
+
excludeAudience: exclude_audience }
|
|
15
|
+
params[:subscribed] = subscribed unless subscribed.nil?
|
|
16
|
+
|
|
10
17
|
client.get("/contacts#{build_query_string(params)}")
|
|
11
18
|
end
|
|
12
19
|
|
|
20
|
+
# Retrieves a contact by id.
|
|
13
21
|
def get(id)
|
|
14
22
|
client.get("/contacts/#{id}")
|
|
15
23
|
end
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
# Creates a contact and optionally attaches custom metadata.
|
|
26
|
+
def create(email:, subscribed: nil, data: nil)
|
|
18
27
|
body = { email: email }
|
|
19
|
-
body[:firstName] = first_name if first_name
|
|
20
|
-
body[:lastName] = last_name if last_name
|
|
21
28
|
body[:subscribed] = subscribed unless subscribed.nil?
|
|
22
|
-
body[:
|
|
23
|
-
|
|
29
|
+
body[:data] = data unless data.nil?
|
|
30
|
+
|
|
31
|
+
client.post('/contacts', body)
|
|
24
32
|
end
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
# Updates a contact. Explicit nil clears metadata; omitted data preserves it.
|
|
35
|
+
def update(id, email: nil, subscribed: nil, data: UNSET)
|
|
27
36
|
body = {}
|
|
28
|
-
body[:
|
|
29
|
-
body[:lastName] = last_name if last_name
|
|
37
|
+
body[:email] = email unless email.nil?
|
|
30
38
|
body[:subscribed] = subscribed unless subscribed.nil?
|
|
31
|
-
body[:
|
|
39
|
+
body[:data] = data unless data.equal?(UNSET)
|
|
40
|
+
|
|
32
41
|
client.put("/contacts/#{id}", body)
|
|
33
42
|
end
|
|
34
43
|
|
|
44
|
+
# Deletes a contact and removes its audience and topic memberships.
|
|
35
45
|
def delete(id)
|
|
36
46
|
client.delete("/contacts/#{id}")
|
|
37
47
|
end
|
|
38
48
|
|
|
49
|
+
# Updates multiple contacts by email and reports entries that were not found.
|
|
39
50
|
def bulk_update(updates)
|
|
40
|
-
client.put(
|
|
51
|
+
client.put('/contacts/bulk-update', updates)
|
|
41
52
|
end
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
# Imports contacts in one request, optionally attaching them to a group.
|
|
55
|
+
def bulk_import(params)
|
|
56
|
+
client.post('/contacts/import', wire_keys(params))
|
|
45
57
|
end
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
# Lists delivery and engagement activity for a contact.
|
|
60
|
+
def activity(id, page: nil, limit: nil, after: nil, before: nil)
|
|
61
|
+
params = { page: page, limit: limit, after: after, before: before }
|
|
62
|
+
|
|
63
|
+
client.get("/contacts/#{id}/activity#{build_query_string(params)}")
|
|
49
64
|
end
|
|
50
65
|
end
|
|
51
66
|
end
|
|
@@ -1,41 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Domains < Base
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Lists sending domains, optionally filtered by verification state.
|
|
9
|
+
def list(verified: nil, page: nil, limit: nil, after: nil, before: nil)
|
|
10
|
+
params = { verified: verified, page: page, limit: limit, after: after, before: before }
|
|
11
|
+
|
|
10
12
|
client.get("/domains#{build_query_string(params)}")
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Retrieves a domain and its current verification state.
|
|
13
16
|
def get(id)
|
|
14
17
|
client.get("/domains/#{id}")
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
# registers an exact domain claim; dns fields depend on domain:dns-read.
|
|
17
21
|
def create(params)
|
|
18
|
-
client.post(
|
|
22
|
+
client.post('/domains', wire_keys(params))
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
# Updates sending, tracking, TLS, or inbound settings for a domain.
|
|
21
26
|
def update(id, params)
|
|
22
|
-
client.put("/domains/#{id}", params)
|
|
27
|
+
client.put("/domains/#{id}", wire_keys(params))
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
# Deletes a domain after the API confirms it is safe to remove.
|
|
25
31
|
def delete(id)
|
|
26
32
|
client.delete("/domains/#{id}")
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# Rechecks sending DNS records and returns the updated domain state.
|
|
29
36
|
def verify(id)
|
|
30
37
|
client.post("/domains/#{id}/verify", {})
|
|
31
38
|
end
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
# Checks the inbound MX record and returns details useful for DNS diagnosis.
|
|
41
|
+
def verify_inbound(id)
|
|
42
|
+
client.post("/domains/#{id}/inbound/verify", {})
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the DNS records required for sending and inbound mail.
|
|
46
|
+
def dns_records(id)
|
|
34
47
|
client.get("/domains/#{id}/dns-records")
|
|
35
48
|
end
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
# Lists region identifiers accepted when registering a domain.
|
|
51
|
+
def regions
|
|
52
|
+
client.get('/domains/regions')
|
|
39
53
|
end
|
|
40
54
|
end
|
|
41
55
|
end
|