broadcast-ruby 0.1.4 → 0.3.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,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadcast
4
+ module Resources
5
+ class EmailServers < Base
6
+ # Fields the API returns redacted (bullet-masked). When updating, never
7
+ # round-trip these values back from a fetch — the gem strips them out
8
+ # of the payload (with a logger warning) to prevent corrupting credentials.
9
+ REDACTED_FIELDS = %i[
10
+ smtp_password
11
+ aws_access_key_id
12
+ aws_secret_access_key
13
+ outbound_aws_access_key_id
14
+ outbound_aws_secret_access_key
15
+ postmark_api_token
16
+ inboxroad_api_token
17
+ smtp_com_api_key
18
+ ].freeze
19
+
20
+ # Matches the API's redaction shape: 8 bullets, OR 4-char prefix + bullets + 4-char suffix.
21
+ REDACTED_PATTERN = /\A(?:•{8}|.{0,4}•+.{0,4})\z/
22
+
23
+ def list(limit: nil, offset: nil)
24
+ params = {}
25
+ params[:limit] = limit unless limit.nil?
26
+ params[:offset] = offset unless offset.nil?
27
+ get('/api/v1/email_servers', params)
28
+ end
29
+
30
+ def get_email_server(id)
31
+ get("/api/v1/email_servers/#{id}")
32
+ end
33
+
34
+ def create(**attrs)
35
+ post('/api/v1/email_servers', { email_server: attrs })
36
+ end
37
+
38
+ # Update an email server. Attrs are wrapped under `email_server:` on the wire.
39
+ #
40
+ # CAUTION: API responses redact credential fields (e.g. `smtp_password`)
41
+ # with bullet characters. Never echo a fetched response back into update —
42
+ # this method scrubs values that match the redaction pattern, but you
43
+ # should pass only the fields you actually want to change.
44
+ def update(id, **attrs)
45
+ scrubbed = scrub_redacted(attrs)
46
+ patch("/api/v1/email_servers/#{id}", { email_server: scrubbed })
47
+ end
48
+
49
+ def delete(id)
50
+ @client.request(:delete, "/api/v1/email_servers/#{id}")
51
+ end
52
+
53
+ def test_connection(id)
54
+ post("/api/v1/email_servers/#{id}/test_connection")
55
+ end
56
+
57
+ # Copy an email server to another channel. Requires an admin/system token.
58
+ # In SaaS mode, target_channel_id is scoped to the token creator's account.
59
+ def copy_to_channel(id, target_channel_id:)
60
+ post("/api/v1/email_servers/#{id}/copy_to_channel", { target_channel_id: target_channel_id })
61
+ end
62
+
63
+ private
64
+
65
+ def scrub_redacted(attrs)
66
+ scrubbed = {}
67
+ attrs.each do |key, value|
68
+ if REDACTED_FIELDS.include?(key.to_sym) && value.is_a?(String) && value.match?(REDACTED_PATTERN)
69
+ warn_redacted(key)
70
+ next
71
+ end
72
+ scrubbed[key] = value
73
+ end
74
+ scrubbed
75
+ end
76
+
77
+ def warn_redacted(field)
78
+ msg = "[broadcast-ruby] Dropped redacted #{field} from update payload — " \
79
+ 'pass the real credential or omit the field'
80
+ if @client.config.logger
81
+ @client.config.logger.warn(msg)
82
+ else
83
+ Kernel.warn(msg)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadcast
4
+ module Resources
5
+ # Read-only export endpoints under /api/migration/v1 — the surface behind
6
+ # backups and instance-to-instance migration.
7
+ #
8
+ # Two things differ from the v1 API:
9
+ #
10
+ # 1. **Admin tokens only.** Channel-scoped tokens are rejected outright;
11
+ # the controller looks up AdminApiToken and nothing else.
12
+ # 2. **broadcast_channel_id is required on every call.** Set it once via
13
+ # `Client.new(broadcast_channel_id:)` or `client.with_channel(id)` and
14
+ # the gem attaches it automatically; otherwise pass it per call.
15
+ #
16
+ # Every list endpoint pages with `limit:` (1..250, default 250) and
17
+ # `offset:`, and returns `{"data" => [...], "pagination" => {...}}`.
18
+ class Migration < Base
19
+ # Endpoints that are a plain paginated list of the channel's records.
20
+ COLLECTIONS = %i[
21
+ channels subscribers templates segments sequences email_servers
22
+ opt_in_forms broadcasts outbound_receipts webhook_endpoints tokens
23
+ suppressions tags users link_redirects link_clicks subscriber_histories
24
+ file_assets
25
+ ].freeze
26
+
27
+ COLLECTIONS.each do |collection|
28
+ define_method(collection) do |**params|
29
+ get("/api/migration/v1/#{collection}", params)
30
+ end
31
+ end
32
+
33
+ # Export summary: format version, channel identity, per-resource counts,
34
+ # and recent-history totals. Call this first to size an export.
35
+ #
36
+ # @param days_history [Integer] window for the time-bounded counts
37
+ # (broadcasts, receipts, histories). Server clamps to 1..365, default 90.
38
+ def manifest(**params)
39
+ get('/api/migration/v1/manifest', params)
40
+ end
41
+
42
+ # Binary contents of a stored file asset. Returns a String of bytes, not
43
+ # JSON — write it straight to disk.
44
+ def download_file_asset(id, **params)
45
+ @client.request(:get, "/api/migration/v1/file_assets/#{id}/download", params, raw: true)
46
+ end
47
+
48
+ # Pages through a collection, yielding each record.
49
+ #
50
+ # client.migration.each_record(:subscribers) { |sub| csv << sub }
51
+ #
52
+ # Stops when the server reports `has_more: false`, so it stays correct if
53
+ # the page size is clamped server-side.
54
+ def each_record(collection, limit: 250, **params, &block)
55
+ return enum_for(:each_record, collection, limit: limit, **params) unless block_given?
56
+
57
+ offset = 0
58
+ loop do
59
+ page = public_send(collection, limit: limit, offset: offset, **params)
60
+ records = Array(page['data'])
61
+ records.each(&block)
62
+
63
+ pagination = page['pagination'] || {}
64
+ break unless pagination['has_more']
65
+
66
+ # Advance by what the server actually returned — it clamps `limit`.
67
+ advanced = pagination['limit'] || records.size
68
+ break if advanced.to_i.zero?
69
+
70
+ offset += advanced.to_i
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Broadcast
6
+ module Resources
7
+ class OptInForms < Base
8
+ # List opt-in forms.
9
+ #
10
+ # NOTE: returns up to 250 results per page along with `pagination`
11
+ # metadata. Variants are excluded (only main forms are returned).
12
+ # Pass `page:` to advance.
13
+ #
14
+ # Optional filters: filter: (label substring), widget_type:, enabled: 'true'
15
+ def list(**params)
16
+ get('/api/v1/opt_in_forms', params)
17
+ end
18
+
19
+ def get_opt_in_form(id)
20
+ get("/api/v1/opt_in_forms/#{id}")
21
+ end
22
+
23
+ # Create an opt-in form. Attrs are wrapped under `opt_in_form:` on the wire.
24
+ # Nested settings hashes (theme_settings, automation_settings, security_settings,
25
+ # trigger_settings, widget_settings) and arrays (opt_in_form_blocks_attributes,
26
+ # opt_in_post_submission_blocks_attributes) are passed through verbatim.
27
+ #
28
+ # Core:
29
+ # label:, form_type:, widget_type:, enabled:, allowed_embedding_domains:
30
+ #
31
+ # Confirmation / welcome flow:
32
+ # confirmation_email_template_id: Template sent to confirm the address
33
+ # welcome_email_template_id: Template sent after confirmation
34
+ # confirmation_redirect_url: where to send the subscriber post-confirm
35
+ # include_unsubscribe_link_in_confirmation: boolean
36
+ #
37
+ # Custom fields declared in opt_in_form_blocks_attributes support a
38
+ # multi-line variant, which renders as a textarea on the hosted form.
39
+ def create(**attrs)
40
+ post('/api/v1/opt_in_forms', { opt_in_form: attrs })
41
+ end
42
+
43
+ def update(id, **attrs)
44
+ patch("/api/v1/opt_in_forms/#{id}", { opt_in_form: attrs })
45
+ end
46
+
47
+ def delete(id)
48
+ @client.request(:delete, "/api/v1/opt_in_forms/#{id}")
49
+ end
50
+
51
+ # Performance analytics for the form. start_date/end_date accept Date,
52
+ # Time, or ISO-8601 strings (default: last 30 days).
53
+ def analytics(id, start_date: nil, end_date: nil)
54
+ params = {}
55
+ params[:start_date] = coerce_date(start_date) if start_date
56
+ params[:end_date] = coerce_date(end_date) if end_date
57
+ get("/api/v1/opt_in_forms/#{id}/analytics", params)
58
+ end
59
+
60
+ def create_variant(id, name: nil, weight: nil)
61
+ body = {}
62
+ body[:name] = name unless name.nil?
63
+ body[:weight] = weight unless weight.nil?
64
+ post("/api/v1/opt_in_forms/#{id}/variants", body)
65
+ end
66
+
67
+ def duplicate(id, label: nil)
68
+ body = {}
69
+ body[:label] = label unless label.nil?
70
+ post("/api/v1/opt_in_forms/#{id}/duplicate", body)
71
+ end
72
+
73
+ private
74
+
75
+ def coerce_date(value)
76
+ case value
77
+ when Date, Time, DateTime then value.iso8601
78
+ else value.to_s
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -3,6 +3,22 @@
3
3
  module Broadcast
