tiktok_business_api 0.1.0 → 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: ad55f88f2cd5e40133b2b5e9e50f531ab05d42c1481dbcb2bc10cce04e32b36c
4
- data.tar.gz: 1d9c0d9cd1d66d5f9b18ed7cafb300554f838d60fa8b3f1e674e6add14b51d16
3
+ metadata.gz: b6df153f640ad431a27d8f06557b1424f2846d8adda17fa1189dfe6bded264e0
4
+ data.tar.gz: '01822882ce8e801f197433adb20e35c74eae2fa156f66521492e71d3f7dd75d9'
5
5
  SHA512:
6
- metadata.gz: f62aad982847e47734da79d75d187819d67f518dbdc82232d2d85671bde0df713e022c930c544cb6a4976b21093f397d2aed810208deed395e1a16e846f70bac
7
- data.tar.gz: 136d73e3995bd71017e27579a5923b93c5e92141ae5828ee479d97a4eeaed835742919251c41d5f512a14e3b0311142a34f184c103d84eee2bdf113d47783e2c
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
- @config = TiktokBusinessApi.config.dup
17
-
16
+ @config = TiktokBusinessApi.config.dup || Config.new
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,57 +6,57 @@ 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
41
41
  # @param params [Hash] Query parameters
42
42
  # @param headers [Hash] Custom headers
43
43
  # @return [Hash] Response data
44
- def get(path, params = {}, headers = {})
44
+ def _http_get(path, params = {}, headers = {})
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
52
52
  # @param params [Hash] Body parameters
53
53
  # @param headers [Hash] Custom headers
54
54
  # @return [Hash] Response data
55
- def post(path, params = {}, headers = {})
55
+ def _http_post(path, params = {}, headers = {})
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