calmlens 0.1.1 → 0.17.1
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 +133 -58
- data/lib/calmlens/assets.rb +169 -50
- data/lib/calmlens/cdn.rb +29 -7
- data/lib/calmlens/error_codes.rb +82 -4
- data/lib/calmlens/errors.rb +26 -17
- data/lib/calmlens/events.rb +15 -12
- data/lib/calmlens/folders.rb +13 -6
- data/lib/calmlens/hash_lists.rb +95 -0
- data/lib/calmlens/internal.rb +13 -7
- data/lib/calmlens/moderation_context.rb +55 -0
- data/lib/calmlens/resource.rb +23 -6
- data/lib/calmlens/server_client.rb +36 -2
- data/lib/calmlens/session.rb +170 -68
- data/lib/calmlens/testing/classification.rb +21 -0
- data/lib/calmlens/testing/fake_api.rb +946 -233
- data/lib/calmlens/testing/mock_decision.rb +158 -0
- data/lib/calmlens/testing/placeholder.rb +111 -0
- data/lib/calmlens/testing.rb +282 -39
- data/lib/calmlens/text.rb +7 -5
- data/lib/calmlens/transforms.rb +37 -18
- data/lib/calmlens/upload_body.rb +20 -9
- data/lib/calmlens/upload_tokens.rb +14 -6
- data/lib/calmlens/version.rb +1 -1
- data/lib/calmlens/webhooks.rb +127 -32
- data/lib/calmlens.rb +2 -0
- data/sig/calmlens.rbs +168 -39
- data/sig/error_codes.rbs +41 -2
- data/sig/qualifiers.rbs +141 -0
- data/sig/types.rbs +340 -162
- metadata +8 -2
data/lib/calmlens/session.rb
CHANGED
|
@@ -10,12 +10,8 @@ module CalmLens
|
|
|
10
10
|
# The resources share one session per client. It never prints the API key.
|
|
11
11
|
# @api private
|
|
12
12
|
class Session
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
MAX_RETRY_DELAY = 60
|
|
16
|
-
MAX_BACKOFF = 8
|
|
17
|
-
ENSURE_LENSES_MAX_PER_REQUEST = 100
|
|
18
|
-
USER_AGENT = "calmlens-ruby/#{VERSION} (ruby #{RUBY_VERSION})".freeze
|
|
13
|
+
RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
|
|
14
|
+
USER_AGENT = "calmlens-ruby/#{VERSION} (ruby/#{RUBY_VERSION})".freeze
|
|
19
15
|
|
|
20
16
|
# Raised through the transport so an exception from the caller's streaming block is
|
|
21
17
|
# re-raised unchanged instead of being mistaken for a network failure.
|
|
@@ -29,9 +25,12 @@ module CalmLens
|
|
|
29
25
|
end
|
|
30
26
|
|
|
31
27
|
attr_reader :project_id, :base_url, :cdn_base_url
|
|
28
|
+
# Leaves the upload size checks the API also makes (part and image sizes) to the API, so an
|
|
29
|
+
# offline stand-in answers them like the API does. {MockServerClient} sets it.
|
|
30
|
+
attr_accessor :defer_upload_checks
|
|
32
31
|
|
|
33
32
|
def initialize(api_key:, project_id:, base_url:, cdn_base_url:, timeout:, open_timeout:, max_retries:,
|
|
34
|
-
retry_base_delay:, transport:)
|
|
33
|
+
retry_base_delay:, max_response_bytes:, upload_timeout:, transport:)
|
|
35
34
|
@api_key = api_key
|
|
36
35
|
@project_id = project_id
|
|
37
36
|
@base_url = base_url
|
|
@@ -40,6 +39,8 @@ module CalmLens
|
|
|
40
39
|
@open_timeout = open_timeout
|
|
41
40
|
@max_retries = max_retries
|
|
42
41
|
@retry_base_delay = retry_base_delay
|
|
42
|
+
@max_response_bytes = max_response_bytes
|
|
43
|
+
@upload_timeout = upload_timeout
|
|
43
44
|
@transport = transport
|
|
44
45
|
@lenses = {}
|
|
45
46
|
@lens_lock = Mutex.new
|
|
@@ -47,17 +48,32 @@ module CalmLens
|
|
|
47
48
|
|
|
48
49
|
def inspect = "#<#{self.class.name} project_id=#{@project_id.inspect} api_key=[REDACTED]>"
|
|
49
50
|
|
|
51
|
+
# Seconds on a monotonic clock, for decision deadlines. {MockServerClient} replaces it.
|
|
52
|
+
def monotonic = @monotonic ? @monotonic.call : Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
53
|
+
|
|
54
|
+
# Waits between retries and decision checks. {MockServerClient} replaces it.
|
|
55
|
+
def pause(seconds) = @pause ? @pause.call(seconds) : sleep(seconds)
|
|
56
|
+
|
|
57
|
+
# Replaces the clock and wait used between retries and decision checks, so a mock's waits take no
|
|
58
|
+
# real time.
|
|
59
|
+
def use_clock(monotonic:, pause:)
|
|
60
|
+
@monotonic = monotonic
|
|
61
|
+
@pause = pause
|
|
62
|
+
end
|
|
63
|
+
|
|
50
64
|
def close
|
|
51
65
|
@transport.close if @transport.respond_to?(:close)
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
def project(path) = "/projects/#{Internal.segment(@project_id)}/#{path}"
|
|
55
69
|
|
|
56
|
-
# Sends an API request and returns the parsed JSON body.
|
|
70
|
+
# Sends an API request and returns the parsed JSON body (nil for an empty 204 or DELETE response).
|
|
57
71
|
#
|
|
58
72
|
# @param idempotent [Boolean] send an `Idempotency-Key` (generated unless the caller gave one),
|
|
59
73
|
# reused across retries so the API runs the request at most once.
|
|
60
|
-
|
|
74
|
+
# @param upload [Boolean] a binary upload: `upload_timeout`, when set, limits each attempt.
|
|
75
|
+
def request(method, path, request_options:, query: {}, body: nil, raw: nil, headers: {}, idempotent: false,
|
|
76
|
+
upload: false)
|
|
61
77
|
url = @base_url + path
|
|
62
78
|
params = Internal.query(query)
|
|
63
79
|
url += "?#{URI.encode_www_form(params)}" unless params.empty?
|
|
@@ -66,20 +82,25 @@ module CalmLens
|
|
|
66
82
|
raw = JSON.generate(body)
|
|
67
83
|
headers['Content-Type'] = 'application/json'
|
|
68
84
|
end
|
|
69
|
-
if raw.is_a?(UploadBody)
|
|
85
|
+
raw = UploadBody.new(raw) if raw && !raw.is_a?(UploadBody)
|
|
86
|
+
if raw&.size
|
|
70
87
|
headers['Content-Length'] = raw.size.to_s
|
|
71
88
|
elsif raw
|
|
72
|
-
|
|
89
|
+
headers['Transfer-Encoding'] = 'chunked'
|
|
73
90
|
end
|
|
74
91
|
headers['Idempotency-Key'] = request_options[:idempotency_key] || SecureRandom.uuid if idempotent || request_options[:idempotency_key]
|
|
75
|
-
|
|
92
|
+
limit = request_options.fetch(:max_response_bytes, @max_response_bytes)
|
|
93
|
+
deadline = request_options.fetch(:upload_timeout, @upload_timeout) if upload
|
|
94
|
+
execute(method, url, headers, raw, request_options, deadline: deadline) { |response| parse(method, response, limit) }
|
|
76
95
|
end
|
|
77
96
|
|
|
78
|
-
# Sends a CDN request
|
|
97
|
+
# Sends a CDN request, retrying like an API read, and returns the last response rather than
|
|
98
|
+
# raising an error status. With a block, yields the streaming {Response} and returns the block's
|
|
79
99
|
# value; otherwise returns the response with its body read.
|
|
80
100
|
def cdn(method, url, headers, request_options, &block)
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
limit = request_options.fetch(:max_response_bytes, nil)
|
|
102
|
+
execute(method, url, headers, nil, request_options, allow_http_errors: true) do |response|
|
|
103
|
+
next response.tap { response.body(limit: limit) } unless block
|
|
83
104
|
|
|
84
105
|
streaming = Response.new(status: response.status, headers: response.headers) do |&sink|
|
|
85
106
|
response.each_chunk do |chunk|
|
|
@@ -99,17 +120,17 @@ module CalmLens
|
|
|
99
120
|
end
|
|
100
121
|
|
|
101
122
|
# Resolves lens definitions, sending only definitions this client has not saved before, at most
|
|
102
|
-
# ENSURE_LENSES_MAX_PER_REQUEST per request, in order.
|
|
123
|
+
# ServerClient::ENSURE_LENSES_MAX_PER_REQUEST per request, in order. Calls are serialized, so concurrent calls
|
|
124
|
+
# for the same definitions send them once.
|
|
103
125
|
def ensure_lenses(lenses, request_options)
|
|
104
126
|
if !lenses.is_a?(Array) || lenses.empty? || lenses.any? { |lens| !lens.is_a?(Hash) || lens['identifier'].to_s.empty? }
|
|
105
127
|
raise Internal.invalid('At least one lens with an identifier is required')
|
|
106
128
|
end
|
|
107
129
|
|
|
108
130
|
@lens_lock.synchronize do
|
|
109
|
-
keys = lenses.map { |lens|
|
|
131
|
+
keys = lenses.map { |lens| lens_key(lens) }
|
|
110
132
|
missing = keys.zip(lenses).to_h.reject { |key, _| @lenses.key?(key) }
|
|
111
|
-
|
|
112
|
-
missing.each_slice(ENSURE_LENSES_MAX_PER_REQUEST) do |slice|
|
|
133
|
+
missing.each_slice(ServerClient::ENSURE_LENSES_MAX_PER_REQUEST) do |slice|
|
|
113
134
|
chunk = slice.to_h
|
|
114
135
|
returned = request('PUT', project('lenses'), body: { 'lenses' => chunk.values },
|
|
115
136
|
request_options: request_options)
|
|
@@ -118,56 +139,113 @@ module CalmLens
|
|
|
118
139
|
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Unexpected lens response')
|
|
119
140
|
end
|
|
120
141
|
|
|
121
|
-
resolved.merge!(chunk.keys.zip(returned).to_h)
|
|
122
142
|
# A new version of a lens replaces the cached definition with the same identifier.
|
|
123
143
|
chunk.keys.zip(returned).each do |key, lens|
|
|
124
144
|
@lenses.delete_if { |_, saved| saved['identifier'] == lens['identifier'] }
|
|
125
145
|
@lenses[key] = lens
|
|
126
146
|
end
|
|
127
147
|
end
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
end
|
|
148
|
+
keys.map do |key|
|
|
149
|
+
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'CalmLens did not return a lens') unless @lenses.key?(key)
|
|
131
150
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
returned.zip(sent).all? { |saved, definition| saved.is_a?(Hash) && saved['identifier'] == definition['identifier'] }
|
|
151
|
+
JSON.parse(JSON.generate(@lenses[key]))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
136
154
|
end
|
|
137
155
|
|
|
138
156
|
def headers_for(extra)
|
|
139
|
-
extra.reject { |name, _| %w[x-api-key user-agent].include?(name.downcase) }
|
|
157
|
+
extra.reject { |name, _| %w[x-api-key user-agent authorization].include?(name.downcase) }
|
|
140
158
|
.merge('X-Api-Key' => @api_key, 'User-Agent' => USER_AGENT)
|
|
141
159
|
end
|
|
142
160
|
|
|
143
161
|
private
|
|
144
162
|
|
|
145
|
-
|
|
163
|
+
# The fields that identify a lens definition, as canonical JSON: an unset description or
|
|
164
|
+
# transforms list is the same definition as an explicit null.
|
|
165
|
+
def lens_key(lens)
|
|
166
|
+
fields = { 'identifier' => lens['identifier'], 'name' => lens['name'], 'description' => lens['description'],
|
|
167
|
+
'type' => lens['type'], 'transforms' => lens['transforms'] }
|
|
168
|
+
%w[active version].each { |name| fields[name] = lens[name] if lens.key?(name) }
|
|
169
|
+
JSON.generate(canonical(fields))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def canonical(value)
|
|
173
|
+
case value
|
|
174
|
+
when Hash then value.sort_by { |key, _| key.to_s }.to_h { |key, item| [key.to_s, canonical(item)] }
|
|
175
|
+
when Array then value.map { |item| canonical(item) }
|
|
176
|
+
else value
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Whether the saved lenses line up, one for one and in order, with the definitions that were sent.
|
|
181
|
+
def lenses_match?(returned, sent)
|
|
182
|
+
returned.is_a?(Array) && returned.size == sent.size &&
|
|
183
|
+
returned.zip(sent).all? { |saved, definition| saved.is_a?(Hash) && saved['identifier'] == definition['identifier'] }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Sends a request, retrying transport failures, timeouts, 429, 502–504 and
|
|
187
|
+
# `idempotency/request-in-progress` when it is safe to repeat: reads, and requests with an
|
|
188
|
+
# `Idempotency-Key` whose body can be sent again. Redirects are never followed.
|
|
189
|
+
def execute(method, url, headers, body, request_options, allow_http_errors: false, deadline: nil, &handler)
|
|
146
190
|
retries = request_options.fetch(:max_retries, @max_retries)
|
|
147
|
-
|
|
191
|
+
retryable = retries.positive? && (%w[GET HEAD].include?(method) || headers.key?('Idempotency-Key')) &&
|
|
192
|
+
(body.nil? || body.replayable?)
|
|
148
193
|
request = Transport::Request.new(method: method, url: url, headers: headers_for(headers), body: body&.content,
|
|
149
194
|
open_timeout: request_options.fetch(:open_timeout, @open_timeout),
|
|
150
195
|
timeout: request_options.fetch(:timeout, @timeout))
|
|
196
|
+
limit = request_options.fetch(:max_response_bytes, @max_response_bytes)
|
|
151
197
|
attempt = 0
|
|
152
|
-
|
|
198
|
+
loop do
|
|
153
199
|
body&.rewind if attempt.positive?
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
context = { again: retryable && attempt < retries, attempt: attempt, limit: limit, deadline: deadline,
|
|
201
|
+
allow_http_errors: allow_http_errors }
|
|
202
|
+
outcome, value = send_attempt(request, context, &handler)
|
|
203
|
+
if outcome == :error
|
|
204
|
+
delay = http_delay(value.retry_after, attempt)
|
|
205
|
+
raise value unless context[:again] && delay <= ServerClient::RETRY_AFTER_MAX && value.retryable?
|
|
206
|
+
|
|
207
|
+
outcome = :retry
|
|
208
|
+
value = delay
|
|
158
209
|
end
|
|
159
|
-
|
|
160
|
-
raise unless retry_allowed && !delivered && attempt < retries && e.retryable? && (body.nil? || body.replayable?)
|
|
161
|
-
|
|
162
|
-
delay = retry_delay(attempt, e.retry_after)
|
|
163
|
-
raise if delay.nil?
|
|
210
|
+
return value if outcome == :ok
|
|
164
211
|
|
|
165
|
-
|
|
212
|
+
pause(value)
|
|
166
213
|
attempt += 1
|
|
167
|
-
retry
|
|
168
214
|
end
|
|
169
215
|
end
|
|
170
216
|
|
|
217
|
+
# One attempt: `[:ok, result]`, `[:error, error]` for an error response, or `[:retry, seconds]`.
|
|
218
|
+
# A response returned to the caller (or its streaming block) is never retried.
|
|
219
|
+
def send_attempt(request, context, &handler)
|
|
220
|
+
delivered = false
|
|
221
|
+
within(context[:deadline]) do
|
|
222
|
+
send_request(request) do |response|
|
|
223
|
+
raise redirect_error(response) if redirect?(response)
|
|
224
|
+
|
|
225
|
+
if context[:allow_http_errors] && !response.success?
|
|
226
|
+
delay = http_delay(Error.parse_retry_after(response.headers['retry-after']), context[:attempt])
|
|
227
|
+
retry_status = RETRYABLE_STATUSES.include?(response.status) && delay <= ServerClient::RETRY_AFTER_MAX
|
|
228
|
+
next [:retry, delay] if context[:again] && retry_status
|
|
229
|
+
end
|
|
230
|
+
next [:error, http_error(response, context[:limit])] unless response.success? || context[:allow_http_errors]
|
|
231
|
+
|
|
232
|
+
delivered = true
|
|
233
|
+
[:ok, handler.call(response)]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
rescue Error => e
|
|
237
|
+
raise unless context[:again] && !delivered && e.retryable?
|
|
238
|
+
|
|
239
|
+
[:retry, backoff(context[:attempt])]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Runs one attempt within an upload deadline, when there is one.
|
|
243
|
+
def within(deadline, &)
|
|
244
|
+
deadline ? ::Timeout.timeout(deadline, &) : yield
|
|
245
|
+
rescue ::Timeout::Error => e
|
|
246
|
+
raise TimeoutError.new(SdkErrorCodes::TIMEOUT, 'Request timed out'), cause: e
|
|
247
|
+
end
|
|
248
|
+
|
|
171
249
|
def send_request(request, &handler)
|
|
172
250
|
yielded = false
|
|
173
251
|
result = nil
|
|
@@ -185,9 +263,9 @@ module CalmLens
|
|
|
185
263
|
rescue Error
|
|
186
264
|
raise
|
|
187
265
|
rescue ::Timeout::Error => e
|
|
188
|
-
raise TimeoutError.new(SdkErrorCodes::TIMEOUT,
|
|
266
|
+
raise TimeoutError.new(SdkErrorCodes::TIMEOUT, 'Request timed out'), cause: e
|
|
189
267
|
rescue *Transport::ERRORS => e
|
|
190
|
-
raise ConnectionError.new(SdkErrorCodes::TRANSPORT_ERROR, e.message), cause: e
|
|
268
|
+
raise ConnectionError.new(SdkErrorCodes::TRANSPORT_ERROR, "Request failed: #{e.message}"), cause: e
|
|
191
269
|
rescue ArgumentError, URI::Error => e
|
|
192
270
|
raise InvalidArgumentError.new(SdkErrorCodes::INVALID_ARGUMENT, e.message), cause: e
|
|
193
271
|
end
|
|
@@ -201,45 +279,69 @@ module CalmLens
|
|
|
201
279
|
Response.new(status: status, headers: headers, body: value.body.to_s)
|
|
202
280
|
end
|
|
203
281
|
|
|
204
|
-
|
|
205
|
-
|
|
282
|
+
# A redirect the SDK must not follow: it would send the API key to another origin.
|
|
283
|
+
def redirect?(response) = response.status.between?(300, 399) && response.headers.key?('location')
|
|
206
284
|
|
|
207
|
-
|
|
285
|
+
def redirect_error(response)
|
|
286
|
+
Error.build(SdkErrorCodes::HTTP_ERROR, "Unexpected redirect (HTTP #{response.status})", status: response.status,
|
|
287
|
+
headers: response.headers)
|
|
208
288
|
end
|
|
209
289
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
raise invalid_body(response) if response.success?
|
|
290
|
+
# Exponential backoff with jitter: half the delay is fixed, the other half random.
|
|
291
|
+
def backoff(attempt)
|
|
292
|
+
delay = [@retry_base_delay * (2**attempt), ServerClient::RETRY_MAX_DELAY].min
|
|
293
|
+
((delay / 2.0) + (rand * delay / 2.0)).round(3)
|
|
294
|
+
end
|
|
216
295
|
|
|
217
|
-
|
|
218
|
-
end
|
|
219
|
-
return data if response.success?
|
|
296
|
+
def http_delay(retry_after, attempt) = retry_after.nil? ? backoff(attempt) : retry_after
|
|
220
297
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
raise if
|
|
298
|
+
def read(response, limit)
|
|
299
|
+
declared = response.content_length
|
|
300
|
+
raise too_large(response, limit) if declared && declared > limit
|
|
224
301
|
|
|
225
|
-
|
|
302
|
+
response.body(limit: limit)
|
|
303
|
+
rescue InvalidResponseError => e
|
|
304
|
+
raise e unless e.code == SdkErrorCodes::RESPONSE_TOO_LARGE
|
|
305
|
+
|
|
306
|
+
raise too_large(response, limit)
|
|
226
307
|
end
|
|
227
308
|
|
|
228
|
-
def
|
|
229
|
-
InvalidResponseError.new(SdkErrorCodes::
|
|
230
|
-
headers: response.headers)
|
|
309
|
+
def too_large(response, limit)
|
|
310
|
+
InvalidResponseError.new(SdkErrorCodes::RESPONSE_TOO_LARGE, "Response body exceeds #{limit} bytes",
|
|
311
|
+
status: response.status, headers: response.headers)
|
|
231
312
|
end
|
|
232
313
|
|
|
233
|
-
def
|
|
314
|
+
def parse(method, response, limit)
|
|
315
|
+
text = read(response, limit).dup.force_encoding(Encoding::UTF_8)
|
|
316
|
+
return nil if text.empty? && (response.status == 204 || method == 'DELETE')
|
|
317
|
+
|
|
318
|
+
JSON.parse(text)
|
|
319
|
+
rescue JSON::ParserError, EncodingError
|
|
320
|
+
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, "Invalid JSON response (HTTP #{response.status})",
|
|
321
|
+
status: response.status, headers: response.headers)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# The error for a non-2xx response: the API's code and details, or `sdk/http-error`.
|
|
325
|
+
def http_error(response, limit)
|
|
326
|
+
text = read(response, limit).dup.force_encoding(Encoding::UTF_8)
|
|
327
|
+
data = begin
|
|
328
|
+
JSON.parse(text)
|
|
329
|
+
rescue JSON::ParserError, EncodingError
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
234
332
|
unless data.is_a?(Hash) && data['code'].is_a?(String)
|
|
235
333
|
return Error.build(SdkErrorCodes::HTTP_ERROR, "HTTP #{response.status}", status: response.status,
|
|
236
334
|
headers: response.headers)
|
|
237
335
|
end
|
|
238
336
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
337
|
+
# The X-Request-Id header, or the body's `requestId` when a proxy dropped the header.
|
|
338
|
+
request_id = response.headers['x-request-id'] || (data['requestId'] if data['requestId'].is_a?(String))
|
|
339
|
+
message = data['message'].is_a?(String) ? data['message'] : nil
|
|
340
|
+
details = data['details'].is_a?(Hash) ? data['details'] : nil
|
|
341
|
+
Error.build(data['code'], message, status: response.status, details: details, headers: response.headers,
|
|
342
|
+
request_id: request_id)
|
|
343
|
+
rescue InvalidResponseError => e
|
|
344
|
+
e
|
|
243
345
|
end
|
|
244
346
|
end
|
|
245
347
|
private_constant :Session
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by packages/sdk-contract/mockClassification.ts from packages/shared/src/ModerationCategories.ts and ModerationPolicyDimensions.ts; do not edit.
|
|
4
|
+
module CalmLens
|
|
5
|
+
module MockClassification
|
|
6
|
+
# The parent categories, in the API's order.
|
|
7
|
+
CATEGORIES = ["sexual", "violence", "self-harm", "harassment", "hate", "criminal-activity", "personal-information", "profanity"].freeze
|
|
8
|
+
|
|
9
|
+
# Each category's qualifiers.
|
|
10
|
+
QUALIFIERS = {
|
|
11
|
+
"sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
|
|
12
|
+
"violence" => ["violence-high-impact", "violence-attack", "violence-threat", "violence-preparation", "violence-encouragement", "violence-wish", "violence-coded-incitement", "violence-extremism", "violence-injury", "violence-violent-death", "violence-natural-death", "violence-graphic", "violence-decapitation-act", "violence-gore", "violence-weapon", "violence-sport", "violence-text", "violence-historical", "violence-documentary", "violence-educational", "violence-discussion", "violence-fictional", "violence-fantasy", "violence-support-prevention"].freeze,
|
|
13
|
+
"self-harm" => ["self-harm-age-restricted", "self-harm-thoughts", "self-harm-preparation", "self-harm-act", "self-harm-methods", "self-harm-encouragement", "self-harm-historical", "self-harm-documentary", "self-harm-educational", "self-harm-fictional", "self-harm-support-prevention"].freeze,
|
|
14
|
+
"harassment" => ["harassment-abuse", "harassment-mild", "harassment-bullying", "harassment-public-target", "harassment-threat", "harassment-sexual", "harassment-encouragement", "harassment-historical", "harassment-documentary", "harassment-educational", "harassment-fictional", "harassment-support-prevention"].freeze,
|
|
15
|
+
"hate" => ["hate-nazi-symbols-public-display", "hate-nazi-insignia-public-display", "hate-nazi-genocide-public-peace", "hate-nazi-genocide-denial-or-approval", "hate-adjudicated-nazi-crimes-denial", "hate-abuse", "hate-slurs", "hate-dehumanization", "hate-inferiority", "hate-exclusion", "hate-symbol", "hate-hostility", "hate-threat", "hate-personal", "hate-historical", "hate-documentary", "hate-educational", "hate-fictional", "hate-counterspeech", "hate-support-prevention"].freeze,
|
|
16
|
+
"criminal-activity" => ["criminal-activity-demonstration", "criminal-activity-preparation", "criminal-activity-request", "criminal-activity-instructions", "criminal-activity-promotion", "criminal-activity-coded-incitement", "criminal-activity-substances", "criminal-activity-grooming", "criminal-activity-csam", "criminal-activity-alcohol", "criminal-activity-tobacco", "criminal-activity-gambling", "criminal-activity-fraud", "criminal-activity-sexual-solicitation", "criminal-activity-graffiti", "criminal-activity-historical", "criminal-activity-documentary", "criminal-activity-educational", "criminal-activity-discussion", "criminal-activity-fictional", "criminal-activity-support-prevention"].freeze,
|
|
17
|
+
"personal-information" => ["personal-information-contact", "personal-information-email", "personal-information-phone", "personal-information-government-id", "personal-information-financial", "personal-information-location", "personal-information-identifiers", "personal-information-credentials", "personal-information-request", "personal-information-self-disclosure", "personal-information-third-party", "personal-information-historical", "personal-information-documentary", "personal-information-educational", "personal-information-fictional", "personal-information-support-prevention"].freeze,
|
|
18
|
+
"profanity" => ["profanity-mild", "profanity-strong", "profanity-slurs", "profanity-obfuscated", "profanity-historical", "profanity-documentary", "profanity-educational", "profanity-fictional", "profanity-support-prevention"].freeze,
|
|
19
|
+
}.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|