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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +2 -2
- data/README.md +91 -3
- data/lib/broadcast/client.rb +9 -2
- data/lib/broadcast/connection.rb +11 -3
- data/lib/broadcast/debug_logger.rb +9 -0
- data/lib/broadcast/multipart.rb +51 -0
- data/lib/broadcast/resources/channel.rb +39 -3
- data/lib/broadcast/resources/file_assets.rb +107 -0
- data/lib/broadcast/version.rb +1 -1
- data/lib/broadcast.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46cb94477a871cda3ac8f5a69d2d947b2ec87b5dd4454a1ec8d179cec7f25b9a
|
|
4
|
+
data.tar.gz: 38f72f3aecd7376ed6f072e831049614ae5f329c8e92cb0e1aeb3d26648f8829
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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 →
|
|
843
|
-
match it.
|
|
844
|
-
|
|
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
|
|
data/lib/broadcast/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/broadcast/connection.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ->
|
|
10
|
-
# (defaults filled in)
|
|
11
|
-
#
|
|
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
|
data/lib/broadcast/version.rb
CHANGED
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.
|
|
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
|