4
4
  module Resources
5
5
  class Subscribers < Base
6
+ # List subscribers (250 per page, with `pagination` metadata; pass `page:`).
7
+ #
8
+ # Filters, all optional and combinable:
9
+ # is_active: true | false
10
+ # source: exact match on the source string
11
+ # created_after: ISO-8601 timestamp
12
+ # created_before: ISO-8601 timestamp
13
+ # tags: array — AND logic, subscriber must have all of them
14
+ # email: partial (case-insensitive) match, not exact
15
+ # confirmation_status: 'confirmed' | 'unconfirmed'
16
+ # custom_data: hash — JSONB containment, e.g. { plan: 'pro' }
17
+ #
18
+ # An unparseable created_after/created_before is *ignored* by the server
19
+ # rather than rejected; it comes back as a `parameter_ignored` warning on
20
+ # the response, so a bad timestamp silently widens the result set unless
21
+ # you check `result.warnings`.
6
22
  def list(**params)
7
23
  get('/api/v1/subscribers.json', params)
8
24
  end
@@ -11,8 +27,34 @@ module Broadcast
11
27
  get('/api/v1/subscribers/find.json', { email: email })
12
28
  end
13
29
 
30
+ # Create or upsert a subscriber.
31
+ #
32
+ # Subscriber attributes (wrapped under `subscriber:` on the wire):
33
+ # email:, first_name:, last_name:, is_active:, source:,
34
+ # subscribed_at:, ip_address:, tags: [...], custom_data: {...}
35
+ #
36
+ # Top-level options (NOT wrapped under `subscriber:`):
37
+ # double_opt_in: true | { reply_to:, confirmation_template_id:, include_unsubscribe_link: }
38
+ # When set, the subscriber is created in unconfirmed state
39
+ # and a confirmation email is queued.
40
+ # confirmation_template_id: custom confirmation template (used with double_opt_in: true)
41
+ #
42
+ # Admin tokens only:
43
+ # confirmed_at: backdate the confirmation timestamp on create.
44
+ # Intended for migrating an already-confirmed list off
45
+ # another provider. Ignored (with a warning) on update,
46
+ # and ignored entirely for channel-scoped tokens.
47
+ #
48
+ # Note `unsubscribed_at` is never settable here — use `unsubscribe(email)`.
14
49
  def create(**attrs)
