calmlens 0.20.0 → 0.21.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/README.md +3 -1
- data/lib/calmlens/assets.rb +107 -98
- data/lib/calmlens/cdn.rb +197 -41
- data/lib/calmlens/error_codes.rb +80 -84
- data/lib/calmlens/errors.rb +2 -2
- data/lib/calmlens/events.rb +50 -1
- data/lib/calmlens/folders.rb +14 -9
- data/lib/calmlens/hash_lists.rb +29 -9
- data/lib/calmlens/internal.rb +9 -19
- data/lib/calmlens/lenses.rb +79 -6
- data/lib/calmlens/option_keys.rb +46 -25
- data/lib/calmlens/policies.rb +50 -18
- data/lib/calmlens/resource.rb +24 -6
- data/lib/calmlens/server_client.rb +8 -6
- data/lib/calmlens/session.rb +29 -12
- data/lib/calmlens/testing/asset_filters.rb +39 -0
- data/lib/calmlens/testing/classification.rb +8 -2
- data/lib/calmlens/testing/fake_api.rb +682 -368
- data/lib/calmlens/testing/mock_decision.rb +5 -4
- data/lib/calmlens/testing/rsql.rb +245 -0
- data/lib/calmlens/testing/test_input_detections.rb +61 -0
- data/lib/calmlens/testing/test_input_scenario.rb +6 -2
- data/lib/calmlens/testing/test_inputs.rb +31 -10
- data/lib/calmlens/testing.rb +36 -12
- data/lib/calmlens/transform_rules.rb +1 -1
- data/lib/calmlens/upload_mime_types.rb +9 -1
- data/lib/calmlens/upload_tokens.rb +3 -3
- data/lib/calmlens/version.rb +1 -1
- data/lib/calmlens/webhooks.rb +58 -13
- data/sig/calmlens.rbs +87 -13
- data/sig/error_codes.rbs +18 -20
- data/sig/qualifiers.rbs +2 -0
- data/sig/types.rbs +326 -187
- metadata +5 -2
data/lib/calmlens/webhooks.rb
CHANGED
|
@@ -15,25 +15,34 @@ module CalmLens
|
|
|
15
15
|
WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300
|
|
16
16
|
|
|
17
17
|
# A verified webhook delivery. The body has the shape of an {Events#list} item:
|
|
18
|
-
# `{id, projectId, type, severity, title, description, assetId, asset, facts, createdAt}`.
|
|
18
|
+
# `{id, projectId, actor, type, severity, title, description, assetId, assetSequence, asset, facts, createdAt}`.
|
|
19
19
|
# Fields CalmLens adds later are kept in {#payload}, as are unknown asset and fact fields.
|
|
20
20
|
#
|
|
21
21
|
# @!attribute id [String] the event ID.
|
|
22
22
|
# @!attribute project_id [String]
|
|
23
|
+
# @!attribute actor [Hash, nil] who caused the event: `{'type', 'id'}`, with `type` `user`, `api-key`,
|
|
24
|
+
# `automated` or `system`, and `id` the user's or API key's ID, or nil when it was not recorded.
|
|
23
25
|
# @!attribute type [String] such as `asset.allowed`. New types may appear: ignore unknown ones.
|
|
24
26
|
# @!attribute severity [String, nil] `low` or `high`.
|
|
25
27
|
# @!attribute title [String, nil]
|
|
26
28
|
# @!attribute description [String, nil]
|
|
27
29
|
# @!attribute asset_id [String, nil]
|
|
30
|
+
# @!attribute asset_sequence [Integer, nil] increases with each event about the same asset; nil for
|
|
31
|
+
# events not about an asset. Deliveries can arrive out of order: see {#newer_than?}.
|
|
28
32
|
# @!attribute asset [Hash, nil] the asset's status when the event was created.
|
|
29
33
|
# @!attribute facts [Array<Hash>] `{'key', 'value', 'url'?}` entries registered for the event type.
|
|
30
34
|
# @!attribute created_at [Integer] milliseconds since the epoch.
|
|
31
35
|
# @!attribute payload [Hash{String => Object}] the delivery body as received, with every field.
|
|
32
|
-
WebhookEvent = Struct.new(:id, :project_id, :type, :severity, :title, :description, :asset_id, :
|
|
33
|
-
:created_at, :payload, keyword_init: true) do
|
|
36
|
+
WebhookEvent = Struct.new(:id, :project_id, :actor, :type, :severity, :title, :description, :asset_id, :asset_sequence, :asset,
|
|
37
|
+
:facts, :created_at, :payload, keyword_init: true) do
|
|
34
38
|
# @return [Boolean] true for a test send from the portal or CLI.
|
|
35
39
|
def test? = Webhooks.test_event?(self)
|
|
36
40
|
|
|
41
|
+
# @param last_sequence [Integer, nil] the highest `assetSequence` applied for this event's asset
|
|
42
|
+
# @return [Boolean] whether to apply this event after it.
|
|
43
|
+
# @see Webhooks.newer_asset_event?
|
|
44
|
+
def newer_than?(last_sequence) = Webhooks.newer_asset_event?(self, last_sequence)
|
|
45
|
+
|
|
37
46
|
# @return [Hash{String => Object}] the delivery body as received, with every field.
|
|
38
47
|
def to_payload = payload
|
|
39
48
|
end
|
|
@@ -79,16 +88,34 @@ module CalmLens
|
|
|
79
88
|
facts.is_a?(Array) && facts.any? { |fact| fact.is_a?(Hash) && fact['key'] == 'test' && fact['value'] == true }
|
|
80
89
|
end
|
|
81
90
|
|
|
91
|
+
# Whether to apply `event` after `last_sequence`, the highest `assetSequence` you have applied
|
|
92
|
+
# for its asset (nil when none). Deliveries can arrive out of order: an event about an asset is
|
|
93
|
+
# newer only with a higher `assetSequence`, so a retried or repeated delivery of an earlier one
|
|
94
|
+
# returns false. Events that aren't about an asset have no sequence and are always new. Store
|
|
95
|
+
# the highest `assetSequence` applied per `assetId` and ignore lower ones.
|
|
96
|
+
# @param event [WebhookEvent, Hash] a parsed delivery, or a delivery body with string keys
|
|
97
|
+
# @param last_sequence [Integer, nil]
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def newer_asset_event?(event, last_sequence)
|
|
100
|
+
sequence = event.is_a?(Hash) ? event['assetSequence'] : event.asset_sequence
|
|
101
|
+
sequence.nil? || last_sequence.nil? || sequence > last_sequence
|
|
102
|
+
end
|
|
103
|
+
|
|
82
104
|
# Converts an event from {Events#list} to the JSON body of its webhook delivery, which has the
|
|
83
|
-
# same fields: `description`, `assetId` and `asset` are nil when absent
|
|
105
|
+
# same fields: `description`, `assetId`, `assetSequence` and `asset` are nil when absent, and
|
|
106
|
+
# `actor` is left out when the event has none.
|
|
84
107
|
# @param event [Hash] an event with string keys, as {Events#list} returns it
|
|
85
|
-
# @return [Hash] `{ 'id', 'projectId', 'type', 'severity', 'title', 'description', 'assetId',
|
|
108
|
+
# @return [Hash] `{ 'id', 'projectId', 'actor', 'type', 'severity', 'title', 'description', 'assetId',
|
|
109
|
+
# 'assetSequence', 'asset', 'facts', 'createdAt' }`
|
|
86
110
|
def to_payload(event)
|
|
87
111
|
raise Internal.invalid('event must be a Hash') unless event.is_a?(Hash)
|
|
88
112
|
|
|
89
|
-
{ 'id' => event['id'], 'projectId' => event['projectId']
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
payload = { 'id' => event['id'], 'projectId' => event['projectId'] }
|
|
114
|
+
payload['actor'] = event['actor'] if event.key?('actor')
|
|
115
|
+
payload.merge('type' => event['type'], 'severity' => event['severity'], 'title' => event['title'],
|
|
116
|
+
'description' => event['description'], 'assetId' => event['assetId'],
|
|
117
|
+
'assetSequence' => event['assetSequence'], 'asset' => event['asset'], 'facts' => event['facts'],
|
|
118
|
+
'createdAt' => event['createdAt'])
|
|
92
119
|
end
|
|
93
120
|
|
|
94
121
|
# Checks a delivery's signature and freshness in constant time. Pass the `signature` and
|
|
@@ -149,9 +176,10 @@ module CalmLens
|
|
|
149
176
|
body = JSON.parse(payload.dup.force_encoding(Encoding::UTF_8))
|
|
150
177
|
raise WebhookVerificationError.new(SdkErrorCodes::INVALID_WEBHOOK_PAYLOAD, 'Webhook body is not a CalmLens event') unless event?(body)
|
|
151
178
|
|
|
152
|
-
WebhookEvent.new(id: body['id'], project_id: body['projectId'], type: body['type'], severity: body['severity'],
|
|
179
|
+
WebhookEvent.new(id: body['id'], project_id: body['projectId'], actor: body['actor'], type: body['type'], severity: body['severity'],
|
|
153
180
|
title: body['title'], description: body['description'], asset_id: body['assetId'],
|
|
154
|
-
asset: body['asset'], facts: body.fetch('facts', []),
|
|
181
|
+
asset_sequence: body['assetSequence'], asset: body['asset'], facts: body.fetch('facts', []),
|
|
182
|
+
created_at: body['createdAt'], payload: body)
|
|
155
183
|
rescue JSON::ParserError, EncodingError => e
|
|
156
184
|
raise WebhookVerificationError.new(SdkErrorCodes::INVALID_WEBHOOK_PAYLOAD, 'Webhook body is not JSON'), cause: e
|
|
157
185
|
end
|
|
@@ -163,9 +191,17 @@ module CalmLens
|
|
|
163
191
|
body['projectId'].is_a?(String) && body['createdAt'].is_a?(Numeric)
|
|
164
192
|
|
|
165
193
|
optional = ->(key, *types) { !body.key?(key) || types.any? { |type| type.nil? ? body[key].nil? : body[key].is_a?(type) } }
|
|
166
|
-
asset = body['asset']
|
|
167
194
|
optional.call('severity', String) && optional.call('title', String) && optional.call('description', String, nil) &&
|
|
168
|
-
optional.call('assetId', String, nil) && (
|
|
195
|
+
optional.call('assetId', String, nil) && optional.call('assetSequence', Numeric, nil) && nested?(body)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The nested fields, when present: `actor` is `{type, id}` with a String type and a String or null
|
|
199
|
+
# ID, `asset` has a String `id` and `facts` lists `{key, value, url?}` entries.
|
|
200
|
+
def nested?(body)
|
|
201
|
+
actor = body.fetch('actor', { 'type' => '' })
|
|
202
|
+
asset = body['asset']
|
|
203
|
+
actor.is_a?(Hash) && actor['type'].is_a?(String) && (actor['id'].nil? || actor['id'].is_a?(String)) &&
|
|
204
|
+
(asset.nil? || (asset.is_a?(Hash) && asset['id'].is_a?(String))) &&
|
|
169
205
|
(!body.key?('facts') || (body['facts'].is_a?(Array) && body['facts'].all? { |fact| fact?(fact) }))
|
|
170
206
|
end
|
|
171
207
|
|
|
@@ -191,7 +227,7 @@ module CalmLens
|
|
|
191
227
|
raise Internal.invalid("Webhook payload must be the raw body String, not a #{payload.class}. " \
|
|
192
228
|
'Read request.raw_post (Rails) or request.body.read (Rack, Sinatra) before parsing.')
|
|
193
229
|
end
|
|
194
|
-
private_class_method :decode_event, :event?, :fact?, :header, :raw_body
|
|
230
|
+
private_class_method :decode_event, :event?, :nested?, :fact?, :header, :raw_body
|
|
195
231
|
end
|
|
196
232
|
|
|
197
233
|
# @see Webhooks.signature_header
|
|
@@ -214,4 +250,13 @@ module CalmLens
|
|
|
214
250
|
|
|
215
251
|
# @see Webhooks.test_event?
|
|
216
252
|
def self.test_webhook_event?(...) = Webhooks.test_event?(...)
|
|
253
|
+
|
|
254
|
+
# @see Webhooks.newer_asset_event?
|
|
255
|
+
def self.newer_asset_event?(...) = Webhooks.newer_asset_event?(...)
|
|
256
|
+
|
|
257
|
+
# @see EventFacts.value
|
|
258
|
+
def self.get_event_fact(...) = EventFacts.value(...)
|
|
259
|
+
|
|
260
|
+
# @see EventFacts.values
|
|
261
|
+
def self.get_event_facts(...) = EventFacts.values(...)
|
|
217
262
|
end
|
data/sig/calmlens.rbs
CHANGED
|
@@ -121,6 +121,7 @@ module CalmLens
|
|
|
121
121
|
attr_reader upload_tokens: UploadTokens
|
|
122
122
|
attr_reader events: Events
|
|
123
123
|
attr_reader cdn: CDN
|
|
124
|
+
attr_reader lenses: Lenses
|
|
124
125
|
attr_reader project_id: String
|
|
125
126
|
attr_reader base_url: String
|
|
126
127
|
attr_reader cdn_base_url: String
|
|
@@ -148,16 +149,12 @@ module CalmLens
|
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
class Assets < Resource
|
|
151
|
-
# The
|
|
152
|
-
|
|
153
|
-
# Processing statuses after which no decision arrives without a retry.
|
|
154
|
-
FINISHED_PROCESSING_STATUSES: Array[String]
|
|
152
|
+
# The classification statuses of a settled asset; wait_for_decision returns at the first.
|
|
153
|
+
SETTLED_STATUSES: Array[String]
|
|
155
154
|
DECISION_POLL_FIRST_DELAY: Float
|
|
156
155
|
DECISION_POLL_FACTOR: Float
|
|
157
156
|
DECISION_POLL_MAX_DELAY: Float
|
|
158
157
|
DEFAULT_DECISION_TIMEOUT: Integer
|
|
159
|
-
DEFAULT_SIGNED_URL_SECONDS: Integer
|
|
160
|
-
MAX_SIGNED_URL_SECONDS: Integer
|
|
161
158
|
BATCH_OPERATIONS: Array[asset_batch_operation]
|
|
162
159
|
MAX_BATCH_ASSETS: Integer
|
|
163
160
|
|
|
@@ -168,24 +165,30 @@ module CalmLens
|
|
|
168
165
|
def list_children: (list_asset_children_options options) -> asset_page
|
|
169
166
|
def each_child: (list_asset_children_options options) { (asset) -> void } -> nil
|
|
170
167
|
| (list_asset_children_options options) -> Enumerator[asset, nil]
|
|
168
|
+
def list_timeline: (list_asset_timeline_options options) -> asset_timeline_page
|
|
169
|
+
def each_timeline_segment: (list_asset_timeline_options options) { (media_segment) -> void } -> nil
|
|
170
|
+
| (list_asset_timeline_options options) -> Enumerator[media_segment, nil]
|
|
171
171
|
def retrieve: (asset_id_options options) -> asset
|
|
172
172
|
def wait_for_decision: (wait_for_decision_options options) -> asset
|
|
173
173
|
def update: (update_asset_options options) -> asset
|
|
174
174
|
def retry: (asset_id_options options) -> asset
|
|
175
175
|
def decide: (decide_asset_options options) -> asset
|
|
176
176
|
def explain: (explain_asset_options options) -> asset
|
|
177
|
-
def
|
|
177
|
+
def remove_content: (asset_id_options options) -> asset
|
|
178
178
|
def delete: (asset_id_options options) -> nil
|
|
179
179
|
def batch: (batch_assets_options options) -> batch_assets_response
|
|
180
180
|
def move: (move_asset_options options) -> asset
|
|
181
|
-
def create_signed_url: (create_signed_url_options options) -> signed_url
|
|
182
181
|
def create: (upload_options options) -> asset
|
|
183
182
|
def classify: (upload_options options) -> asset
|
|
184
183
|
def upload_part: (upload_part_options options) -> asset
|
|
184
|
+
# Waits between decision checks; a mock client's waits take no real time.
|
|
185
|
+
private def sleep: (Numeric seconds) -> void
|
|
185
186
|
end
|
|
186
187
|
|
|
187
188
|
class Folders < Resource
|
|
188
189
|
def list: (?list_folders_options options) -> folder_page
|
|
190
|
+
def each: (?list_folders_options options) { (folder) -> void } -> nil
|
|
191
|
+
| (?list_folders_options options) -> Enumerator[folder, nil]
|
|
189
192
|
def create: (create_folder_options options) -> folder
|
|
190
193
|
def retrieve: (folder_id_options options) -> folder
|
|
191
194
|
def update: (update_folder_options options) -> folder
|
|
@@ -195,26 +198,45 @@ module CalmLens
|
|
|
195
198
|
|
|
196
199
|
class HashLists < Resource
|
|
197
200
|
def list: (?list_hash_lists_options options) -> hash_list_page
|
|
201
|
+
def each: (?list_hash_lists_options options) { (hash_list) -> void } -> nil
|
|
202
|
+
| (?list_hash_lists_options options) -> Enumerator[hash_list, nil]
|
|
198
203
|
def retrieve: (hash_list_id_options options) -> hash_list
|
|
199
204
|
def create: (create_hash_list_options options) -> hash_list
|
|
200
205
|
def update: (update_hash_list_options options) -> hash_list
|
|
201
206
|
def delete: (hash_list_id_options options) -> nil
|
|
202
207
|
def list_entries: (list_hash_list_entries_options options) -> hash_list_entry_page
|
|
208
|
+
def each_entry: (list_hash_list_entries_options options) { (hash_list_entry) -> void } -> nil
|
|
209
|
+
| (list_hash_list_entries_options options) -> Enumerator[hash_list_entry, nil]
|
|
203
210
|
def add_entry: (add_hash_list_entry_options options) -> hash_list_entry
|
|
204
211
|
def remove_entry: (hash_list_entry_id_options options) -> nil
|
|
205
212
|
end
|
|
206
213
|
|
|
207
214
|
class Policies < Resource
|
|
208
|
-
def list: (?list_policies_options options) ->
|
|
215
|
+
def list: (?list_policies_options options) -> policy_page
|
|
216
|
+
def each: (?list_policies_options options) { (policy) -> void } -> nil
|
|
217
|
+
| (?list_policies_options options) -> Enumerator[policy, nil]
|
|
209
218
|
def retrieve: (policy_id_options options) -> policy
|
|
219
|
+
# The policy the project moderates with.
|
|
220
|
+
def retrieve_assigned: (?request_only_options options) -> policy
|
|
210
221
|
def create: (create_policy_options options) -> policy
|
|
211
222
|
def update: (update_policy_options options) -> policy
|
|
212
|
-
def
|
|
223
|
+
def reset: (restore_policy_options options) -> policy
|
|
224
|
+
def upgrade: (restore_policy_options options) -> policy
|
|
213
225
|
def set_automatic_updates: (set_policy_automatic_updates_options options) -> policy
|
|
214
226
|
def delete: (policy_id_options options) -> nil
|
|
215
227
|
def assign: (assign_policy_options options) -> policy
|
|
216
228
|
end
|
|
217
229
|
|
|
230
|
+
class Lenses < Resource
|
|
231
|
+
def list: (?list_lenses_options options) -> lens_page
|
|
232
|
+
def each: (?list_lenses_options options) { (lens) -> void } -> nil
|
|
233
|
+
| (?list_lenses_options options) -> Enumerator[lens, nil]
|
|
234
|
+
def create: (create_lens_options options) -> lens
|
|
235
|
+
def retrieve: (lens_id_options options) -> lens
|
|
236
|
+
def update: (update_lens_options options) -> lens
|
|
237
|
+
def delete: (lens_id_options options) -> nil
|
|
238
|
+
end
|
|
239
|
+
|
|
218
240
|
class UploadTokens < Resource
|
|
219
241
|
TOKEN_PREFIX: String
|
|
220
242
|
SIGNING_CONTEXT: String
|
|
@@ -238,7 +260,19 @@ module CalmLens
|
|
|
238
260
|
end
|
|
239
261
|
|
|
240
262
|
class CDN < Resource
|
|
263
|
+
# Every CDN resource beneath an asset URL.
|
|
264
|
+
RESOURCES: Array[cdn_resource]
|
|
265
|
+
# A signed asset URL's lifetime in seconds when `expires_in` is omitted.
|
|
266
|
+
DEFAULT_SIGNED_URL_EXPIRES_IN: Integer
|
|
267
|
+
# The longest signed asset URL lifetime, in seconds.
|
|
268
|
+
MAX_SIGNED_URL_EXPIRES_IN: Integer
|
|
269
|
+
# The start of the asset segment that names an asset by its external ID.
|
|
270
|
+
EXTERNAL_ID_PREFIX: String
|
|
271
|
+
# The ISO 3166-1 alpha-2 countries a viewer's `region` can be.
|
|
272
|
+
VIEWER_COUNTRIES: Array[String]
|
|
241
273
|
def get_asset_url: (asset_url_options options) -> String
|
|
274
|
+
def sign_asset_url: (sign_asset_url_options options) -> signed_asset_url
|
|
275
|
+
def get_asset_info: (String url, ?viewer_claims: viewer_claims?) -> asset_info
|
|
242
276
|
def serve: (serve_options options) -> Response
|
|
243
277
|
| [T] (serve_options options) { (Response) -> T } -> T
|
|
244
278
|
end
|
|
@@ -247,12 +281,16 @@ module CalmLens
|
|
|
247
281
|
class WebhookEvent < Struct[untyped]
|
|
248
282
|
attr_accessor id: String
|
|
249
283
|
attr_accessor project_id: String
|
|
284
|
+
# nil for a delivery without one.
|
|
285
|
+
attr_accessor actor: actor?
|
|
250
286
|
# New types may appear: ignore unknown ones.
|
|
251
287
|
attr_accessor type: event_type | String
|
|
252
288
|
attr_accessor severity: event_severity?
|
|
253
289
|
attr_accessor title: String?
|
|
254
290
|
attr_accessor description: String?
|
|
255
291
|
attr_accessor asset_id: String?
|
|
292
|
+
# Increases with each event about the same asset; nil for events not about an asset.
|
|
293
|
+
attr_accessor asset_sequence: Integer?
|
|
256
294
|
attr_accessor asset: event_asset?
|
|
257
295
|
attr_accessor facts: Array[event_fact]
|
|
258
296
|
# Milliseconds since the Unix epoch.
|
|
@@ -260,9 +298,10 @@ module CalmLens
|
|
|
260
298
|
# The delivery body as received, with every field.
|
|
261
299
|
attr_accessor payload: Hash[String, json_value]
|
|
262
300
|
def initialize: (?id: String, ?project_id: String, ?type: event_type | String, ?severity: event_severity?, ?title: String?,
|
|
263
|
-
?description: String?, ?asset_id: String?, ?asset: event_asset?, ?facts: Array[event_fact],
|
|
301
|
+
?description: String?, ?asset_id: String?, ?asset_sequence: Integer?, ?asset: event_asset?, ?facts: Array[event_fact],
|
|
264
302
|
?created_at: Integer, ?payload: Hash[String, json_value]) -> void
|
|
265
303
|
def test?: () -> bool
|
|
304
|
+
def newer_than?: (Integer? last_sequence) -> bool
|
|
266
305
|
# The delivery body as received, with every field.
|
|
267
306
|
def to_payload: () -> Hash[String, json_value]
|
|
268
307
|
end
|
|
@@ -286,6 +325,16 @@ module CalmLens
|
|
|
286
325
|
def self.parse_event: (payload: String, secret: String, ?signature: String?, ?timestamp: String?, ?headers: webhook_headers?,
|
|
287
326
|
?tolerance_seconds: Integer, ?now: number) -> WebhookEvent
|
|
288
327
|
def self.test_event?: (WebhookEvent | Hash[String, untyped] event) -> bool
|
|
328
|
+
def self.newer_asset_event?: (WebhookEvent | Hash[String, untyped] event, Integer? last_sequence) -> bool
|
|
329
|
+
# The value of an event's fact by key (the first, when the event repeats it), or nil without one.
|
|
330
|
+
def self.get_event_fact: (WebhookEvent | Hash[String, untyped] event, String key) -> event_fact_value
|
|
331
|
+
# An event's facts as values by key; a repeated key keeps its first value.
|
|
332
|
+
def self.get_event_facts: (WebhookEvent | Hash[String, untyped] event) -> Hash[String, event_fact_value]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
module EventFacts
|
|
336
|
+
def self.value: (WebhookEvent | Hash[String, untyped] event, String key) -> event_fact_value
|
|
337
|
+
def self.values: (WebhookEvent | Hash[String, untyped] event) -> Hash[String, event_fact_value]
|
|
289
338
|
end
|
|
290
339
|
|
|
291
340
|
def self.create_webhook_signature_header: (payload: String, timestamp: String | Integer, secret: String) -> String
|
|
@@ -298,6 +347,11 @@ module CalmLens
|
|
|
298
347
|
def self.parse_webhook_event: (payload: String, secret: String, ?signature: String?, ?timestamp: String?,
|
|
299
348
|
?headers: webhook_headers?, ?tolerance_seconds: Integer, ?now: number) -> WebhookEvent
|
|
300
349
|
def self.test_webhook_event?: (WebhookEvent | Hash[String, untyped] event) -> bool
|
|
350
|
+
def self.newer_asset_event?: (WebhookEvent | Hash[String, untyped] event, Integer? last_sequence) -> bool
|
|
351
|
+
# The value of an event's fact by key (the first, when the event repeats it), or nil without one.
|
|
352
|
+
def self.get_event_fact: (WebhookEvent | Hash[String, untyped] event, String key) -> event_fact_value
|
|
353
|
+
# An event's facts as values by key; a repeated key keeps its first value.
|
|
354
|
+
def self.get_event_facts: (WebhookEvent | Hash[String, untyped] event) -> Hash[String, event_fact_value]
|
|
301
355
|
|
|
302
356
|
# require 'calmlens/testing'
|
|
303
357
|
module Testing
|
|
@@ -366,6 +420,8 @@ module CalmLens
|
|
|
366
420
|
def enqueue: (body: json_input, ?status: Integer, ?headers: Hash[String, String]) -> self
|
|
367
421
|
def enqueue_raw: (body: String, ?status: Integer, ?headers: Hash[String, String]) -> self
|
|
368
422
|
def enqueue_error: (Exception error) -> self
|
|
423
|
+
# Fetches a mock CDN URL with a GET and without the API key, as a browser would.
|
|
424
|
+
def fetch_cdn: (String url) -> Response
|
|
369
425
|
def enqueue_event: (mock_event_input event) -> event
|
|
370
426
|
# Applies a reviewer's decision and adds the matching `asset.<decision>` event.
|
|
371
427
|
def decide: (String asset_id, asset_moderation_decision decision) -> asset
|
|
@@ -416,6 +472,12 @@ module CalmLens
|
|
|
416
472
|
QUALIFIERS: Hash[String, Array[String]]
|
|
417
473
|
# Qualifiers the input type sets, which a TestInputs scenario cannot list.
|
|
418
474
|
INPUT_QUALIFIERS: Array[String]
|
|
475
|
+
# Test scenario detections: entries per list, the basis points of a full side, the oldest age
|
|
476
|
+
# and the age presets.
|
|
477
|
+
TEST_DETECTIONS_MAX: Integer
|
|
478
|
+
TEST_DETECTION_BOX_MAX: Integer
|
|
479
|
+
TEST_FACE_AGE_MAX: Integer
|
|
480
|
+
TEST_FACE_AGE_PRESETS: Array[String]
|
|
419
481
|
end
|
|
420
482
|
|
|
421
483
|
# Signed test inputs: text codes, test images and media phrases signed with a project's secret
|
|
@@ -426,9 +488,20 @@ module CalmLens
|
|
|
426
488
|
type regional_action = { "region" => region, "action" => "limit" | "age-restrict" | "block" }
|
|
427
489
|
# One qualifying instance; set an action on every instance or on none.
|
|
428
490
|
type test_instance = { "category" => classification_category, ?"score" => number, ?"action" => action, "qualifiers" => Array[String] }
|
|
491
|
+
# `[left, top, width, height]`: Integers in basis points of the image, 0 to 10000, inside it.
|
|
492
|
+
type detection_box = [Integer, Integer, Integer, Integer]
|
|
493
|
+
# A person a test image reports, with an optional Integer confidence from 0 to 100.
|
|
494
|
+
type test_person = { "box" => detection_box, ?"confidence" => Integer }
|
|
495
|
+
# A face's age: a preset, or a range of Integers from 0 to 120 with `low` at most `high`.
|
|
496
|
+
type test_face_age = "adult" | "minor" | { "low" => Integer, "high" => Integer }
|
|
497
|
+
# A face a test image reports, with an optional confidence and age.
|
|
498
|
+
type test_face = { "box" => detection_box, ?"confidence" => Integer, ?"age" => test_face_age }
|
|
499
|
+
# The people and faces a test image reports: at most 32 of each, or `unavailable` to report the
|
|
500
|
+
# feature as not evaluated.
|
|
501
|
+
type test_detections = { ?"people" => Array[test_person] | "unavailable", ?"faces" => Array[test_face] | "unavailable" }
|
|
429
502
|
# A scenario with String keys; Symbol and snake_case keys are accepted as a plain Hash.
|
|
430
503
|
type scenario = { ?"regionalActions" => Array[regional_action], ?"unassessedCategories" => Array[classification_category],
|
|
431
|
-
"instances" => Array[test_instance] }
|
|
504
|
+
"instances" => Array[test_instance], ?"detections" => test_detections }
|
|
432
505
|
type signal = classification_category | "sexual.involving-minors" | "harassment.insult" | "sexual.adult"
|
|
433
506
|
| "hate.regional-symbol" | "violence.graphic"
|
|
434
507
|
type media_signal = classification_category | "violence.graphic"
|
|
@@ -447,7 +520,8 @@ module CalmLens
|
|
|
447
520
|
REGIONS: Array[region]
|
|
448
521
|
SIGNALS: Array[signal]
|
|
449
522
|
MEDIA_SIGNALS: Array[media_signal]
|
|
450
|
-
# clean, limited, adult_image, adult_text, regional_delivery, germany
|
|
523
|
+
# clean, limited, adult_image, adult_text, regional_delivery, germany, mixed, one_face, no_face,
|
|
524
|
+
# adult_face, minor_face and group (deeply frozen).
|
|
451
525
|
PRESETS: Hash[Symbol, scenario]
|
|
452
526
|
|
|
453
527
|
def self.for_signal: (signal signal, ?("text" | "image") kind) -> scenario
|
data/sig/error_codes.rbs
CHANGED
|
@@ -9,6 +9,8 @@ module CalmLens
|
|
|
9
9
|
REGIONAL_EVIDENCE_REQUIRED: String
|
|
10
10
|
SIGNED_URL_EXPIRED: String
|
|
11
11
|
SIGNED_URL_INVALID: String
|
|
12
|
+
SIGNED_URL_ORIGINAL_NOT_GRANTED: String
|
|
13
|
+
INVALID_REQUEST: String
|
|
12
14
|
INVALID_TYPE: String
|
|
13
15
|
TOO_SMALL: String
|
|
14
16
|
TOO_BIG: String
|
|
@@ -17,7 +19,7 @@ module CalmLens
|
|
|
17
19
|
UNKNOWN_FIELD: String
|
|
18
20
|
UNSUPPORTED_CONTENT_TYPE: String
|
|
19
21
|
INVALID_FILTER: String
|
|
20
|
-
|
|
22
|
+
UNSUPPORTED_ORDERING_FIELD: String
|
|
21
23
|
ROUTE_NOT_FOUND: String
|
|
22
24
|
INTERNAL_ERROR: String
|
|
23
25
|
UNAUTHENTICATED: String
|
|
@@ -26,24 +28,20 @@ module CalmLens
|
|
|
26
28
|
SEARCH_UNAVAILABLE: String
|
|
27
29
|
INVALID_TRANSFORM: String
|
|
28
30
|
INVALID_TRANSFORM_OPTIONS: String
|
|
29
|
-
|
|
30
|
-
COPYRIGHT_CONTENT_HELD: String
|
|
31
|
-
DOWNLOAD_PERMISSION_REQUIRED: String
|
|
31
|
+
REMOTE_URL_TOO_MANY_REDIRECTS: String
|
|
32
32
|
INVALID_CURSOR: String
|
|
33
|
-
|
|
33
|
+
MISSING_API_KEY: String
|
|
34
34
|
INVALID_API_KEY: String
|
|
35
35
|
API_KEY_PROJECT_MISMATCH: String
|
|
36
36
|
API_KEY_NOT_ACCEPTED: String
|
|
37
37
|
PERMISSION_DENIED: String
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
SPEND_LIMIT_REACHED: String
|
|
46
|
-
INVALID_UPLOAD_MIME_TYPE: String
|
|
38
|
+
UPLOAD_TOO_LARGE: String
|
|
39
|
+
DOCUMENT_TOO_MANY_ITEMS: String
|
|
40
|
+
INVALID_IMAGE: String
|
|
41
|
+
IMAGE_TOO_LARGE: String
|
|
42
|
+
FREE_UPLOADS_EXHAUSTED: String
|
|
43
|
+
UPLOADS_PAUSED: String
|
|
44
|
+
UNSUPPORTED_UPLOAD_TYPE: String
|
|
47
45
|
INVALID_UPLOAD_PAYLOAD: String
|
|
48
46
|
USE_UPLOAD_PART: String
|
|
49
47
|
UPLOAD_NOT_OPEN: String
|
|
@@ -53,12 +51,14 @@ module CalmLens
|
|
|
53
51
|
INVALID_IDEMPOTENCY_KEY: String
|
|
54
52
|
IDEMPOTENCY_KEY_REUSED: String
|
|
55
53
|
IDEMPOTENT_REQUEST_IN_PROGRESS: String
|
|
54
|
+
NO_RENDERED_VERSION: String
|
|
56
55
|
ASSET_NOT_FOUND: String
|
|
57
56
|
PROJECT_NOT_FOUND: String
|
|
57
|
+
ASSET_CHILD_NOT_DELETABLE: String
|
|
58
58
|
ASSET_REVIEW_CHANGED: String
|
|
59
59
|
ASSET_REVIEW_NOTE_REQUIRED: String
|
|
60
60
|
ASSET_UPDATE_FAILED: String
|
|
61
|
-
|
|
61
|
+
ASSET_HELD: String
|
|
62
62
|
ASSET_PROCESSING: String
|
|
63
63
|
ASSET_DECISION_NOT_ALLOWED: String
|
|
64
64
|
ASSET_DECISION_UNCHANGED: String
|
|
@@ -74,7 +74,8 @@ module CalmLens
|
|
|
74
74
|
DUPLICATE_FOLDER_NAME: String
|
|
75
75
|
FOLDER_NOT_EMPTY: String
|
|
76
76
|
FOLDER_PARENT_CYCLE: String
|
|
77
|
-
|
|
77
|
+
LENS_NOT_FOUND: String
|
|
78
|
+
LENS_IDENTIFIER_TAKEN: String
|
|
78
79
|
LENS_IDENTIFIER_RESERVED: String
|
|
79
80
|
HASH_LIST_NOT_FOUND: String
|
|
80
81
|
HASH_LIST_ENTRY_NOT_FOUND: String
|
|
@@ -94,9 +95,7 @@ module CalmLens
|
|
|
94
95
|
UPLOAD_PRESET_DISABLED: String
|
|
95
96
|
UPLOAD_PRESET_TOKEN_REQUIRED: String
|
|
96
97
|
REMOTE_URL_NOT_ALLOWED: String
|
|
97
|
-
|
|
98
|
-
UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED: String
|
|
99
|
-
UPLOAD_PRESET_TOO_LARGE: String
|
|
98
|
+
WEBSITE_RATE_LIMIT_REACHED: String
|
|
100
99
|
UPLOAD_PRESET_DAILY_LIMIT_REACHED: String
|
|
101
100
|
UPLOAD_PRESET_ORIGIN_NOT_ALLOWED: String
|
|
102
101
|
UPLOAD_PRESET_FIELD_NOT_ALLOWED: String
|
|
@@ -112,7 +111,6 @@ module CalmLens
|
|
|
112
111
|
APPEAL_ALREADY_REVIEWED: String
|
|
113
112
|
APPEAL_DECISION_CHANGED: String
|
|
114
113
|
APPEAL_INVALID_ACTION: String
|
|
115
|
-
APPEAL_SAFETY_HELD: String
|
|
116
114
|
APPEAL_CONTENT_UNAVAILABLE: String
|
|
117
115
|
STATUSES: Hash[String, Integer]
|
|
118
116
|
end
|
data/sig/qualifiers.rbs
CHANGED
|
@@ -42,6 +42,8 @@ module CalmLens
|
|
|
42
42
|
?"sexual-non-consensual" => qualifier_result,
|
|
43
43
|
?"sexual-intimate-image-abuse" => qualifier_result,
|
|
44
44
|
?"sexual-extreme" => qualifier_result,
|
|
45
|
+
?"sexual-strangulation" => qualifier_result,
|
|
46
|
+
?"sexual-incest" => qualifier_result,
|
|
45
47
|
?"sexual-minors" => qualifier_result,
|
|
46
48
|
?"sexual-solicitation" => qualifier_result,
|
|
47
49
|
?"sexual-paid" => qualifier_result,
|