zelf-sdk 1.0.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/LICENSE +202 -0
- data/README.md +119 -0
- data/lib/zelf/errors.rb +101 -0
- data/lib/zelf/model.rb +57 -0
- data/lib/zelf/models.rb +1213 -0
- data/lib/zelf/page.rb +114 -0
- data/lib/zelf/resources/ai_analysis.rb +110 -0
- data/lib/zelf/resources/brand_accounts.rb +65 -0
- data/lib/zelf/resources/brands.rb +83 -0
- data/lib/zelf/resources/comments.rb +33 -0
- data/lib/zelf/resources/content.rb +256 -0
- data/lib/zelf/resources/creator_sync.rb +57 -0
- data/lib/zelf/resources/creator_vetting.rb +124 -0
- data/lib/zelf/resources/creators.rb +33 -0
- data/lib/zelf/resources/detection.rb +89 -0
- data/lib/zelf/resources/sub_brands.rb +33 -0
- data/lib/zelf/resources/usage.rb +26 -0
- data/lib/zelf/resources/webhooks.rb +199 -0
- data/lib/zelf/transport.rb +170 -0
- data/lib/zelf/version.rb +5 -0
- data/lib/zelf/webhooks.rb +293 -0
- data/lib/zelf.rb +59 -0
- metadata +64 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Zelf
|
|
7
|
+
# The production ZelfAI API.
|
|
8
|
+
DEFAULT_BASE_URL = 'https://api.hellozelf.com'.freeze
|
|
9
|
+
# Read when no key is passed explicitly.
|
|
10
|
+
API_KEY_ENV = 'ZELF_API_KEY'.freeze
|
|
11
|
+
# Read when no base URL is passed explicitly.
|
|
12
|
+
BASE_URL_ENV = 'ZELF_BASE_URL'.freeze
|
|
13
|
+
|
|
14
|
+
# Carries out the HTTP work: auth, retries, and envelope decoding.
|
|
15
|
+
class Transport
|
|
16
|
+
RETRY_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
17
|
+
# The API documents no Idempotency-Key header, so a retried write could
|
|
18
|
+
# create a second analysis, vetting, or sync job.
|
|
19
|
+
RETRY_SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
|
|
20
|
+
MAX_BACKOFF = 8.0
|
|
21
|
+
|
|
22
|
+
attr_reader :base_url, :max_retries, :retry_writes
|
|
23
|
+
|
|
24
|
+
def initialize(api_key: nil, base_url: nil, timeout: 60,
|
|
25
|
+
max_retries: 2, retry_writes: false, headers: {})
|
|
26
|
+
base_url ||= ENV[BASE_URL_ENV]
|
|
27
|
+
base_url = DEFAULT_BASE_URL if base_url.nil? || base_url.empty?
|
|
28
|
+
key = api_key || ENV[API_KEY_ENV]
|
|
29
|
+
raise ArgumentError, "an API key is required; pass api_key: or set #{API_KEY_ENV}" if key.nil? || key.empty?
|
|
30
|
+
raise ArgumentError, 'max_retries must be zero or greater' if max_retries.negative?
|
|
31
|
+
|
|
32
|
+
@api_key = key
|
|
33
|
+
@base_url = base_url.sub(%r{/+\z}, '')
|
|
34
|
+
@timeout = timeout
|
|
35
|
+
@max_retries = max_retries
|
|
36
|
+
@retry_writes = retry_writes
|
|
37
|
+
# A caller's copy of the API key is dropped whatever its casing, so it
|
|
38
|
+
# cannot replace the client's own.
|
|
39
|
+
@headers = headers.reject { |name, _| name.to_s.casecmp?('x-api-key') }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The key must never reach a log line.
|
|
43
|
+
def inspect
|
|
44
|
+
"#<Zelf::Transport base_url=#{@base_url.inspect} api_key=\"***\">"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_path(path, path_params)
|
|
48
|
+
path_params.each do |name, value|
|
|
49
|
+
if value.nil? || value.to_s.empty?
|
|
50
|
+
raise ArgumentError, "#{name} is required and must not be empty"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
path = path.sub("{#{name}}", URI.encode_www_form_component(value.to_s).gsub('+', '%20'))
|
|
54
|
+
end
|
|
55
|
+
path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.encode_query(query)
|
|
59
|
+
pairs = []
|
|
60
|
+
query.each do |key, value|
|
|
61
|
+
next if value.nil?
|
|
62
|
+
|
|
63
|
+
Array(value).each do |item|
|
|
64
|
+
next if item.nil?
|
|
65
|
+
|
|
66
|
+
pairs << [key.to_s, stringify(item)]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
pairs
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.stringify(value)
|
|
73
|
+
case value
|
|
74
|
+
when true then 'true'
|
|
75
|
+
when false then 'false'
|
|
76
|
+
when Time then value.iso8601
|
|
77
|
+
when Date then value.iso8601
|
|
78
|
+
else value.to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def request(method, path, path_params: {}, query: {}, body: nil)
|
|
83
|
+
resolved = resolve_path(path.dup, path_params)
|
|
84
|
+
pairs = self.class.encode_query(query)
|
|
85
|
+
last = nil
|
|
86
|
+
|
|
87
|
+
(0..@max_retries).each do |attempt|
|
|
88
|
+
response = nil
|
|
89
|
+
begin
|
|
90
|
+
response = send_once(method, resolved, pairs, body)
|
|
91
|
+
return decode(response) if response.code.to_i < 400
|
|
92
|
+
|
|
93
|
+
last = Errors.from_response(response)
|
|
94
|
+
rescue Timeout::Error, Net::OpenTimeout, Net::ReadTimeout => e
|
|
95
|
+
last = TimeoutError.new(e.message)
|
|
96
|
+
rescue SystemCallError, SocketError, IOError => e
|
|
97
|
+
last = ConnectionError.new(e.message)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
break if attempt == @max_retries || !retry?(method, response)
|
|
101
|
+
|
|
102
|
+
sleep(backoff(attempt, response))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
raise(last || ConnectionError.new('the request was never sent'))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def retry?(method, response)
|
|
111
|
+
return false unless RETRY_SAFE_METHODS.include?(method.to_s.upcase) || @retry_writes
|
|
112
|
+
return true if response.nil?
|
|
113
|
+
|
|
114
|
+
RETRY_STATUSES.include?(response.code.to_i)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def backoff(attempt, response)
|
|
118
|
+
header = response && response['retry-after']
|
|
119
|
+
if header
|
|
120
|
+
seconds = Float(header, exception: false)
|
|
121
|
+
return [seconds, 60.0].min if seconds && seconds >= 0
|
|
122
|
+
end
|
|
123
|
+
base = [MAX_BACKOFF, 0.5 * (2**attempt)].min
|
|
124
|
+
base * (0.5 + (rand / 2))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def send_once(method, path, pairs, body)
|
|
128
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
129
|
+
uri.query = URI.encode_www_form(pairs) unless pairs.empty?
|
|
130
|
+
|
|
131
|
+
request_class = Net::HTTP.const_get(method.to_s.capitalize)
|
|
132
|
+
request = request_class.new(uri)
|
|
133
|
+
request['accept'] = 'application/json'
|
|
134
|
+
request['user-agent'] = "zelf-ruby/#{Zelf::VERSION}"
|
|
135
|
+
@headers.each { |name, value| request[name.to_s] = value.to_s }
|
|
136
|
+
# Last, so the client's own key always wins.
|
|
137
|
+
request['x-api-key'] = @api_key
|
|
138
|
+
if body
|
|
139
|
+
request['content-type'] = 'application/json'
|
|
140
|
+
request.body = JSON.generate(body)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
144
|
+
use_ssl: uri.scheme == 'https',
|
|
145
|
+
open_timeout: @timeout,
|
|
146
|
+
read_timeout: @timeout) do |http|
|
|
147
|
+
http.request(request)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def decode(response)
|
|
152
|
+
return {} if response.code.to_i == 204
|
|
153
|
+
|
|
154
|
+
raw = response.body.to_s
|
|
155
|
+
return {} if raw.strip.empty?
|
|
156
|
+
|
|
157
|
+
parsed = begin
|
|
158
|
+
JSON.parse(raw)
|
|
159
|
+
rescue JSON::ParserError
|
|
160
|
+
raise APIError.new('the API returned a response that is not valid JSON',
|
|
161
|
+
status: response.code.to_i)
|
|
162
|
+
end
|
|
163
|
+
unless parsed.is_a?(Hash)
|
|
164
|
+
raise APIError.new('the API returned a response envelope that is not an object',
|
|
165
|
+
status: response.code.to_i)
|
|
166
|
+
end
|
|
167
|
+
parsed
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/zelf/version.rb
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Generated by codegen. Do not edit by hand.
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
module Zelf
|
|
10
|
+
# Verifies and routes the deliveries ZelfAI posts to a receiver.
|
|
11
|
+
module Webhooks
|
|
12
|
+
SIGNATURE_HEADER = 'Webhook-Signature'.freeze
|
|
13
|
+
TIMESTAMP_HEADER = 'Webhook-Timestamp'.freeze
|
|
14
|
+
ID_HEADER = 'Webhook-Id'.freeze
|
|
15
|
+
|
|
16
|
+
# How far a delivery's timestamp may sit from now before it reads as a replay.
|
|
17
|
+
DEFAULT_TOLERANCE_SECONDS = 300
|
|
18
|
+
|
|
19
|
+
# Event types the API can deliver, one per `webhooks` declaration.
|
|
20
|
+
module EventType
|
|
21
|
+
# Fires when an AI analysis run reaches a terminal state (COMPLETED or FAILED)
|
|
22
|
+
AI_ANALYSIS_COMPLETED = "ai_analysis.completed".freeze
|
|
23
|
+
# Scheduled snapshot of the content set that a subscription matches
|
|
24
|
+
CONTENT_SNAPSHOT = "content.snapshot".freeze
|
|
25
|
+
# Scheduled snapshot of the creator set that a subscription matches
|
|
26
|
+
CREATOR_SNAPSHOT = "creator.snapshot".freeze
|
|
27
|
+
# Fires when a creator vetting analysis run reaches a terminal state (COMPLETED or...
|
|
28
|
+
CREATOR_VETTING_COMPLETED = "creator_vetting.completed".freeze
|
|
29
|
+
|
|
30
|
+
ALL = [AI_ANALYSIS_COMPLETED, CONTENT_SNAPSHOT, CREATOR_SNAPSHOT, CREATOR_VETTING_COMPLETED].freeze
|
|
31
|
+
|
|
32
|
+
# Whether the API declares this event type.
|
|
33
|
+
def self.known?(value)
|
|
34
|
+
ALL.include?(value.to_s)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Raised for every failed verification. Answer one with a 400 and never
|
|
39
|
+
# process the body.
|
|
40
|
+
class VerificationError < StandardError; end
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def header(headers, name)
|
|
45
|
+
wanted = name.downcase
|
|
46
|
+
headers.each do |key, value|
|
|
47
|
+
return Array(value).first if key.to_s.downcase == wanted
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_signature(raw)
|
|
53
|
+
timestamp = nil
|
|
54
|
+
digests = []
|
|
55
|
+
raw.to_s.split(',').each do |part|
|
|
56
|
+
key, _, value = part.strip.partition('=')
|
|
57
|
+
if key == 't' && timestamp.nil?
|
|
58
|
+
timestamp = Integer(value, exception: false)
|
|
59
|
+
raise VerificationError, 'signature timestamp is not an integer' if timestamp.nil?
|
|
60
|
+
elsif key == 'v1'
|
|
61
|
+
digests << value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
raise VerificationError, 'signature header is missing a t or v1 component' if timestamp.nil? || digests.empty?
|
|
65
|
+
|
|
66
|
+
[timestamp, digests]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Verify a delivery and return the decoded payload as a plain hash.
|
|
70
|
+
#
|
|
71
|
+
# +payload+ must be the exact bytes received: re-encoding the JSON changes
|
|
72
|
+
# the bytes and the signature will not match.
|
|
73
|
+
#
|
|
74
|
+
# A rotation switches at once: the API signs the next delivery with the new
|
|
75
|
+
# secret only and rejects the old one immediately. An array of secrets is
|
|
76
|
+
# accepted, so both can be held during a rollout.
|
|
77
|
+
#
|
|
78
|
+
# The comparison runs through OpenSSL.secure_compare, which is constant
|
|
79
|
+
# time, so a wrong signature cannot be recovered from response timing.
|
|
80
|
+
def verify(payload, headers, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
|
|
81
|
+
secrets = Array(secret)
|
|
82
|
+
raise ArgumentError, 'at least one signing secret is required' if secrets.empty?
|
|
83
|
+
|
|
84
|
+
raw = header(headers, SIGNATURE_HEADER)
|
|
85
|
+
raise VerificationError, "missing #{SIGNATURE_HEADER} header" if raw.nil? || raw.empty?
|
|
86
|
+
|
|
87
|
+
timestamp, digests = parse_signature(raw)
|
|
88
|
+
|
|
89
|
+
if tolerance_seconds.positive?
|
|
90
|
+
age = ((now || Time.now.to_f) - timestamp).abs
|
|
91
|
+
if age > tolerance_seconds
|
|
92
|
+
raise VerificationError,
|
|
93
|
+
"timestamp is #{age.round}s away from now, outside the " \
|
|
94
|
+
"#{tolerance_seconds}s tolerance, so this is a replay"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
signed = "#{timestamp}.#{payload}"
|
|
99
|
+
matched = false
|
|
100
|
+
secrets.each do |candidate|
|
|
101
|
+
want = OpenSSL::HMAC.hexdigest('SHA256', candidate, signed)
|
|
102
|
+
digests.each do |got|
|
|
103
|
+
matched = true if secure_compare(want, got)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
raise VerificationError, 'no signature matched the supplied secrets' unless matched
|
|
107
|
+
|
|
108
|
+
body = begin
|
|
109
|
+
JSON.parse(payload)
|
|
110
|
+
rescue JSON::ParserError
|
|
111
|
+
raise VerificationError, 'body is not valid JSON'
|
|
112
|
+
end
|
|
113
|
+
raise VerificationError, 'body is not a JSON object' unless body.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
body
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def secure_compare(left, right)
|
|
119
|
+
if OpenSSL.respond_to?(:secure_compare)
|
|
120
|
+
OpenSSL.secure_compare(left, right)
|
|
121
|
+
else
|
|
122
|
+
# Ruby 2.6 has no OpenSSL.secure_compare; compare fixed-width digests
|
|
123
|
+
# byte by byte without an early exit.
|
|
124
|
+
return false unless left.bytesize == right.bytesize
|
|
125
|
+
|
|
126
|
+
left.bytes.zip(right.bytes).reduce(0) { |acc, (a, b)| acc | (a ^ b) }.zero?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Verify a delivery and return it as a typed Event.
|
|
131
|
+
def parse(payload, headers, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
|
|
132
|
+
Event.new(verify(payload, headers, secret,
|
|
133
|
+
tolerance_seconds: tolerance_seconds, now: now))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A delivery whose signature has already been checked.
|
|
137
|
+
class Event
|
|
138
|
+
attr_reader :raw
|
|
139
|
+
|
|
140
|
+
def initialize(raw)
|
|
141
|
+
@raw = raw
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def id
|
|
145
|
+
@raw['id']
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The wire value, which may be newer than this SDK's EVENT_TYPES.
|
|
149
|
+
def type
|
|
150
|
+
@raw['type']
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The declared event type, or nil for one the SDK predates.
|
|
154
|
+
def event_type
|
|
155
|
+
EventType.known?(type) ? type : nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def subscription_id
|
|
159
|
+
@raw['subscription_id']
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def livemode
|
|
163
|
+
@raw['livemode'] ? true : false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def delivered_at
|
|
167
|
+
Time.at(@raw['delivered_at'].to_i).utc
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def delivery_mode
|
|
171
|
+
@raw['delivery_mode']
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def data
|
|
175
|
+
@raw['data'] || {}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def item_count
|
|
179
|
+
data['item_count'].to_i
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Downloadable snapshot files. Populated in +manifest+ mode only.
|
|
183
|
+
def files
|
|
184
|
+
Array(data['files'])
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Rows carried in the body. Populated in +inline+ mode only.
|
|
188
|
+
def items
|
|
189
|
+
Array(data['items'])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def statistics
|
|
193
|
+
data['statistics']
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def inspect
|
|
197
|
+
"#<Zelf::Webhooks::Event #{type} id=#{id} items=#{item_count}>"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Download the manifest files and yield one decoded row per line.
|
|
201
|
+
#
|
|
202
|
+
# Each download_url is signed and expires, so fetch soon after delivery.
|
|
203
|
+
# With +verify_checksum+ the published SHA-256 is recomputed over the
|
|
204
|
+
# body, and a mismatch or a missing digest raises before any row is
|
|
205
|
+
# yielded.
|
|
206
|
+
def rows(verify_checksum: true, http_get: nil)
|
|
207
|
+
return enum_for(:rows, verify_checksum: verify_checksum, http_get: http_get) unless block_given?
|
|
208
|
+
|
|
209
|
+
files.each do |handle|
|
|
210
|
+
body = (http_get || method(:fetch)).call(handle['download_url'])
|
|
211
|
+
if verify_checksum
|
|
212
|
+
published = handle['sha256'].to_s
|
|
213
|
+
if published.empty?
|
|
214
|
+
raise VerificationError,
|
|
215
|
+
"the manifest published no sha256 for #{handle['download_url']}, so the " \
|
|
216
|
+
'file cannot be verified; pass verify_checksum: false to read it unchecked'
|
|
217
|
+
end
|
|
218
|
+
digest = Digest::SHA256.hexdigest(body)
|
|
219
|
+
unless Webhooks.secure_compare(digest, published)
|
|
220
|
+
raise VerificationError,
|
|
221
|
+
"downloaded file digest #{digest} does not match the published #{published}"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
body.each_line do |line|
|
|
225
|
+
next if line.strip.empty?
|
|
226
|
+
|
|
227
|
+
yield JSON.parse(line)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def fetch(url)
|
|
236
|
+
uri = URI.parse(url)
|
|
237
|
+
response = Net::HTTP.get_response(uri)
|
|
238
|
+
unless response.code.to_i < 400
|
|
239
|
+
raise VerificationError, "downloading the snapshot failed with HTTP #{response.code}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
response.body.to_s
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Verify a delivery once, then route it to the listener for its type.
|
|
247
|
+
#
|
|
248
|
+
# Framework agnostic: hand it the raw body and the request headers from
|
|
249
|
+
# whatever server is in front of it.
|
|
250
|
+
class Handler
|
|
251
|
+
def initialize(secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS)
|
|
252
|
+
@secret = secret
|
|
253
|
+
@tolerance_seconds = tolerance_seconds
|
|
254
|
+
@listeners = {}
|
|
255
|
+
@fallback = nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Register a listener for one or more event types.
|
|
259
|
+
def on(*event_types, &listener)
|
|
260
|
+
raise ArgumentError, 'pass at least one event type to listen for' if event_types.empty?
|
|
261
|
+
|
|
262
|
+
unknown = event_types.map(&:to_s).reject { |name| EventType.known?(name) }
|
|
263
|
+
unless unknown.empty?
|
|
264
|
+
raise ArgumentError,
|
|
265
|
+
"unknown event type(s) #{unknown.join(', ')}. " \
|
|
266
|
+
"Known types: #{EventType::ALL.join(', ')}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
event_types.each { |name| (@listeners[name.to_s] ||= []) << listener }
|
|
270
|
+
self
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Register the listener used when no type specific one matched.
|
|
274
|
+
def on_any(&listener)
|
|
275
|
+
@fallback = listener
|
|
276
|
+
self
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Verify, then call every listener registered for the event type. Raises
|
|
280
|
+
# before any listener runs if the delivery does not check out.
|
|
281
|
+
def handle(payload, headers, now: nil)
|
|
282
|
+
event = Webhooks.parse(payload, headers, @secret,
|
|
283
|
+
tolerance_seconds: @tolerance_seconds, now: now)
|
|
284
|
+
listeners = @listeners[event.type]
|
|
285
|
+
if listeners.nil? || listeners.empty?
|
|
286
|
+
return @fallback ? [@fallback.call(event)] : []
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
listeners.map { |listener| listener.call(event) }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
data/lib/zelf.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Generated by codegen. Do not edit by hand.
|
|
2
|
+
require_relative 'zelf/version'
|
|
3
|
+
require_relative 'zelf/errors'
|
|
4
|
+
require_relative 'zelf/model'
|
|
5
|
+
require_relative 'zelf/models'
|
|
6
|
+
require_relative 'zelf/transport'
|
|
7
|
+
require_relative 'zelf/page'
|
|
8
|
+
require_relative 'zelf/webhooks'
|
|
9
|
+
require_relative 'zelf/resources/ai_analysis'
|
|
10
|
+
require_relative 'zelf/resources/brand_accounts'
|
|
11
|
+
require_relative 'zelf/resources/brands'
|
|
12
|
+
require_relative 'zelf/resources/comments'
|
|
13
|
+
require_relative 'zelf/resources/content'
|
|
14
|
+
require_relative 'zelf/resources/creator_sync'
|
|
15
|
+
require_relative 'zelf/resources/creator_vetting'
|
|
16
|
+
require_relative 'zelf/resources/creators'
|
|
17
|
+
require_relative 'zelf/resources/detection'
|
|
18
|
+
require_relative 'zelf/resources/sub_brands'
|
|
19
|
+
require_relative 'zelf/resources/usage'
|
|
20
|
+
require_relative 'zelf/resources/webhooks'
|
|
21
|
+
|
|
22
|
+
# Ruby client for the ZelfAI APIs (spec version 1.0.0).
|
|
23
|
+
module Zelf
|
|
24
|
+
# Client for the ZelfAI APIs.
|
|
25
|
+
#
|
|
26
|
+
# The key comes from the api_key: argument, falling back to the
|
|
27
|
+
# ZELF_API_KEY environment variable. It is sent as the x-api-key header
|
|
28
|
+
# and is kept out of inspect output.
|
|
29
|
+
class Client
|
|
30
|
+
attr_reader :ai_analysis, :brand_accounts, :brands, :comments, :content, :creator_sync, :creator_vetting, :creators, :detection, :sub_brands, :usage, :webhooks
|
|
31
|
+
|
|
32
|
+
def initialize(api_key: nil, base_url: nil, timeout: 60,
|
|
33
|
+
max_retries: 2, retry_writes: false, headers: {})
|
|
34
|
+
@transport = Transport.new(api_key: api_key, base_url: base_url, timeout: timeout,
|
|
35
|
+
max_retries: max_retries, retry_writes: retry_writes,
|
|
36
|
+
headers: headers)
|
|
37
|
+
@ai_analysis = Resources::AiAnalysis.new(@transport)
|
|
38
|
+
@brand_accounts = Resources::BrandAccounts.new(@transport)
|
|
39
|
+
@brands = Resources::Brands.new(@transport)
|
|
40
|
+
@comments = Resources::Comments.new(@transport)
|
|
41
|
+
@content = Resources::Content.new(@transport)
|
|
42
|
+
@creator_sync = Resources::CreatorSync.new(@transport)
|
|
43
|
+
@creator_vetting = Resources::CreatorVetting.new(@transport)
|
|
44
|
+
@creators = Resources::Creators.new(@transport)
|
|
45
|
+
@detection = Resources::Detection.new(@transport)
|
|
46
|
+
@sub_brands = Resources::SubBrands.new(@transport)
|
|
47
|
+
@usage = Resources::Usage.new(@transport)
|
|
48
|
+
@webhooks = Resources::Webhooks.new(@transport)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def base_url
|
|
52
|
+
@transport.base_url
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def inspect
|
|
56
|
+
"#<Zelf::Client base_url=#{base_url.inspect} api_key=\"***\">"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zelf-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zelf
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Official Ruby client for the Zelf API.
|
|
13
|
+
executables: []
|
|
14
|
+
extensions: []
|
|
15
|
+
extra_rdoc_files: []
|
|
16
|
+
files:
|
|
17
|
+
- LICENSE
|
|
18
|
+
- README.md
|
|
19
|
+
- lib/zelf.rb
|
|
20
|
+
- lib/zelf/errors.rb
|
|
21
|
+
- lib/zelf/model.rb
|
|
22
|
+
- lib/zelf/models.rb
|
|
23
|
+
- lib/zelf/page.rb
|
|
24
|
+
- lib/zelf/resources/ai_analysis.rb
|
|
25
|
+
- lib/zelf/resources/brand_accounts.rb
|
|
26
|
+
- lib/zelf/resources/brands.rb
|
|
27
|
+
- lib/zelf/resources/comments.rb
|
|
28
|
+
- lib/zelf/resources/content.rb
|
|
29
|
+
- lib/zelf/resources/creator_sync.rb
|
|
30
|
+
- lib/zelf/resources/creator_vetting.rb
|
|
31
|
+
- lib/zelf/resources/creators.rb
|
|
32
|
+
- lib/zelf/resources/detection.rb
|
|
33
|
+
- lib/zelf/resources/sub_brands.rb
|
|
34
|
+
- lib/zelf/resources/usage.rb
|
|
35
|
+
- lib/zelf/resources/webhooks.rb
|
|
36
|
+
- lib/zelf/transport.rb
|
|
37
|
+
- lib/zelf/version.rb
|
|
38
|
+
- lib/zelf/webhooks.rb
|
|
39
|
+
homepage: https://github.com/hellozelf/zelf-sdk
|
|
40
|
+
licenses:
|
|
41
|
+
- Apache-2.0
|
|
42
|
+
metadata:
|
|
43
|
+
rubygems_mfa_required: 'true'
|
|
44
|
+
source_code_uri: https://github.com/hellozelf/zelf-sdk
|
|
45
|
+
changelog_uri: https://github.com/hellozelf/zelf-sdk/blob/main/CHANGELOG.md
|
|
46
|
+
bug_tracker_uri: https://github.com/hellozelf/zelf-sdk/issues
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.1'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.6.9
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Ruby client for the ZelfAI APIs
|
|
64
|
+
test_files: []
|