15
- post('/api/v1/subscribers.json', { subscriber: attrs })
50
+ double_opt_in = attrs.delete(:double_opt_in)
51
+ confirmation_template_id = attrs.delete(:confirmation_template_id)
52
+
53
+ payload = { subscriber: attrs }
54
+ payload[:double_opt_in] = double_opt_in unless double_opt_in.nil?
55
+ payload[:confirmation_template_id] = confirmation_template_id unless confirmation_template_id.nil?
56
+
57
+ post('/api/v1/subscribers.json', payload)
16
58
  end
17
59
 
18
60
  def update(email, **attrs)
@@ -11,6 +11,23 @@ module Broadcast
11
11
  get("/api/v1/templates/#{id}")
12
12
  end
13
13
 
14
+ # Create a template. Attrs are wrapped under `template:` on the wire.
15
+ #
16
+ # Content:
17
+ # label:, subject:, preheader:, body:, html_body:
18
+ #
19
+ # Confirmation templates (double opt-in):
20
+ # template_purpose: marks the template's role, e.g. 'confirmation'
21
+ # confirmation_text: copy shown in the confirmation email
22
+ # default_confirmation: make this the channel's default confirmation template
23
+ # confirmation_page_settings: per-state page copy, keyed by state, each
24
+ # taking { heading:, body: } — e.g.
25
+ # { 'confirmed' => { heading: 'You're in',
26
+ # body: 'Thanks for confirming.' },
27
+ # 'expired' => { heading: 'Link expired', body: '...' } }
28
+ #
29
+ # Anything the server doesn't recognize comes back as an
30
+ # `unrecognized_parameter` warning on the response rather than an error.
14
31
  def create(**attrs)
