broadcast-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +44 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +105 -0
- data/LICENSE.txt +21 -0
- data/README.md +522 -0
- data/Rakefile +15 -0
- data/lib/broadcast/client.rb +175 -0
- data/lib/broadcast/configuration.rb +31 -0
- data/lib/broadcast/errors.rb +19 -0
- data/lib/broadcast/resources/base.rb +25 -0
- data/lib/broadcast/resources/broadcasts.rb +54 -0
- data/lib/broadcast/resources/segments.rb +28 -0
- data/lib/broadcast/resources/sequences.rb +68 -0
- data/lib/broadcast/resources/subscribers.rb +51 -0
- data/lib/broadcast/resources/templates.rb +27 -0
- data/lib/broadcast/resources/webhook_endpoints.rb +35 -0
- data/lib/broadcast/version.rb +5 -0
- data/lib/broadcast/webhook.rb +51 -0
- data/lib/broadcast.rb +17 -0
- metadata +79 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Broadcast
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
def initialize(**settings)
|
|
12
|
+
@config = Configuration.new
|
|
13
|
+
settings.each { |k, v| @config.public_send(:"#{k}=", v) }
|
|
14
|
+
@config.validate!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# --- Transactional email ---
|
|
18
|
+
|
|
19
|
+
def send_email(to:, subject:, body:, reply_to: nil)
|
|
20
|
+
payload = { to: to, subject: subject, body: body }
|
|
21
|
+
payload[:reply_to] = reply_to if reply_to
|
|
22
|
+
request(:post, '/api/v1/transactionals.json', payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get_email(id)
|
|
26
|
+
request(:get, "/api/v1/transactionals/#{id}.json")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# --- Resource sub-clients ---
|
|
30
|
+
|
|
31
|
+
def subscribers
|
|
32
|
+
@subscribers ||= Resources::Subscribers.new(self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sequences
|
|
36
|
+
@sequences ||= Resources::Sequences.new(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def broadcasts
|
|
40
|
+
@broadcasts ||= Resources::Broadcasts.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def segments
|
|
44
|
+
@segments ||= Resources::Segments.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def templates
|
|
48
|
+
@templates ||= Resources::Templates.new(self)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def webhook_endpoints
|
|
52
|
+
@webhook_endpoints ||= Resources::WebhookEndpoints.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @api private
|
|
56
|
+
def request(method, path, body_or_params = nil)
|
|
57
|
+
uri = URI("#{@config.host}#{path}")
|
|
58
|
+
|
|
59
|
+
if method == :get && body_or_params.is_a?(Hash) && body_or_params.any?
|
|
60
|
+
uri.query = URI.encode_www_form(flatten_params(body_or_params))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
retry_with_backoff do
|
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
65
|
+
http.use_ssl = uri.scheme == 'https'
|
|
66
|
+
http.open_timeout = @config.open_timeout
|
|
67
|
+
http.read_timeout = @config.timeout
|
|
68
|
+
|
|
69
|
+
req = build_request(method, uri)
|
|
70
|
+
req.body = body_or_params.to_json if method != :get && body_or_params.is_a?(Hash) && body_or_params.any?
|
|
71
|
+
|
|
72
|
+
log_request(req, method == :get ? nil : body_or_params) if @config.debug
|
|
73
|
+
|
|
74
|
+
response = http.request(req)
|
|
75
|
+
log_response(response) if @config.debug
|
|
76
|
+
handle_response(response)
|
|
77
|
+
end
|
|
78
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
79
|
+
raise Broadcast::TimeoutError, "Request timeout: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def build_request(method, uri)
|
|
85
|
+
klass = case method
|
|
86
|
+
when :get then Net::HTTP::Get
|
|
87
|
+
when :post then Net::HTTP::Post
|
|
88
|
+
when :patch then Net::HTTP::Patch
|
|
89
|
+
when :delete then Net::HTTP::Delete
|
|
90
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
req = klass.new(uri)
|
|
94
|
+
req['Authorization'] = "Bearer #{@config.api_token}"
|
|
95
|
+
req['Content-Type'] = 'application/json'
|
|
96
|
+
req['User-Agent'] = "broadcast-ruby/#{Broadcast::VERSION}"
|
|
97
|
+
req
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_response(response)
|
|
101
|
+
case response.code.to_i
|
|
102
|
+
when 200, 201
|
|
103
|
+
return {} if response.body.nil? || response.body.strip.empty?
|
|
104
|
+
|
|
105
|
+
JSON.parse(response.body)
|
|
106
|
+
when 401
|
|
107
|
+
raise AuthenticationError, parse_error(response) || 'Authentication failed'
|
|
108
|
+
when 404
|
|
109
|
+
raise NotFoundError, parse_error(response) || 'Resource not found'
|
|
110
|
+
when 422
|
|
111
|
+
raise ValidationError, parse_error(response) || 'Validation failed'
|
|
112
|
+
when 429
|
|
113
|
+
raise RateLimitError, parse_error(response) || 'Rate limit exceeded'
|
|
114
|
+
when 500, 502, 503, 504
|
|
115
|
+
raise APIError, parse_error(response) || "Server error (#{response.code})"
|
|
116
|
+
else
|
|
117
|
+
raise APIError, parse_error(response) || "Unexpected response: #{response.code}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_error(response)
|
|
122
|
+
JSON.parse(response.body)['error']
|
|
123
|
+
rescue JSON::ParserError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def retry_with_backoff
|
|
128
|
+
attempts = 0
|
|
129
|
+
begin
|
|
130
|
+
attempts += 1
|
|
131
|
+
yield
|
|
132
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
133
|
+
raise if attempts >= @config.retry_attempts
|
|
134
|
+
|
|
135
|
+
sleep(@config.retry_delay * attempts)
|
|
136
|
+
retry
|
|
137
|
+
rescue APIError => e
|
|
138
|
+
raise unless attempts < @config.retry_attempts && e.message.include?('Server error')
|
|
139
|
+
|
|
140
|
+
sleep(@config.retry_delay * attempts)
|
|
141
|
+
retry
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def flatten_params(params)
|
|
146
|
+
result = []
|
|
147
|
+
params.each do |key, value|
|
|
148
|
+
case value
|
|
149
|
+
when Array
|
|
150
|
+
value.each { |v| result << ["#{key}[]", v.to_s] }
|
|
151
|
+
when Hash
|
|
152
|
+
value.each { |k, v| result << ["#{key}[#{k}]", v.to_s] }
|
|
153
|
+
when nil
|
|
154
|
+
next
|
|
155
|
+
else
|
|
156
|
+
result << [key.to_s, value.to_s]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def log_request(request, body)
|
|
163
|
+
return unless @config.logger
|
|
164
|
+
|
|
165
|
+
@config.logger.debug("[Broadcast] #{request.method} #{request.uri}")
|
|
166
|
+
@config.logger.debug("[Broadcast] Body: #{body.to_json}") if body.is_a?(Hash) && body.any?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def log_response(response)
|
|
170
|
+
return unless @config.logger
|
|
171
|
+
|
|
172
|
+
@config.logger.debug("[Broadcast] Response: #{response.code} #{response.body}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_token,
|
|
6
|
+
:host,
|
|
7
|
+
:timeout,
|
|
8
|
+
:open_timeout,
|
|
9
|
+
:retry_attempts,
|
|
10
|
+
:retry_delay,
|
|
11
|
+
:logger,
|
|
12
|
+
:debug
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@api_token = nil
|
|
16
|
+
@host = 'https://sendbroadcast.com'
|
|
17
|
+
@timeout = 30
|
|
18
|
+
@open_timeout = 10
|
|
19
|
+
@retry_attempts = 3
|
|
20
|
+
@retry_delay = 1
|
|
21
|
+
@logger = nil
|
|
22
|
+
@debug = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate!
|
|
26
|
+
raise ConfigurationError, 'api_token is required' if api_token.nil? || api_token.to_s.strip.empty?
|
|
27
|
+
|
|
28
|
+
self.host = host.chomp('/') if host&.end_with?('/')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class APIError < Error; end
|
|
9
|
+
|
|
10
|
+
class AuthenticationError < APIError; end
|
|
11
|
+
|
|
12
|
+
class NotFoundError < APIError; end
|
|
13
|
+
|
|
14
|
+
class RateLimitError < APIError; end
|
|
15
|
+
|
|
16
|
+
class ValidationError < Error; end
|
|
17
|
+
|
|
18
|
+
class TimeoutError < Error; end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(client)
|
|
7
|
+
@client = client
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def get(path, params = {})
|
|
13
|
+
@client.request(:get, path, params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def post(path, body = {})
|
|
17
|
+
@client.request(:post, path, body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def patch(path, body = {})
|
|
21
|
+
@client.request(:patch, path, body)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Broadcasts < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/broadcasts', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_broadcast(id)
|
|
11
|
+
get("/api/v1/broadcasts/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(**attrs)
|
|
15
|
+
post('/api/v1/broadcasts', attrs)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update(id, **attrs)
|
|
19
|
+
patch("/api/v1/broadcasts/#{id}", attrs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete(id)
|
|
23
|
+
@client.request(:delete, "/api/v1/broadcasts/#{id}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def send_broadcast(id)
|
|
27
|
+
post("/api/v1/broadcasts/#{id}/send_broadcast")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def schedule(id, scheduled_send_at:, scheduled_timezone:)
|
|
31
|
+
post("/api/v1/broadcasts/#{id}/schedule_broadcast", {
|
|
32
|
+
scheduled_send_at: scheduled_send_at,
|
|
33
|
+
scheduled_timezone: scheduled_timezone
|
|
34
|
+
})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cancel_schedule(id)
|
|
38
|
+
post("/api/v1/broadcasts/#{id}/cancel_schedule")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def statistics(id)
|
|
42
|
+
get("/api/v1/broadcasts/#{id}/statistics")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def statistics_timeline(id, **params)
|
|
46
|
+
get("/api/v1/broadcasts/#{id}/statistics/timeline", params)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def statistics_links(id, **params)
|
|
50
|
+
get("/api/v1/broadcasts/#{id}/statistics/links", params)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Segments < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/segments.json', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_segment(id, page: nil)
|
|
11
|
+
params = page ? { page: page } : {}
|
|
12
|
+
get("/api/v1/segments/#{id}.json", params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create(**attrs)
|
|
16
|
+
post('/api/v1/segments', { segment: attrs })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update(id, **attrs)
|
|
20
|
+
patch("/api/v1/segments/#{id}", { segment: attrs })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(id)
|
|
24
|
+
@client.request(:delete, "/api/v1/segments/#{id}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Sequences < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/sequences', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_sequence(id, include_steps: false)
|
|
11
|
+
params = include_steps ? { include_steps: true } : {}
|
|
12
|
+
get("/api/v1/sequences/#{id}", params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create(**attrs)
|
|
16
|
+
post('/api/v1/sequences', attrs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update(id, **attrs)
|
|
20
|
+
patch("/api/v1/sequences/#{id}", attrs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(id)
|
|
24
|
+
@client.request(:delete, "/api/v1/sequences/#{id}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# --- Subscriber enrollment ---
|
|
28
|
+
|
|
29
|
+
def add_subscriber(sequence_id, **attrs)
|
|
30
|
+
post("/api/v1/sequences/#{sequence_id}/add_subscriber", attrs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove_subscriber(sequence_id, email:)
|
|
34
|
+
@client.request(:delete, "/api/v1/sequences/#{sequence_id}/remove_subscriber", { email: email })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def list_subscribers(sequence_id, page: 1)
|
|
38
|
+
get("/api/v1/sequences/#{sequence_id}/list_subscribers", { page: page })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# --- Steps ---
|
|
42
|
+
|
|
43
|
+
def list_steps(sequence_id)
|
|
44
|
+
get("/api/v1/sequences/#{sequence_id}/steps")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_step(sequence_id, step_id)
|
|
48
|
+
get("/api/v1/sequences/#{sequence_id}/steps/#{step_id}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_step(sequence_id, **attrs)
|
|
52
|
+
post("/api/v1/sequences/#{sequence_id}/steps", attrs)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def update_step(sequence_id, step_id, **attrs)
|
|
56
|
+
patch("/api/v1/sequences/#{sequence_id}/steps/#{step_id}", attrs)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def move_step(sequence_id, step_id, under_id:)
|
|
60
|
+
post("/api/v1/sequences/#{sequence_id}/steps/#{step_id}/move", { under_id: under_id })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def delete_step(sequence_id, step_id)
|
|
64
|
+
@client.request(:delete, "/api/v1/sequences/#{sequence_id}/steps/#{step_id}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Subscribers < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/subscribers.json', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(email:)
|
|
11
|
+
get('/api/v1/subscribers/find.json', { email: email })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(**attrs)
|
|
15
|
+
post('/api/v1/subscribers.json', { subscriber: attrs })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update(email, **attrs)
|
|
19
|
+
patch('/api/v1/subscribers.json', { email: email, subscriber: attrs })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_tags(email, tags)
|
|
23
|
+
post('/api/v1/subscribers/add_tag.json', { email: email, tags: tags })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_tags(email, tags)
|
|
27
|
+
@client.request(:delete, '/api/v1/subscribers/remove_tag.json', { email: email, tags: tags })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deactivate(email)
|
|
31
|
+
post('/api/v1/subscribers/deactivate.json', { email: email })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def activate(email)
|
|
35
|
+
post('/api/v1/subscribers/activate.json', { email: email })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unsubscribe(email)
|
|
39
|
+
post('/api/v1/subscribers/unsubscribe.json', { email: email })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resubscribe(email)
|
|
43
|
+
post('/api/v1/subscribers/resubscribe.json', { email: email })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def redact(email)
|
|
47
|
+
post('/api/v1/subscribers/redact.json', { email: email })
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Templates < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/templates', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_template(id)
|
|
11
|
+
get("/api/v1/templates/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(**attrs)
|
|
15
|
+
post('/api/v1/templates', { template: attrs })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update(id, **attrs)
|
|
19
|
+
patch("/api/v1/templates/#{id}", { template: attrs })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete(id)
|
|
23
|
+
@client.request(:delete, "/api/v1/templates/#{id}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class WebhookEndpoints < Base
|
|
6
|
+
def list(**params)
|
|
7
|
+
get('/api/v1/webhook_endpoints', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_endpoint(id)
|
|
11
|
+
get("/api/v1/webhook_endpoints/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(**attrs)
|
|
15
|
+
post('/api/v1/webhook_endpoints', { webhook_endpoint: attrs })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update(id, **attrs)
|
|
19
|
+
patch("/api/v1/webhook_endpoints/#{id}", { webhook_endpoint: attrs })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete(id)
|
|
23
|
+
@client.request(:delete, "/api/v1/webhook_endpoints/#{id}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test(id, event_type: 'test.webhook')
|
|
27
|
+
post("/api/v1/webhook_endpoints/#{id}/test", { event_type: event_type })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deliveries(id, **params)
|
|
31
|
+
get("/api/v1/webhook_endpoints/#{id}/deliveries", params)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module Broadcast
|
|
7
|
+
module Webhook
|
|
8
|
+
TIMESTAMP_TOLERANCE = 300 # 5 minutes
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def verify(payload, signature_header, timestamp_header, secret:, now: nil)
|
|
13
|
+
return false if payload.nil? || signature_header.nil? || timestamp_header.nil? || secret.nil?
|
|
14
|
+
|
|
15
|
+
timestamp = timestamp_header.to_i
|
|
16
|
+
current_time = (now || Time.now).to_i
|
|
17
|
+
return false unless timestamp_valid?(timestamp, current_time)
|
|
18
|
+
|
|
19
|
+
expected = compute_signature(payload, timestamp, secret)
|
|
20
|
+
actual = extract_signature(signature_header)
|
|
21
|
+
return false if actual.nil?
|
|
22
|
+
|
|
23
|
+
secure_compare(expected, actual)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def compute_signature(payload, timestamp, secret)
|
|
27
|
+
signed_content = "#{timestamp}.#{payload}"
|
|
28
|
+
hmac = OpenSSL::HMAC.digest('SHA256', secret, signed_content)
|
|
29
|
+
Base64.strict_encode64(hmac)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def timestamp_valid?(timestamp, current_time = Time.now.to_i)
|
|
33
|
+
(current_time - timestamp).abs <= TIMESTAMP_TOLERANCE
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_signature(header)
|
|
37
|
+
return nil unless header&.start_with?('v1,')
|
|
38
|
+
|
|
39
|
+
sig = header.delete_prefix('v1,')
|
|
40
|
+
return nil if sig.empty?
|
|
41
|
+
|
|
42
|
+
sig
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def secure_compare(a, b)
|
|
46
|
+
return false unless a.bytesize == b.bytesize
|
|
47
|
+
|
|
48
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/broadcast.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'broadcast/version'
|
|
4
|
+
require_relative 'broadcast/errors'
|
|
5
|
+
require_relative 'broadcast/configuration'
|
|
6
|
+
require_relative 'broadcast/client'
|
|
7
|
+
require_relative 'broadcast/webhook'
|
|
8
|
+
require_relative 'broadcast/resources/base'
|
|
9
|
+
require_relative 'broadcast/resources/subscribers'
|
|
10
|
+
require_relative 'broadcast/resources/sequences'
|
|
11
|
+
require_relative 'broadcast/resources/broadcasts'
|
|
12
|
+
require_relative 'broadcast/resources/segments'
|
|
13
|
+
require_relative 'broadcast/resources/templates'
|
|
14
|
+
require_relative 'broadcast/resources/webhook_endpoints'
|
|
15
|
+
|
|
16
|
+
module Broadcast
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: broadcast-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Simon Chiu
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Full API client for Broadcast. Subscribers, sequences, broadcasts, segments,
|
|
27
|
+
templates, webhooks, and transactional email. Works with any Broadcast instance.
|
|
28
|
+
email:
|
|
29
|
+
- simon@furvur.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".rubocop.yml"
|
|
35
|
+
- Gemfile
|
|
36
|
+
- Gemfile.lock
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- lib/broadcast.rb
|
|
41
|
+
- lib/broadcast/client.rb
|
|
42
|
+
- lib/broadcast/configuration.rb
|
|
43
|
+
- lib/broadcast/errors.rb
|
|
44
|
+
- lib/broadcast/resources/base.rb
|
|
45
|
+
- lib/broadcast/resources/broadcasts.rb
|
|
46
|
+
- lib/broadcast/resources/segments.rb
|
|
47
|
+
- lib/broadcast/resources/sequences.rb
|
|
48
|
+
- lib/broadcast/resources/subscribers.rb
|
|
49
|
+
- lib/broadcast/resources/templates.rb
|
|
50
|
+
- lib/broadcast/resources/webhook_endpoints.rb
|
|
51
|
+
- lib/broadcast/version.rb
|
|
52
|
+
- lib/broadcast/webhook.rb
|
|
53
|
+
homepage: https://github.com/send-broadcast/broadcast-ruby
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
allowed_push_host: https://rubygems.org
|
|
58
|
+
homepage_uri: https://github.com/send-broadcast/broadcast-ruby
|
|
59
|
+
source_code_uri: https://github.com/send-broadcast/broadcast-ruby
|
|
60
|
+
changelog_uri: https://github.com/send-broadcast/broadcast-ruby/blob/main/CHANGELOG.md
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.2.0
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.7.2
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Ruby client for the Broadcast email platform
|
|
79
|
+
test_files: []
|