calmlens 0.19.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 +110 -100
- 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 +19 -19
- data/lib/calmlens/lenses.rb +80 -6
- data/lib/calmlens/option_keys.rb +64 -20
- data/lib/calmlens/policies.rb +50 -18
- data/lib/calmlens/resource.rb +29 -5
- 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 +722 -348
- 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 +33 -11
- data/lib/calmlens/testing.rb +40 -13
- data/lib/calmlens/transform_rules.rb +120 -0
- data/lib/calmlens/transform_schema.rb +236 -0
- data/lib/calmlens/transforms.rb +16 -0
- data/lib/calmlens/upload_mime_types.rb +51 -0
- data/lib/calmlens/upload_tokens.rb +3 -3
- data/lib/calmlens/version.rb +1 -1
- data/lib/calmlens/webhooks.rb +58 -13
- data/lib/calmlens.rb +3 -0
- data/sig/calmlens.rbs +89 -14
- data/sig/error_codes.rbs +18 -20
- data/sig/qualifiers.rbs +2 -0
- data/sig/types.rbs +326 -185
- metadata +8 -2
data/lib/calmlens/events.rb
CHANGED
|
@@ -8,11 +8,17 @@ module CalmLens
|
|
|
8
8
|
|
|
9
9
|
# Lists events after a cursor, oldest first. Without `cursor`, returns no events and a
|
|
10
10
|
# `nextCursor` for the current time: save it, then pass each response's `nextCursor` as `cursor`.
|
|
11
|
+
# `types` and `asset_id` narrow the feed to some event types or one asset's events. A cursor
|
|
12
|
+
# belongs to the filters it was issued with, so pass the same ones with each `cursor`.
|
|
11
13
|
# @option options [String] :cursor the `nextCursor` of a previous response
|
|
12
14
|
# @option options [Integer] :page_size 1 to 100, default 50
|
|
15
|
+
# @option options [Array<String>] :types event types to list, such as `asset.blocked`
|
|
16
|
+
# @option options [String] :asset_id list only this asset's events
|
|
13
17
|
# @return [Hash] `{'items', 'nextCursor', 'hasMore'}`; `nextCursor` is never nil
|
|
14
18
|
def list(options = {})
|
|
15
|
-
call(options, __method__)
|
|
19
|
+
call(options, __method__) do |api, request|
|
|
20
|
+
request('GET', project('events'), query: feed_query(api), request_options: request)
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
# Iterates pages from `cursor` until `hasMore` is false. Persist each page's `nextCursor` once its
|
|
@@ -48,5 +54,48 @@ module CalmLens
|
|
|
48
54
|
|
|
49
55
|
each_page(options) { |page| page['items'].each(&block) }
|
|
50
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# The API takes the types as one comma-separated value; an empty list filters nothing.
|
|
61
|
+
def feed_query(api)
|
|
62
|
+
types = api['types']
|
|
63
|
+
return api unless api.key?('types')
|
|
64
|
+
raise Internal.invalid('types must be an Array of event type Strings') unless types.is_a?(Array) && types.all?(String)
|
|
65
|
+
|
|
66
|
+
api.merge('types' => types.empty? ? nil : types.join(','))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Reads an event's facts: an {Events#list} item, a webhook delivery body or a {WebhookEvent}.
|
|
71
|
+
# {CalmLens.get_event_fact} and {CalmLens.get_event_facts} call it.
|
|
72
|
+
module EventFacts
|
|
73
|
+
module_function
|
|
74
|
+
|
|
75
|
+
# The value of one of an event's facts, by key, such as `'flagged-categories'`, or nil when the
|
|
76
|
+
# event doesn't carry it. A key the event repeats gives its first value. The fact's `url`, when
|
|
77
|
+
# it has one, is on the fact in the event's `facts`.
|
|
78
|
+
# @param event [WebhookEvent, Hash] a parsed delivery, or an event or delivery body with string keys
|
|
79
|
+
# @param key [String]
|
|
80
|
+
# @return [Object, nil]
|
|
81
|
+
def value(event, key)
|
|
82
|
+
found = facts_of(event).find { |fact| fact['key'] == key }
|
|
83
|
+
found && found['value']
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# An event's facts as a Hash of values by key, such as `{ 'flagged-categories' => ['hate'] }`.
|
|
87
|
+
# A key the event repeats keeps its first value, as {.value} reads it.
|
|
88
|
+
# @param event [WebhookEvent, Hash] a parsed delivery, or an event or delivery body with string keys
|
|
89
|
+
# @return [Hash{String => Object}]
|
|
90
|
+
def values(event)
|
|
91
|
+
facts_of(event).each_with_object({}) { |fact, found| found[fact['key']] = fact['value'] unless found.key?(fact['key']) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The well-formed facts of an event, in order.
|
|
95
|
+
def facts_of(event)
|
|
96
|
+
facts = event.is_a?(Hash) ? event['facts'] : event.facts
|
|
97
|
+
facts.is_a?(Array) ? facts.select { |fact| fact.is_a?(Hash) && fact['key'].is_a?(String) } : []
|
|
98
|
+
end
|
|
99
|
+
private_class_method :facts_of
|
|
51
100
|
end
|
|
52
101
|
end
|
data/lib/calmlens/folders.rb
CHANGED
|
@@ -6,24 +6,29 @@ module CalmLens
|
|
|
6
6
|
# The prefix of this resource's operations in Internal::OPTION_KEYS.
|
|
7
7
|
OPERATIONS = 'folders'
|
|
8
8
|
|
|
9
|
-
# Lists one page of child folders, sorted by name unless `
|
|
9
|
+
# Lists one page of child folders, sorted by name unless `sort` says otherwise.
|
|
10
10
|
# @option options [String] :parent_id list this folder's children; omit for the project root
|
|
11
11
|
# @option options [String] :cursor the previous page's `nextCursor`
|
|
12
12
|
# @option options [Integer] :page_size 1 to 100, default 50
|
|
13
|
-
# @option options [
|
|
13
|
+
# @option options [String] :sort `name` (the default) or `createdAt`, descending when prefixed with `-`
|
|
14
14
|
# @option options [String] :search only children whose name contains it
|
|
15
|
-
# @return [Hash] a cursor page (items, nextCursor, hasMore
|
|
15
|
+
# @return [Hash] a cursor page (items, nextCursor, hasMore) with ancestors.
|
|
16
16
|
def list(options = {})
|
|
17
17
|
call(options, __method__) do |api, request|
|
|
18
|
-
|
|
19
|
-
valid = response.is_a?(Hash) && response['items'].is_a?(Array) && response['ancestors'].is_a?(Array) &&
|
|
20
|
-
[true, false].include?(response['hasMore'])
|
|
21
|
-
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Invalid folder page') unless valid
|
|
22
|
-
|
|
23
|
-
response
|
|
18
|
+
cursor_page(request('GET', project('folders'), query: api, request_options: request), 'folder')
|
|
24
19
|
end
|
|
25
20
|
end
|
|
26
21
|
|
|
22
|
+
# Iterates every child folder of `parent_id` (or the project root), fetching pages as needed.
|
|
23
|
+
# @param options [Hash] the options of {#list}; `cursor` sets the starting position.
|
|
24
|
+
# @yieldparam folder [Hash]
|
|
25
|
+
# @return [Enumerator, nil] an Enumerator when no block is given.
|
|
26
|
+
def each(options = {}, &block)
|
|
27
|
+
return enum_for(:each, options) unless block
|
|
28
|
+
|
|
29
|
+
each_item(options, __method__, method(:list), &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
# @option options [String] :name
|
|
28
33
|
# @option options [String, nil] :parent_id
|
|
29
34
|
# @return [Hash] the folder
|
data/lib/calmlens/hash_lists.rb
CHANGED
|
@@ -8,20 +8,28 @@ module CalmLens
|
|
|
8
8
|
# The prefix of this resource's operations in Internal::OPTION_KEYS.
|
|
9
9
|
OPERATIONS = 'hashLists'
|
|
10
10
|
|
|
11
|
-
# Lists one page of the project's hash lists, newest first.
|
|
11
|
+
# Lists one page of the project's hash lists, newest first unless `sort` says otherwise.
|
|
12
12
|
# @option options [String] :cursor the previous page's `nextCursor`
|
|
13
13
|
# @option options [Integer] :page_size 1 to 100, default 50
|
|
14
|
-
# @
|
|
14
|
+
# @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
|
|
15
|
+
# with `-`; `-createdAt` by default
|
|
16
|
+
# @return [Hash] a cursor page: items, nextCursor and hasMore.
|
|
15
17
|
def list(options = {})
|
|
16
18
|
call(options, __method__) do |api, request|
|
|
17
|
-
|
|
18
|
-
valid = response.is_a?(Hash) && response['items'].is_a?(Array)
|
|
19
|
-
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Invalid hash list page') unless valid
|
|
20
|
-
|
|
21
|
-
response
|
|
19
|
+
cursor_page(request('GET', project('hash-lists'), query: api, request_options: request), 'hash list')
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
|
|
23
|
+
# Iterates every hash list, fetching pages as needed.
|
|
24
|
+
# @param options [Hash] the options of {#list}; `cursor` sets the starting position.
|
|
25
|
+
# @yieldparam hash_list [Hash]
|
|
26
|
+
# @return [Enumerator, nil] an Enumerator when no block is given.
|
|
27
|
+
def each(options = {}, &block)
|
|
28
|
+
return enum_for(:each, options) unless block
|
|
29
|
+
|
|
30
|
+
each_item(options, __method__, method(:list), &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
# @option options [String] :hash_list_id
|
|
26
34
|
# @return [Hash] the hash list
|
|
27
35
|
def retrieve(options)
|
|
@@ -58,17 +66,29 @@ module CalmLens
|
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
|
|
61
|
-
# Lists one page of a list's entries, newest first.
|
|
69
|
+
# Lists one page of a list's entries, newest first unless `sort` says otherwise.
|
|
62
70
|
# @option options [String] :hash_list_id
|
|
63
71
|
# @option options [String] :cursor the previous page's `nextCursor`
|
|
64
72
|
# @option options [Integer] :page_size 1 to 100, default 50
|
|
65
|
-
# @
|
|
73
|
+
# @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
|
|
74
|
+
# with `-`; `-createdAt` by default
|
|
75
|
+
# @return [Hash] a cursor page: items, nextCursor and hasMore.
|
|
66
76
|
def list_entries(options)
|
|
67
77
|
call(options, __method__) do |api, request|
|
|
68
78
|
request('GET', "#{list_path(api)}/entries", query: without(api, 'hashListId'), request_options: request)
|
|
69
79
|
end
|
|
70
80
|
end
|
|
71
81
|
|
|
82
|
+
# Iterates every entry of a list, fetching pages as needed.
|
|
83
|
+
# @param options [Hash] the options of {#list_entries}; `cursor` sets the starting position.
|
|
84
|
+
# @yieldparam entry [Hash]
|
|
85
|
+
# @return [Enumerator, nil] an Enumerator when no block is given.
|
|
86
|
+
def each_entry(options, &block)
|
|
87
|
+
return enum_for(:each_entry, options) unless block
|
|
88
|
+
|
|
89
|
+
each_item(options, __method__, method(:list_entries), &block)
|
|
90
|
+
end
|
|
91
|
+
|
|
72
92
|
# Adds a PDQ hash, or an image asset's hash; a hash already in the list returns its entry.
|
|
73
93
|
# @option options [String] :hash_list_id
|
|
74
94
|
# @option options [String] :hash 64 hexadecimal characters; send this or `asset_id`
|
data/lib/calmlens/internal.rb
CHANGED
|
@@ -13,6 +13,9 @@ module CalmLens
|
|
|
13
13
|
# ASCII characters without a space at either end) and answers `idempotency/invalid-key`.
|
|
14
14
|
IDEMPOTENCY_KEY = /\A[^\r\n\0]+\z/
|
|
15
15
|
HEADER_VALUE = /\A[^\r\n\0]*\z/
|
|
16
|
+
# The CDN resources that take no lens or transform: the exact upload, the adaptive stream, the embed page,
|
|
17
|
+
# the public placeholder and the display info.
|
|
18
|
+
UNSELECTABLE_CDN_RESOURCES = %w[original hls embed placeholder info].freeze
|
|
16
19
|
|
|
17
20
|
module_function
|
|
18
21
|
|
|
@@ -79,6 +82,16 @@ module CalmLens
|
|
|
79
82
|
value.is_a?(Numeric) && value.real? && value.to_f.finite? && value.positive?
|
|
80
83
|
end
|
|
81
84
|
|
|
85
|
+
# Whether the API accepts an upload of this MIME type: an image, audio, video, text, document or
|
|
86
|
+
# website type, not a container such as `application/octet-stream`. It refuses any other type
|
|
87
|
+
# with `upload/invalid-mime-type`.
|
|
88
|
+
def accepted_upload_mime_type?(type)
|
|
89
|
+
mime = type.to_s.split(';', 2).first.to_s.strip.downcase
|
|
90
|
+
return false if UPLOAD_MIME_REJECTED.include?(mime)
|
|
91
|
+
|
|
92
|
+
mime.start_with?(*UPLOAD_MIME_PREFIXES) || mime.end_with?(*UPLOAD_MIME_SUFFIXES) || UPLOAD_MIME_TYPES.include?(mime)
|
|
93
|
+
end
|
|
94
|
+
|
|
82
95
|
# Fetches a required option.
|
|
83
96
|
def required(options, key, code = SdkErrorCodes::INVALID_ARGUMENT)
|
|
84
97
|
raise invalid("#{key} is required", code) unless options.key?(key)
|
|
@@ -93,10 +106,12 @@ module CalmLens
|
|
|
93
106
|
raise invalid('Identifier is required') if text.empty?
|
|
94
107
|
raise invalid('Identifier cannot be "." or ".."') if %w[. ..].include?(text)
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
text.encode(Encoding::UTF_8).b.gsub(/[^A-Za-z0-9\-_.!~*'()]/n) { |byte| format('%%%02X', byte.ord) }
|
|
109
|
+
component(text)
|
|
98
110
|
end
|
|
99
111
|
|
|
112
|
+
# Percent-encodes text like JavaScript's encodeURIComponent.
|
|
113
|
+
def component(text) = text.encode(Encoding::UTF_8).b.gsub(/[^A-Za-z0-9\-_.!~*'()]/n) { |byte| format('%%%02X', byte.ord) }
|
|
114
|
+
|
|
100
115
|
# Splits an absolute URL into its parsed origin and its raw request target. Only the origin is
|
|
101
116
|
# parsed, so the request target is sent exactly as the SDK built it.
|
|
102
117
|
# @return [Array(URI::Generic, String)]
|
|
@@ -123,24 +138,9 @@ module CalmLens
|
|
|
123
138
|
end
|
|
124
139
|
end
|
|
125
140
|
|
|
126
|
-
# Encodes query values the way the API's parser expects: strings as-is, everything else as
|
|
127
|
-
# JSON, and ordering as `ordering[i][field]` pairs.
|
|
141
|
+
# Encodes query values the way the API's parser expects: strings as-is, everything else as JSON.
|
|
128
142
|
def query(params)
|
|
129
|
-
|
|
130
|
-
params.each do |key, value|
|
|
131
|
-
next if value.nil?
|
|
132
|
-
|
|
133
|
-
if key == 'ordering' && value.is_a?(Array)
|
|
134
|
-
value.each_with_index do |order, index|
|
|
135
|
-
raise invalid('Each ordering entry must be a Hash') unless order.is_a?(Hash)
|
|
136
|
-
|
|
137
|
-
order.each { |name, item| encoded["ordering[#{index}][#{name}]"] = item.to_s }
|
|
138
|
-
end
|
|
139
|
-
else
|
|
140
|
-
encoded[key] = value.is_a?(String) ? value : JSON.generate(value)
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
encoded
|
|
143
|
+
params.compact.transform_values { |value| value.is_a?(String) ? value : JSON.generate(value) }
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
146
|
private_constant :Internal
|
data/lib/calmlens/lenses.rb
CHANGED
|
@@ -1,19 +1,93 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CalmLens
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# The project's lenses: named image transforms the CDN applies at `/@<identifier>`. The CDN serves
|
|
5
|
+
# files made with each lens's current definition. Reach it through {ServerClient#lenses};
|
|
6
|
+
# {ServerClient#ensure_lenses} creates or changes lenses by identifier, for applications that
|
|
7
|
+
# declare theirs in code.
|
|
6
8
|
class Lenses < Resource
|
|
7
9
|
# The prefix of this resource's operations in Internal::OPTION_KEYS.
|
|
8
|
-
OPERATIONS =
|
|
10
|
+
OPERATIONS = 'lenses'
|
|
9
11
|
|
|
12
|
+
# Lists one page of the project's lenses, newest first unless `sort` says otherwise.
|
|
13
|
+
# @option options [String] :cursor the previous page's `nextCursor`
|
|
14
|
+
# @option options [Integer] :page_size 1 to 100, default 50
|
|
15
|
+
# @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
|
|
16
|
+
# with `-`; `-createdAt` by default
|
|
17
|
+
# @return [Hash] a cursor page: items, nextCursor and hasMore.
|
|
18
|
+
def list(options = {})
|
|
19
|
+
call(options, __method__) do |api, request|
|
|
20
|
+
cursor_page(request('GET', project('lenses'), query: api, request_options: request), 'lens')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Iterates every lens of the project, fetching pages as needed.
|
|
25
|
+
# @param options [Hash] the options of {#list}; `cursor` sets the starting position.
|
|
26
|
+
# @yieldparam lens [Hash]
|
|
27
|
+
# @return [Enumerator, nil] an Enumerator when no block is given.
|
|
28
|
+
def each(options = {}, &block)
|
|
29
|
+
return enum_for(:each, options) unless block
|
|
30
|
+
|
|
31
|
+
each_item(options, __method__, method(:list), &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Creates a lens; its identifier must be new to the project (`lens/identifier-taken` otherwise).
|
|
35
|
+
# @option options [String] :identifier
|
|
36
|
+
# @option options [String] :name
|
|
37
|
+
# @option options [String] :type `image`
|
|
38
|
+
# @option options [Boolean] :active whether the CDN serves it
|
|
39
|
+
# @option options [String, nil] :description
|
|
40
|
+
# @option options [Hash, String, nil] :transform
|
|
41
|
+
# @return [Hash] the lens
|
|
42
|
+
def create(options)
|
|
43
|
+
call(options, __method__) do |api, request|
|
|
44
|
+
lens = request('POST', project('lenses'), body: api, request_options: request)
|
|
45
|
+
@session.forget_lenses { |cached| cached['identifier'] == lens['identifier'] } if lens.is_a?(Hash)
|
|
46
|
+
lens
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @option options [String] :lens_id
|
|
51
|
+
# @return [Hash] the lens
|
|
52
|
+
def retrieve(options)
|
|
53
|
+
call(options, __method__) { |api, request| request('GET', lens_path(api), request_options: request) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Changes a lens's name, description, type, transform or whether the CDN serves it.
|
|
57
|
+
# @option options [String] :lens_id
|
|
58
|
+
# @return [Hash] the lens
|
|
59
|
+
def update(options)
|
|
60
|
+
call(options, __method__) do |api, request|
|
|
61
|
+
lens = request('PATCH', lens_path(api), body: without(api, 'lensId'), request_options: request)
|
|
62
|
+
@session.forget_lenses { |cached| cached['id'] == lens['id'] } if lens.is_a?(Hash)
|
|
63
|
+
lens
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Deletes a lens; URLs that select it stop being served.
|
|
68
|
+
# @option options [String] :lens_id
|
|
69
|
+
# @return [nil]
|
|
70
|
+
def delete(options)
|
|
71
|
+
call(options, __method__) do |api, request|
|
|
72
|
+
request('DELETE', lens_path(api), request_options: request)
|
|
73
|
+
@session.forget_lenses { |cached| cached['id'] == api['lensId'] }
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lens definitions are sent as given, nil fields included.
|
|
79
|
+
# @api private
|
|
10
80
|
def ensure_lenses(options)
|
|
11
|
-
call(options, __method__) { |api, request| @session.ensure_lenses(api['lenses'], request) }
|
|
81
|
+
call(options, __method__, keep_nil: true, prefix: nil) { |api, request| @session.ensure_lenses(api['lenses'], request) }
|
|
12
82
|
end
|
|
13
83
|
|
|
84
|
+
# @api private
|
|
14
85
|
def ensure_lens(options)
|
|
15
|
-
call(options, __method__) { |api, request| @session.ensure_lenses([api], request).first }
|
|
86
|
+
call(options, __method__, keep_nil: true, prefix: nil) { |api, request| @session.ensure_lenses([api], request).first }
|
|
16
87
|
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def lens_path(api) = "/lenses/#{Internal.segment(api['lensId'])}"
|
|
17
92
|
end
|
|
18
|
-
private_constant :Lenses
|
|
19
93
|
end
|
data/lib/calmlens/option_keys.rb
CHANGED
|
@@ -9,58 +9,102 @@ module CalmLens
|
|
|
9
9
|
'assets.batch' => %w[assetIds operation].freeze,
|
|
10
10
|
'assets.classify' => %w[
|
|
11
11
|
authorId content context description externalId externalUrl folderId isFinal metadata name part
|
|
12
|
-
processingOptions sizeInBytes totalSizeInBytes uploadPresetId url visibility
|
|
12
|
+
processingOptions sizeInBytes tags totalSizeInBytes uploadPresetId url visibility
|
|
13
13
|
].freeze,
|
|
14
14
|
'assets.create' => %w[
|
|
15
15
|
authorId content context description externalId externalUrl folderId isFinal metadata name part
|
|
16
|
-
processingOptions sizeInBytes totalSizeInBytes uploadPresetId url visibility
|
|
16
|
+
processingOptions sizeInBytes tags totalSizeInBytes uploadPresetId url visibility
|
|
17
17
|
].freeze,
|
|
18
|
-
'assets.createSignedUrl' => %w[assetId expiresInSeconds lens resource transform viewerClaims].freeze,
|
|
19
18
|
'assets.decide' => %w[assetId confirmPurge decision note].freeze,
|
|
20
19
|
'assets.delete' => %w[assetId].freeze,
|
|
21
|
-
'assets.
|
|
22
|
-
'assets.
|
|
23
|
-
'assets.
|
|
20
|
+
'assets.each' => %w[cursor filter pageSize sort].freeze,
|
|
21
|
+
'assets.eachChild' => %w[assetId cursor pageSize sort].freeze,
|
|
22
|
+
'assets.eachTimelineSegment' => %w[assetId cursor pageSize].freeze,
|
|
24
23
|
'assets.explain' => %w[assetId rerun].freeze,
|
|
25
|
-
'assets.list' => %w[cursor filter
|
|
26
|
-
'assets.listChildren' => %w[assetId cursor
|
|
24
|
+
'assets.list' => %w[cursor filter pageSize sort].freeze,
|
|
25
|
+
'assets.listChildren' => %w[assetId cursor pageSize sort].freeze,
|
|
26
|
+
'assets.listTimeline' => %w[assetId cursor pageSize].freeze,
|
|
27
27
|
'assets.move' => %w[assetId folderId].freeze,
|
|
28
|
+
'assets.removeContent' => %w[assetId].freeze,
|
|
28
29
|
'assets.retrieve' => %w[assetId].freeze,
|
|
29
30
|
'assets.retry' => %w[assetId].freeze,
|
|
30
|
-
'assets.search' => %w[
|
|
31
|
+
'assets.search' => %w[mode pageSize query].freeze,
|
|
31
32
|
'assets.update' => %w[assetId description externalId externalUrl metadata name tags visibility].freeze,
|
|
32
|
-
'assets.uploadPart' => %w[assetId part
|
|
33
|
-
'
|
|
33
|
+
'assets.uploadPart' => %w[assetId part wait].freeze,
|
|
34
|
+
'cdn.signAssetUrl' => %w[
|
|
35
|
+
assetId expiresIn externalId lens original projectId resource transform viewerClaims
|
|
36
|
+
].freeze,
|
|
37
|
+
'ensureLens' => %w[active description identifier name transform type].freeze,
|
|
34
38
|
'ensureLenses' => %w[lenses].freeze,
|
|
35
|
-
'events.each' => %w[cursor pageSize].freeze,
|
|
36
|
-
'events.eachPage' => %w[cursor pageSize].freeze,
|
|
37
|
-
'events.list' => %w[cursor pageSize].freeze,
|
|
39
|
+
'events.each' => %w[assetId cursor pageSize types].freeze,
|
|
40
|
+
'events.eachPage' => %w[assetId cursor pageSize types].freeze,
|
|
41
|
+
'events.list' => %w[assetId cursor pageSize types].freeze,
|
|
38
42
|
'folders.create' => %w[name parentId].freeze,
|
|
39
43
|
'folders.delete' => %w[folderId].freeze,
|
|
40
|
-
'folders.
|
|
44
|
+
'folders.each' => %w[cursor pageSize parentId search sort].freeze,
|
|
45
|
+
'folders.list' => %w[cursor pageSize parentId search sort].freeze,
|
|
41
46
|
'folders.move' => %w[folderId parentId].freeze,
|
|
42
47
|
'folders.retrieve' => %w[folderId].freeze,
|
|
43
48
|
'folders.update' => %w[folderId name].freeze,
|
|
44
49
|
'hashLists.addEntry' => %w[assetId hash hashListId label quality].freeze,
|
|
45
50
|
'hashLists.create' => %w[action description name].freeze,
|
|
46
51
|
'hashLists.delete' => %w[hashListId].freeze,
|
|
47
|
-
'hashLists.
|
|
48
|
-
'hashLists.
|
|
52
|
+
'hashLists.each' => %w[cursor pageSize sort].freeze,
|
|
53
|
+
'hashLists.eachEntry' => %w[cursor hashListId pageSize sort].freeze,
|
|
54
|
+
'hashLists.list' => %w[cursor pageSize sort].freeze,
|
|
55
|
+
'hashLists.listEntries' => %w[cursor hashListId pageSize sort].freeze,
|
|
49
56
|
'hashLists.removeEntry' => %w[entryId].freeze,
|
|
50
57
|
'hashLists.retrieve' => %w[hashListId].freeze,
|
|
51
58
|
'hashLists.update' => %w[action description hashListId name].freeze,
|
|
52
|
-
'
|
|
59
|
+
'lenses.create' => %w[active description identifier name transform type].freeze,
|
|
60
|
+
'lenses.delete' => %w[lensId].freeze,
|
|
61
|
+
'lenses.each' => %w[cursor pageSize sort].freeze,
|
|
62
|
+
'lenses.list' => %w[cursor pageSize sort].freeze,
|
|
63
|
+
'lenses.retrieve' => %w[lensId].freeze,
|
|
64
|
+
'lenses.update' => %w[active description lensId name transform type].freeze,
|
|
53
65
|
'policies.assign' => %w[policyId].freeze,
|
|
54
66
|
'policies.create' => %w[description name presetId settings].freeze,
|
|
55
67
|
'policies.delete' => %w[policyId].freeze,
|
|
56
|
-
'policies.
|
|
68
|
+
'policies.each' => %w[cursor pageSize sort].freeze,
|
|
69
|
+
'policies.list' => %w[cursor pageSize sort].freeze,
|
|
70
|
+
'policies.reset' => %w[policyId presetVersion revision].freeze,
|
|
57
71
|
'policies.retrieve' => %w[policyId].freeze,
|
|
72
|
+
'policies.retrieveAssigned' => %w[].freeze,
|
|
58
73
|
'policies.setAutomaticUpdates' => %w[enabled policyId presetVersion revision].freeze,
|
|
59
74
|
'policies.update' => %w[description name policyId revision settings].freeze,
|
|
75
|
+
'policies.upgrade' => %w[policyId presetVersion revision].freeze,
|
|
60
76
|
'text.classify' => %w[
|
|
61
|
-
authorId context description externalId externalUrl folderId metadata name processingOptions text
|
|
77
|
+
authorId context description externalId externalUrl folderId metadata name processingOptions tags text
|
|
62
78
|
uploadPresetId visibility
|
|
63
79
|
].freeze
|
|
64
80
|
}.freeze
|
|
81
|
+
|
|
82
|
+
# The option keys each method sends as null when given nil; any other option given as nil
|
|
83
|
+
# is left out, as the API refuses null for it.
|
|
84
|
+
NULLABLE_OPTION_KEYS = {
|
|
85
|
+
'assets.classify' => %w[
|
|
86
|
+
authorId context description externalId folderId metadata processingOptions tags uploadPresetId
|
|
87
|
+
visibility
|
|
88
|
+
].freeze,
|
|
89
|
+
'assets.create' => %w[
|
|
90
|
+
authorId context description externalId folderId metadata processingOptions tags uploadPresetId
|
|
91
|
+
visibility
|
|
92
|
+
].freeze,
|
|
93
|
+
'assets.each' => %w[filter].freeze,
|
|
94
|
+
'assets.list' => %w[filter].freeze,
|
|
95
|
+
'assets.move' => %w[folderId].freeze,
|
|
96
|
+
'assets.update' => %w[description externalId externalUrl metadata tags visibility].freeze,
|
|
97
|
+
'folders.create' => %w[parentId].freeze,
|
|
98
|
+
'folders.move' => %w[parentId].freeze,
|
|
99
|
+
'hashLists.addEntry' => %w[label quality].freeze,
|
|
100
|
+
'hashLists.create' => %w[description].freeze,
|
|
101
|
+
'hashLists.update' => %w[description].freeze,
|
|
102
|
+
'lenses.create' => %w[description transform].freeze,
|
|
103
|
+
'lenses.update' => %w[description transform].freeze,
|
|
104
|
+
'text.classify' => %w[
|
|
105
|
+
authorId context description externalId folderId metadata processingOptions tags uploadPresetId
|
|
106
|
+
visibility
|
|
107
|
+
].freeze
|
|
108
|
+
}.freeze
|
|
65
109
|
end
|
|
66
110
|
end
|
data/lib/calmlens/policies.rb
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CalmLens
|
|
4
|
-
# Moderation policies belong to the
|
|
4
|
+
# Moderation policies belong to the API key's organisation and its projects share them. Every
|
|
5
5
|
# change sends the policy's `revision`, so a policy changed since is refused with
|
|
6
6
|
# `policy/revision-conflict`. Through a project's key you can change only policies no other
|
|
7
|
-
# project uses
|
|
7
|
+
# project uses. Reach it through {ServerClient#policies}.
|
|
8
8
|
class Policies < Resource
|
|
9
9
|
# The prefix of this resource's operations in Internal::OPTION_KEYS.
|
|
10
10
|
OPERATIONS = 'policies'
|
|
11
11
|
|
|
12
|
-
# Lists the organisation's policies, oldest first
|
|
13
|
-
# @
|
|
12
|
+
# Lists one page of the organisation's policies, oldest first unless `sort` says otherwise.
|
|
13
|
+
# @option options [String] :cursor the previous page's `nextCursor`
|
|
14
|
+
# @option options [Integer] :page_size 1 to 100, default 50
|
|
15
|
+
# @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
|
|
16
|
+
# with `-`; `createdAt` by default
|
|
17
|
+
# @return [Hash] a cursor page: items, nextCursor and hasMore.
|
|
14
18
|
def list(options = {})
|
|
15
|
-
call(options, __method__)
|
|
19
|
+
call(options, __method__) do |api, request|
|
|
20
|
+
cursor_page(request('GET', '/policies', query: api, request_options: request), 'policy')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Iterates every policy of the organisation, fetching pages as needed.
|
|
25
|
+
# @param options [Hash] the options of {#list}; `cursor` sets the starting position.
|
|
26
|
+
# @yieldparam policy [Hash]
|
|
27
|
+
# @return [Enumerator, nil] an Enumerator when no block is given.
|
|
28
|
+
def each(options = {}, &block)
|
|
29
|
+
return enum_for(:each, options) unless block
|
|
30
|
+
|
|
31
|
+
each_item(options, __method__, method(:list), &block)
|
|
16
32
|
end
|
|
17
33
|
|
|
18
34
|
# @option options [String] :policy_id
|
|
@@ -21,21 +37,28 @@ module CalmLens
|
|
|
21
37
|
call(options, __method__) { |api, request| request('GET', policy_path(api), request_options: request) }
|
|
22
38
|
end
|
|
23
39
|
|
|
40
|
+
# The policy the project moderates with.
|
|
41
|
+
# @return [Hash] the policy
|
|
42
|
+
def retrieve_assigned(options = {})
|
|
43
|
+
call(options, __method__) { |_api, request| request('GET', project('policy'), request_options: request) }
|
|
44
|
+
end
|
|
45
|
+
|
|
24
46
|
# Creates a policy from `settings` (defining every category) with a `name`, or a copy of the
|
|
25
47
|
# latest release of a built-in preset with `preset_id`. It moderates nothing until it is
|
|
26
48
|
# assigned to a project or named on an upload preset.
|
|
27
49
|
# @option options [String] :name required with `settings`; a preset copy defaults to its name
|
|
28
50
|
# @option options [String] :description
|
|
29
|
-
# @option options [Hash] :settings
|
|
51
|
+
# @option options [Hash] :settings `categorySettings` with every category, each with its
|
|
52
|
+
# `action`, `allowances` and `restrictions`
|
|
30
53
|
# @option options [String] :preset_id `general-community`, `children-platform`, `education-support`
|
|
31
54
|
# or `wholesome-community`
|
|
32
55
|
# @return [Hash] the policy
|
|
33
56
|
def create(options)
|
|
34
|
-
call(options, __method__) { |api, request| request('POST',
|
|
57
|
+
call(options, __method__) { |api, request| request('POST', '/policies', body: api, request_options: request) }
|
|
35
58
|
end
|
|
36
59
|
|
|
37
60
|
# Changes the name, description or settings (complete, defining every category); later
|
|
38
|
-
# moderation uses them. Saving a policy
|
|
61
|
+
# moderation uses them. Saving a `live` policy pins it.
|
|
39
62
|
# @option options [String] :policy_id
|
|
40
63
|
# @option options [Integer] :revision the policy's current revision
|
|
41
64
|
# @option options [String] :name
|
|
@@ -48,22 +71,31 @@ module CalmLens
|
|
|
48
71
|
end
|
|
49
72
|
end
|
|
50
73
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
74
|
+
# Restores the settings of the preset release a `pinned` or `copy` policy is pinned to.
|
|
75
|
+
# @option options [String] :policy_id
|
|
76
|
+
# @option options [Integer] :revision the policy's current revision
|
|
77
|
+
# @option options [Integer] :preset_version the pinned `preset.version` you reviewed
|
|
78
|
+
# @return [Hash] the policy
|
|
79
|
+
def reset(options)
|
|
80
|
+
call(options, __method__) do |api, request|
|
|
81
|
+
request('POST', "#{policy_path(api)}/reset", body: without(api, 'policyId'), request_options: request)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Moves an unchanged `pinned` or `copy` policy to the latest release of its preset.
|
|
53
86
|
# @option options [String] :policy_id
|
|
54
|
-
# @option options [String] :operation `reset` or `update`
|
|
55
87
|
# @option options [Integer] :revision the policy's current revision
|
|
56
|
-
# @option options [Integer] :preset_version
|
|
88
|
+
# @option options [Integer] :preset_version the `preset.latestVersion` you reviewed
|
|
57
89
|
# @return [Hash] the policy
|
|
58
|
-
def
|
|
90
|
+
def upgrade(options)
|
|
59
91
|
call(options, __method__) do |api, request|
|
|
60
|
-
request('POST', "#{policy_path(api)}/
|
|
92
|
+
request('POST', "#{policy_path(api)}/upgrade", body: without(api, 'policyId'), request_options: request)
|
|
61
93
|
end
|
|
62
94
|
end
|
|
63
95
|
|
|
64
|
-
# Switches automatic updates on or off for the
|
|
65
|
-
# the saved
|
|
66
|
-
# current preset version
|
|
96
|
+
# Switches automatic updates on (mode `live`) or off (mode `pinned`) for the organisation's own
|
|
97
|
+
# preset policy. Switching on replaces the saved settings with the latest preset settings under
|
|
98
|
+
# the same ID; switching off pins the current preset version with unchanged settings.
|
|
67
99
|
# @option options [String] :policy_id
|
|
68
100
|
# @option options [Boolean] :enabled
|
|
69
101
|
# @option options [Integer] :revision the policy's current revision
|
|
@@ -95,6 +127,6 @@ module CalmLens
|
|
|
95
127
|
|
|
96
128
|
private
|
|
97
129
|
|
|
98
|
-
def policy_path(api) =
|
|
130
|
+
def policy_path(api) = "/policies/#{Internal.segment(api['policyId'])}"
|
|
99
131
|
end
|
|
100
132
|
end
|
data/lib/calmlens/resource.rb
CHANGED
|
@@ -19,18 +19,25 @@ module CalmLens
|
|
|
19
19
|
# beside a request's; Internal::OPTION_KEYS, generated from the API, has the rest.
|
|
20
20
|
LOCAL_OPTION_KEYS = {
|
|
21
21
|
'assets.waitForDecision' => %w[assetId timeout].freeze,
|
|
22
|
-
'cdn.getAssetUrl' => %w[
|
|
23
|
-
'cdn.serve' => %w[
|
|
24
|
-
'uploadTokens.sign' => %w[
|
|
22
|
+
'cdn.getAssetUrl' => %w[assetId externalId lens projectId resource transform viewerClaims].freeze,
|
|
23
|
+
'cdn.serve' => %w[assetId externalId init lens projectId resource transform viewerClaims].freeze,
|
|
24
|
+
'uploadTokens.sign' => %w[expiresIn externalId uploadPresetId].freeze
|
|
25
25
|
}.freeze
|
|
26
26
|
private_constant :LOCAL_OPTION_KEYS
|
|
27
27
|
|
|
28
28
|
# Runs a public method: splits options, and raises every argument problem as {InvalidArgumentError}.
|
|
29
29
|
# `method` names the public method: an option it doesn't take, such as a misspelt one, raises
|
|
30
30
|
# `sdk/invalid-argument` instead of being silently dropped.
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# An option given as nil is left out, as if omitted, unless the API takes null for it
|
|
32
|
+
# (Internal::NULLABLE_OPTION_KEYS), such as `description: nil` to clear a description;
|
|
33
|
+
# `keep_nil: true` sends every option as given, as for a lens definition. `prefix` names the
|
|
34
|
+
# operation's resource, the resource's own unless the method belongs to the client itself.
|
|
35
|
+
def call(options, method, keep_nil: false, prefix: self.class::OPERATIONS)
|
|
36
|
+
operation = [prefix, Internal.camelize(method)].compact.join('.')
|
|
37
|
+
check_option_keys(options, operation)
|
|
33
38
|
api, request = Internal.options(options)
|
|
39
|
+
nullable = Internal::NULLABLE_OPTION_KEYS.fetch(operation, [])
|
|
40
|
+
api = api.reject { |key, value| value.nil? && !nullable.include?(key) } unless keep_nil
|
|
34
41
|
yield api, request
|
|
35
42
|
rescue Error
|
|
36
43
|
raise
|
|
@@ -78,6 +85,23 @@ module CalmLens
|
|
|
78
85
|
end
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
# A list's response, once it is a cursor page: a Hash with an `items` Array. `what` names the
|
|
89
|
+
# items in the `sdk/invalid-response` error otherwise.
|
|
90
|
+
def cursor_page(response, what)
|
|
91
|
+
return response if response.is_a?(Hash) && response['items'].is_a?(Array)
|
|
92
|
+
|
|
93
|
+
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, "Invalid #{what} page")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Iterates the items of every page `list` returns for `options`, from its `cursor`, yielding each
|
|
97
|
+
# to `block`. `method` names the public iterator, whose options are checked like the list's.
|
|
98
|
+
def each_item(options, method, list, &block)
|
|
99
|
+
api, request = call(options, method) { |*split| split }
|
|
100
|
+
listing = pages(api['cursor']) { |cursor| list.call(api.merge('cursor' => cursor, 'request_options' => request)) }
|
|
101
|
+
listing.each { |page| page['items'].each(&block) }
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
81
105
|
# Records a cursor an iteration sends, raising `sdk/invalid-response` (with `details['cursor']`)
|
|
82
106
|
# when a page with more to follow hands back one it already sent, which would repeat pages forever.
|
|
83
107
|
def new_cursor!(sent, cursor)
|