15
32
  post('/api/v1/templates', { template: attrs })
16
33
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadcast
4
+ module Resources
5
+ class Transactionals < Base
6
+ MAX_IDEMPOTENCY_KEY_LENGTH = 255
7
+
8
+ # Send a transactional email.
9
+ #
10
+ # Required:
11
+ # to: recipient email address
12
+ #
13
+ # One of subject/body or template_id is required (template_id resolves
14
+ # subject and body server-side; subject/body override the template).
15
+ #
16
+ # Optional:
17
+ # subject:, body:, preheader:
18
+ # reply_to:
19
+ # template_id: resolve subject/body/preheader from a Template
20
+ # include_unsubscribe_link: boolean
21
+ # double_opt_in: true | { reply_to:, confirmation_template_id:, include_unsubscribe_link: }
22
+ # Holds the email until the recipient confirms.
23
+ # confirmation_template_id: custom confirmation template (used with double_opt_in: true)
24
+ # subscriber: { first_name:, last_name: } — populates Subscriber on first send
25
+ # idempotency_key: see below
26
+ #
27
+ # Idempotency
28
+ # -----------
29
+ # Pass `idempotency_key:` to make a retry safe. The server stores the
30
+ # response for 24 hours keyed on (token, key) and replays it rather than
31
+ # sending a second email. Check `result.idempotent_replay?` to tell a
32
+ # replay from a fresh send.
33
+ #
34
+ # client.transactionals.create(to: 'a@b.com', subject: 'Receipt',
35
+ # body: '<p>Thanks</p>',
36
+ # idempotency_key: "receipt-#{order.id}")
37
+ #
38
+ # The key is part of a fingerprint over method + full path + body:
39
+ # - same key, same payload, still running -> Broadcast::ConflictError (409)
40
+ # - same key, *different* payload -> Broadcast::ValidationError (422)
41
+ # That 422 means "this key was already used for something else", not that
42
+ # the email was invalid — don't retry it with the same key.
43
+ # rubocop:disable Metrics/ParameterLists -- mirrors the API's flat param surface
44
+ def create(to:, subject: nil, body: nil, reply_to: nil, preheader: nil,
45
+ template_id: nil, include_unsubscribe_link: nil,
46
+ double_opt_in: nil, confirmation_template_id: nil,
47
+ subscriber: nil, idempotency_key: nil, **extra)
48
+ # rubocop:enable Metrics/ParameterLists
49
+ payload = { to: to }
50
+ payload[:subject] = subject unless subject.nil?
51
+ payload[:body] = body unless body.nil?
52
+ payload[:preheader] = preheader unless preheader.nil?
53
+ payload[:reply_to] = reply_to unless reply_to.nil?
54
+ payload[:template_id] = template_id unless template_id.nil?
55
+ payload[:include_unsubscribe_link] = include_unsubscribe_link unless include_unsubscribe_link.nil?
56
+ payload[:double_opt_in] = double_opt_in unless double_opt_in.nil?
57
+ payload[:confirmation_template_id] = confirmation_template_id unless confirmation_template_id.nil?
58
+ payload[:subscriber] = subscriber unless subscriber.nil?
59
+ payload.merge!(extra) unless extra.empty?
60
+
61
+ @client.request(:post, '/api/v1/transactionals.json', payload,
62
+ headers: idempotency_headers(idempotency_key))
63
+ end
64
+
65
+ def get_transactional(id)
66
+ get("/api/v1/transactionals/#{id}.json")
67
+ end
68
+
69
+ private
70
+
71
+ def idempotency_headers(key)
72
+ return {} if key.nil?
73
+
74
+ key = key.to_s.strip
75
+ return {} if key.empty?
76
+
77
+ if key.length > MAX_IDEMPOTENCY_KEY_LENGTH
78
+ raise ArgumentError,
79
+ "idempotency_key must be #{MAX_IDEMPOTENCY_KEY_LENGTH} characters or fewer (got #{key.length})"
80
+ end
81
+
82
+ { 'Idempotency-Key' => key }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Broadcast
6
+ # A single entry from the API's `warnings` array (docs: api-response-warnings).
7
+ # The API raises these on successful 2xx responses when it accepted the request
8
+ # but ignored part of it — an unrecognized parameter, a parameter that only
9
+ # applies in another mode, a value the server overrode.
10
+ #
11
+ # `param` is a dot-path to the offending parameter (e.g. "subscriber.foo").
12
+ # The API never includes submitted values, so a warning is safe to log.
13
+ Warning = Struct.new(:code, :param, :message) do
14
+ def to_s
15
+ param ? "[#{code}] #{param}: #{message}" : "[#{code}] #{message}"
16
+ end
17
+ end
18
+
19
+ # Parsed X-RateLimit-* response headers. `reset` is the time the current
20
+ # window rolls over, not a duration.
21
+ RateLimit = Struct.new(:limit, :remaining, :reset)
22
+
23
+ # The value returned by every JSON API call.
24
+ #
25
+ # Response subclasses Hash rather than wrapping it, so everything that worked
26
+ # against the raw parsed body still works — `result['id']`, `result.is_a?(Hash)`,
27
+ # `result.dig(...)`, equality against a plain Hash. The response metadata the
28
+ # API sends alongside the body is exposed as extra readers.
29
+ #
30
+ # result = client.subscribers.create(email: 'user@example.com', foo: 'bar')
31
+ # result['id'] # => 42 (unchanged from v0.2)
32
+ # result.warnings # => [#<Warning code="unrecognized_parameter" ...>]
33
+ # result.rate_limit.remaining # => 118
34
+ # result.status # => 201
35
+ #
36
+ # Non-Hash JSON bodies (a bare array) are returned as-is and carry no metadata.
37
+ class Response < Hash
38
+ attr_reader :status, :headers
39
+
40
+ class << self
41
+ # Wraps a parsed JSON body when it is a Hash; passes anything else through.
42
+ def build(parsed, status:, headers: {})
43
+ return parsed unless parsed.is_a?(::Hash)
44
+
45
+ response = new
46
+ response.replace(parsed)
47
+ response.attach_metadata(status: status, headers: headers)
48
+ response
49
+ end
50
+ end
51
+
52
+ # @api private — set once by .build immediately after construction
53
+ def attach_metadata(status:, headers:)
54
+ @status = status
55
+ @headers = headers
56
+ self
57
+ end
58
+
59
+ def warnings
60
+ @warnings ||= Array(self['warnings']).filter_map do |entry|
61
+ next unless entry.is_a?(::Hash)
62
+
63
+ Warning.new(code: entry['code'], param: entry['param'], message: entry['message'])
64
+ end
65
+ end
66
+
67
+ def warnings?
68
+ !warnings.empty?
69
+ end
70
+
71
+ def rate_limit
72
+ return @rate_limit if defined?(@rate_limit)
73
+
74
+ limit = header('x-ratelimit-limit')
75
+ @rate_limit = if limit.nil?
76
+ nil
77
+ else
78
+ RateLimit.new(
79
+ limit: limit.to_i,
80
+ remaining: header('x-ratelimit-remaining')&.to_i,
81
+ reset: parse_time(header('x-ratelimit-reset'))
82
+ )
83
+ end
84
+ end
85
+
86
+ # True when the API replayed a stored response for a repeated
87
+ # Idempotency-Key rather than performing the write again.
88
+ def idempotent_replay?
89
+ header('idempotency-replayed') == 'true'
90
+ end
91
+
92
+ private
93
+
94
+ def header(name)
95
+ (@headers || {})[name]
96
+ end
97
+
98
+ def parse_time(value)
99
+ value && Time.iso8601(value)
100
+ rescue ArgumentError
101
+ nil
102
+ end
103
+ end
104
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Broadcast
4
- VERSION = '0.1.4'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -7,6 +7,40 @@ module Broadcast
7
7
  module Webhook
