broadcast-ruby 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 722b6b8aba5a3d87fbdacbab42f757071d0f4b7bc548a803f9b28fb0b4596ec9
4
- data.tar.gz: 78c247ac8e18e66f3fff0e380b8a0d299047d15994765a0ad6ddb8f13cb766ba
3
+ metadata.gz: 46cb94477a871cda3ac8f5a69d2d947b2ec87b5dd4454a1ec8d179cec7f25b9a
4
+ data.tar.gz: 38f72f3aecd7376ed6f072e831049614ae5f329c8e92cb0e1aeb3d26648f8829
5
5
  SHA512:
6
- metadata.gz: a3cd09cc4c96d57ed30c64c8c48fade4f75bb6f5c15136819f7725e9eea1f8811b8e3bd91ec70125d92ad8411fcf60ca04cc0b5461bd50858e5f6c611f2246fe
7
- data.tar.gz: edf1130a392a68a08fc95c1a29619e1a0399dbe0373c85048b6c45bba12a8dff88fb9ed84b1364c06ba5b5c9fa2c90e5da8fd083bd99b7d832a122cb72d3501e
6
+ metadata.gz: e46bf11d5142f63a5bde2fc5d6d0c34c5d352fc05e428e1c3bc05931e346c9cff163f664d611f2ea88c1d5af819ed9d09b2fde518d5fe4fd6629ab650626bb4f
7
+ data.tar.gz: ca25863674f4560506aca95d62739ca8998a0671cfc52561b99b1dac26f6c519d6ff19c1781b121a4f3b72d6902b5e00360f30418d954aac3a34ad3fbbe83515
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.0] - 2026-10-16
8
+
9
+ ### Added
10
+
11
+ - `file_assets` for the File Assets API (Broadcast 2.48.0): `list(filter:,
12
+ limit:, offset:)`, `get_file_asset(id)`, `upload(path_or_io, filename:,
13
+ content_type:, channel_restricted:, idempotency_key:)` (multipart),
14
+ `upload_base64(filename:, data:, ...)` (JSON) and `delete(id)`. The returned
15
+ `url` is what `channel.update_design(brand: { logo_url: })` takes. Needs the
16
+ new `file_assets` permissions.
17
+ - Multipart request support in the HTTP layer (`Broadcast::FilePart`). The
18
+ debug log shows a file part as its name and size, and cuts strings over
19
+ 1,000 characters short, so an upload never writes the file into the log.
20
+ - `channel.settings` and `channel.update_settings(**groups)` for
21
+ `GET`/`PATCH /api/v1/channel/settings` (Broadcast 2.46.0): general, sender,
22
+ unsubscribe page (with the privacy policy and legal notice links),
23
+ confirmation page and opt-in forms (`opt_in_forms.match_brand_kit`) settings. Needs the new `channel_settings` permissions.
24
+ - `channel.update_design(**kit)` for `PATCH /api/v1/channel/design`
25
+ (Broadcast 2.46.0): change the brand kit. Needs `channel_settings_write`.
26
+ The logo is the `logo_url` of an image in the channel's file library.
27
+
7
28
  ## [0.8.0] - 2026-10-10
8
29
 
9
30
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- broadcast-ruby (0.8.0)
4
+ broadcast-ruby (0.9.0)
5
5
  base64
6
6
 
7
7
  GEM
@@ -177,7 +177,7 @@ CHECKSUMS
177
177
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
178
178
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
179
179
  bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
180
- broadcast-ruby (0.8.0)
180
+ broadcast-ruby (0.9.0)
181
181
  builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
182
182
  concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
183
183
  connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
data/README.md CHANGED
@@ -839,9 +839,10 @@ client.templates.delete(1)
839
839
 
840
840
  ### Brand Kit
841
841
 
842
- Read the channel's brand kit (Settings → Design) so your own templates can
843
- match it. Read-only, fully resolved (defaults filled in), and always the
844
- token's own channel, so it takes no arguments. Needs `templates_read`.
842
+ Read the channel's brand kit (Settings → Brand Kit) so your own templates can
843
+ match it. Fully resolved (defaults filled in), and always the token's own
844
+ channel, so it takes no arguments. Needs `templates_read` (or, from Broadcast
845
+ 2.46.0, `channel_settings_read`).
845
846
 
