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 +4 -4
- data/README.md +50 -0
- data/lib/tiktok_business_api/client.rb +83 -43
- data/lib/tiktok_business_api/errors.rb +10 -2
- data/lib/tiktok_business_api/resources/ad.rb +102 -0
- data/lib/tiktok_business_api/resources/adgroup.rb +92 -0
- data/lib/tiktok_business_api/resources/base_resource.rb +13 -13
- data/lib/tiktok_business_api/resources/campaign.rb +6 -86
- data/lib/tiktok_business_api/resources/crud_resource.rb +202 -0
- data/lib/tiktok_business_api/resources/identity.rb +122 -0
- data/lib/tiktok_business_api/resources/image.rb +151 -0
- data/lib/tiktok_business_api/utils.rb +58 -0
- data/lib/tiktok_business_api/version.rb +1 -1
- data/lib/tiktok_business_api.rb +7 -0
- metadata +36 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6df153f640ad431a27d8f06557b1424f2846d8adda17fa1189dfe6bded264e0
|
4
|
+
data.tar.gz: '01822882ce8e801f197433adb20e35c74eae2fa156f66521492e71d3f7dd75d9'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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 <
|
7
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
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
|
data/lib/tiktok_business_api.rb
CHANGED
@@ -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.
|
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-
|
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:
|