8
8
  TIMESTAMP_TOLERANCE = 300 # 5 minutes
9
9
 
10
+ # Every event type a webhook endpoint can subscribe to, mirroring
11
+ # WebhookEndpoint::AVAILABLE_EVENT_TYPES server-side. Use these when
12
+ # creating an endpoint — an unknown event type is dropped silently.
13
+ EMAIL_EVENTS = %w[
14
+ email.sent email.delivered email.delivery_delayed email.complained
15
+ email.bounced email.opened email.clicked email.failed
16
+ ].freeze
17
+
18
+ SUBSCRIBER_EVENTS = %w[
19
+ subscriber.created subscriber.updated subscriber.deleted
20
+ subscriber.subscribed subscriber.unsubscribed subscriber.bounced
21
+ subscriber.complained
22
+ ].freeze
23
+
24
+ BROADCAST_EVENTS = %w[
25
+ broadcast.scheduled broadcast.queueing broadcast.sending broadcast.sent
26
+ broadcast.failed broadcast.partial_failure broadcast.aborted
27
+ broadcast.paused
28
+ ].freeze
29
+
30
+ SEQUENCE_EVENTS = %w[
31
+ sequence.subscriber_added sequence.subscriber_completed
32
+ sequence.subscriber_moved sequence.subscriber_removed
33
+ sequence.subscriber_paused sequence.subscriber_resumed
34
+ sequence.subscriber_error
35
+ ].freeze
36
+
37
+ # Delivery-machinery events, not content events.
38
+ SYSTEM_EVENTS = %w[message.attempt.exhausted test.webhook].freeze
39
+
40
+ EVENT_TYPES = (
41
+ EMAIL_EVENTS + SUBSCRIBER_EVENTS + BROADCAST_EVENTS + SEQUENCE_EVENTS + SYSTEM_EVENTS
42
+ ).freeze
43
+
10
44
  module_function
11
45
 
12
46
  def verify(payload, signature_header, timestamp_header, secret:, now: nil)
data/lib/broadcast.rb CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  require_relative 'broadcast/version'
4
4
  require_relative 'broadcast/errors'
5
+ require_relative 'broadcast/response'
5
6
  require_relative 'broadcast/configuration'
7
+ require_relative 'broadcast/debug_logger'
8
+ require_relative 'broadcast/connection'
6
9
  require_relative 'broadcast/client'
7
10
  require_relative 'broadcast/webhook'
8
11
  require_relative 'broadcast/resources/base'
@@ -12,6 +15,12 @@ require_relative 'broadcast/resources/broadcasts'
12
15
  require_relative 'broadcast/resources/segments'
13
16
  require_relative 'broadcast/resources/templates'
14
17
  require_relative 'broadcast/resources/webhook_endpoints'
18
+ require_relative 'broadcast/resources/transactionals'
19
+ require_relative 'broadcast/resources/opt_in_forms'
20
+ require_relative 'broadcast/resources/email_servers'
21
+ require_relative 'broadcast/resources/autopilots'
22
+ require_relative 'broadcast/resources/discovery'
23
+ require_relative 'broadcast/resources/migration'
15
24
 
16
25
  # ActionMailer integration — only loaded when Rails is present
17
26
  if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: broadcast-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiu
@@ -31,27 +31,36 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
- - ".rubocop.yml"
35
34
  - CHANGELOG.md
36
35
  - Gemfile
37
36
  - Gemfile.lock
38
37
  - LICENSE.txt
39
38
  - README.md
40
39
  - Rakefile
40
+ - SDK-COVERAGE.md
41
41
  - lib/broadcast-ruby.rb
42
42
  - lib/broadcast.rb
43
43
  - lib/broadcast/client.rb
44
44
  - lib/broadcast/configuration.rb
45
+ - lib/broadcast/connection.rb
46
+ - lib/broadcast/debug_logger.rb
45
47
  - lib/broadcast/delivery_method.rb
46
48
  - lib/broadcast/errors.rb
47
49
  - lib/broadcast/railtie.rb
50
+ - lib/broadcast/resources/autopilots.rb
48
51
  - lib/broadcast/resources/base.rb
49
52
  - lib/broadcast/resources/broadcasts.rb
53
+ - lib/broadcast/resources/discovery.rb
54
+ - lib/broadcast/resources/email_servers.rb
55
+ - lib/broadcast/resources/migration.rb
56
+ - lib/broadcast/resources/opt_in_forms.rb
50
57
  - lib/broadcast/resources/segments.rb
51
58
  - lib/broadcast/resources/sequences.rb
52
59
  - lib/broadcast/resources/subscribers.rb
53
60
  - lib/broadcast/resources/templates.rb
61
+ - lib/broadcast/resources/transactionals.rb
54
62
  - lib/broadcast/resources/webhook_endpoints.rb
63
+ - lib/broadcast/response.rb
55
64
  - lib/broadcast/version.rb
56
65
  - lib/broadcast/webhook.rb
57
66
  homepage: https://github.com/send-broadcast/broadcast-ruby
@@ -77,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
86
  - !ruby/object:Gem::Version
78
87
  version: '0'
79
88
  requirements: []
80
- rubygems_version: 3.7.2
89
+ rubygems_version: 4.0.16
81
90
  specification_version: 4
82
91
  summary: Ruby client for the Broadcast email platform
83
92
  test_files: []