846
847
  ```ruby
847
848
  design = client.channel.design
@@ -854,8 +855,93 @@ design['brand']['website_url'] # => 'https://acme.example'
854
855
  design['brand']['social_links'] # => [{ 'network' => 'x', 'url' => 'https://x.com/acme' }]
855
856
  ```
856
857
 
858
+ Change it with `update_design` (Broadcast 2.46.0 or later, needs
859
+ `channel_settings_write`). Send only what changes, in the same shape. An
860
+ empty string or `nil` puts a value back to its default, and `social_links`
861
+ replaces the list. `font_stack` and `width` are read-only. The logo is the
862
+ `logo_url` of an image in the channel's file library; the server never
863
+ fetches a URL.
864
+
865
+ ```ruby
866
+ kit = client.channel.update_design(
867
+ colors: { accent: '#ff5500' },
868
+ typography: { font: 'georgia' },
869
+ brand: { logo_url: 'https://your-broadcast.example/files/eyJ...-logo.png', logo_width: 200 }
870
+ )
871
+ kit['colors']['accent'] # => '#ff5500' (the whole kit comes back)
872
+ kit.warnings # => e.g. an unrecognized key you sent
873
+ ```
874
+
875
+ An unknown font, icon style or social network, a logo from outside the file
876
+ library, or a value the Brand Kit page would refuse raises
877
+ `Broadcast::ValidationError` (422), and nothing is saved.
878
+
879
+ ### Channel Settings
880
+
881
+ Read and change the channel's settings (Broadcast 2.46.0 or later), grouped
882
+ like the Settings pages: `general`, `sender`, `unsubscribe_page` (including
883
+ the privacy policy and legal notice links), `confirmation_page` and
884
+ `opt_in_forms` (`match_brand_kit`: whether new opt-in forms start from the
885
+ brand kit). Needs `channel_settings_read` to read and `channel_settings_write`
886
+ to change; both are off on every token until you tick them.
887
+
888
+ The `match_brand_kit` switches (`unsubscribe_page`, `confirmation_page`
889
+ and `opt_in_forms`) decide where the brand kit is used. They are set
890
+ here, not with the brand kit update.
891
+
892
+ ```ruby
893
+ settings = client.channel.settings
894
+ settings['unsubscribe_page']['legal_notice_url'] # => 'https://example.com/impressum'
895
+
896
+ result = client.channel.update_settings(
897
+ unsubscribe_page: {
898
+ privacy_policy_url: 'https://example.com/privacy',
899
+ legal_notice_url: 'https://example.com/impressum'
900
+ }
901
+ )
902
+ result.warnings # => e.g. [#<Warning code="unrecognized_parameter" param="channel_settings.general.domain" ...>]
903
+ ```
904
+
905
+ Updates are partial: nested text and colours merge, an empty string or `nil`
906
+ removes a value, and `languages` replaces the list. A value outside a fixed
907
+ set, or one that fails validation, raises `Broadcast::ValidationError` and
908
+ saves nothing.
909
+
857
910
  ---
858
911
 
912
+ ## File Assets
913
+
914
+ The channel's file library (Broadcast 2.48.0 or later): PNG, JPEG and GIF
915
+ images under 10 MB, each with a public URL that never expires. Use the URL in
916
+ emails, or as the brand kit logo. Needs `file_assets_read` to list and get,
917
+ and `file_assets_write` to upload and delete.
918
+
919
+ ```ruby
920
+ # Upload from a path (multipart). The content type follows the extension.
921
+ file = client.file_assets.upload('logo.png')
922
+ file['url'] # => 'https://your-broadcast.example/files/eyJ...-logo.png'
923
+
924
+ # From an IO, with a filename; link it to this channel only; safe to retry
925
+ client.file_assets.upload(io, filename: 'banner.gif', channel_restricted: true,
926
+ idempotency_key: "banner-#{campaign.id}")
927
+
928
+ # Already have base64? Send it as JSON.
929
+ client.file_assets.upload_base64(filename: 'logo.png', content_type: 'image/png',
930
+ data: Base64.strict_encode64(bytes))
931
+
932
+ # Use it as the brand kit logo
933
+ client.channel.update_design(brand: { logo_url: file['url'] })
934
+
935
+ client.file_assets.list(filter: 'logo', limit: 20) # => { 'data' => [...], 'total' => n }
936
+ client.file_assets.get_file_asset(file['id'])
937
+ client.file_assets.delete(file['id'])
938
+ ```
939
+
940
+ A file that is not a PNG, JPEG or GIF, a file of 10 MB or more, or invalid
941
+ base64 raises `Broadcast::ValidationError` (422). So does deleting a file
942
+ that is a channel's brand kit logo: set another logo first. The server never
943
+ fetches a URL; upload the bytes.
944
+
859
945
  ## Opt-In Forms
