tiktok_business_api 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b75b59a548d691df5ead2efd45a9f7ea8bf1bd046f14cfd0286b3f564279041
4
- data.tar.gz: 51cf8b92af0775d12cbc0f94aee289437b7ae3723b889933efe4b36a23dc9f00
3
+ metadata.gz: b6df153f640ad431a27d8f06557b1424f2846d8adda17fa1189dfe6bded264e0
4
+ data.tar.gz: '01822882ce8e801f197433adb20e35c74eae2fa156f66521492e71d3f7dd75d9'
5
5
  SHA512:
6
- metadata.gz: 5a956599241be58b694721840c447856d6ba58f0e16352309bd402eeafac0d6c5e70ffefc1c642122b3e6b0a716bc8a25ad2d3b92d952b72115c49868b90088c
7
- data.tar.gz: 2efcfa2e2caf123b88a5c3d863812443a09ec59cb51da37618b4fe08f804b25d3448bf51bf41f4c53c7682f7b3c4fef6234a40a45a5b4ce66ff921dd5edcdfcc
6
+ metadata.gz: 2b1a377795b3a0cf988b83f1e0f2325166cad9ffbb19ee9d01b3e047e4afab9d9ef0a1824f4ba0faf0b971b5e58fb10b40e2c7a40c02633c383d2e1687f97a8a
7
+ data.tar.gz: 64840256b05f91aad973ad96b5a6ad980bd01d82e9086e0784df96abd9f5a08a59dc7cf3615a821ad8f2a63d0e29b555d45fcab7c71d42e81e9dacfdcaeff256
data/README.md CHANGED
@@ -113,6 +113,56 @@ client.campaigns.list_all(advertiser_id) do |campaign|
113
113
  end
