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.
data/lib/zelf/page.rb ADDED
@@ -0,0 +1,114 @@
1
+ module Zelf
2
+ # One page of results, plus the envelope metadata that came with it.
3
+ #
4
+ # The page keeps the request that produced it, so the next page is fetched by
5
+ # re-issuing that request with a new +page+ value.
6
+ class Page
7
+ include Enumerable
8
+
9
+ attr_reader :results, :log_id, :page, :item_count, :next_page_number,
10
+ :previous_page_number, :applied_filters
11
+
12
+ def initialize(envelope, model, transport, path, query)
13
+ @results = Array(envelope['results']).map { |row| model ? model.new(row) : row }
14
+ @log_id = envelope['log_id']
15
+ @page = envelope['page']
16
+ @item_count = envelope['item_count']
17
+ @next_page_number = envelope['next']
18
+ @previous_page_number = envelope['previous']
19
+ @applied_filters = envelope['applied_filters'] || {}
20
+ @model = model
21
+ @transport = transport
22
+ @path = path
23
+ @query = query
24
+ end
25
+
26
+ def each(&block)
27
+ @results.each(&block)
28
+ end
29
+
30
+ def size
31
+ @results.size
32
+ end
33
+
34
+ def next?
35
+ !@next_page_number.nil?
36
+ end
37
+
38
+ def inspect
39
+ "#<Zelf::Page page=#{@page} item_count=#{@item_count} next=#{next?}>"
40
+ end
41
+
42
+ # The page after this one, or nil when there is none.
43
+ def next_page
44
+ return nil unless next?
45
+
46
+ query = @query.reject { |key, _| key.to_s == 'page' }.merge('page' => @next_page_number)
47
+ envelope = @transport.request(:get, @path, query: query)
48
+ Page.new(envelope, @model, @transport, @path, query)
49
+ end
50
+
51
+ # Yield this page and the ones after it.
52
+ #
53
+ # +max_pages+ counts this page as the first, so <tt>max_pages: 1</tt>
54
+ # yields only what has already been fetched and makes no further request.
55
+ def pages(max_pages: nil)
56
+ return enum_for(:pages, max_pages: max_pages) unless block_given?
57
+
58
+ check_limit!(max_pages, 'max_pages')
59
+ current = self
60
+ seen = 0
61
+ while current
62
+ yield current
63
+ seen += 1
64
+ break if max_pages && seen >= max_pages
65
+
66
+ current = current.next_page
67
+ end
68
+ end
69
+
70
+ # Walk the remaining pages, yielding one item at a time.
71
+ #
72
+ # +max_pages+ bounds how many pages are fetched and +max_items+ how many
73
+ # items come back. Either one stops the walk, so no page is requested once
74
+ # enough items are in hand.
75
+ def auto_paging(max_pages: nil, max_items: nil)
76
+ return enum_for(:auto_paging, max_pages: max_pages, max_items: max_items) unless block_given?
77
+
78
+ check_limit!(max_items, 'max_items')
79
+ produced = 0
80
+ pages(max_pages: max_pages) do |page|
81
+ page.results.each do |row|
82
+ yield row
83
+ produced += 1
84
+ return nil if max_items && produced >= max_items
85
+ end
86
+ end
87
+ nil
88
+ end
89
+
90
+ private
91
+
92
+ def check_limit!(value, name)
93
+ return if value.nil?
94
+ raise ArgumentError, "#{name} must be 1 or greater" if value < 1
95
+ end
96
+ end
97
+
98
+ # Take the single row out of the envelope.
99
+ #
100
+ # Reads that match nothing come back as 200 with an empty +results+ array
101
+ # rather than a 404, so the empty case is raised as a not-found error.
102
+ def self.one(envelope, model, what)
103
+ rows = Array(envelope['results'])
104
+ if rows.empty?
105
+ raise NotFoundError.new("No #{what} matched the request.",
106
+ status: 404, code: 'NOT_FOUND', log_id: envelope['log_id'])
107
+ end
108
+ model ? model.new(rows.first) : rows.first
109
+ end
110
+
111
+ def self.rows(envelope, model)
112
+ Array(envelope['results']).map { |row| model ? model.new(row) : row }
113
+ end
114
+ end
@@ -0,0 +1,110 @@
1
+ # Generated by codegen. Do not edit by hand.
2
+ module Zelf
3
+ module Resources
4
+ # The ai_analysis endpoints.
5
+ class AiAnalysis
6
+ def initialize(transport)
7
+ @transport = transport
8
+ end
9
+
10
+ # Get AI Analysis Content Statistics. Returns aggregate totals (post count, views,
11
+ # engagement, earned media value, and unique creators) over the content an AI analysis
12
+ # ran over. Requires `analysis_id` (an analysis `id` from `ai_analysis.list`) and
13
+ # returns the same totals shape as `content.statistics`.
14
+ #
15
+ # Calls GET /api/v1/ai_analysis/contents/statistics/ (ai_analysis.content_statistics).
16
+ def content_statistics(analysis_id:)
17
+ query = {
18
+ "analysis_id" => analysis_id,
19
+ }.reject { |_, value| value.nil? }
20
+ envelope = @transport.request(:get, "/api/v1/ai_analysis/contents/statistics/", query: query)
21
+ Zelf.one(envelope, Models::ContentStatistics, "ContentStatistics")
22
+ end
23
+
24
+ # List AI Analysis Content. Returns the content an AI analysis ran over, in the
25
+ # standard content shape. Use it to inspect the exact posts included in an analysis.
26
+ # Requires `analysis_id` (an analysis `id` from `ai_analysis.list`). Brand-scoped
27
+ # fields (`is_owned`, `is_tagged`, `net_promoter_score`, ...) are `null` in this
28
+ # response.
29
+ #
30
+ # Calls GET /api/v1/ai_analysis/contents/ (ai_analysis.contents).
31
+ def contents(analysis_id:, page: nil, page_size: nil)
32
+ query = {
33
+ "analysis_id" => analysis_id,
34
+ "page" => page,
35
+ "page_size" => page_size,
36
+ }.reject { |_, value| value.nil? }
37
+ envelope = @transport.request(:get, "/api/v1/ai_analysis/contents/", query: query)
38
+ path = @transport.resolve_path("/api/v1/ai_analysis/contents/".dup, {})
39
+ Zelf::Page.new(envelope, Models::Content, @transport, path, query)
40
+ end
41
+
42
+ # Create AI Analysis. Starts an AI analysis over the content matched by
43
+ # `content_filters` (or its comments) for a given `topic`. Runs complete
44
+ # asynchronously; this method returns immediately with the created analysis and its
45
+ # initial runs. The analysis runs over the highest-ranked matching content by
46
+ # `content_filters.order_by`, up to 500 posts or the limit configured for your
47
+ # workspace. When `order_by` is omitted, content is ranked by
48
+ # `earned_media_value_cents` descending. The created analysis is returned as the
49
+ # single item in `results`. Take its `id` and poll `ai_analysis.list` until a run
50
+ # reports `COMPLETED`. An analysis can take an hour or more, so poll at a moderate
51
+ # interval; prefer the `ai_analysis.completed` webhook over frequent polling, which
52
+ # can trigger `RATE_LIMIT_EXCEEDED`. Once complete, retrieve the payload from
53
+ # `ai_analysis.results`, passing `analysis_id=<id>`.
54
+ #
55
+ # Calls POST /api/v1/ai_analysis/ (ai_analysis.create).
56
+ def create(name:, topic:, content_filters:, analysis_scope: nil)
57
+ body = {
58
+ "name" => name,
59
+ "topic" => topic,
60
+ "content_filters" => content_filters,
61
+ "analysis_scope" => analysis_scope,
62
+ }.reject { |_, value| value.nil? }
63
+ envelope = @transport.request(:post, "/api/v1/ai_analysis/", body: body)
64
+ Zelf.one(envelope, Models::AiAnalysis, "AiAnalysis")
65
+ end
66
+
67
+ # List AI Analyses. Returns the AI analyses that have been created, ordered newest
68
+ # first, each with its runs and their current `status`. It lists the analyses
69
+ # available and tracks run progress without retrieving the full result payload. An
70
+ # analysis `id` maps to `analysis_id` on `ai_analysis.results` and
71
+ # `ai_analysis.contents`, and each run's `id` maps to `id` on `ai_analysis.results`.
72
+ # The run `result` download is not included in this response; retrieve it from
73
+ # `ai_analysis.results` once a run reports `COMPLETED` (statuses: `IN_PROGRESS`,
74
+ # `COMPLETED`, `EMPTY_RESULT`, `FAILED`). Supply `id` to return a single analysis.
75
+ #
76
+ # Calls GET /api/v1/ai_analysis/ (ai_analysis.list).
77
+ def list(id: nil, page: nil, page_size: nil)
78
+ query = {
79
+ "id" => id,
80
+ "page" => page,
81
+ "page_size" => page_size,
82
+ }.reject { |_, value| value.nil? }
83
+ envelope = @transport.request(:get, "/api/v1/ai_analysis/", query: query)
84
+ path = @transport.resolve_path("/api/v1/ai_analysis/".dup, {})
85
+ Zelf::Page.new(envelope, Models::AiAnalysis, @transport, path, query)
86
+ end
87
+
88
+ # Get AI Analysis Results. Returns each run's `result`, a signed, time-limited
89
+ # download of the analysis payload. Call this method after a run reaches `COMPLETED`;
90
+ # for earlier statuses the row is returned with a `null` `result`. Supply
91
+ # `analysis_id` (the analysis `id` from `ai_analysis.list`) to return every run of
92
+ # that analysis, or `id` (a single run's `id`) to return one run. Exactly one is
93
+ # required. A malformed id returns `INVALID_ID`; omitting both returns
94
+ # `VALIDATION_ERROR`.
95
+ #
96
+ # Calls GET /api/v1/ai_analysis/results/ (ai_analysis.results).
97
+ def results(analysis_id: nil, id: nil, page: nil, page_size: nil)
98
+ query = {
99
+ "analysis_id" => analysis_id,
100
+ "id" => id,
101
+ "page" => page,
102
+ "page_size" => page_size,
103
+ }.reject { |_, value| value.nil? }
104
+ envelope = @transport.request(:get, "/api/v1/ai_analysis/results/", query: query)
105
+ path = @transport.resolve_path("/api/v1/ai_analysis/results/".dup, {})
106
+ Zelf::Page.new(envelope, Models::AiAnalysisResult, @transport, path, query)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,65 @@
1
+ # Generated by codegen. Do not edit by hand.
2
+ module Zelf
3
+ module Resources
4
+ # The brand_accounts endpoints.
5
+ class BrandAccounts
6
+ def initialize(transport)
7
+ @transport = transport
8
+ end
9
+
10
+ # Add Brand Accounts. Adds profiles to a brand's `tracked`, `influencer` or `owned`
11
+ # list. Use `owned` for the brand's own profiles. Each `profile_url` is matched and
12
+ # deduplicated by `(platform, username)`, so re-adding an existing account has no
13
+ # effect. URLs that cannot be parsed fail validation on `profile_urls`. Supply
14
+ # `brand_id` in the request body to target another brand; otherwise the brand the key
15
+ # acts on is used. Any brand listed by `brands.list` is a valid target, which includes
16
+ # linked top-level brands and their sub-brands.
17
+ #
18
+ # Calls POST /api/v1/brands/accounts/ (brand_accounts.add).
19
+ def add(account_type:, profile_urls:, brand_id: nil)
20
+ body = {
21
+ "account_type" => account_type,
22
+ "profile_urls" => profile_urls,
23
+ "brand_id" => brand_id,
24
+ }.reject { |_, value| value.nil? }
25
+ envelope = @transport.request(:post, "/api/v1/brands/accounts/", body: body)
26
+ Zelf.rows(envelope, Models::BrandAccountGroups)
27
+ end
28
+
29
+ # List Brand Accounts. Returns a brand's tracked and influencer creator accounts, plus
30
+ # the `owned` accounts the brand operates itself. Defaults to the brand the key acts
31
+ # on. Supply `brand_id` to target another brand listed by `brands.list`, which
32
+ # includes linked top-level brands and their sub-brands. Restrict the response to a
33
+ # single list with the `account_type` parameter (`tracked`, `influencer` or `owned`).
34
+ #
35
+ # Calls GET /api/v1/brands/accounts/ (brand_accounts.list).
36
+ def list(account_type: nil, brand_id: nil)
37
+ query = {
38
+ "account_type" => account_type,
39
+ "brand_id" => brand_id,
40
+ }.reject { |_, value| value.nil? }
41
+ envelope = @transport.request(:get, "/api/v1/brands/accounts/", query: query)
42
+ Zelf.rows(envelope, Models::BrandAccountGroups)
43
+ end
44
+
45
+ # Remove Brand Accounts. Removes profiles from a brand's `tracked`, `influencer` or
46
+ # `owned` list. This operation is destructive; call it only when the intent is to stop
47
+ # following these accounts. Profiles are matched by `(platform, username)` parsed from
48
+ # each `profile_url`. Returns the updated list. Supply `brand_id` in the request body
49
+ # to target another brand; otherwise the brand the key acts on is used. Any brand
50
+ # listed by `brands.list` is a valid target, which includes linked top-level brands
51
+ # and their sub-brands.
52
+ #
53
+ # Calls DELETE /api/v1/brands/accounts/ (brand_accounts.remove).
54
+ def remove(account_type:, profile_urls:, brand_id: nil)
55
+ body = {
56
+ "account_type" => account_type,
57
+ "profile_urls" => profile_urls,
58
+ "brand_id" => brand_id,
59
+ }.reject { |_, value| value.nil? }
60
+ envelope = @transport.request(:delete, "/api/v1/brands/accounts/", body: body)
61
+ Zelf.rows(envelope, Models::BrandAccountGroups)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ # Generated by codegen. Do not edit by hand.
2
+ module Zelf
3
+ module Resources
4
+ # The brands endpoints.
5
+ class Brands
6
+ def initialize(transport)
7
+ @transport = transport
8
+ end
9
+
10
+ # Search Brand Catalog. Returns the brands Zelf curates for competitive benchmarking.
11
+ # Every api key can read content for these brands, whether or not they are linked to
12
+ # yours, so this is where to look for a competitor that is not one of your own sub-
13
+ # brands. Each row's `id` is the value to supply as `brand_id` on the content and AI
14
+ # analysis endpoints; the brand write endpoints do not accept it. Narrow the list with
15
+ # `search` (title substring), `industry_id`, or `category_id`; the id values for the
16
+ # last two come from `brands.catalog_categories`.
17
+ #
18
+ # Calls GET /api/v1/brands/catalog/ (brands.catalog).
19
+ def catalog(category_id: nil, industry_id: nil, order_by: nil, page: nil, search: nil)
20
+ query = {
21
+ "category_id" => category_id,
22
+ "industry_id" => industry_id,
23
+ "order_by" => order_by,
24
+ "page" => page,
25
+ "search" => search,
26
+ }.reject { |_, value| value.nil? }
27
+ envelope = @transport.request(:get, "/api/v1/brands/catalog/", query: query)
28
+ path = @transport.resolve_path("/api/v1/brands/catalog/".dup, {})
29
+ Zelf::Page.new(envelope, Models::BrandCatalog, @transport, path, query)
30
+ end
31
+
32
+ # List Brand Catalog Categories. Returns every industry and the categories under it,
33
+ # as one flat list. An industry has `level: industry` and a null `parent_id`; a
34
+ # category has `level: category` and the `parent_id` of its industry. Each `id` is the
35
+ # value to supply as `industry_id` or `category_id` to `brands.catalog`.
36
+ #
37
+ # Calls GET /api/v1/brands/catalog/categories/ (brands.catalog_categories).
38
+ def catalog_categories
39
+ envelope = @transport.request(:get, "/api/v1/brands/catalog/categories/")
40
+ Zelf.rows(envelope, Models::BrandCatalogItem)
41
+ end
42
+
43
+ # List Brands. Returns every brand this key can reach: the workspace brand, every
44
+ # brand linked to it, and the sub-brands of each. Start here when building a new
45
+ # integration, to find out which brand ids are available. Each row's `id` is the value
46
+ # to supply as `brand_id` wherever a brand is named. `is_primary` identifies the
47
+ # workspace brand, and `parent_brand_id` links a sub-brand to its parent.
48
+ #
49
+ # Calls GET /api/v1/brands/ (brands.list).
50
+ def list(page: nil)
51
+ query = {
52
+ "page" => page,
53
+ }.reject { |_, value| value.nil? }
54
+ envelope = @transport.request(:get, "/api/v1/brands/", query: query)
55
+ path = @transport.resolve_path("/api/v1/brands/".dup, {})
56
+ Zelf::Page.new(envelope, Models::Brand, @transport, path, query)
57
+ end
58
+
59
+ # Update Brand. Partially updates a brand's monitoring keywords. Only the supplied
60
+ # fields are modified. Targets the brand the key acts on unless `brand_id` is provided
61
+ # in the request body. Any brand listed by `brands.list` is a valid target. That list
62
+ # holds every brand linked to the key, each linked brand's sub-brands, and the key's
63
+ # own brand. A single key therefore reaches whole brand families, often dozens of
64
+ # brands, and this endpoint writes to all of them. Returns the updated brand.
65
+ #
66
+ # Calls PATCH /api/v1/brands/ (brands.update).
67
+ def update(search_terms: nil, inclusion_terms: nil, exclusion_terms: nil, compound_inclusion_terms: nil, influencer_terms: nil, allowed_languages: nil, boolean_query: nil, brand_id: nil)
68
+ body = {
69
+ "search_terms" => search_terms,
70
+ "inclusion_terms" => inclusion_terms,
71
+ "exclusion_terms" => exclusion_terms,
72
+ "compound_inclusion_terms" => compound_inclusion_terms,
73
+ "influencer_terms" => influencer_terms,
74
+ "allowed_languages" => allowed_languages,
75
+ "boolean_query" => boolean_query,
76
+ "brand_id" => brand_id,
77
+ }.reject { |_, value| value.nil? }
78
+ envelope = @transport.request(:patch, "/api/v1/brands/", body: body)
79
+ Zelf.one(envelope, Models::Brand, "Brand")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,33 @@
1
+ # Generated by codegen. Do not edit by hand.
2
+ module Zelf
3
+ module Resources
4
+ # The comments endpoints.
5
+ class Comments
6
+ def initialize(transport)
7
+ @transport = transport
8
+ end
9
+
10
+ # List Comments. Returns a paginated list of comments, ordered by newest first. At
11
+ # least one filter is required to scope the query. Filter by `content_id` (a content
12
+ # `id`) to retrieve the comments on a piece of content, by `id` to retrieve a single
13
+ # comment, or by `parent_comment_id` to retrieve the replies to a comment. A request
14
+ # that supplies none of these returns `INVALID_ID`.
15
+ #
16
+ # Calls GET /api/v1/comment/ (comments.list).
17
+ def list(content_id: nil, id: nil, is_parent_comment: nil, order_by: nil, page: nil, page_size: nil, parent_comment_id: nil)
18
+ query = {
19
+ "content_id" => content_id,
20
+ "id" => id,
21
+ "is_parent_comment" => is_parent_comment,
22
+ "order_by" => order_by,
23
+ "page" => page,
24
+ "page_size" => page_size,
25
+ "parent_comment_id" => parent_comment_id,
26
+ }.reject { |_, value| value.nil? }
27
+ envelope = @transport.request(:get, "/api/v1/comment/", query: query)
28
+ path = @transport.resolve_path("/api/v1/comment/".dup, {})
29
+ Zelf::Page.new(envelope, Models::Comment, @transport, path, query)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,256 @@
1
+ # Generated by codegen. Do not edit by hand.
2
+ module Zelf
3
+ module Resources
4
+ # The content endpoints.
5
+ class Content
6
+ def initialize(transport)
7
+ @transport = transport
8
+ end
9
+
10
+ # AI Summarize Content. Writes a short AI summary of what the content is about.
11
+ # Returns `202` with `status: PENDING` when a run starts. Call again to follow it:
12
+ # `PENDING` while it runs, `COMPLETED` once the content has a summary, `FAILED` if all
13
+ # 3 tries failed.
14
+ #
15
+ # Calls POST /api/v1/content/ai_summarize/ (content.ai_summarize).
16
+ def ai_summarize(content_id:)
17
+ body = {
18
+ "content_id" => content_id,
19
+ }.reject { |_, value| value.nil? }
20
+ envelope = @transport.request(:post, "/api/v1/content/ai_summarize/", body: body)
21
+ Zelf.one(envelope, Models::ContentActionResponse, "ContentActionResponse")
22
+ end
23
+
24
+ # Get Content. Returns a single piece of content identified by `id`. Supply `brand_id`
25
+ # to additionally require the content to exist under that brand and to populate the
26
+ # brand-specific fields. If the content does not exist under that brand, the response
27
+ # is an empty result set rather than a 404. When `brand_id` is omitted, the brand-
28
+ # specific fields are returned as `null`.
29
+ #
30
+ # Calls GET /api/v1/content/{id}/ (content.details).
31
+ def details(id, brand_id: nil)
32
+ path_params = { "id" => id }
33
+ query = {
34
+ "brand_id" => brand_id,
35
+ }.reject { |_, value| value.nil? }
36
+ envelope = @transport.request(:get, "/api/v1/content/{id}/", path_params: path_params, query: query)
37
+ Zelf.one(envelope, Models::Content, "Content")
38
+ end
39
+
40
+ # Create Content Discovery. Starts an asynchronous job that indexes fresh content
41
+ # matching a search term across the requested platforms. Use it to ingest content Zelf
42
+ # does not yet cover. The response describes the job, not the content itself. Once the
43
+ # job completes, query `content.list` to retrieve the indexed content. An identical
44
+ # term requested again within the deduplication window reuses the in-flight job and
45
+ # returns `dispatched: false` instead of starting a new one.
46
+ #
47
+ # Calls POST /api/v1/content/discovery/ (content.discover).
48
+ def discover(search_term:, platforms: nil)
49
+ body = {
50
+ "search_term" => search_term,
51
+ "platforms" => platforms,
52
+ }.reject { |_, value| value.nil? }
53
+ envelope = @transport.request(:post, "/api/v1/content/discovery/", body: body)
54
+ Zelf.one(envelope, Models::ContentDiscovery, "ContentDiscovery")
55
+ end
56
+
57
+ # List Content. Returns a paginated list of content. Every request must scope what it
58
+ # covers: supply at least one of `creator_id`, `creator_username`, `search`,
59
+ # `brand_id`, or `external_id`, otherwise the request is rejected with
60
+ # `VALIDATION_ERROR`. By default results are ordered by `posted_at` descending (most
61
+ # recently posted first); override with `order_by`. Every response echoes the
62
+ # effective defaults in `applied_filters` (the resolved date window, `page`,
63
+ # `page_size`, and `brand_id`), so an empty `results` set can be explained. ### Dates
64
+ # Every request covers a time window. Four parameters define it. | Parameter | Type |
65
+ # Defines | |---|---|---| | `posted_after` | timestamp | Start of the window | |
66
+ # `posted_within_hours` | integer, 1 to 8760 | Start of the window, counted back from
67
+ # the current time | | `posted_within_days` | integer, 1 to 365 | Start of the window,
68
+ # counted back from the current time | | `posted_before` | timestamp | End of the
69
+ # window | A request with no date parameter covers the last 7 days. `applied_filters`
70
+ # reports this as `posted_within_days: 7`. Three parameters define the start of the
71
+ # window, so a request may carry more than one. Such a request is valid. The API
72
+ # applies the first parameter present in this order and ignores the rest: 1.
73
+ # `posted_after` 2. `posted_within_hours` 3. `posted_within_days` `applied_filters`
74
+ # contains only the parameter that was applied, so it identifies the window that
75
+ # produced a response. `posted_before` defines the end of the window and combines with
76
+ # any of the three. On its own it covers the 366 days before it. With
77
+ # `posted_within_hours` or `posted_within_days` the window is counted back from
78
+ # `posted_before` instead of from the current time, so
79
+ # `posted_before=2026-09-01T00:00:00Z` with `posted_within_hours=24` covers
80
+ # `2026-08-31T00:00:00Z` to `2026-09-01T00:00:00Z`. Two limits apply to every window.
81
+ # `posted_after` and `posted_before` may be at most 366 days apart, and a longer
82
+ # period is read as consecutive requests. Content posted before 2025-01-01 is not
83
+ # available. A value outside the ranges in the table returns `VALIDATION_ERROR`. Such
84
+ # values are rejected, not clamped: `posted_within_days=400` is an error rather than
85
+ # 365. Results are paginated: `page` defaults to `1` and `page_size` to `30`, which is
86
+ # also the maximum (larger values are reduced to `30`). `item_count` is the number of
87
+ # rows in the returned page, not the size of the whole match set, and `next` is non-
88
+ # null while further pages remain. For the total number of matching posts, call
89
+ # `content.statistics` with the same query and read `post_count`. When `brand_id` is
90
+ # omitted, the brand-specific fields (`brand_id`, `is_owned`, `is_tagged`,
91
+ # `is_influencer`, `mention_type`, `net_promoter_score`) are returned as `null`, and
92
+ # the brand-only filters and sorts return `BRAND_ID_REQUIRED`. Supply `brand_id` (one
93
+ # or more values, comma-separated) to populate those fields and enable the brand-only
94
+ # filters. Each row's `id` is the value to use for `content.details` and for the
95
+ # `comments.list` `content_id`.
96
+ #
97
+ # Calls GET /api/v1/content/ (content.list).
98
+ def list(brand_id: nil, comment_count: nil, comment_count_max: nil, comment_count_min: nil, content_form: nil, creator_follower_count: nil, creator_follower_count_max: nil, creator_follower_count_min: nil, creator_id: nil, creator_username: nil, earned_media_value_cents: nil, earned_media_value_cents_max: nil, earned_media_value_cents_min: nil, engagement_rate: nil, engagement_rate_max: nil, engagement_rate_min: nil, external_id: nil, is_influencer: nil, is_owned: nil, is_tagged: nil, language: nil, like_count: nil, like_count_max: nil, like_count_min: nil, media_duration: nil, media_duration_max: nil, media_duration_min: nil, mention_type: nil, mention_type_not: nil, net_promoter_score: nil, net_promoter_score_max: nil, net_promoter_score_min: nil, order_by: nil, page: nil, page_size: nil, platform: nil, posted: nil, posted_after: nil, posted_before: nil, posted_within_days: nil, posted_within_hours: nil, search: nil, share_count: nil, share_count_max: nil, share_count_min: nil, total_engagement: nil, total_engagement_max: nil, total_engagement_min: nil, view_count: nil, view_count_max: nil, view_count_min: nil)
99
+ query = {
100
+ "brand_id" => brand_id,
101
+ "comment_count" => comment_count,
102
+ "comment_count_max" => comment_count_max,
103
+ "comment_count_min" => comment_count_min,
104
+ "content_form" => content_form,
105
+ "creator_follower_count" => creator_follower_count,
106
+ "creator_follower_count_max" => creator_follower_count_max,
107
+ "creator_follower_count_min" => creator_follower_count_min,
108
+ "creator_id" => creator_id,
109
+ "creator_username" => creator_username,
110
+ "earned_media_value_cents" => earned_media_value_cents,
111
+ "earned_media_value_cents_max" => earned_media_value_cents_max,
112
+ "earned_media_value_cents_min" => earned_media_value_cents_min,
113
+ "engagement_rate" => engagement_rate,
114
+ "engagement_rate_max" => engagement_rate_max,
115
+ "engagement_rate_min" => engagement_rate_min,
116
+ "external_id" => external_id,
117
+ "is_influencer" => is_influencer,
118
+ "is_owned" => is_owned,
119
+ "is_tagged" => is_tagged,
120
+ "language" => language,
121
+ "like_count" => like_count,
122
+ "like_count_max" => like_count_max,
123
+ "like_count_min" => like_count_min,
124
+ "media_duration" => media_duration,
125
+ "media_duration_max" => media_duration_max,
126
+ "media_duration_min" => media_duration_min,
127
+ "mention_type" => mention_type,
128
+ "mention_type_not" => mention_type_not,
129
+ "net_promoter_score" => net_promoter_score,
130
+ "net_promoter_score_max" => net_promoter_score_max,
131
+ "net_promoter_score_min" => net_promoter_score_min,
132
+ "order_by" => order_by,
133
+ "page" => page,
134
+ "page_size" => page_size,
135
+ "platform" => platform,
136
+ "posted" => posted,
137
+ "posted_after" => posted_after,
138
+ "posted_before" => posted_before,
139
+ "posted_within_days" => posted_within_days,
140
+ "posted_within_hours" => posted_within_hours,
141
+ "search" => search,
142
+ "share_count" => share_count,
143
+ "share_count_max" => share_count_max,
144
+ "share_count_min" => share_count_min,
145
+ "total_engagement" => total_engagement,
146
+ "total_engagement_max" => total_engagement_max,
147
+ "total_engagement_min" => total_engagement_min,
148
+ "view_count" => view_count,
149
+ "view_count_max" => view_count_max,
150
+ "view_count_min" => view_count_min,
151
+ }.reject { |_, value| value.nil? }
152
+ envelope = @transport.request(:get, "/api/v1/content/", query: query)
153
+ path = @transport.resolve_path("/api/v1/content/".dup, {})
154
+ Zelf::Page.new(envelope, Models::Content, @transport, path, query)
155
+ end
156
+
157
+ # Get Content Statistics. Returns a single aggregate of reach, engagement, value, and
158
+ # creator counts over the **same content set*; that `content.list` would return for an
159
+ # identical query. The `post_count` is the number of posts in that whole set, across
160
+ # every page the list would return, and each other total is the sum over those same
161
+ # posts. `content.list` never counts the full set: its `item_count` is only the number
162
+ # of rows in the page it returned, so `post_count` here is the only total available.
163
+ # This accepts every filter `content.list` accepts and applies the **same defaults*;
164
+ # and requirements: every request must scope what it covers with at least one of
165
+ # `creator_id`, `creator_username`, `search`, `brand_id`, or `external_id` (else
166
+ # `VALIDATION_ERROR`); brand-only filters and sorts still require `brand_id`.
167
+ # `order_by` is validated the same as in `content.list` (brand-only sorts still
168
+ # require `brand_id`) but has no effect on the aggregated totals. Returns a single
169
+ # totals object (`item_count: 1`), not paginated, and echoes the resolved date window
170
+ # and `brand_id` in `applied_filters`. ### Dates `content.statistics` takes the same
171
+ # four date parameters as `content.list` and applies them identically. `posted_after`,
172
+ # `posted_within_hours` (1 to 8760) and `posted_within_days` (1 to 365) define the
173
+ # start of the window; `posted_before` defines the end. A request with no date
174
+ # parameter covers the last 7 days. A request carrying more than one start parameter
175
+ # is valid, and the first present in the order `posted_after`, `posted_within_hours`,
176
+ # `posted_within_days` is applied; `applied_filters` reports that one. `posted_after`
177
+ # and `posted_before` may be at most 366 days apart. Both endpoints apply these
178
+ # parameters identically, so one set of dates covers the same posts on each.
179
+ # `post_count` is therefore the size of the set `content.list` pages through. Supply
180
+ # `brand_id` (one or more values, comma-separated) to scope the totals the same way
181
+ # the list does. Each piece of content is counted once even when several `brand_id`
182
+ # values match it.
183
+ #
184
+ # Calls GET /api/v1/content/statistics/ (content.statistics).
185
+ def statistics(brand_id: nil, comment_count: nil, comment_count_max: nil, comment_count_min: nil, content_form: nil, creator_follower_count: nil, creator_follower_count_max: nil, creator_follower_count_min: nil, creator_id: nil, creator_username: nil, earned_media_value_cents: nil, earned_media_value_cents_max: nil, earned_media_value_cents_min: nil, engagement_rate: nil, engagement_rate_max: nil, engagement_rate_min: nil, external_id: nil, is_influencer: nil, is_owned: nil, is_tagged: nil, language: nil, like_count: nil, like_count_max: nil, like_count_min: nil, media_duration: nil, media_duration_max: nil, media_duration_min: nil, mention_type: nil, mention_type_not: nil, net_promoter_score: nil, net_promoter_score_max: nil, net_promoter_score_min: nil, order_by: nil, platform: nil, posted: nil, posted_after: nil, posted_before: nil, posted_within_days: nil, posted_within_hours: nil, search: nil, share_count: nil, share_count_max: nil, share_count_min: nil, total_engagement: nil, total_engagement_max: nil, total_engagement_min: nil, view_count: nil, view_count_max: nil, view_count_min: nil)
186
+ query = {
187
+ "brand_id" => brand_id,
188
+ "comment_count" => comment_count,
189
+ "comment_count_max" => comment_count_max,
190
+ "comment_count_min" => comment_count_min,
191
+ "content_form" => content_form,
192
+ "creator_follower_count" => creator_follower_count,
193
+ "creator_follower_count_max" => creator_follower_count_max,
194
+ "creator_follower_count_min" => creator_follower_count_min,
195
+ "creator_id" => creator_id,
196
+ "creator_username" => creator_username,
197
+ "earned_media_value_cents" => earned_media_value_cents,
198
+ "earned_media_value_cents_max" => earned_media_value_cents_max,
199
+ "earned_media_value_cents_min" => earned_media_value_cents_min,
200
+ "engagement_rate" => engagement_rate,
201
+ "engagement_rate_max" => engagement_rate_max,
202
+ "engagement_rate_min" => engagement_rate_min,
203
+ "external_id" => external_id,
204
+ "is_influencer" => is_influencer,
205
+ "is_owned" => is_owned,
206
+ "is_tagged" => is_tagged,
207
+ "language" => language,
208
+ "like_count" => like_count,
209
+ "like_count_max" => like_count_max,
210
+ "like_count_min" => like_count_min,
211
+ "media_duration" => media_duration,
212
+ "media_duration_max" => media_duration_max,
213
+ "media_duration_min" => media_duration_min,
214
+ "mention_type" => mention_type,
215
+ "mention_type_not" => mention_type_not,
216
+ "net_promoter_score" => net_promoter_score,
217
+ "net_promoter_score_max" => net_promoter_score_max,
218
+ "net_promoter_score_min" => net_promoter_score_min,
219
+ "order_by" => order_by,
220
+ "platform" => platform,
221
+ "posted" => posted,
222
+ "posted_after" => posted_after,
223
+ "posted_before" => posted_before,
224
+ "posted_within_days" => posted_within_days,
225
+ "posted_within_hours" => posted_within_hours,
226
+ "search" => search,
227
+ "share_count" => share_count,
228
+ "share_count_max" => share_count_max,
229
+ "share_count_min" => share_count_min,
230
+ "total_engagement" => total_engagement,
231
+ "total_engagement_max" => total_engagement_max,
232
+ "total_engagement_min" => total_engagement_min,
233
+ "view_count" => view_count,
234
+ "view_count_max" => view_count_max,
235
+ "view_count_min" => view_count_min,
236
+ }.reject { |_, value| value.nil? }
237
+ envelope = @transport.request(:get, "/api/v1/content/statistics/", query: query)
238
+ Zelf.one(envelope, Models::ContentStatistics, "ContentStatistics")
239
+ end
240
+
241
+ # Transcribe Content. Transcribes the speech in the content's audio or video into
242
+ # text. Returns `202` with `status: PENDING` when a run starts. Call again to follow
243
+ # it: `PENDING` while it runs, `COMPLETED` once the content has a transcription,
244
+ # `FAILED` if all 3 tries failed.
245
+ #
246
+ # Calls POST /api/v1/content/transcribe/ (content.transcribe).
247
+ def transcribe(content_id:)
248
+ body = {
249
+ "content_id" => content_id,
250
+ }.reject { |_, value| value.nil? }
251
+ envelope = @transport.request(:post, "/api/v1/content/transcribe/", body: body)
252
+ Zelf.one(envelope, Models::ContentActionResponse, "ContentActionResponse")
253
+ end
254
+ end
255
+ end
256
+ end