860
946
 
861
947
  Embeddable subscription forms with theming, A/B variants, and analytics.
@@ -1488,7 +1574,9 @@ Each token can be scoped to specific resources. The ActionMailer delivery method
1488
1574
  | Users (admin token only) | `users_read` -- list, get, channel_permissions, system_permissions | `users_write` -- create, update, deactivate, activate, delete, permission writes |
1489
1575
  | Webhook Endpoints | `webhook_endpoints_read` -- list, get, deliveries | `webhook_endpoints_write` -- create, update, delete, test |
1490
1576
  | Autopilot | `autopilot_read` -- list, get, runs | `autopilot_write` -- create, update, delete, activate, pause, deactivate, trigger_run |
1577
+ | Channel Settings | `channel_settings_read` -- `channel.settings`, `channel.design` | `channel_settings_write` -- `channel.update_settings`, `channel.update_design` |
1491
1578
  | Suppressions | `suppressions_read` -- list, check | `suppressions_write` -- add, remove, bulk add/remove |
1579
+ | File Assets | `file_assets_read` -- list, get | `file_assets_write` -- upload, upload_base64, delete |
1492
1580
 
1493
1581
  ---
1494
1582
 
@@ -153,6 +153,11 @@ module Broadcast
153
153
  @channel ||= Resources::Channel.new(self)
154
154
  end
155
155
 
156
+ # The channel's file library: upload images and get their public URLs.
157
+ def file_assets
158
+ @file_assets ||= Resources::FileAssets.new(self)
159
+ end
160
+
156
161
  # Read-only export endpoints under /api/migration/v1. Requires an admin
157
162
  # (system) API token.
158
163
  def migration
@@ -160,9 +165,11 @@ module Broadcast
160
165
  end
161
166
 
162
167
  # @api private
163
- def request(method, path, body_or_params = nil, headers: {}, raw: false)
168
+ # rubocop:disable Metrics/ParameterLists -- transport options passed straight through to Connection
169
+ def request(method, path, body_or_params = nil, headers: {}, raw: false, multipart: false)
170
+ # rubocop:enable Metrics/ParameterLists
164
171
  payload = inject_channel_scope(body_or_params)
165
- @connection.request(method, path, payload, headers: headers, raw: raw)
172
+ @connection.request(method, path, payload, headers: headers, raw: raw, multipart: multipart)
166
173
  end
167
174
 
168
175
  private
@@ -28,9 +28,13 @@ module Broadcast
28
28
 
29
29
  # @param raw [Boolean] return the response body as a String instead of
30
30
  # parsing it as JSON — for text/plain endpoints such as /api/v1/skill.
31
- def request(method, path, payload = nil, headers: {}, raw: false)
31
+ # @param multipart [Boolean] send the payload as multipart/form-data
32
+ # (Broadcast::FilePart values become file parts) instead of JSON.
33
+ # rubocop:disable Metrics/ParameterLists -- one keyword per transport option
34
+ def request(method, path, payload = nil, headers: {}, raw: false, multipart: false)
35
+ # rubocop:enable Metrics/ParameterLists
32
36
  uri = build_uri(path, method, payload)
33
- context = { headers: headers, raw: raw, redirects: 0 }
37
+ context = { headers: headers, raw: raw, redirects: 0, multipart: multipart }
34
38
 
35
39
  retry_with_backoff { execute(method, uri, payload, context) }
36
40
  rescue Net::OpenTimeout, Net::ReadTimeout => e