114
114
  ```
115
115
 
116
+ ### Working with Identities
117
+
118
+ The Identity feature allows you to create Spark Ads by working with authorized TikTok accounts.
119
+
120
+ ```ruby
121
+ # List available identities
122
+ identities = client.identities.list(advertiser_id: 'your_advertiser_id')
123
+
124
+ # Get information about a specific identity
125
+ identity_info = client.identities.get_info(
126
+ advertiser_id: 'your_advertiser_id',
127
+ identity_id: 'identity_id',
128
+ identity_type: 'TT_USER'
129
+ )
130
+
131
+ # Create a custom user identity
132
+ new_identity = client.identities.create(
133
+ advertiser_id: 'your_advertiser_id',
134
+ display_name: 'My Custom Identity',
135
+ image_uri: 'image_id_from_uploaded_image' # Optional
136
+ )
137
+
138
+ # List all identities with pagination
139
+ client.identities.list_all(advertiser_id: 'your_advertiser_id') do |identity|
140
+ puts "Identity: #{identity['display_name']} (#{identity['identity_type']})"
141
+ end
142
+ ```
143
+
144
+ ### Working with Images
145
+
146
+ Upload and manage images for your ads:
147
+
148
+ ```ruby
149
+ # Upload an image file
150
+ uploaded_image = client.images.upload(
151
+ advertiser_id: 'your_advertiser_id',
152
+ image_file: File.open('/path/to/image.jpg')
153
+ )
154
+
155
+ # Get image info
156
+ image_info = client.images.get_info('your_advertiser_id', uploaded_image['image_id'])
157
+
158
+ # Search for images
159
+ images = client.images.search(
160
+ advertiser_id: 'your_advertiser_id',
161
+ page: 1,
162
+ page_size: 20
163
+ )
164
+ ```
165
+
116
166
  ## Logging Requests and Responses
117
167
 
118
168
  You can enable debug logging to see all API requests and responses:
@@ -5,42 +5,42 @@ module TiktokBusinessApi
5
5
  class Client
6
6
  # @return [TiktokBusinessApi::Config] Client configuration
7
7
  attr_reader :config
8
-
8
+
9
9
  # @return [TiktokBusinessApi::Auth] Authentication handler
10
10
  attr_reader :auth
11
-
11
+
12
12
  # Initialize a new client
13
13
  #
14
14
  # @param options [Hash] Override configuration options
15
15
  def initialize(options = {})
16
16
  @config = TiktokBusinessApi.config.dup || Config.new
17
-
17
+
18
18
  # Override config with options
19
19
  options.each do |key, value|
20
20
  @config.send("#{key}=", value) if @config.respond_to?("#{key}=")
21
21
  end
22
-
22
+
23
23
  @auth = Auth.new(self)
24
24
  @resources = {}
25
25
  end
26
-
26
+
27
27
  # Get or create a resource instance
28
28
  #
29
29
  # @param resource_name [Symbol] Name of the resource
30
30
  # @return [BaseResource] Resource instance
31
31
  def resource(resource_name)
32
32
  @resources[resource_name] ||= begin
33
- # Convert resource_name to class name (e.g., :campaign => Campaign)
34
- class_name = resource_name.to_s.split('_').map(&:capitalize).join
35
-
36
- # Get the resource class
37
- resource_class = TiktokBusinessApi::Resources.const_get(class_name)
38
-
39
- # Create an instance
40
- resource_class.new(self)
41
- end
33
+ # Convert resource_name to class name (e.g., :campaign => Campaign)
34
+ class_name = resource_name.to_s.split('_').map(&:capitalize).join
35
+
36
+ # Get the resource class
37
+ resource_class = TiktokBusinessApi::Resources.const_get(class_name)
38
+
39
+ # Create an instance
40
+ resource_class.new(self)
41
+ end
42
42
  end
43
-
43
+
44
44
  # Make an HTTP request to the TikTok Business API
45
45
  #
46
46
  # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
@@ -50,46 +50,83 @@ module TiktokBusinessApi
50
50
  # @return [Hash] Parsed API response
51
51
  def request(method, path, params = {}, headers = {})
52
52
  url = File.join(@config.api_base_url, path)
53
-
53
+
54
54
  # Set up default headers
55
55
  default_headers = {
56
56
  'Content-Type' => 'application/json'
57
57
  }
58
-
58
+
59
59
  # Add access token if available
60
60
  if @config.access_token
61
61
  default_headers['Access-Token'] = @config.access_token
62
62
  end
63
-
63
+
64
64
  # Merge with custom headers
65
65
  headers = default_headers.merge(headers)
66
-
66
+
67
67
  # Log the request
68
68
  log_request(method, url, params, headers)
69
-
69
+
70
70
  # Build the request
71
71
  response = connection.run_request(method, url, nil, headers) do |req|
72
72
  case method
73
73
  when :get, :delete
74
74
  req.params = params
75
75
  when :post, :put
76
- req.body = JSON.generate(params) unless params.empty?
76
+ if headers['Content-Type'] == 'multipart/form-data'
77
+ # For multipart form data, let Faraday handle it
78
+ req.options.timeout = 120 # Extend timeout for file uploads
79
+ req.body = {} # Initialize the body as an empty hash
80
+ params.each do |key, value|
81
+ req.body[key.to_sym] = value
82
+ end
83
+ else
84
+ req.body = JSON.generate(params) unless params.empty?
85
+ end
77
86
  end
78
87
  end
79
-
88
+
80
89
  # Parse and handle the response
81
90
  handle_response(response)
82
91
  end
83
-
92
+
84
93
  # Access to campaign resource
85
94
  #
86
95
  # @return [TiktokBusinessApi::Resources::Campaign] Campaign resource
87
96
  def campaigns
88
97
  resource(:campaign)
89
98
  end
90
-
99
+
100
+ # Access to ad group resource
101
+ #
102
+ # @return [TiktokBusinessApi::Resources::Adgroup] Ad group resource
103
+ def adgroups
104
+ resource(:adgroup)
105
+ end
106
+
107
+ # Access to ad resource
108
+ #
109
+ # @return [TiktokBusinessApi::Resources::Ad] Ad resource
110
+ def ads
111
+ resource(:ad)
112
+ end
113
+
114
+ # Access to image resource
115
+ #
116
+ # @return [TiktokBusinessApi::Resources::Image] Image resource
117
+ def images
118
+ resource(:image)
119
+ end
120
+
121
+ # Access to identity resource
122
+ #
123
+ # @return [TiktokBusinessApi::Resources::Identity] Identity resource
124
+ def identities
125
+ resource(:identity)
126
+ end
127
+
91
128
  private
92
-
129
+
93
130
  # Set up Faraday connection
94
131
  #
95
132
  # @return [Faraday::Connection] Faraday connection
@@ -97,16 +134,19 @@ module TiktokBusinessApi
97
134
  @connection ||= Faraday.new do |conn|
98
135
  conn.options.timeout = @config.timeout
99
136
  conn.options.open_timeout = @config.open_timeout
100
-
137
+
101
138
  # Set up middleware
102
139
  conn.use Faraday::Response::Logger, @config.logger if @config.logger
103
140
  conn.use Faraday::FollowRedirects::Middleware
104
141
  conn.use Faraday::Retry::Middleware, max: 3
105
-
142
+
143
+ # Use multipart middleware for file uploads
144
+ conn.request :multipart
145
+
106
146
  conn.adapter Faraday.default_adapter
107
147
  end
108
148
  end
109
-
149
+
110
150
  # Parse and handle the API response
111
151
  #
112
152
  # @param response [Faraday::Response] HTTP response
@@ -115,26 +155,26 @@ module TiktokBusinessApi
115
155
  def handle_response(response)
116
156
  # Log the response
117
157
  log_response(response)
118
-
158
+
119
159
  # Parse the response body
120
160
  body = if response.body && !response.body.empty?
121
- begin
122
- JSON.parse(response.body)
123
- rescue JSON::ParserError
124
- { error: "Invalid JSON response: #{response.body}" }
125
- end
126
- else
127
- {}
128
- end
129
-
161
+ begin
162
+ JSON.parse(response.body)
163
+ rescue JSON::ParserError
164
+ { error: "Invalid JSON response: #{response.body}" }
165
+ end
166
+ else
167
+ {}
168
+ end
169
+
130
170
  # Check for API errors
131
171
  if !response.success? || (body.is_a?(Hash) && body['code'] != 0)
132
172
  raise ErrorFactory.from_response(response)
133
173
  end
134
-
174
+
135
175
  body
136
176
  end
137
-
177
+
138
178
  # Log the request details
139
179
  #
140
180
  # @param method [Symbol] HTTP method
@@ -143,18 +183,18 @@ module TiktokBusinessApi
143
183
  # @param headers [Hash] Request headers
144
184
  def log_request(method, url, params, headers)
145
185
  return unless @config.debug && @config.logger
146
-
186
+
147
187
  @config.logger.debug "[TiktokBusinessApi] Request: #{method.upcase} #{url}"
148
188
  @config.logger.debug "[TiktokBusinessApi] Parameters: #{params.inspect}"
149
189
  @config.logger.debug "[TiktokBusinessApi] Headers: #{headers.inspect}"
150
190
  end
151
-
191
+
152
192
  # Log the response details
153
193
  #
154
194
  # @param response [Faraday::Response] HTTP response
155
195
  def log_response(response)
156
196
  return unless @config.debug && @config.logger
157
-
197
+
158
198
  @config.logger.debug "[TiktokBusinessApi] Response Status: #{response.status}"
159
199
  @config.logger.debug "[TiktokBusinessApi] Response Body: #{response.body}"
160
200
  end
@@ -50,12 +50,20 @@ module TiktokBusinessApi
50
50
  # @return [TiktokBusinessApi::Error] The appropriate error object
51
51
  def self.from_response(response, request = nil)
52
52
  status = response.status
53
- body = response.body
53
+ body = if response.body && !response.body.empty?
54
+ begin
55
+ JSON.parse(response.body)
56
+ rescue JSON::ParserError
57
+ { error: "Invalid JSON response: #{response.body}" }
58
+ end
59
+ else
60
+ {}
61
+ end
54
62
 
55
63
  # Parse TikTok API response which has its own error structure
56
64
  error_code = body.is_a?(Hash) ? body['code'] : nil
57
65
  error_message = body.is_a?(Hash) ? body['message'] : nil
58
-
66
+
59
67
  # Determine the error class based on status and error code
60
68
  klass = case status
61
69
  when 401
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # Ad resource for the TikTok Business API
6
+ class Ad < CrudResource
7
+ RESOURCE_NAME = 'ad'
8
+
9
+ def get(advertiser_id:, ad_id:)
10
+ list(advertiser_id: advertiser_id, filtering: {ad_ids: [ad_id]}).first
11
+ end
12
+
13
+ # Create a new ad
14
+ #
15
+ # @param advertiser_id [String] Advertiser ID
16
+ # @param adgroup_id [String] Ad group ID
17
+ # @param creatives [Array<Hash>] Array of creative objects
18
+ # @return [Hash] New ad data with created ad IDs
19
+ def create(advertiser_id:, adgroup_id:, creatives:)
20
+ params = {
21
+ advertiser_id: advertiser_id,
22
+ adgroup_id: adgroup_id,
23
+ creatives: creatives
24
+ }
25
+
26
+ response = _http_post(create_path, params)
27
+ response['data']
28
+ end
29
+
30
+ def list(advertiser_id:, campaign_id: nil, adgroup_id: nil, filtering: {}, page_size: nil, page: nil, **other_params, &block)
31
+ filtering[:campaign_ids] = [campaign_id] if campaign_id
32
+ filtering[:adgroup_ids] = [adgroup_id] if adgroup_id
33
+ super(filtering: filtering, page_size: page_size, page: page, **other_params.merge(advertiser_id: advertiser_id), &block)
34
+ end
35
+
36
+ # Update an ad
37
+ #
38
+ # @param advertiser_id [String] Advertiser ID
39
+ # @param ad_id [String] Ad ID
40
+ # @param params [Hash] Ad parameters to update
41
+ # @return [Hash] Updated ad data
42
+ def update(advertiser_id:, ad_id:, **params)
43
+ params = params.merge(
44
+ advertiser_id: advertiser_id,
45
+ ad_id: ad_id
46
+ )
47
+
48
+ response = _http_post(update_path, params)
49
+ response['data']
50
+ end
51
+
52
+ # Update ad status (enable/disable)
53
+ #
54
+ # @param advertiser_id [String] Advertiser ID
55
+ # @param ad_id [String] Ad ID
56
+ # @param status [String] New status ('ENABLE' or 'DISABLE')
57
+ # @return [Hash] Result
58
+ def update_status(advertiser_id:, ad_id:, status:)
59
+ params = {
60
+ advertiser_id: advertiser_id,
61
+ ad_ids: [ad_id],
62
+ operation_status: status
63
+ }
64
+
65
+ response = _http_post('status/update/', params)
66
+ response['data']
67
+ end
68
+
69
+ # Delete an ad
70
+ #
71
+ # @param advertiser_id [String] Advertiser ID
72
+ # @param ad_id [String] Ad ID
73
+ # @return [Hash] Result
74
+ def delete(advertiser_id:, ad_id:)
75
+ params = {
76
+ advertiser_id: advertiser_id,
77
+ ad_ids: [ad_id]
78
+ }
79
+
80
+ response = _http_post(delete_path, params)
81
+ response['data']
82
+ end
83
+
84
+ # Create Smart Creative ads
85
+ #
86
+ # @param advertiser_id [String] Advertiser ID
87
+ # @param adgroup_id [String] Ad group ID
88
+ # @param creatives [Array<Hash>] Array of creative objects
89
+ # @return [Hash] New Smart Creative ad data
90
+ def create_aco(advertiser_id:, adgroup_id:, creatives:)
91
+ params = {
92
+ advertiser_id: advertiser_id,
93
+ adgroup_id: adgroup_id,
94
+ creatives: creatives
95
+ }
96
+
97
+ response = _http_post('aco/create/', params)
98
+ response['data']
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # AdGroup resource for the TikTok Business API
6
+ class Adgroup < CrudResource
7
+ RESOURCE_NAME = 'adgroup'
8
+
9
+ def get(advertiser_id:, adgroup_id:)
10
+ list(advertiser_id: advertiser_id, filtering: {adgroup_ids: [adgroup_id]}).first
11
+ end
12
+
13
+ # Create a new ad group
14
+ #
15
+ # @param advertiser_id [String] Advertiser ID
16
+ # @param params [Hash] Ad group parameters
17
+ # @return [Hash] New ad group data
18
+ def create(advertiser_id, params = {})
19
+ # Ensure advertiser_id is included in the params
20
+ params = params.merge(advertiser_id: advertiser_id)
21
+
22
+ response = _http_post(create_path, params)
23
+ response['data']
24
+ end
25
+
26
+ def list(advertiser_id:, campaign_id: nil, filtering: {}, page_size: nil, page: nil, **other_params, &block)
27
+ filtering[:campaign_ids] = [campaign_id] if campaign_id
28
+ super(filtering: filtering, page_size: page_size, page: page, **other_params.merge(advertiser_id: advertiser_id), &block)
29
+ end
30
+
31
+ # Update an ad group
32
+ #
33
+ # @param advertiser_id [String] Advertiser ID
34
+ # @param adgroup_id [String] Ad group ID
35
+ # @param params [Hash] Ad group parameters to update
36
+ # @return [Hash] Updated ad group data
37
+ def update(advertiser_id, adgroup_id, params = {})
38
+ params = params.merge(
39
+ advertiser_id: advertiser_id,
40
+ adgroup_id: adgroup_id
41
+ )
42
+
43
+ response = _http_post(update_path, params)
44
+ response['data']
45
+ end
46
+
47
+ # Update ad group status (enable/disable)
48
+ #
49
+ # @param advertiser_id [String] Advertiser ID
50
+ # @param adgroup_id [String] Ad group ID
51
+ # @param status [String] New status ('ENABLE' or 'DISABLE')
52
+ # @return [Hash] Result
53
+ def update_status(advertiser_id, adgroup_id, status)
54
+ params = {
55
+ advertiser_id: advertiser_id,
56
+ adgroup_ids: [adgroup_id],
57
+ operation_status: status
58
+ }
59
+
60
+ response = _http_post('status/update/', params)
61
+ response['data']
62
+ end
63
+
64
+ # Delete an ad group
65
+ #
66
+ # @param advertiser_id [String] Advertiser ID
67
+ # @param adgroup_id [String] Ad group ID
68
+ # @return [Hash] Result
69
+ def delete(advertiser_id, adgroup_id)
70
+ params = {
71
+ advertiser_id: advertiser_id,
72
+ adgroup_ids: [adgroup_id]
73
+ }
74
+
75
+ response = _http_post(delete_path, params)
76
+ response['data']
77
+ end
78
+
79
+ # Estimate audience size for an ad group
80
+ #
81
+ # @param advertiser_id [String] Advertiser ID
82
+ # @param params [Hash] Targeting parameters for estimation
83
+ # @return [Hash] Audience size estimation
84
+ def estimate_audience_size(advertiser_id, params = {})
85
+ params = params.merge(advertiser_id: advertiser_id)
86
+
87
+ response = _http_post('audience_size/estimate/', params)
88
+ response['data']
89
+ end
90
+ end
91
+ end
92
+ end
@@ -6,35 +6,35 @@ module TiktokBusinessApi
6
6
  class BaseResource
7
7
  # @return [TiktokBusinessApi::Client] Client instance
8
8
  attr_reader :client
9
-
9
+
10
10
  # Initialize a new resource
11
11
  #
12
12
  # @param client [TiktokBusinessApi::Client] Client instance
13
13
  def initialize(client)
14
14
  @client = client
15
15
  end
16
-
16
+
17
17
  # Get the resource name (used for endpoint paths)
18
18
  #
19
19
  # @return [String] Resource name
20
20
  def resource_name
21
21
  self.class.name.split('::').last.downcase
22
22
  end
23
-
23
+
24
24
  # Get the API version
25
25
  #
26
26
  # @return [String] API version
27
27
  def api_version
28
28
  'v1.3'
29
29
  end
30
-
30
+
31
31
  # Get the base path for this resource
32
32
  #
33
33
  # @return [String] Base path
34
34
  def base_path
35
35
  "#{api_version}/#{resource_name}/"
36
36
  end
37
-
37
+
38
38
  # Make a GET request to the resource
39
39
  #
40
40
  # @param path [String] Path relative to the resource base path
@@ -45,7 +45,7 @@ module TiktokBusinessApi
45
45
  full_path = File.join(base_path, path)
46
46
  client.request(:get, full_path, params, headers)
47
47
  end
48
-
48
+
49
49
  # Make a POST request to the resource
50
50
  #
51
51
  # @param path [String] Path relative to the resource base path
@@ -56,7 +56,7 @@ module TiktokBusinessApi
56
56
  full_path = File.join(base_path, path)
57
57
  client.request(:post, full_path, params, headers)
58
58
  end
59
-
59
+
60
60
  # Handle pagination for list endpoints
61
61
  #
62
62
  # @param path [String] Path relative to the resource base path
@@ -71,28 +71,28 @@ module TiktokBusinessApi
71
71
  page = 1
72
72
  page_size = params[:page_size] || 10
73
73
  has_more = true
74
-
74
+
75
75
  while has_more
76
76
  params[:page] = page
77
77
  params[:page_size] = page_size
78
-
78
+
79
79
  response = get(path, params, headers)
80
-
80
+
81
81
  # Extract data from the response
82
82
  current_items = response.dig('data', data_key) || []
83
-
83
+
84
84
  if block_given?
85
85
  current_items.each { |item| yield(item) }
86
86
  else
87
87
  items.concat(current_items)
88
88
  end
89
-
89
+
90
90
  # Check if there are more pages
91
91
  page_info = response.dig('data', 'page_info') || {}
92
92
  has_more = page_info['has_more'] == true
93
93
  page += 1
94
94
  end
95
-
95
+
96
96
  block_given? ? nil : items
97
97
  end
98
98
  end
@@ -3,97 +3,17 @@
3
3
  module TiktokBusinessApi
4
4
  module Resources
5
5
  # Campaign resource for the TikTok Business API
6
- class Campaign < BaseResource
7
- # Create a new campaign
8
- #
9
- # @param advertiser_id [String] Advertiser ID
10
- # @param params [Hash] Campaign parameters
11
- # @return [Hash] New campaign data
12
- def create(advertiser_id, params = {})
13
- # Ensure advertiser_id is included in the params
14
- params = params.merge(advertiser_id: advertiser_id)
6
+ class Campaign < CrudResource
7
+ RESOURCE_NAME = 'campaign'
15
8
 
16
- response = post('create/', params)
17
- response['data']
9
+ def get(advertiser_id:, campaign_id:)
10
+ list(advertiser_id: advertiser_id, filtering: {campaign_ids: [campaign_id]}).first
18
11
  end
19
12
 
20
- # Get a list of campaigns
21
- #
22
- # @param advertiser_id [String] Advertiser ID
23
- # @param params [Hash] Filter parameters
24
- # @return [Hash] Campaign list response
25
- def list(advertiser_id, params = {})
26
- # Ensure advertiser_id is included in the params
27
- params = params.merge(advertiser_id: advertiser_id)
28
-
29
- response = _http_get('get/', params)
30
- response['data']
31
- end
32
-
33
- # Get a campaign by ID
34
- #
35
- # @param advertiser_id [String] Advertiser ID
36
- # @param campaign_id [String] Campaign ID
37
- # @return [Hash] Campaign data
38
- def get(advertiser_id, campaign_id)
39
- params = {
40
- advertiser_id: advertiser_id,
41
- campaign_ids: [campaign_id]
42
- }
43
-
44
- response = _http_get('get/', params)
45
- campaigns = response.dig('data', 'list') || []
46
- campaigns.first
47
- end
48
-
49
- # Update a campaign
50
- #
51
- # @param advertiser_id [String] Advertiser ID
52
- # @param campaign_id [String] Campaign ID
53
- # @param params [Hash] Campaign parameters to update
54
- # @return [Hash] Updated campaign data
55
- def update(advertiser_id, campaign_id, params = {})
56
- # Ensure required parameters are included
57
- params = params.merge(
58
- advertiser_id: advertiser_id,
59
- campaign_id: campaign_id
60
- )
61
-
62
- response = _http_post('update/', params)
63
- response['data']
64
- end
65
-
66
- # Update campaign status (enable/disable)
67
- #
68
- # @param advertiser_id [String] Advertiser ID
69
- # @param campaign_id [String] Campaign ID
70
- # @param status [String] New status ('ENABLE' or 'DISABLE')
71
- # @return [Hash] Result
72
- def update_status(advertiser_id, campaign_id, status)
73
- params = {
74
- advertiser_id: advertiser_id,
75
- campaign_ids: [campaign_id],
76
- operation_status: status
77
- }
78
-
79
- response = post('status/update/', params)
80
- response['data']
13
+ def list(advertiser_id:, filtering: {}, page_size: nil, page: nil, **other_params, &block)
14
+ super(filtering: filtering, page_size: page_size, page: page, **other_params.merge(advertiser_id: advertiser_id), &block)
81
15
  end
82
16
 
83
- # Delete a campaign
84
- #
85
- # @param advertiser_id [String] Advertiser ID
86
- # @param campaign_id [String] Campaign ID
87
- # @return [Hash] Result
88
- def delete(advertiser_id, campaign_id)
89
- params = {
90
- advertiser_id: advertiser_id,
91
- campaign_ids: [campaign_id]
92
- }
93
-
94
- response = _http_post('delete/', params)
95
- response['data']
96
- end
97
17
  end
98
18
  end
99
19
  end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # Base CRUD class for all API resources
6
+ class CrudResource < BaseResource
7
+ # Default path for create operations
8
+ def create_path
9
+ 'create/'
10
+ end
11
+
12
+ # Default path for read/list operations
13
+ def list_path
14
+ 'get/'
15
+ end
16
+
17
+ # Default path for update operations
18
+ def update_path
19
+ 'update/'
20
+ end
21
+
22
+ # Default path for delete operations
23
+ def delete_path
24
+ 'delete/'
25
+ end
26
+
27
+ # Default path for status updates
28
+ def status_update_path
29
+ 'status/update/'
30
+ end
31
+
32
+ def resource_name
33
+ self::class::RESOURCE_NAME
34
+ end
35
+
36
+ # Default ID parameter name
37
+ def id_param_name
38
+ "#{resource_name}_id"
39
+ end
40
+
41
+ # Default IDs parameter name for bulk operations
42
+ def ids_param_name
43
+ "#{resource_name}_ids"
44
+ end
45
+
46
+ # Create a new resource
47
+ #
48
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
49
+ # @param params [Hash] Resource parameters
50
+ # @param owner_param_name [String] Parameter name for the owner ID
51
+ # @return [Hash] New resource data
52
+ def create(owner_id, params = {}, owner_param_name = 'advertiser_id')
53
+ params = params.merge(owner_param_name => owner_id)
54
+ response = _http_post(create_path, params)
55
+ response['data']
56
+ end
57
+
58
+ def list(filtering: {}, page_size: nil, page: nil, **other_params)
59
+ # Build request paramss
60
+ request_params = other_params.merge(filtering: filtering.to_json)
61
+
62
+ page_size ||= 100
63
+ page ||= 1
64
+ request_params[:page_size] = [page_size, 100].min # Most APIs limit page size
65
+ request_params[:page] = page
66
+
67
+ # Make the request
68
+ response = _http_get(list_path, request_params)
69
+
70
+ # If a block is given, yield each item
71
+ if block_given?
72
+ items = response.dig('data', 'list') || []
73
+ items.each { |item| yield(item) }
74
+
75
+ # Return the response for method chaining
76
+ response
77
+ else
78
+ # Just return the data otherwise
79
+ response['data']['list']
80
+ end
81
+ end
82
+
83
+ # List all resources with automatic pagination
84
+ #
85
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
86
+ # @param params [Hash] Filter parameters
87
+ # @param owner_param_name [String] Parameter name for the owner ID
88
+ # @param list_key [String] Key in the response that contains the data array
89
+ # @yield [resource] Block to process each resource
90
+ # @yieldparam resource [Hash] Resource from the response
91
+ # @return [Array] All resources if no block is given
92
+ def list_all(owner_id, params = {}, owner_param_name = 'advertiser_id', list_key = 'list')
93
+ items = []
94
+ page = 1
95
+ page_size = params[:page_size] || 10
96
+ has_more = true
97
+
98
+ # Ensure owner_id is included in the params
99
+ request_params = params.merge(owner_param_name => owner_id)
100
+
101
+ while has_more
102
+ request_params[:page] = page
103
+ request_params[:page_size] = page_size
104
+
105
+ response = _http_get(list_path, request_params)
106
+
107
+ # Extract data from the response
108
+ current_items = response.dig('data', list_key) || []
109
+
110
+ if block_given?
111
+ current_items.each { |item| yield(item) }
112
+ else
113
+ items.concat(current_items)
114
+ end
115
+
116
+ # Check if there are more pages
117
+ page_info = response.dig('data', 'page_info') || {}
118
+ total_number = page_info['total_number'] || 0
119
+ total_fetched = page * page_size
120
+
121
+ has_more = page_info['has_more'] == true ||
122
+ (total_number > 0 && total_fetched < total_number)
123
+ page += 1
124
+
125
+ # Break if we've reached the end or there's an empty result
126
+ break if current_items.empty?
127
+ end
128
+
129
+ block_given? ? nil : items
130
+ end
131
+
132
+ # Get a resource by ID
133
+ #
134
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
135
+ # @param resource_id [String] Resource ID
136
+ # @param owner_param_name [String] Parameter name for the owner ID
137
+ # @return [Hash] Resource data
138
+ def get(owner_id, resource_id, owner_param_name = 'advertiser_id')
139
+ params = {
140
+ owner_param_name => owner_id,
141
+ ids_param_name => [resource_id]
142
+ }
143
+
144
+ response = _http_get(list_path, params)
145
+ items = response.dig('data', 'list') || []
146
+ items.first
147
+ end
148
+
149
+ # Update a resource
150
+ #
151
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
152
+ # @param resource_id [String] Resource ID
153
+ # @param params [Hash] Resource parameters to update
154
+ # @param owner_param_name [String] Parameter name for the owner ID
155
+ # @return [Hash] Updated resource data
156
+ def update(owner_id, resource_id, params = {}, owner_param_name = 'advertiser_id')
157
+ # Ensure required parameters are included
158
+ params = params.merge(
159
+ owner_param_name => owner_id,
160
+ id_param_name => resource_id
161
+ )
162
+
163
+ response = _http_post(update_path, params)
164
+ response['data']
165
+ end
166
+
167
+ # Update resource status
168
+ #
169
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
170
+ # @param resource_id [String] Resource ID
171
+ # @param status [String] New status
172
+ # @param owner_param_name [String] Parameter name for the owner ID
173
+ # @return [Hash] Result
174
+ def update_status(owner_id, resource_id, status, owner_param_name = 'advertiser_id')
175
+ params = {
176
+ owner_param_name => owner_id,
177
+ ids_param_name => [resource_id],
178
+ 'operation_status' => status
179
+ }
180
+
181
+ response = _http_post(status_update_path, params)
182
+ response['data']
183
+ end
184
+
185
+ # Delete a resource
186
+ #
187
+ # @param owner_id [String] ID of the resource owner (e.g., advertiser_id)
188
+ # @param resource_id [String] Resource ID
189
+ # @param owner_param_name [String] Parameter name for the owner ID
190
+ # @return [Hash] Result
191
+ def delete(owner_id, resource_id, owner_param_name = 'advertiser_id')
192
+ params = {
193
+ owner_param_name => owner_id,
194
+ ids_param_name => [resource_id]
195
+ }
196
+
197
+ response = _http_post(delete_path, params)
198
+ response['data']
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # Identity resource for the TikTok Business API
6
+ class Identity < BaseResource
7
+ RESOURCE_NAME = 'identity'
8
+
9
+ # Get a list of identities
10
+ #
11
+ # @param advertiser_id [String] Advertiser ID
12
+ # @param options [Hash] Additional options for the request
13
+ # @option options [String] :identity_type Identity type. Enum values: CUSTOMIZED_USER, AUTH_CODE, TT_USER, BC_AUTH_TT
14
+ # @option options [String] :identity_authorized_bc_id ID of the Business Center (required when identity_type is BC_AUTH_TT)
15
+ # @option options [Hash] :filtering Filtering conditions (valid only for CUSTOMIZED_USER or when identity_type not specified)
16
+ # @option options [Integer] :page Page number
17
+ # @option options [Integer] :page_size Number of results per page
18
+ # @return [Hash] Response with list of identities and pagination info
19
+ def list(advertiser_id:, **options)
20
+ params = {
21
+ advertiser_id: advertiser_id
22
+ }
23
+
24
+ # Add optional parameters if provided
25
+ params[:identity_type] = options[:identity_type] if options[:identity_type]
26
+ params[:identity_authorized_bc_id] = options[:identity_authorized_bc_id] if options[:identity_authorized_bc_id]
27
+ params[:filtering] = options[:filtering].to_json if options[:filtering]
28
+ params[:page] = options[:page] if options[:page]
29
+ params[:page_size] = options[:page_size] if options[:page_size]
30
+
31
+ response = client.request(:get, "#{base_path}get/", params)
32
+
33
+ if block_given? && response['data']['identity_list']
34
+ response['data']['identity_list'].each { |identity| yield(identity) }
35
+ response['data']
36
+ else
37
+ response['data']
38
+ end
39
+ end
40
+
41
+ # Get information about a specific identity
42
+ #
43
+ # @param advertiser_id [String] Advertiser ID
44
+ # @param identity_id [String] Identity ID
45
+ # @param identity_type [String] Identity type. Enum values: CUSTOMIZED_USER, AUTH_CODE, TT_USER, BC_AUTH_TT
46
+ # @param identity_authorized_bc_id [String] ID of the Business Center (required when identity_type is BC_AUTH_TT)
47
+ # @return [Hash] Identity information
48
+ def get_info(advertiser_id:, identity_id:, identity_type:, identity_authorized_bc_id: nil)
49
+ params = {
50
+ advertiser_id: advertiser_id,
51
+ identity_id: identity_id,
52
+ identity_type: identity_type
53
+ }
54
+
55
+ # Add Business Center ID if provided (required for BC_AUTH_TT)
56
+ params[:identity_authorized_bc_id] = identity_authorized_bc_id if identity_authorized_bc_id
57
+
58
+ response = client.request(:get, "#{base_path}info/", params)
59
+ response['data']['identity_info']
60
+ end
61
+
62
+ # Create a new Custom User identity
63
+ #
64
+ # @param advertiser_id [String] Advertiser ID
65
+ # @param display_name [String] Display name (max 100 characters)
66
+ # @param image_uri [String] The ID of the avatar image (optional)
67
+ # @return [Hash] Response containing the new identity ID
68
+ def create(advertiser_id:, display_name:, image_uri: nil)
69
+ params = {
70
+ advertiser_id: advertiser_id,
71
+ display_name: display_name
72
+ }
73
+
74
+ # Add image URI if provided
75
+ params[:image_uri] = image_uri if image_uri
76
+
77
+ response = client.request(:post, "#{base_path}create/", params)
78
+ response['data']
79
+ end
80
+
81
+ # List all identities with automatic pagination
82
+ #
83
+ # @param advertiser_id [String] Advertiser ID
84
+ # @param options [Hash] Additional options for the request
85
+ # @yield [identity] Block to process each identity
86
+ # @yieldparam identity [Hash] Identity from the response
87
+ # @return [Array, nil] All identities if no block is given, nil otherwise
88
+ def list_all(advertiser_id:, **options, &block)
89
+ page = options[:page] || 1
90
+ page_size = options[:page_size] || 100
91
+ all_identities = []
92
+ has_more = true
93
+
94
+ while has_more
95
+ current_options = options.merge(page: page, page_size: page_size)
96
+ response = list(advertiser_id: advertiser_id, **current_options)
97
+
98
+ identities = response['identity_list'] || []
99
+
100
+ if block_given?
101
+ identities.each { |identity| yield(identity) }
102
+ else
103
+ all_identities.concat(identities)
104
+ end
105
+
106
+ # Check pagination info
107
+ page_info = response['page_info'] || {}
108
+ current_page = page_info['page'].to_i
109
+ total_pages = page_info['total_page'].to_i
110
+
111
+ has_more = current_page < total_pages
112
+ page += 1
113
+
114
+ # Break if empty result or no more pages
115
+ break if identities.empty? || !has_more
116
+ end
117
+
118
+ block_given? ? nil : all_identities
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # Image resource for the TikTok Business API
6
+ class Image < BaseResource
7
+ # Get the resource name (used for endpoint paths)
8
+ #
9
+ # @return [String] Resource name
10
+ def resource_name
11
+ 'file/image/ad'
12
+ end
13
+
14
+ # Upload an image
15
+ #
16
+ # @param advertiser_id [String] Advertiser ID
17
+ # @param options [Hash] Upload options
18
+ # @option options [String] :upload_type Upload type ('UPLOAD_BY_FILE', 'UPLOAD_BY_URL', 'UPLOAD_BY_FILE_ID')
19
+ # @option options [String] :file_name Image file name
20
+ # @option options [File] :image_file Image file (required when upload_type is 'UPLOAD_BY_FILE')
21
+ # @option options [String] :image_signature MD5 of image file (optional when upload_type is 'UPLOAD_BY_FILE', will be calculated if not provided)
22
+ # @option options [String] :image_url Image URL (required when upload_type is 'UPLOAD_BY_URL')
23
+ # @option options [String] :file_id File ID (required when upload_type is 'UPLOAD_BY_FILE_ID')
24
+ # @return [Hash] Upload result with image ID
25
+ def upload(advertiser_id:, **options)
26
+ upload_type = options[:upload_type] || 'UPLOAD_BY_FILE'
27
+
28
+ params = {
29
+ advertiser_id: advertiser_id,
30
+ upload_type: upload_type
31
+ }
32
+
33
+ # Add file_name if provided
34
+ params[:file_name] = options[:file_name] if options[:file_name]
35
+
36
+ case upload_type
37
+ when 'UPLOAD_BY_FILE'
38
+ raise ArgumentError, 'image_file is required for UPLOAD_BY_FILE' unless options[:image_file]
39
+
40
+ # Auto-calculate image signature if not provided
41
+ if !options[:image_signature]
42
+ options[:image_signature] = TiktokBusinessApi::Utils.calculate_md5(options[:image_file])
43
+ end
44
+
45
+ # Create a FilePart for multipart file upload
46
+ params[:image_file] = Faraday::Multipart::FilePart.new(
47
+ options[:image_file],
48
+ TiktokBusinessApi::Utils.detect_content_type(options[:image_file])
49
+ )
50
+ params[:image_signature] = options[:image_signature]
51
+
52
+ # For file uploads, we need to use multipart/form-data
53
+ headers = { 'Content-Type' => 'multipart/form-data' }
54
+ response = client.request(:post, "#{base_path}/upload/", params, headers)
55
+ when 'UPLOAD_BY_URL'
56
+ raise ArgumentError, 'image_url is required for UPLOAD_BY_URL' unless options[:image_url]
57
+
58
+ params[:image_url] = options[:image_url]
59
+ response = client.request(:post, "#{base_path}/upload/", params)
60
+ when 'UPLOAD_BY_FILE_ID'
61
+ raise ArgumentError, 'file_id is required for UPLOAD_BY_FILE_ID' unless options[:file_id]
62
+
63
+ params[:file_id] = options[:file_id]
64
+ response = client.request(:post, "#{base_path}/upload/", params)
65
+ else
66
+ raise ArgumentError, "Invalid upload_type: #{upload_type}"
67
+ end
68
+
69
+ response['data']
70
+ end
71
+
72
+ # Get image info by image ID
73
+ #
74
+ # @param advertiser_id [String] Advertiser ID
75
+ # @param image_id [String] Image ID
76
+ # @return [Hash] Image info
77
+ def get_info(advertiser_id, image_id)
78
+ params = {
79
+ advertiser_id: advertiser_id,
80
+ image_ids: [image_id]
81
+ }
82
+
83
+ response = client.request(:get, "#{base_path}/info/", params)
84
+ images = response.dig('data', 'list') || []
85
+ images.first
86
+ end
87
+
88
+ # Search for images
89
+ #
90
+ # @param advertiser_id [String] Advertiser ID
91
+ # @param options [Hash] Search options
92
+ # @option options [Integer] :page Current page number (default: 1)
93
+ # @option options [Integer] :page_size Page size (default: 20)
94
+ # @option options [String] :image_ids Image IDs, comma-separated
95
+ # @option options [String] :material_ids Material IDs, comma-separated
96
+ # @option options [Integer] :width Image width
97
+ # @option options [Integer] :height Image height
98
+ # @option options [String] :signature Image MD5 hash
99
+ # @option options [Integer] :start_time Start time, in seconds
100
+ # @option options [Integer] :end_time End time, in seconds
101
+ # @option options [Boolean] :displayable Whether image can be displayed
102
+ # @yield [image] Block to process each image
103
+ # @yieldparam image [Hash] Image data from the response
104
+ # @return [Hash, Array] Image list response or processed images
105
+ def search(advertiser_id:, **options)
106
+ # Set up default values
107
+ page = options[:page] || 1
108
+ page_size = options[:page_size] || 20
109
+
110
+ params = {
111
+ advertiser_id: advertiser_id,
112
+ page: page,
113
+ page_size: page_size
114
+ }
115
+
116
+ # Add optional parameters if provided
117
+ search_fields = [:image_ids, :material_ids, :width, :height, :signature,
118
+ :start_time, :end_time, :displayable]
119
+
120
+ search_fields.each do |field|
121
+ params[field] = options[field] if options.key?(field)
122
+ end
123
+
124
+ response = client.request(:get, "#{base_path}/search/", params)
125
+ image_list = response.dig('data', 'list') || []
126
+
127
+ if block_given?
128
+ image_list.each { |image| yield(image) }
129
+ response['data']
130
+ else
131
+ image_list
132
+ end
133
+ end
134
+
135
+ # Check if an image file name is already in use
136
+ #
137
+ # @param advertiser_id [String] Advertiser ID
138
+ # @param file_names [Array<String>] List of file names to check
139
+ # @return [Hash] Result with available and unavailable file names
140
+ def check_name(advertiser_id, file_names)
141
+ params = {
142
+ advertiser_id: advertiser_id,
143
+ file_names: file_names
144
+ }
145
+
146
+ response = client.request(:post, 'file/name/check/', params)
147
+ response['data']
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require 'digest/md5'
3
+
4
+ module TiktokBusinessApi
5
+ # Utility methods for TikTok Business API
6
+ module Utils
7
+ # Calculate MD5 hash of the file
8
+ #
9
+ # @param file [File, String] File object or file path
10
+ # @return [String] MD5 hash
11
+ def self.calculate_md5(file)
12
+ if file.is_a?(String) && File.exist?(file)
13
+ # File path was provided
14
+ Digest::MD5.file(file).hexdigest
15
+ elsif file.respond_to?(:path) && File.exist?(file.path)
16
+ # File object with path
17
+ Digest::MD5.file(file.path).hexdigest
18
+ elsif file.respond_to?(:read)
19
+ # IO-like object
20
+ content = file.read
21
+ file.rewind if file.respond_to?(:rewind) # Reset the file pointer
22
+ Digest::MD5.hexdigest(content)
23
+ else
24
+ raise ArgumentError, "Unable to calculate MD5: invalid file object"
25
+ end
26
+ end
27
+
28
+ # Detect content type based on file extension
29
+ #
30
+ # @param file [File, String] File object or file path
31
+ # @return [String] MIME type
32
+ def self.detect_content_type(file)
33
+ file_path = if file.is_a?(String)
34
+ file
35
+ elsif file.respond_to?(:path)
36
+ file.path
37
+ else
38
+ return "application/octet-stream" # Default if we can't determine
39
+ end
40
+
41
+ # Simple extension to MIME type mapping for common image formats
42
+ case File.extname(file_path).downcase
43
+ when '.jpg', '.jpeg'
44
+ 'image/jpeg'
45
+ when '.png'
46
+ 'image/png'
47
+ when '.gif'
48
+ 'image/gif'
49
+ when '.bmp'
50
+ 'image/bmp'
51
+ when '.webp'
52
+ 'image/webp'
53
+ else
54
+ 'application/octet-stream'
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TiktokBusinessApi
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -3,17 +3,24 @@
3
3
  require 'faraday'
4
4
  require 'faraday/retry'
5
5
  require 'faraday/follow_redirects'
6
+ require 'faraday/multipart'
6
7
  require 'json'
7
8
 
8
9
  require_relative 'tiktok_business_api/version'
9
10
  require_relative 'tiktok_business_api/config'
10
11
  require_relative 'tiktok_business_api/errors'
12
+ require_relative 'tiktok_business_api/utils'
11
13
  require_relative 'tiktok_business_api/client'
12
14
  require_relative 'tiktok_business_api/auth'
13
15
 
14
16
  # Resources
15
17
  require_relative 'tiktok_business_api/resources/base_resource'
18
+ require_relative 'tiktok_business_api/resources/crud_resource'
16
19
  require_relative 'tiktok_business_api/resources/campaign'
20
+ require_relative 'tiktok_business_api/resources/adgroup'
21
+ require_relative 'tiktok_business_api/resources/ad'
22
+ require_relative 'tiktok_business_api/resources/image'
23
+ require_relative 'tiktok_business_api/resources/identity'
17
24
 
18
25
  module TiktokBusinessApi
19
26
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tiktok_business_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vlad Zloteanu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-15 00:00:00.000000000 Z
11
+ date: 2025-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday-multipart
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -136,6 +150,20 @@ dependencies:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0.9'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry-byebug
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.10'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.10'
139
167
  description: A Ruby interface to the TikTok Business API (Tiktok Ads API) with support
140
168
  for campaigns, ad groups, ads, and more
141
169
  email:
@@ -151,8 +179,14 @@ files:
151
179
  - lib/tiktok_business_api/client.rb
152
180
  - lib/tiktok_business_api/config.rb
153
181
  - lib/tiktok_business_api/errors.rb
182
+ - lib/tiktok_business_api/resources/ad.rb
183
+ - lib/tiktok_business_api/resources/adgroup.rb
154
184
  - lib/tiktok_business_api/resources/base_resource.rb
155
185
  - lib/tiktok_business_api/resources/campaign.rb
186
+ - lib/tiktok_business_api/resources/crud_resource.rb
187
+ - lib/tiktok_business_api/resources/identity.rb
188
+ - lib/tiktok_business_api/resources/image.rb
189
+ - lib/tiktok_business_api/utils.rb
156
190
  - lib/tiktok_business_api/version.rb
157
191
  homepage: https://github.com/vladzloteanu/tiktok_business_api
158
192
  licenses: