kit-rb 0.0.0 → 0.2.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +56 -5
  4. data/Rakefile +17 -0
  5. data/lib/kit/client.rb +56 -0
  6. data/lib/kit/configuration.rb +7 -2
  7. data/lib/kit/connection.rb +37 -11
  8. data/lib/kit/errors.rb +15 -0
  9. data/lib/kit/oauth/client.rb +132 -0
  10. data/lib/kit/oauth/pkce.rb +42 -0
  11. data/lib/kit/oauth/token.rb +38 -0
  12. data/lib/kit/objects/broadcast.rb +26 -0
  13. data/lib/kit/objects/broadcast_click.rb +17 -0
  14. data/lib/kit/objects/broadcast_stats.rb +30 -0
  15. data/lib/kit/objects/creator_profile.rb +13 -0
  16. data/lib/kit/objects/custom_field.rb +14 -0
  17. data/lib/kit/objects/email_stats.rb +25 -0
  18. data/lib/kit/objects/email_template.rb +14 -0
  19. data/lib/kit/objects/form.rb +22 -0
  20. data/lib/kit/objects/growth_stats.rb +20 -0
  21. data/lib/kit/objects/post.rb +24 -0
  22. data/lib/kit/objects/purchase.rb +24 -0
  23. data/lib/kit/objects/segment.rb +12 -0
  24. data/lib/kit/objects/sequence.rb +26 -0
  25. data/lib/kit/objects/sequence_email.rb +23 -0
  26. data/lib/kit/objects/snippet.rb +21 -0
  27. data/lib/kit/objects/subscriber.rb +25 -0
  28. data/lib/kit/objects/subscriber_stats.rb +26 -0
  29. data/lib/kit/objects/tag.rb +17 -0
  30. data/lib/kit/objects/webhook.rb +15 -0
  31. data/lib/kit/objects/webhook_endpoint.rb +22 -0
  32. data/lib/kit/pagination.rb +66 -0
  33. data/lib/kit/resources/account.rb +27 -1
  34. data/lib/kit/resources/base.rb +38 -6
  35. data/lib/kit/resources/broadcasts.rb +56 -0
  36. data/lib/kit/resources/bulk.rb +63 -0
  37. data/lib/kit/resources/custom_fields.rb +29 -0
  38. data/lib/kit/resources/email_templates.rb +13 -0
  39. data/lib/kit/resources/forms.rb +31 -0
  40. data/lib/kit/resources/posts.rb +18 -0
  41. data/lib/kit/resources/purchases.rb +26 -0
  42. data/lib/kit/resources/segments.rb +13 -0
  43. data/lib/kit/resources/sequences.rb +78 -0
  44. data/lib/kit/resources/snippets.rb +29 -0
  45. data/lib/kit/resources/subscribers.rb +67 -0
  46. data/lib/kit/resources/tags.rb +52 -0
  47. data/lib/kit/resources/webhook_endpoints.rb +46 -0
  48. data/lib/kit/resources/webhooks.rb +24 -0
  49. data/lib/kit/version.rb +1 -1
  50. data/lib/kit-rb.rb +38 -0
  51. data/sig/kit-rb.rbs +429 -1
  52. metadata +43 -5
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A custom field as returned by /v4/custom_fields. `label` is what a creator
6
+ # sets; `key` is the derived attribute used on subscribers (e.g. a `label`
7
+ # of "Last name" yields a `key` of "last_name").
8
+ CustomField = Data.define(:id, :name, :key, :label) do
9
+ def self.from(hash)
10
+ new(id: hash["id"], name: hash["name"], key: hash["key"], label: hash["label"])
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # Account-wide email engagement stats (/v4/account/email_stats), covering the
6
+ # window [starting, ending].
7
+ EmailStats = Data.define(
8
+ :sent, :clicked, :opened, :email_stats_mode,
9
+ :open_tracking_enabled, :click_tracking_enabled,
10
+ :starting, :ending, :open_rate, :click_rate, :unsubscribe_rate, :bounce_rate
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ sent: hash["sent"], clicked: hash["clicked"], opened: hash["opened"],
15
+ email_stats_mode: hash["email_stats_mode"],
16
+ open_tracking_enabled: hash["open_tracking_enabled"],
17
+ click_tracking_enabled: hash["click_tracking_enabled"],
18
+ starting: hash["starting"], ending: hash["ending"],
19
+ open_rate: hash["open_rate"], click_rate: hash["click_rate"],
20
+ unsubscribe_rate: hash["unsubscribe_rate"], bounce_rate: hash["bounce_rate"]
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # An email template as returned by /v4/email_templates. `is_default` marks the
6
+ # account's default; `category` groups templates in the UI.
7
+ EmailTemplate = Data.define(:id, :name, :is_default, :category) do
8
+ def self.from(hash)
9
+ new(id: hash["id"], name: hash["name"],
10
+ is_default: hash["is_default"], category: hash["category"])
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A form as returned by /v4/forms. `type` is e.g. "embed"/"hosted"; `uid` is
6
+ # the public identifier used in embed URLs. `subscriber_count` is present
7
+ # only when the request asks to include it.
8
+ Form = Data.define(
9
+ :id, :name, :created_at, :type, :format,
10
+ :embed_js, :embed_url, :archived, :uid, :subscriber_count
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], name: hash["name"], created_at: hash["created_at"],
15
+ type: hash["type"], format: hash["format"], embed_js: hash["embed_js"],
16
+ embed_url: hash["embed_url"], archived: hash["archived"], uid: hash["uid"],
17
+ subscriber_count: hash["subscriber_count"]
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # Account subscriber-growth stats (/v4/account/growth_stats) over the window
6
+ # [starting, ending].
7
+ GrowthStats = Data.define(
8
+ :cancellations, :net_new_subscribers, :new_subscribers, :subscribers,
9
+ :starting, :ending
10
+ ) do
11
+ def self.from(hash)
12
+ new(
13
+ cancellations: hash["cancellations"], net_new_subscribers: hash["net_new_subscribers"],
14
+ new_subscribers: hash["new_subscribers"], subscribers: hash["subscribers"],
15
+ starting: hash["starting"], ending: hash["ending"]
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A post (a broadcast published to the web) as returned by /v4/posts.
6
+ # `publication_id` ties it back to the broadcast it was published from.
7
+ Post = Data.define(
8
+ :id, :publication_id, :created_at, :title, :slug, :description,
9
+ :meta_description, :status, :published_at, :sent_at,
10
+ :thumbnail_alt, :thumbnail_url, :is_paid, :public_url
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], publication_id: hash["publication_id"], created_at: hash["created_at"],
15
+ title: hash["title"], slug: hash["slug"], description: hash["description"],
16
+ meta_description: hash["meta_description"], status: hash["status"],
17
+ published_at: hash["published_at"], sent_at: hash["sent_at"],
18
+ thumbnail_alt: hash["thumbnail_alt"], thumbnail_url: hash["thumbnail_url"],
19
+ is_paid: hash["is_paid"], public_url: hash["public_url"]
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A purchase as returned by /v4/purchases. Monetary fields are numbers in the
6
+ # purchase `currency`; `products` is the raw line-item array (each with
7
+ # name/pid/lid/sku/unit_price/quantity).
8
+ Purchase = Data.define(
9
+ :id, :transaction_id, :subscriber_id, :status, :email_address, :currency,
10
+ :transaction_time, :subtotal, :discount, :tax, :total, :products, :source
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], transaction_id: hash["transaction_id"],
15
+ subscriber_id: hash["subscriber_id"], status: hash["status"],
16
+ email_address: hash["email_address"], currency: hash["currency"],
17
+ transaction_time: hash["transaction_time"], subtotal: hash["subtotal"],
18
+ discount: hash["discount"], tax: hash["tax"], total: hash["total"],
19
+ products: hash["products"], source: hash["source"]
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A segment (a saved subscriber filter) as returned by /v4/segments.
6
+ Segment = Data.define(:id, :name, :created_at) do
7
+ def self.from(hash)
8
+ new(id: hash["id"], name: hash["name"], created_at: hash["created_at"])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A sequence (formerly "course") as returned by /v4/sequences. Only id, name,
6
+ # hold, repeat, and created_at are always present; the scheduling fields and
7
+ # counts appear on full reads, and `stats` only when the request includes it.
8
+ Sequence = Data.define(
9
+ :id, :name, :hold, :repeat, :created_at, :updated_at,
10
+ :email_address, :email_template_id, :send_days, :send_hour, :time_zone,
11
+ :active, :exclude_subscriber_sources, :email_count, :subscriber_count, :stats
12
+ ) do
13
+ def self.from(hash)
14
+ new(
15
+ id: hash["id"], name: hash["name"], hold: hash["hold"], repeat: hash["repeat"],
16
+ created_at: hash["created_at"], updated_at: hash["updated_at"],
17
+ email_address: hash["email_address"], email_template_id: hash["email_template_id"],
18
+ send_days: hash["send_days"], send_hour: hash["send_hour"], time_zone: hash["time_zone"],
19
+ active: hash["active"], exclude_subscriber_sources: hash["exclude_subscriber_sources"],
20
+ email_count: hash["email_count"], subscriber_count: hash["subscriber_count"],
21
+ stats: hash["stats"]
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # An email within a sequence, as returned by
6
+ # /v4/sequences/:sequence_id/emails. `position` is its order in the sequence;
7
+ # `delay_value`/`delay_unit` set how long after the previous step it sends.
8
+ SequenceEmail = Data.define(
9
+ :id, :sequence_id, :subject, :preview_text, :email_address, :email_template_id,
10
+ :published, :position, :delay_value, :delay_unit, :send_days, :stats
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], sequence_id: hash["sequence_id"], subject: hash["subject"],
15
+ preview_text: hash["preview_text"], email_address: hash["email_address"],
16
+ email_template_id: hash["email_template_id"], published: hash["published"],
17
+ position: hash["position"], delay_value: hash["delay_value"],
18
+ delay_unit: hash["delay_unit"], send_days: hash["send_days"], stats: hash["stats"]
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A snippet (reusable content) as returned by /v4/snippets. `snippet_type` is
6
+ # "inline" or "document"; `content` holds an inline snippet's body and
7
+ # `document` a document snippet's; `key` is the reference used in emails.
8
+ Snippet = Data.define(
9
+ :id, :name, :snippet_type, :archived, :key,
10
+ :created_at, :updated_at, :content, :document
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], name: hash["name"], snippet_type: hash["snippet_type"],
15
+ archived: hash["archived"], key: hash["key"], created_at: hash["created_at"],
16
+ updated_at: hash["updated_at"], content: hash["content"], document: hash["document"]
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A subscriber as returned by /v4/subscribers. `fields` holds custom-field
6
+ # values; `location` is present on some responses; both are plain Hashes.
7
+ Subscriber = Data.define(
8
+ :id, :first_name, :email_address, :state,
9
+ :created_at, :canceled_at, :location, :fields
10
+ ) do
11
+ def self.from(hash)
12
+ new(
13
+ id: hash["id"],
14
+ first_name: hash["first_name"],
15
+ email_address: hash["email_address"],
16
+ state: hash["state"],
17
+ created_at: hash["created_at"],
18
+ canceled_at: hash["canceled_at"],
19
+ location: hash["location"],
20
+ fields: hash["fields"]
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # Engagement stats for a subscriber (/v4/subscribers/:id/stats). The API
6
+ # nests the metrics under "stats" next to the id; this flattens them.
7
+ SubscriberStats = Data.define(
8
+ :id, :sent, :opened, :clicked, :bounced, :open_rate, :click_rate,
9
+ :last_sent, :last_opened, :last_clicked,
10
+ :sends_since_last_open, :sends_since_last_click
11
+ ) do
12
+ def self.from(hash)
13
+ stats = hash["stats"] || {}
14
+ new(
15
+ id: hash["id"], sent: stats["sent"], opened: stats["opened"],
16
+ clicked: stats["clicked"], bounced: stats["bounced"],
17
+ open_rate: stats["open_rate"], click_rate: stats["click_rate"],
18
+ last_sent: stats["last_sent"], last_opened: stats["last_opened"],
19
+ last_clicked: stats["last_clicked"],
20
+ sends_since_last_open: stats["sends_since_last_open"],
21
+ sends_since_last_click: stats["sends_since_last_click"]
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A tag as returned by /v4/tags. Fields are all non-nil.
6
+ Tag = Data.define(:id, :name, :created_at, :subscriber_count) do
7
+ def self.from(hash)
8
+ new(
9
+ id: hash["id"],
10
+ name: hash["name"],
11
+ created_at: hash["created_at"],
12
+ subscriber_count: hash["subscriber_count"]
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A webhook (an automation rule that POSTs to target_url) as returned by
6
+ # /v4/webhooks. `event` is the raw trigger hash ({ "name" => ..., plus the
7
+ # optional tag_id/form_id/etc. that scope it }).
8
+ Webhook = Data.define(:id, :account_id, :event, :target_url) do
9
+ def self.from(hash)
10
+ new(id: hash["id"], account_id: hash["account_id"],
11
+ event: hash["event"], target_url: hash["target_url"])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Objects
5
+ # A webhook endpoint as returned by /v4/webhook_endpoints — the newer signed-
6
+ # delivery model, with a secret that can be rotated. `events` is the list of
7
+ # subscribed event names; `previous_secret_expires_at` is set after a rotate.
8
+ WebhookEndpoint = Data.define(
9
+ :id, :name, :url, :events, :status, :source, :description,
10
+ :created_by_app, :created_at, :previous_secret_expires_at
11
+ ) do
12
+ def self.from(hash)
13
+ new(
14
+ id: hash["id"], name: hash["name"], url: hash["url"], events: hash["events"],
15
+ status: hash["status"], source: hash["source"], description: hash["description"],
16
+ created_by_app: hash["created_by_app"], created_at: hash["created_at"],
17
+ previous_secret_expires_at: hash["previous_secret_expires_at"]
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ # The `pagination` object Kit returns alongside every list. Kit uses cursor
5
+ # pagination: to walk forward, pass `after: end_cursor`; backward, `before:
6
+ # start_cursor`.
7
+ Pagination = Data.define(
8
+ :has_previous_page, :has_next_page, :start_cursor, :end_cursor, :per_page
9
+ ) do
10
+ def self.from(hash)
11
+ new(
12
+ has_previous_page: hash["has_previous_page"],
13
+ has_next_page: hash["has_next_page"],
14
+ start_cursor: hash["start_cursor"],
15
+ end_cursor: hash["end_cursor"],
16
+ per_page: hash["per_page"]
17
+ )
18
+ end
19
+ end
20
+
21
+ # One page of results plus the cursor to fetch the next. `Collection` is
22
+ # Enumerable over the *current* page; `auto_paging_each` lazily walks every
23
+ # remaining page by following `end_cursor`, so callers never touch cursors:
24
+ #
25
+ # client.subscribers.list.auto_paging_each { |s| puts s.email_address }
26
+ # client.subscribers.list.auto_paging_each.lazy.first(500)
27
+ #
28
+ # The block given to `.new` fetches the next Collection from an `after` cursor.
29
+ class Collection
30
+ include Enumerable
31
+
32
+ attr_reader :data, :pagination
33
+
34
+ def initialize(data:, pagination:, &fetch_after)
35
+ @data = data
36
+ @pagination = pagination
37
+ @fetch_after = fetch_after
38
+ end
39
+
40
+ # Iterates the current page only.
41
+ def each(&)
42
+ @data.each(&)
43
+ end
44
+
45
+ # The next Collection, or nil when there is no next page.
46
+ def next_page
47
+ return nil unless @pagination.has_next_page
48
+
49
+ @fetch_after.call(@pagination.end_cursor)
50
+ end
51
+
52
+ # Iterates every item across every remaining page, fetching lazily. Returns
53
+ # an Enumerator when no block is given (so `.lazy` composes).
54
+ def auto_paging_each(&block)
55
+ return enum_for(:auto_paging_each) unless block
56
+
57
+ page = self
58
+ loop do
59
+ page.data.each(&block)
60
+ break unless page.pagination.has_next_page
61
+
62
+ page = page.next_page
63
+ end
64
+ end
65
+ end
66
+ end
@@ -9,7 +9,33 @@ module Kit
9
9
  #
10
10
  # @return [Kit::Objects::AccountInfo]
11
11
  def get
12
- Objects::AccountInfo.from(super("/v4/account"))
12
+ Objects::AccountInfo.from(http_get("/v4/account"))
13
+ end
14
+
15
+ # GET /v4/account/colors — the account's brand color palette (hex strings).
16
+ def colors
17
+ http_get("/v4/account/colors").fetch("colors")
18
+ end
19
+
20
+ # PUT /v4/account/colors — replace the palette; returns the saved colors.
21
+ def update_colors(colors)
22
+ http_put("/v4/account/colors", body: { colors: colors }).fetch("colors")
23
+ end
24
+
25
+ # GET /v4/account/creator_profile
26
+ def creator_profile
27
+ one(:get, "/v4/account/creator_profile", "profile", Objects::CreatorProfile)
28
+ end
29
+
30
+ # GET /v4/account/email_stats — account-wide email engagement stats.
31
+ def email_stats
32
+ one(:get, "/v4/account/email_stats", "stats", Objects::EmailStats)
33
+ end
34
+
35
+ # GET /v4/account/growth_stats — subscriber-growth stats; accepts
36
+ # starting/ending to bound the window.
37
+ def growth_stats(**params)
38
+ one(:get, "/v4/account/growth_stats", "stats", Objects::GrowthStats, params: params)
13
39
  end
14
40
  end
15
41
  end
@@ -3,7 +3,8 @@
3
3
  module Kit
4
4
  module Resources
5
5
  # Shared base for every resource group. Holds the connection and exposes
6
- # thin verb helpers so resource classes read as `get("/v4/account")`.
6
+ # verb helpers under `http_*` names so a resource can define public methods
7
+ # like `get(id)` or `list` without colliding with the transport helpers.
7
8
  class Base
8
9
  def initialize(connection)
9
10
  @connection = connection
@@ -11,20 +12,51 @@ module Kit
11
12
 
12
13
  private
13
14
 
14
- def get(path, params: {})
15
+ # Non-enveloped read (whole body) and list reads go through http_get;
16
+ # deletes return no object and go through http_delete. Enveloped
17
+ # single-object and list responses are built by `one` and `collection`.
18
+ def http_get(path, params: {})
15
19
  @connection.request(:get, path, params: params)
16
20
  end
17
21
 
18
- def post(path, body: nil, params: {})
22
+ # Raw POST/PUT for payloads that are not a single wrapped object — the
23
+ # account colors array, and the bulk endpoints' composite
24
+ # { <resource>, failures } result. Enveloped objects go through `one`.
25
+ def http_post(path, body: nil, params: {})
19
26
  @connection.request(:post, path, params: params, body: body)
20
27
  end
21
28
 
22
- def put(path, body: nil, params: {})
29
+ def http_put(path, body: nil, params: {})
23
30
  @connection.request(:put, path, params: params, body: body)
24
31
  end
25
32
 
26
- def delete(path, params: {})
27
- @connection.request(:delete, path, params: params)
33
+ # Bulk deletes carry a body (the items to remove), so body is accepted.
34
+ def http_delete(path, body: nil, params: {})
35
+ @connection.request(:delete, path, params: params, body: body)
36
+ end
37
+
38
+ # Sends one request that returns a single wrapped object and builds it.
39
+ # `key` is the envelope key (e.g. "subscriber"), `klass` the object built
40
+ # via `klass.from`. Centralised so a resource never hand-writes the read/
41
+ # build/return plumbing — it declares only verb, path, key, class, body.
42
+ def one(verb, path, key, klass, body: nil, params: {})
43
+ response = @connection.request(verb, path, params: params, body: body)
44
+ klass.from(response.fetch(key))
45
+ end
46
+
47
+ # Fetches a cursor-paginated list and wraps it in a Collection whose next
48
+ # page follows end_cursor. `key` is the array key in the envelope (e.g.
49
+ # "subscribers"), `klass` the object built from each element. Centralised
50
+ # here so every list resource paginates identically and correctly.
51
+ #
52
+ # `verb`/`body` default to a GET with no body; the POST-based filter
53
+ # endpoints pass verb: :post with a filter body, still paging by cursor.
54
+ def collection(path, key, klass, params, verb: :get, body: nil)
55
+ response = @connection.request(verb, path, params: params, body: body)
56
+ data = response.fetch(key).map { |element| klass.from(element) }
57
+ Collection.new(data: data, pagination: Pagination.from(response.fetch("pagination"))) do |after|
58
+ collection(path, key, klass, params.merge(after: after), verb: verb, body: body)
59
+ end
28
60
  end
29
61
  end
30
62
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Resources
5
+ # The /v4/broadcasts endpoints — one-off emails, plus their stats and link
6
+ # click reports.
7
+ class Broadcasts < Base
8
+ # GET /v4/broadcasts
9
+ def list(**params)
10
+ collection("/v4/broadcasts", "broadcasts", Objects::Broadcast, params)
11
+ end
12
+
13
+ # GET /v4/broadcasts/:id
14
+ def get(id)
15
+ one(:get, "/v4/broadcasts/#{id}", "broadcast", Objects::Broadcast)
16
+ end
17
+
18
+ # POST /v4/broadcasts
19
+ def create(**attributes)
20
+ one(:post, "/v4/broadcasts", "broadcast", Objects::Broadcast, body: attributes)
21
+ end
22
+
23
+ # PUT /v4/broadcasts/:id
24
+ def update(id, **attributes)
25
+ one(:put, "/v4/broadcasts/#{id}", "broadcast", Objects::Broadcast, body: attributes)
26
+ end
27
+
28
+ # DELETE /v4/broadcasts/:id
29
+ def delete(id)
30
+ http_delete("/v4/broadcasts/#{id}")
31
+ nil
32
+ end
33
+
34
+ # GET /v4/broadcasts/stats — a cursor-paginated list of per-broadcast stats.
35
+ def stats_list(**params)
36
+ collection("/v4/broadcasts/stats", "broadcasts", Objects::BroadcastStats, params)
37
+ end
38
+
39
+ # GET /v4/broadcasts/:id/stats — one broadcast's performance stats.
40
+ def stats(id)
41
+ one(:get, "/v4/broadcasts/#{id}/stats", "broadcast", Objects::BroadcastStats)
42
+ end
43
+
44
+ # GET /v4/broadcasts/:id/clicks — a cursor-paginated Collection of the
45
+ # broadcast's clicked links. The API nests the array under "broadcast", so
46
+ # this is built directly rather than through Base#collection.
47
+ def clicks(id, **params)
48
+ body = http_get("/v4/broadcasts/#{id}/clicks", params: params)
49
+ rows = body.fetch("broadcast").fetch("clicks").map { |row| Objects::BroadcastClick.from(row) }
50
+ Collection.new(data: rows, pagination: Pagination.from(body.fetch("pagination"))) do |after|
51
+ clicks(id, **params, after: after)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kit
4
+ module Resources
5
+ # The /v4/bulk endpoints — batch operations that all require OAuth. Each call
6
+ # takes an array of items and an optional callback_url, and returns the raw
7
+ # result hash: the created/affected items under their resource key plus a
8
+ # "failures" array. Large batches are processed asynchronously and reported
9
+ # to callback_url; the response still carries whatever completed inline.
10
+ class Bulk < Base
11
+ # POST /v4/bulk/subscribers
12
+ def create_subscribers(subscribers, callback_url: nil)
13
+ post("/v4/bulk/subscribers", subscribers: subscribers, callback_url: callback_url)
14
+ end
15
+
16
+ # POST /v4/bulk/custom_fields
17
+ def create_custom_fields(custom_fields, callback_url: nil)
18
+ post("/v4/bulk/custom_fields", custom_fields: custom_fields, callback_url: callback_url)
19
+ end
20
+
21
+ # POST /v4/bulk/custom_fields/subscribers
22
+ def update_custom_field_values(custom_field_values, callback_url: nil)
23
+ post("/v4/bulk/custom_fields/subscribers",
24
+ custom_field_values: custom_field_values, callback_url: callback_url)
25
+ end
26
+
27
+ # POST /v4/bulk/forms/subscribers
28
+ def add_subscribers_to_forms(additions, callback_url: nil)
29
+ post("/v4/bulk/forms/subscribers", additions: additions, callback_url: callback_url)
30
+ end
31
+
32
+ # POST /v4/bulk/tags
33
+ def create_tags(tags, callback_url: nil)
34
+ post("/v4/bulk/tags", tags: tags, callback_url: callback_url)
35
+ end
36
+
37
+ # DELETE /v4/bulk/tags
38
+ def delete_tags(tags, callback_url: nil)
39
+ delete("/v4/bulk/tags", tags: tags, callback_url: callback_url)
40
+ end
41
+
42
+ # POST /v4/bulk/tags/subscribers
43
+ def tag_subscribers(taggings, callback_url: nil)
44
+ post("/v4/bulk/tags/subscribers", taggings: taggings, callback_url: callback_url)
45
+ end
46
+
47
+ # DELETE /v4/bulk/tags/subscribers
48
+ def remove_tag_subscribers(taggings, callback_url: nil)
49
+ delete("/v4/bulk/tags/subscribers", taggings: taggings, callback_url: callback_url)
50
+ end
51
+
52
+ private
53
+
54
+ def post(path, **body)
55
+ http_post(path, body: body.compact)
56
+ end
57
+
58
+ def delete(path, **body)
59
+ http_delete(path, body: body.compact)
60
+ end
61
+ end
62
+ end
63
+ end