@@ -52,7 +56,11 @@ module Broadcast
52
56
  http.read_timeout = @config.timeout
53
57
 
54
58
  req = build_request(method, uri, context[:headers])
55
- req.body = payload.to_json if method != :get && payload_present?(payload)
59
+ if context[:multipart]
60
+ req.body, req['Content-Type'] = Multipart.build(payload || {})
61
+ elsif method != :get && payload_present?(payload)
62
+ req.body = payload.to_json
63
+ end
56
64
 
57
65
  @debug_logger.request(req, method == :get ? nil : payload)
58
66
  response = http.request(req)
@@ -17,6 +17,9 @@ module Broadcast
17
17
  api_key api_token password secret
18
18
  ].freeze
19
19
 
20
+ # Longer strings are cut short in the log.
21
+ MAX_LOGGED_STRING = 1000
22
+
20
23
  def initialize(config)
21
24
  @config = config
22
25
  end
@@ -56,6 +59,12 @@ module Broadcast
56
59
  end
57
60
  when Array
58
61
  value.map { |item| redact(item) }
62
+ when FilePart
63
+ value.inspect
64
+ when String
65
+ # A base64 file upload (file_assets.upload_base64) would otherwise
66
+ # write megabytes into the log.
67
+ value.length > MAX_LOGGED_STRING ? "#{value[0, 40]}... (#{value.length} characters)" : value
59
68
  else
60
69
  value
61
70
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Broadcast
6
+ # A file in a multipart/form-data request: the bytes, and the filename and
7
+ # content type sent with them. Holding the bytes (not an IO) keeps a retry
8
+ # safe: every attempt builds the body afresh.
9
+ FilePart = Struct.new(:bytes, :filename, :content_type) do
10
+ def inspect
11
+ "#<Broadcast::FilePart #{filename} (#{content_type}, #{bytes.bytesize} bytes)>"
12
+ end
13
+ alias_method :to_s, :inspect
14
+ end
15
+
16
+ # Builds a multipart/form-data body by hand. Net::HTTP's set_form streams
17
+ # the body at send time, which the debug logger and test doubles cannot see.
18
+ module Multipart
19
+ module_function
20
+
21
+ # @param fields [Hash] values are FileParts or anything with #to_s; nil is left out
22
+ # @return [Array(String, String)] the body (binary) and the Content-Type header
23
+ def build(fields)
24
+ boundary = "BroadcastRuby#{SecureRandom.hex(16)}"
25
+ body = String.new(encoding: Encoding::BINARY)
26
+
27
+ fields.each do |name, value|
28
+ next if value.nil?
29
+
30
+ body << "--#{boundary}\r\n"
31
+ if value.is_a?(FilePart)
32
+ body << "Content-Disposition: form-data; name=\"#{quote(name)}\"; filename=\"#{quote(value.filename)}\"\r\n"
33
+ body << "Content-Type: #{value.content_type}\r\n\r\n"
34
+ body << value.bytes.b
35
+ else
36
+ body << "Content-Disposition: form-data; name=\"#{quote(name)}\"\r\n\r\n"
37
+ body << value.to_s.b
38
+ end
39
+ body << "\r\n"
40
+ end
41
+ body << "--#{boundary}--\r\n"
42
+
43
+ [body, "multipart/form-data; boundary=#{boundary}"]
44
+ end
45
+
46
+ # A quote or a line break in a name would end the header early.
47
+ def quote(value)
48
+ value.to_s.gsub(/[\r\n]/, ' ').gsub('"', '%22').b
49
+ end
50
+ end
51
+ end
@@ -6,9 +6,9 @@ module Broadcast
6
6
  # (or, for an admin token, the channel it is scoped to); there is no
7
7
  # channel id argument.
8
8
  class Channel < Base
9
- # The channel's brand kit from Settings -> Design, fully resolved
10
- # (defaults filled in), read-only. Requires the `templates_read`
11
- # permission.
9
+ # The channel's brand kit from Settings -> Brand Kit, fully resolved
10
+ # (defaults filled in). Requires the `templates_read` permission (or,
11
+ # from Broadcast 2.46.0, `channel_settings_read`).
12
12
  #
13
13
  # Returns a Hash with `colors`, `typography` (`font` key and email-safe
