sendclipper 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.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SendClipper
6
+ module Resources
7
+ class Contacts < Base
8
+ # Fields: audience_id, status, page, page_size.
9
+ def list(params = nil, **kw)
10
+ query, options = split(params, kw)
11
+ api_get("/contacts", query: query, options: options)
12
+ end
13
+
14
+ # Every contact, page after page (an Enumerator).
15
+ def each(params = nil, **kw, &block)
16
+ query, options = split(params, kw)
17
+ size = (query["pageSize"] || 200).to_i
18
+ stop = ->(res, page, _size, _items) { page >= res["totalPages"].to_i }
19
+ enum = Paging.numbered(size, "items", stop) do |page, page_size|
20
+ api_get("/contacts", query: query.merge("page" => page, "pageSize" => page_size), options: options)
21
+ end
22
+ block ? enum.each(&block) : enum
23
+ end
24
+
25
+ # Fields: name, email, audience_id, status.
26
+ def create(params = nil, **kw)
27
+ body, options = split(params, kw)
28
+ api_post("/contacts", body, options: options)
29
+ end
30
+
31
+ # Import a CSV or vCard file. Importing never suppresses anyone.
32
+ #
33
+ # client.contacts.import(file: File.read("contacts.csv"), filename: "contacts.csv", on_duplicate: "update")
34
+ #
35
+ # The filename's extension picks the parser. Other fields: audience_id,
36
+ # new_audience, create_audiences, mapping.
37
+ def import(file:, filename: "contacts.csv", **kw)
38
+ fields, options = split(nil, kw)
39
+ @http.call("POST", "/contacts/import", form: import_form(file, filename, fields), options: options)
40
+ end
41
+
42
+ # What an import would do, without importing.
43
+ def import_preview(file:, filename: "contacts.csv", **options)
44
+ @http.call("POST", "/contacts/import/preview", form: import_form(file, filename, {}), options: options)
45
+ end
46
+
47
+ # Every contact as CSV (re-importable) or vCard 4.0 (format: "vcf").
48
+ def export(params = nil, **kw)
49
+ query, options = split(params, kw)
50
+ @http.call("GET", "/contacts/export", query: query, as: :text, options: options)
51
+ end
52
+
53
+ private
54
+
55
+ def import_form(file, filename, fields)
56
+ type = filename.to_s.downcase.end_with?(".vcf") ? "text/vcard" : "text/csv"
57
+ parts = fields.filter_map do |k, v|
58
+ next if v.nil? || v == ""
59
+
60
+ [k, k == "mapping" ? JSON.generate(v) : v.to_s]
61
+ end
62
+ HttpClient.multipart(parts, [["file", filename.to_s, type, file]])
63
+ end
64
+ end
65
+
66
+ class Audiences < Base
67
+ # { "audiences" => [...], "totalContacts" => n }.
68
+ def list(**options) = api_get("/audiences", options: options)
69
+ end
70
+
71
+ class Suppressions < Base
72
+ # Fields: q (address search), reason, cursor, limit.
73
+ def list(params = nil, **kw)
74
+ query, options = split(params, kw)
75
+ api_get("/suppressions", query: query, options: options)
76
+ end
77
+
78
+ def each(params = nil, **kw, &block)
79
+ query, options = split(params, kw)
80
+ query.delete("cursor")
81
+ enum = Paging.cursor("data") { |c| api_get("/suppressions", query: query.merge("cursor" => c), options: options) }
82
+ block ? enum.each(&block) : enum
83
+ end
84
+
85
+ # Stop all mail to an address (reason +manual+). Fields: email, note.
86
+ # "created" is false when the address was already suppressed; "row" is
87
+ # the entry either way.
88
+ def create(params = nil, **kw)
89
+ body, options = split(params, kw)
90
+ api_post("/suppressions", body, options: options)
91
+ end
92
+
93
+ # Lift a suppression. +reason+ (10+ characters) is recorded. Complaint and
94
+ # provider suppressions cannot be removed (+suppression_not_removable+).
95
+ def remove(id, reason, **options) = api_delete("/suppressions/#{seg(id)}", { "reason" => reason }, options: options)
96
+
97
+ # The list as CSV. Fields: q, reason.
98
+ def export(params = nil, **kw)
99
+ query, options = split(params, kw)
100
+ query = query.reject { |k, _| %w[cursor limit].include?(k) }
101
+ @http.call("GET", "/suppressions/export", query: query, as: :text, options: options)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ # Every method takes its fields as a Hash or as keyword arguments, and the
6
+ # request options +idempotency_key:+, +timeout:+ (seconds), +max_retries:+
7
+ # and +headers:+ alongside them.
8
+ #
9
+ # Top-level field names may be the API's own (+templateId+) or snake_case
10
+ # (+template_id+); snake_case is turned into the API's camelCase. Values,
11
+ # such as the +variables+ map, are sent exactly as given.
12
+ class Base
13
+ def initialize(http)
14
+ @http = http
15
+ end
16
+
17
+ private
18
+
19
+ def seg(value) = HttpClient.seg(value)
20
+
21
+ # Split keyword arguments into [fields, options].
22
+ def split(params, kw)
23
+ options = kw.slice(*HttpClient::OPTION_KEYS)
24
+ fields = (params || {}).to_h.merge(kw.reject { |k, _| HttpClient::OPTION_KEYS.include?(k) })
25
+ [camelize(fields), options]
26
+ end
27
+
28
+ def camelize(fields)
29
+ fields.each_with_object({}) do |(k, v), out|
30
+ next if v.nil?
31
+
32
+ out[k.to_s.gsub(/_([a-z0-9])/) { Regexp.last_match(1).upcase }] = v
33
+ end
34
+ end
35
+
36
+ def api_get(path, query: nil, as: :json, options: {})
37
+ @http.call("GET", path, query: query, as: as, options: options)
38
+ end
39
+
40
+ def api_post(path, body = {}, options: {}, bearer: nil)
41
+ @http.call("POST", path, body: body, bearer: bearer, options: options)
42
+ end
43
+
44
+ def api_patch(path, body, options: {}) = @http.call("PATCH", path, body: body, options: options)
45
+
46
+ def api_put(path, body, options: {}) = @http.call("PUT", path, body: body, options: options)
47
+
48
+ def api_delete(path, body = nil, options: {}) = @http.call("DELETE", path, body: body, options: options)
49
+ end
50
+
51
+ # Iterators over a numbered or a cursor list. They raise ApiError on a
52
+ # refusal, since a loop has no +error+ to hand back.
53
+ module Paging
54
+ module_function
55
+
56
+ def numbered(page_size, items_key, stop)
57
+ Enumerator.new do |y|
58
+ page = 1
59
+ loop do
60
+ res = yield(page, page_size).unwrap
61
+ items = res[items_key] || []
62
+ items.each { |item| y << item }
63
+ break if items.empty? || stop.call(res, page, page_size, items)
64
+
65
+ page += 1
66
+ end
67
+ end
68
+ end
69
+
70
+ def cursor(items_key)
71
+ Enumerator.new do |y|
72
+ cursor = nil
73
+ loop do
74
+ res = yield(cursor).unwrap
75
+ (res[items_key] || []).each { |item| y << item }
76
+ info = res["page"] || {}
77
+ break unless info["hasMore"] && info["cursor"].to_s != ""
78
+
79
+ cursor = info["cursor"]
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Domains < Base
6
+ def list(**options) = api_get("/domains", options: options)
7
+
8
+ # Register a sending domain (name:, region: "us-east-1" | "eu-west-1" | "us-west-2").
9
+ # Next: #records, publish them, then #verify.
10
+ def create(params = nil, **kw)
11
+ body, options = split(params, kw)
12
+ api_post("/domains", body, options: options)
13
+ end
14
+
15
+ # The DNS records to publish, each with its current status.
16
+ def records(id, **options) = api_get("/domains/#{seg(id)}/records", options: options)
17
+
18
+ # Check DNS now instead of waiting for the background check.
19
+ def verify(id, **options) = api_post("/domains/#{seg(id)}/verify", nil, options: options)
20
+
21
+ # Start receiving mail on this domain. Adds receiving records; never blocks sending.
22
+ def enable_receiving(id, **options) = api_post("/domains/#{seg(id)}/receiving", nil, options: options)
23
+
24
+ def disable_receiving(id, **options) = api_delete("/domains/#{seg(id)}/receiving", options: options)
25
+
26
+ # Move a domain to another region. Refused (+region_immutable+) once it holds messages.
27
+ def set_region(id, region, **options) = api_patch("/domains/#{seg(id)}/region", { "region" => region }, options: options)
28
+
29
+ # What deleting the domain would take with it.
30
+ def deletion_preview(id, **options) = api_get("/domains/#{seg(id)}/deletion", options: options)
31
+
32
+ def remove(id, **options) = api_delete("/domains/#{seg(id)}", options: options)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Emails < Base
6
+ # Send one message. Returns once SendClipper accepted it (status +queued+);
7
+ # delivery is reported later through #get and webhooks.
8
+ #
9
+ # client.emails.send(from: "hello@send.acme.io", to: "ada@lumen.io", subject: "Hi", html: "<p>Hi</p>")
10
+ #
11
+ # Every call carries an Idempotency-Key, so the SDK's own retries never
12
+ # send twice. Pass +idempotency_key:+ to make YOUR retries safe too.
13
+ def send(params = nil, **kw)
14
+ body, options = split(params, kw)
15
+ api_post("/emails", body, options: options)
16
+ end
17
+
18
+ # Same as client.batch.send.
19
+ def send_batch(emails, **options) = Batch.post(@http, emails, options)
20
+
21
+ # One message with its body and event timeline.
22
+ def get(id, **options) = api_get("/emails/#{seg(id)}", options: options)
23
+
24
+ # One page of the sent-mail log, newest first. Fields: search, status,
25
+ # domain, broadcast_id, from, to, page, page_size.
26
+ def list(params = nil, **kw)
27
+ query, options = split(params, kw)
28
+ get_list("/emails", query, options)
29
+ end
30
+
31
+ # Every message matching the fields, page after page (an Enumerator).
32
+ def each(params = nil, **kw, &block)
33
+ query, options = split(params, kw)
34
+ size = (query["pageSize"] || 100).to_i
35
+ stop = ->(res, page, page_size, items) { items.length < page_size || page * page_size >= res["total"].to_i }
36
+ enum = Paging.numbered(size, "items", stop) do |page, page_size|
37
+ get_list("/emails", query.merge("page" => page, "pageSize" => page_size), options)
38
+ end
39
+ block ? enum.each(&block) : enum
40
+ end
41
+
42
+ # Counts per status.
43
+ def stats(**options) = get_list("/emails/stats/summary", nil, options)
44
+
45
+ private
46
+
47
+ def get_list(path, query, options) = @http.call("GET", path, query: query, options: options)
48
+ end
49
+
50
+ class Batch < Base
51
+ # Up to 500 messages in one request (one recipient each). Every item is
52
+ # accepted or refused on its own; a refused item costs its siblings nothing.
53
+ def send(emails, **options) = self.class.post(@http, emails, options)
54
+
55
+ # @api private
56
+ def self.post(http, emails, options)
57
+ items = emails.map { |e| new(http).__send__(:camelize, e.to_h) }
58
+ http.call("POST", "/emails/batch", body: { "emails" => items }, options: options)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Mailboxes < Base
6
+ attr_reader :messages
7
+
8
+ def initialize(http)
9
+ super
10
+ @messages = MailboxMessages.new(http)
11
+ end
12
+
13
+ # Fields: include_archived.
14
+ def list(params = nil, **kw)
15
+ query, options = split(params, kw)
16
+ api_get("/mailboxes", query: query, options: options)
17
+ end
18
+
19
+ def get(id, **options) = api_get("/mailboxes/#{seg(id)}", options: options)
20
+
21
+ # A new address on a receiving domain. Fields: domain_id, local_part,
22
+ # display_name, color, catch_all.
23
+ def create(params = nil, **kw)
24
+ body, options = split(params, kw)
25
+ api_post("/mailboxes", body, options: options)
26
+ end
27
+
28
+ def update(id, params = nil, **kw)
29
+ body, options = split(params, kw)
30
+ api_patch("/mailboxes/#{seg(id)}", body, options: options)
31
+ end
32
+
33
+ # Archives the address. Its mail is kept.
34
+ def archive(id, **options) = api_delete("/mailboxes/#{seg(id)}", options: options)
35
+ end
36
+
37
+ class MailboxMessages < Base
38
+ # Mail filed under one address. Reading never marks anything read.
39
+ # Fields: folder, search, label, limit, cursor.
40
+ def list(mailbox_id, params = nil, **kw)
41
+ query, options = split(params, kw)
42
+ api_get("/mailboxes/#{seg(mailbox_id)}/messages", query: query, options: options)
43
+ end
44
+
45
+ def each(mailbox_id, params = nil, **kw, &block)
46
+ query, options = split(params, kw)
47
+ query.delete("cursor")
48
+ enum = Paging.cursor("items") do |c|
49
+ api_get("/mailboxes/#{seg(mailbox_id)}/messages", query: query.merge("cursor" => c), options: options)
50
+ end
51
+ block ? enum.each(&block) : enum
52
+ end
53
+
54
+ def get(mailbox_id, message_id, **options) = api_get("/mailboxes/#{seg(mailbox_id)}/messages/#{seg(message_id)}", options: options)
55
+
56
+ # Download one attachment: a DownloadedFile with content, content_type and filename.
57
+ def attachment(mailbox_id, message_id, attachment_id, **options)
58
+ api_get("/mailboxes/#{seg(mailbox_id)}/messages/#{seg(message_id)}/parts/#{seg(attachment_id)}",
59
+ query: { "download" => 1 }, as: :bytes, options: options)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module SendClipper
6
+ module Resources
7
+ class Sandboxes < Base
8
+ attr_reader :messages
9
+
10
+ def initialize(http)
11
+ super
12
+ @messages = SandboxMessages.new(http)
13
+ end
14
+
15
+ def list(**options) = api_get("/sandboxes", options: options)
16
+
17
+ def get(id, **options) = api_get("/sandboxes/#{seg(id)}", options: options)
18
+
19
+ # Fields: name, project_label, color (#rrggbb).
20
+ def create(params = nil, **kw)
21
+ body, options = split(params, kw)
22
+ api_post("/sandboxes", body, options: options)
23
+ end
24
+
25
+ # Renaming never changes the address or the credentials.
26
+ def update(id, params = nil, **kw)
27
+ body, options = split(params, kw)
28
+ api_patch("/sandboxes/#{seg(id)}", body, options: options)
29
+ end
30
+
31
+ # Deletes the sandbox and every message in it.
32
+ def remove(id, **options) = api_delete("/sandboxes/#{seg(id)}", options: options)
33
+
34
+ # SMTP host, port, username and password to point a project at.
35
+ def credentials(id, **options) = api_get("/sandboxes/#{seg(id)}/credentials", options: options)
36
+
37
+ # Replace BOTH the SMTP password and the +sc_sbx_+ token. The token is shown only here.
38
+ def reset_credentials(id, **options) = api_post("/sandboxes/#{seg(id)}/credentials", nil, options: options)
39
+
40
+ # Send to a sandbox through its JSON door, with the sandbox's own
41
+ # +sc_sbx_...+ token. Same fields as emails.send; nothing is delivered.
42
+ def send(sandbox_token, params = nil, **kw)
43
+ body, options = split(params, kw)
44
+ api_post("/sandbox/emails", body, options: options, bearer: sandbox_token)
45
+ end
46
+ end
47
+
48
+ class SandboxMessages < Base
49
+ # Fields: search, limit.
50
+ def list(sandbox_id, params = nil, **kw)
51
+ query, options = split(params, kw)
52
+ api_get("/sandboxes/#{seg(sandbox_id)}/messages", query: query, options: options)
53
+ end
54
+
55
+ def get(sandbox_id, message_id, **options) = api_get("/sandboxes/#{seg(sandbox_id)}/messages/#{seg(message_id)}", options: options)
56
+
57
+ # The message's raw MIME source.
58
+ def raw(sandbox_id, message_id, **options)
59
+ @http.call("GET", "/sandboxes/#{seg(sandbox_id)}/messages/#{seg(message_id)}/raw", as: :text, options: options)
60
+ end
61
+
62
+ def remove(sandbox_id, message_id, **options) = api_delete("/sandboxes/#{seg(sandbox_id)}/messages/#{seg(message_id)}", options: options)
63
+
64
+ # Empty the sandbox. { "deleted" => n }.
65
+ def clear(sandbox_id, **options) = api_delete("/sandboxes/#{seg(sandbox_id)}/messages", options: options)
66
+
67
+ # { "updated" => n }.
68
+ def mark_all_read(sandbox_id, **options) = api_post("/sandboxes/#{seg(sandbox_id)}/read-all", nil, options: options)
69
+
70
+ # Poll until a message arrives that the block accepts (default: any
71
+ # message trapped after this call started). Handy in end-to-end tests.
72
+ # +timeout:+ and +interval:+ are seconds; on timeout +error.name+ is "timeout".
73
+ #
74
+ # msg, error = client.sandboxes.messages.wait_for(box["id"]) { |m| m["subject"] == "Hi" }
75
+ def wait_for(sandbox_id, timeout: 30, interval: 1, &match)
76
+ started = Time.now
77
+ deadline = started + timeout
78
+ match ||= lambda do |m|
79
+ Time.iso8601(m["trappedAt"].to_s) >= started - 1
80
+ rescue ArgumentError
81
+ false
82
+ end
83
+ loop do
84
+ page = list(sandbox_id, limit: 50)
85
+ return page unless page.ok?
86
+
87
+ found = (page.data || []).find { |m| match.call(m) }
88
+ return get(sandbox_id, found["id"]) if found
89
+
90
+ if Time.now >= deadline
91
+ return Result.new(nil, ErrorResponse.new(name: "timeout", status_code: nil,
92
+ message: "No matching message reached sandbox #{sandbox_id} within #{timeout} s."), nil)
93
+ end
94
+ sleep(interval) if interval.positive?
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Templates < Base
6
+ attr_reader :tags, :assets
7
+
8
+ def initialize(http)
9
+ super
10
+ @tags = TemplateTags.new(http)
11
+ @assets = TemplateAssets.new(http)
12
+ end
13
+
14
+ # Fields: tag (a tag id), project (a domain id).
15
+ def list(params = nil, **kw)
16
+ query, options = split(params, kw)
17
+ api_get("/templates", query: query, options: options)
18
+ end
19
+
20
+ # By uuid or public id (+tpl_...+).
21
+ def get(id, **options) = api_get("/templates/#{seg(id)}", options: options)
22
+
23
+ # The rendered HTML, tags left in place.
24
+ def html(id, **options) = @http.call("GET", "/templates/#{seg(id)}/html", as: :text, options: options)
25
+
26
+ # A new draft. Publish it before sending with it. Fields: name, subject,
27
+ # preheader, blocks, project_domain_id, key.
28
+ def create(params = nil, **kw)
29
+ body, options = split(params, kw)
30
+ api_post("/templates", body, options: options)
31
+ end
32
+
33
+ def update(id, params = nil, **kw)
34
+ body, options = split(params, kw)
35
+ api_patch("/templates/#{seg(id)}", body, options: options)
36
+ end
37
+
38
+ def remove(id, **options) = api_delete("/templates/#{seg(id)}", options: options)
39
+
40
+ def duplicate(id, **options) = api_post("/templates/#{seg(id)}/duplicate", nil, options: options)
41
+
42
+ def publish(id, **options) = api_post("/templates/#{seg(id)}/publish", nil, options: options)
43
+
44
+ def unpublish(id, **options) = api_post("/templates/#{seg(id)}/unpublish", nil, options: options)
45
+
46
+ # Render with +variables+ without sending anything.
47
+ def preview(id, variables = {}, **options)
48
+ api_post("/templates/#{seg(id)}/preview", { "variables" => variables.to_h }, options: options)
49
+ end
50
+
51
+ # Replace the template's tags with these tag ids.
52
+ def set_tags(id, tag_ids, **options) = api_put("/templates/#{seg(id)}/tags", { "tagIds" => tag_ids.to_a }, options: options)
53
+
54
+ # The projects (domains) templates can belong to.
55
+ def projects(**options) = api_get("/templates/projects", options: options)
56
+ end
57
+
58
+ class TemplateTags < Base
59
+ # Every tag, each with its templateCount.
60
+ def list(**options) = api_get("/templates/tags", options: options)
61
+
62
+ # Fields: name, color (gray, red, orange, amber, green, teal, blue, purple, pink).
63
+ def create(params = nil, **kw)
64
+ body, options = split(params, kw)
65
+ api_post("/templates/tags", body, options: options)
66
+ end
67
+
68
+ def update(id, params = nil, **kw)
69
+ body, options = split(params, kw)
70
+ api_patch("/templates/tags/#{seg(id)}", body, options: options)
71
+ end
72
+
73
+ # { "deleted" => true, "templateCount" => n }: the tag is taken off n templates.
74
+ def remove(id, **options) = api_delete("/templates/tags/#{seg(id)}", options: options)
75
+ end
76
+
77
+ class TemplateAssets < Base
78
+ def list(**options) = api_get("/templates/assets", options: options)
79
+
80
+ # Upload an image a template can show and get its public URL. +data:+ is
81
+ # the file's bytes (base64-encoded for you); +mime_type:+ is image/png,
82
+ # image/jpeg, image/gif or image/webp.
83
+ #
84
+ # client.templates.assets.upload(data: File.binread("logo.png"), mime_type: "image/png", name: "logo")
85
+ def upload(data:, mime_type:, name: nil, **options)
86
+ body = { "mimeType" => mime_type, "base64" => [data.to_s].pack("m0") }
87
+ body["name"] = name if name && !name.empty?
88
+ api_post("/templates/assets", body, options: options)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Webhooks < Base
6
+ attr_reader :dispatches
7
+
8
+ def initialize(http)
9
+ super
10
+ @dispatches = WebhookDispatches.new(http)
11
+ end
12
+
13
+ def list(**options) = api_get("/webhook-endpoints", options: options)
14
+
15
+ # Fields: url, events, allow_insecure_http. The response carries the
16
+ # signing "secret" once: store it.
17
+ def create(params = nil, **kw)
18
+ body, options = split(params, kw)
19
+ api_post("/webhook-endpoints", body, options: options)
20
+ end
21
+
22
+ def update(id, params = nil, **kw)
23
+ body, options = split(params, kw)
24
+ api_patch("/webhook-endpoints/#{seg(id)}", body, options: options)
25
+ end
26
+
27
+ def remove(id, **options) = api_delete("/webhook-endpoints/#{seg(id)}", options: options)
28
+
29
+ # A new secret. The old one keeps signing alongside it for the grace window.
30
+ def rotate_secret(id, **options) = api_post("/webhook-endpoints/#{seg(id)}/rotate-secret", nil, options: options)
31
+
32
+ # Recent delivery attempts. Fields: endpoint_id, final_only, limit.
33
+ def deliveries(params = nil, **kw)
34
+ query, options = split(params, kw)
35
+ api_get("/webhook-deliveries", query: query, options: options)
36
+ end
37
+
38
+ # Verify a delivery's signature and return the parsed event. Makes no
39
+ # request. Same as SendClipper.verify_webhook.
40
+ def verify(payload, headers, secret, **kw) = SendClipper.verify_webhook(payload, headers, secret, **kw)
41
+ end
42
+
43
+ class WebhookDispatches < Base
44
+ # Undelivered events: dead letters by default, or state: "retrying".
45
+ # Fields: state, endpoint_id, cursor, limit.
46
+ def list(params = nil, **kw)
47
+ query, options = split(params, kw)
48
+ api_get("/webhook-dispatches", query: query, options: options)
49
+ end
50
+
51
+ def each(params = nil, **kw, &block)
52
+ query, options = split(params, kw)
53
+ query.delete("cursor")
54
+ enum = Paging.cursor("data") { |c| api_get("/webhook-dispatches", query: query.merge("cursor" => c), options: options) }
55
+ block ? enum.each(&block) : enum
56
+ end
57
+
58
+ def get(id, **options) = api_get("/webhook-dispatches/#{seg(id)}", options: options)
59
+
60
+ # One more immediate attempt for a dead-lettered event.
61
+ def replay(id, **options) = api_post("/webhook-dispatches/#{seg(id)}/replay", nil, options: options)
62
+
63
+ # Replay every dead letter (optionally of one endpoint), up to the server's cap.
64
+ def replay_all(params = nil, **kw)
65
+ body, options = split(params, kw)
66
+ api_post("/webhook-dispatches/replay-all", body, options: options)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SendClipper
4
+ module Resources
5
+ class Workspace < Base
6
+ # The workspace this key belongs to.
7
+ def get(**options) = api_get("/workspace", options: options)
8
+ end
9
+
10
+ class ApiKeys < Base
11
+ # The workspace's keys, masked. Keys are created and revoked in the dashboard only.
12
+ def list(**options) = api_get("/api-keys", options: options)
13
+ end
14
+
15
+ class Automations < Base
16
+ # Start a Live automation with a +manual+ trigger for one contact.
17
+ def enroll(automation_id, email, **options)
18
+ api_post("/automations/#{seg(automation_id)}/enrollments", { "email" => email }, options: options)
19
+ end
20
+ end
21
+ end
22
+ end