14
14
  # `font_stack`), `layout` (`width`, `radius`), and `brand` (`logo_url` as
@@ -17,6 +17,42 @@ module Broadcast
17
17
  def design
18
18
  get('/api/v1/channel/design')
19
19
  end
20
+
21
+ # Changes the brand kit (Broadcast 2.46.0 or later). Requires the
22
+ # `channel_settings_write` permission.
23
+ #
24
+ # Partial: send only what changes, in the shape #design returns. An empty
25
+ # string or nil puts a value back to its default; `social_links`
26
+ # replaces the list. Set the logo with the `logo_url` of an image in the
27
+ # channel's file library. Returns the whole kit after the change, with
28
+ # any `warnings`.
29
+ #
30
+ # client.channel.update_design(colors: { accent: '#ff5500' }, brand: { logo_url: nil })
31
+ def update_design(**design)
32
+ patch('/api/v1/channel/design', { channel_design: design })
33
+ end
34
+
35
+ # The channel's settings (Broadcast 2.46.0 or later), grouped like the
36
+ # Settings pages: `general`, `sender`, `unsubscribe_page` (with the legal
37
+ # links), `confirmation_page` and `opt_in_forms` (`match_brand_kit`, whether
38
+ # new opt-in forms start from the brand kit). The brand kit switches are
39
+ # set here, not with #update_design. Requires `channel_settings_read`.
40
+ def settings
41
+ get('/api/v1/channel/settings')
42
+ end
43
+
44
+ # Changes channel settings (Broadcast 2.46.0 or later). Requires
45
+ # `channel_settings_write`.
46
+ #
47
+ # Partial: send only the groups and keys that change. Nested text and
48
+ # colours merge, an empty string or nil removes a value, and
49
+ # `languages` replaces the list. Returns every setting after the
50
+ # change, with any `warnings`.
51
+ #
52
+ # client.channel.update_settings(unsubscribe_page: { legal_notice_url: 'https://example.com/impressum' })
53
+ def update_settings(**settings)
54
+ patch('/api/v1/channel/settings', { channel_settings: settings })
55
+ end
20
56
  end
21
57
  end
22
58
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadcast
4
+ module Resources
5
+ # The channel's file library (Broadcast 2.48.0 or later): images (PNG,
6
+ # JPEG, GIF, under 10 MB) with public URLs that never expire. Use a file's
7
+ # `url` in emails, or as the brand kit logo:
8
+ #
9
+ # file = client.file_assets.upload('logo.png')
10
+ # client.channel.update_design(brand: { logo_url: file['url'] })
11
+ #
12
+ # Requires the `file_assets_read` permission to read and
13
+ # `file_assets_write` to upload and delete.
14
+ class FileAssets < Base
15
+ MAX_IDEMPOTENCY_KEY_LENGTH = 255
16
+
17
+ CONTENT_TYPES = {
18
+ '.png' => 'image/png',
19
+ '.jpg' => 'image/jpeg',
20
+ '.jpeg' => 'image/jpeg',
21
+ '.gif' => 'image/gif'
22
+ }.freeze
23
+
24
+ # Files the channel can see, newest first.
25
+ #
26
+ # filter: find files by file name (closest matches first)
27
+ # limit:, offset:
28
+ #
29
+ # Returns `{ 'data' => [...], 'total' => n }`.
30
+ def list(**params)
31
+ get('/api/v1/file_assets', params)
32
+ end
33
+
34
+ def get_file_asset(id)
35
+ get("/api/v1/file_assets/#{id}")
36
+ end
37
+
38
+ # Uploads a file as multipart/form-data.
39
+ #
40
+ # `file` is a path, or an IO (then `filename:` is required). The content
41
+ # type follows the file extension unless `content_type:` is given.
42
+ #
43
+ # channel_restricted: true links the file to this channel only
44
+ # (default: every channel, as in the web app)
45
+ # idempotency_key: makes a retry safe; see Transactionals#create
46
+ def upload(file, filename: nil, content_type: nil, channel_restricted: nil, idempotency_key: nil)
47
+ bytes, name = read_file(file, filename)
48
+ part = FilePart.new(bytes: bytes, filename: name, content_type: content_type || content_type_for(name))
49
+
50
+ fields = { file: part }
51
+ fields[:channel_restricted] = channel_restricted unless channel_restricted.nil?
52
+
53
+ @client.request(:post, '/api/v1/file_assets', fields,
54
+ headers: idempotency_headers(idempotency_key), multipart: true)
55
+ end
56
+
57
+ # Uploads base64-encoded bytes in a JSON body, for callers that already
58
+ # hold base64 (or cannot send multipart).
59
+ def upload_base64(filename:, data:, content_type: nil, channel_restricted: nil, idempotency_key: nil)
60
+ payload = { filename: filename, data: data }
61
+ payload[:content_type] = content_type unless content_type.nil?
62
+ payload[:channel_restricted] = channel_restricted unless channel_restricted.nil?
63
+
64
+ @client.request(:post, '/api/v1/file_assets', payload, headers: idempotency_headers(idempotency_key))
65
+ end
66
+
67
+ # Deletes a file. A file that is a channel's brand kit logo is refused
68
+ # with Broadcast::ValidationError (422): choose another logo first.
69
+ def delete(id)
70
+ @client.request(:delete, "/api/v1/file_assets/#{id}")
71
+ end
72
+
73
+ private
74
+
75
+ def read_file(file, filename)
76
+ if file.respond_to?(:read)
77
+ if filename.nil? && !file.respond_to?(:path)
78
+ raise ArgumentError, 'filename: is required when uploading from an IO'
79
+ end
80
+
81
+ [file.read, filename || File.basename(file.path)]
82
+ else
83
+ path = file.to_s
84
+ [File.binread(path), filename || File.basename(path)]
85
+ end
86
+ end
87
+
88
+ def content_type_for(name)
89
+ CONTENT_TYPES.fetch(File.extname(name.to_s).downcase, 'application/octet-stream')
90
+ end
91
+
92
+ def idempotency_headers(key)
93
+ return {} if key.nil?
94
+
95
+ key = key.to_s.strip
96
+ return {} if key.empty?
97
+
98
+ if key.length > MAX_IDEMPOTENCY_KEY_LENGTH
99
+ raise ArgumentError,
100
+ "idempotency_key must be #{MAX_IDEMPOTENCY_KEY_LENGTH} characters or fewer (got #{key.length})"
101
+ end
102
+
103
+ { 'Idempotency-Key' => key }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Broadcast
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/broadcast.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'broadcast/errors'
5
5
  require_relative 'broadcast/response'
6
6
  require_relative 'broadcast/configuration'
7
7
  require_relative 'broadcast/debug_logger'
8
+ require_relative 'broadcast/multipart'
8
9
  require_relative 'broadcast/connection'
9
10
  require_relative 'broadcast/client'
10
11
  require_relative 'broadcast/webhook'
@@ -25,6 +26,7 @@ require_relative 'broadcast/resources/discovery'
25
26
  require_relative 'broadcast/resources/suppressions'
26
27
  require_relative 'broadcast/resources/global_suppressions'
27
28
  require_relative 'broadcast/resources/channel'
29
+ require_relative 'broadcast/resources/file_assets'
28
30
  require_relative 'broadcast/resources/migration'
29
31
 
30
32
  # ActionMailer integration — only loaded when Rails is present
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiu
@@ -46,6 +46,7 @@ files:
46
46
  - lib/broadcast/debug_logger.rb
47
47
  - lib/broadcast/delivery_method.rb
48
48
  - lib/broadcast/errors.rb
49
+ - lib/broadcast/multipart.rb
49
50
  - lib/broadcast/railtie.rb
50
51
  - lib/broadcast/resources/autopilots.rb
51
52
  - lib/broadcast/resources/base.rb
@@ -53,6 +54,7 @@ files:
53
54
  - lib/broadcast/resources/channel.rb
54
55
  - lib/broadcast/resources/discovery.rb
55
56
  - lib/broadcast/resources/email_servers.rb
57
+ - lib/broadcast/resources/file_assets.rb
56
58
  - lib/broadcast/resources/global_suppressions.rb
57
59
  - lib/broadcast/resources/migration.rb
58
60
  - lib/broadcast/resources/opt_in_forms.rb