tiktok_business_api 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad55f88f2cd5e40133b2b5e9e50f531ab05d42c1481dbcb2bc10cce04e32b36c
4
+ data.tar.gz: 1d9c0d9cd1d66d5f9b18ed7cafb300554f838d60fa8b3f1e674e6add14b51d16
5
+ SHA512:
6
+ metadata.gz: f62aad982847e47734da79d75d187819d67f518dbdc82232d2d85671bde0df713e022c930c544cb6a4976b21093f397d2aed810208deed395e1a16e846f70bac
7
+ data.tar.gz: 136d73e3995bd71017e27579a5923b93c5e92141ae5828ee479d97a4eeaed835742919251c41d5f512a14e3b0311142a34f184c103d84eee2bdf113d47783e2c
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # TikTok Business API Ruby Gem
2
+
3
+ A Ruby interface to the TikTok Business API with support for campaigns, ad groups, ads, and more.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'tiktok_business_api'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install tiktok_business_api
23
+ ```
24
+
25
+ ## Getting Started
26
+
27
+ ### Configuration
28
+
29
+ You need to configure the client with your TikTok app credentials:
30
+
31
+ ```ruby
32
+ TiktokBusinessApi.configure do |config|
33
+ config.app_id = 'your_app_id'
34
+ config.secret = 'your_app_secret'
35
+ config.debug = true # Enable request/response logging
36
+ end
37
+ ```
38
+
39
+ If you want to use a custom logger:
40
+
41
+ ```ruby
42
+ require 'logger'
43
+ logger = Logger.new(STDOUT)
44
+ logger.level = Logger::DEBUG
45
+
46
+ TiktokBusinessApi.configure do |config|
47
+ config.logger = logger
48
+ end
49
+ ```
50
+
51
+ ### Authentication
52
+
53
+ #### Generate an authorization URL
54
+
55
+ ```ruby
56
+ client = TiktokBusinessApi.client
57
+ auth_url = client.auth.authorization_url('https://your-redirect-url.com/callback')
58
+ ```
59
+
60
+ After the user authorizes your app, TikTok will redirect to your callback URL with an authorization code.
61
+
62
+ #### Generate an access token
63
+
64
+ ```ruby
65
+ auth_code = 'auth_code_from_callback'
66
+ redirect_uri = 'https://your-redirect-url.com/callback'
67
+
68
+ response = client.auth.generate_access_token(auth_code, redirect_uri)
69
+ access_token = response['data']['access_token']
70
+ ```
71
+
72
+ The access token will automatically be stored in your client configuration for future requests.
73
+
74
+ ### Working with Campaigns
75
+
76
+ ```ruby
77
+ # Create a new client instance
78
+ client = TiktokBusinessApi.client(access_token: 'your_access_token')
79
+
80
+ # Create a campaign
81
+ advertiser_id = '6000000000000'
82
+ campaign_params = {
83
+ campaign_name: 'My First Campaign',
84
+ objective_type: 'TRAFFIC',
85
+ budget_mode: 'BUDGET_MODE_TOTAL',
86
+ budget: 1000
87
+ }
88
+
89
+ campaign = client.campaigns.create(advertiser_id, campaign_params)
90
+ campaign_id = campaign['campaign_id']
91
+
92
+ # Get a list of campaigns
93
+ campaigns = client.campaigns.list(advertiser_id)
94
+
95
+ # Get a specific campaign
96
+ campaign = client.campaigns.get(advertiser_id, campaign_id)
97
+
98
+ # Update a campaign
99
+ update_params = {
100
+ campaign_name: 'Updated Campaign Name'
101
+ }
102
+ client.campaigns.update(advertiser_id, campaign_id, update_params)
103
+
104
+ # Enable/disable a campaign
105
+ client.campaigns.update_status(advertiser_id, campaign_id, 'DISABLE')
106
+
107
+ # Delete a campaign
108
+ client.campaigns.delete(advertiser_id, campaign_id)
109
+
110
+ # List all campaigns with pagination (yields each campaign to a block)
111
+ client.campaigns.list_all(advertiser_id) do |campaign|
112
+ puts "Campaign: #{campaign['campaign_name']}"
113
+ end
114
+ ```
115
+
116
+ ## Logging Requests and Responses
117
+
118
+ You can enable debug logging to see all API requests and responses:
119
+
120
+ ```ruby
121
+ TiktokBusinessApi.configure do |config|
122
+ config.debug = true
123
+ config.logger = Logger.new(STDOUT)
124
+ end
125
+ ```
126
+
127
+ ## Error Handling
128
+
129
+ The gem raises specific exceptions for different error types:
130
+
131
+ ```ruby
132
+ begin
133
+ client.campaigns.create(advertiser_id, campaign_params)
134
+ rescue TiktokBusinessApi::AuthenticationError => e
135
+ puts "Authentication failed: #{e.message}"
136
+ rescue TiktokBusinessApi::RateLimitError => e
137
+ puts "Rate limit exceeded: #{e.message}"
138
+ rescue TiktokBusinessApi::ApiError => e
139
+ puts "API error: #{e.message}, Status code: #{e.status_code}, Body: #{e.body}"
140
+ rescue TiktokBusinessApi::Error => e
141
+ puts "General error: #{e.message}"
142
+ end
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ### With Docker (recommended)
148
+
149
+ This gem includes a Docker-based development environment to ensure consistent development and testing.
150
+
151
+ #### Prerequisites
152
+
153
+ - Docker and Docker Compose
154
+ - Make (optional, but recommended)
155
+
156
+ #### Setup using Make
157
+
158
+ ```bash
159
+ # Set up the development environment
160
+ make setup
161
+
162
+ # Run tests
163
+ make test
164
+
165
+ # Open a shell in the Docker container
166
+ make shell
167
+
168
+ # Start a console with the gem loaded
169
+ make console
170
+
171
+ # Run RuboCop linting
172
+ make lint
173
+
174
+ # Generate documentation
175
+ make docs
176
+
177
+ # Build the gem
178
+ make build
179
+
180
+ # Prepare a new release
181
+ make release
182
+
183
+ # Publish to RubyGems
184
+ make publish
185
+
186
+ # View all available commands
187
+ make help
188
+ ```
189
+
190
+ #### Without Make
191
+
192
+ If you prefer to use Docker Compose directly:
193
+
194
+ ```bash
195
+ # Set up the development environment
196
+ docker-compose build
197
+
198
+ # Run tests
199
+ docker-compose run --rm test
200
+
201
+ # Start a console with the gem loaded
202
+ docker-compose run --rm console
203
+
204
+ # Open a shell in the Docker container
205
+ docker-compose run --rm gem bash
206
+ ```
207
+
208
+ ### Without Docker
209
+
210
+ If you prefer to develop without Docker:
211
+
212
+ ```bash
213
+ # Install dependencies
214
+ bundle install
215
+
216
+ # Run tests
217
+ bundle exec rake spec
218
+
219
+ # Run linting
220
+ bundle exec rubocop
221
+
222
+ # Generate documentation
223
+ bundle exec yard
224
+
225
+ # Build the gem
226
+ bundle exec rake build
227
+
228
+ # Start a console with the gem loaded
229
+ bin/console
230
+ ```
231
+
232
+ ## Contributing
233
+
234
+ 1. Fork it
235
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
236
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
237
+ 4. Push to the branch (`git push origin my-new-feature`)
238
+ 5. Create a new Pull Request
239
+
240
+ ## License
241
+
242
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "tiktok_business_api"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/publish_gem ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # This script publishes the gem to RubyGems without requiring Git operations
5
+
6
+ # Build the gem if needed
7
+ if [ ! -f "pkg/tiktok_business_api-$(ruby -r ./lib/tiktok_business_api/version -e 'print TiktokBusinessApi::VERSION').gem" ]; then
8
+ echo "Building gem..."
9
+ bundle exec rake build
10
+ fi
11
+
12
+ # Get the current version
13
+ CURRENT_VERSION=$(ruby -r ./lib/tiktok_business_api/version -e 'print TiktokBusinessApi::VERSION')
14
+ echo "Publishing tiktok_business_api version $CURRENT_VERSION to RubyGems..."
15
+
16
+ # Prompt for RubyGems credentials if needed
17
+ if [ -z "$RUBYGEMS_API_KEY" ]; then
18
+ if [ ! -f ~/.gem/credentials ]; then
19
+ echo "RubyGems credentials not found. You need to log in to RubyGems."
20
+ gem push --help
21
+ read -p "Press Enter after you've set up your credentials..." _
22
+ fi
23
+ fi
24
+
25
+ # Push to RubyGems
26
+ echo "Pushing gem to RubyGems..."
27
+ gem push "pkg/tiktok_business_api-$CURRENT_VERSION.gem"
28
+
29
+ echo "Gem successfully published!"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'faraday/follow_redirects'
6
+ require 'json'
7
+
8
+ require_relative 'tiktok_ads_api/version'
9
+ require_relative 'tiktok_ads_api/config'
10
+ require_relative 'tiktok_ads_api/errors'
11
+ require_relative 'tiktok_ads_api/client'
12
+ require_relative 'tiktok_ads_api/auth'
13
+
14
+ # Resources
15
+ require_relative 'tiktok_ads_api/resources/base_resource'
16
+ require_relative 'tiktok_ads_api/resources/campaign'
17
+
18
+ module TiktokAdsApi
19
+ class << self
20
+ attr_accessor :config
21
+
22
+ # Configure the TikTok Ads API client
23
+ #
24
+ # @yield [config] Configuration object that can be modified
25
+ # @return [TiktokAdsApi::Config] The configuration object
26
+ def configure
27
+ self.config ||= Config.new
28
+ yield(config) if block_given?
29
+ config
30
+ end
31
+
32
+ # Create a new client instance
33
+ #
34
+ # @param options [Hash] Optional configuration overrides
35
+ # @return [TiktokAdsApi::Client] A new client instance
36
+ def client(options = {})
37
+ TiktokAdsApi::Client.new(options)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ # Handles authentication with the TikTok Business API
5
+ class Auth
6
+ # @return [TiktokBusinessApi::Client] Client instance
7
+ attr_reader :client
8
+
9
+ # Initialize the Auth handler
10
+ #
11
+ # @param client [TiktokBusinessApi::Client] Client instance
12
+ def initialize(client)
13
+ @client = client
14
+ end
15
+
16
+ # Generate an access token using an authorization code
17
+ #
18
+ # @param auth_code [String] Authorization code from TikTok
19
+ # @param redirect_uri [String] Redirect URI used in the authorization request
20
+ # @return [Hash] Access token response
21
+ def generate_access_token(auth_code, redirect_uri)
22
+ params = {
23
+ app_id: client.config.app_id,
24
+ secret: client.config.secret,
25
+ auth_code: auth_code,
26
+ grant_type: 'auth_code'
27
+ }
28
+
29
+ # Add redirect_uri if provided
30
+ params[:redirect_uri] = redirect_uri if redirect_uri
31
+
32
+ response = client.request(:post, 'v1.3/oauth2/access_token/', params)
33
+
34
+ # Store the access token in the client config
35
+ client.config.access_token = response['data']['access_token'] if response['data'] && response['data']['access_token']
36
+
37
+ response
38
+ end
39
+
40
+ # Refresh an access token (for TikTok account access tokens)
41
+ #
42
+ # @param refresh_token [String] Refresh token
43
+ # @return [Hash] New access token response
44
+ def refresh_access_token(refresh_token)
45
+ params = {
46
+ app_id: client.config.app_id,
47
+ secret: client.config.secret,
48
+ refresh_token: refresh_token,
49
+ grant_type: 'refresh_token'
50
+ }
51
+
52
+ response = client.request(:post, 'v1.3/tt_user/oauth2/refresh_token/', params)
53
+
54
+ # Update the access token in the client config
55
+ client.config.access_token = response['data']['access_token'] if response['data'] && response['data']['access_token']
56
+
57
+ response
58
+ end
59
+
60
+ # Revoke an access token
61
+ #
62
+ # @param token [String] Access token to revoke (defaults to the current access token)
63
+ # @return [Hash] Response
64
+ def revoke_access_token(token = nil)
65
+ token ||= client.config.access_token
66
+
67
+ params = {
68
+ app_id: client.config.app_id,
69
+ secret: client.config.secret,
70
+ access_token: token
71
+ }
72
+
73
+ response = client.request(:post, 'v1.3/oauth2/revoke_token/', params)
74
+
75
+ # Clear the access token if it was revoked
76
+ client.config.access_token = nil if response['code'] == 0
77
+
78
+ response
79
+ end
80
+
81
+ # Get a list of authorized advertiser accounts
82
+ #
83
+ # @param app_id [String] App ID (defaults to the configured app_id)
84
+ # @param secret [String] App secret (defaults to the configured secret)
85
+ # @param access_token [String] Access token (defaults to the configured access_token)
86
+ # @return [Hash] List of authorized advertisers
87
+ def get_authorized_advertisers(app_id = nil, secret = nil, access_token = nil)
88
+ params = {}
89
+
90
+ # Use provided values or fallback to client config
91
+ headers = {}
92
+ headers['Access-Token'] = access_token || client.config.access_token if access_token || client.config.access_token
93
+
94
+ client.request(:get, 'v1.3/oauth2/advertiser/get/', params, headers)
95
+ end
96
+
97
+ # Generate authorization URL for TikTok Business API
98
+ #
99
+ # @param redirect_uri [String] Redirect URI where the authorization code will be sent
100
+ # @param state [String] Optional state parameter for security
101
+ # @param scope [Array<String>] Optional array of permission scopes
102
+ # @return [String] Authorization URL
103
+ def authorization_url(redirect_uri, state = nil, scope = nil)
104
+ params = {
105
+ app_id: client.config.app_id,
106
+ redirect_uri: redirect_uri
107
+ }
108
+
109
+ params[:state] = state if state
110
+ params[:scope] = scope.join(',') if scope && !scope.empty?
111
+
112
+ query = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
113
+ "https://ads.tiktok.com/marketing_api/auth?#{query}"
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ # Main client for interacting with the TikTok Business API
5
+ class Client
6
+ # @return [TiktokBusinessApi::Config] Client configuration
7
+ attr_reader :config
8
+
9
+ # @return [TiktokBusinessApi::Auth] Authentication handler
10
+ attr_reader :auth
11
+
12
+ # Initialize a new client
13
+ #
14
+ # @param options [Hash] Override configuration options
15
+ def initialize(options = {})
16
+ @config = TiktokBusinessApi.config.dup
17
+
18
+ # Override config with options
19
+ options.each do |key, value|
20
+ @config.send("#{key}=", value) if @config.respond_to?("#{key}=")
21
+ end
22
+
23
+ @auth = Auth.new(self)
24
+ @resources = {}
25
+ end
26
+
27
+ # Get or create a resource instance
28
+ #
29
+ # @param resource_name [Symbol] Name of the resource
30
+ # @return [BaseResource] Resource instance
31
+ def resource(resource_name)
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
42
+ end
43
+
44
+ # Make an HTTP request to the TikTok Business API
45
+ #
46
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
47
+ # @param path [String] API endpoint path
48
+ # @param params [Hash] URL parameters for GET, or body parameters for POST/PUT
49
+ # @param headers [Hash] Additional HTTP headers
50
+ # @return [Hash] Parsed API response
51
+ def request(method, path, params = {}, headers = {})
52
+ url = File.join(@config.api_base_url, path)
53
+
54
+ # Set up default headers
55
+ default_headers = {
56
+ 'Content-Type' => 'application/json'
57
+ }
58
+
59
+ # Add access token if available
60
+ if @config.access_token
61
+ default_headers['Access-Token'] = @config.access_token
62
+ end
63
+
64
+ # Merge with custom headers
65
+ headers = default_headers.merge(headers)
66
+
67
+ # Log the request
68
+ log_request(method, url, params, headers)
69
+
70
+ # Build the request
71
+ response = connection.run_request(method, url, nil, headers) do |req|
72
+ case method
73
+ when :get, :delete
74
+ req.params = params
75
+ when :post, :put
76
+ req.body = JSON.generate(params) unless params.empty?
77
+ end
78
+ end
79
+
80
+ # Parse and handle the response
81
+ handle_response(response)
82
+ end
83
+
84
+ # Access to campaign resource
85
+ #
86
+ # @return [TiktokBusinessApi::Resources::Campaign] Campaign resource
87
+ def campaigns
88
+ resource(:campaign)
89
+ end
90
+
91
+ private
92
+
93
+ # Set up Faraday connection
94
+ #
95
+ # @return [Faraday::Connection] Faraday connection
96
+ def connection
97
+ @connection ||= Faraday.new do |conn|
98
+ conn.options.timeout = @config.timeout
99
+ conn.options.open_timeout = @config.open_timeout
100
+
101
+ # Set up middleware
102
+ conn.use Faraday::Response::Logger, @config.logger if @config.logger
103
+ conn.use Faraday::FollowRedirects::Middleware
104
+ conn.use Faraday::Retry::Middleware, max: 3
105
+
106
+ conn.adapter Faraday.default_adapter
107
+ end
108
+ end
109
+
110
+ # Parse and handle the API response
111
+ #
112
+ # @param response [Faraday::Response] HTTP response
113
+ # @return [Hash] Parsed response body
114
+ # @raise [TiktokBusinessApi::Error] If the response indicates an error
115
+ def handle_response(response)
116
+ # Log the response
117
+ log_response(response)
118
+
119
+ # Parse the response body
120
+ 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
+
130
+ # Check for API errors
131
+ if !response.success? || (body.is_a?(Hash) && body['code'] != 0)
132
+ raise ErrorFactory.from_response(response)
133
+ end
134
+
135
+ body
136
+ end
137
+
138
+ # Log the request details
139
+ #
140
+ # @param method [Symbol] HTTP method
141
+ # @param url [String] Request URL
142
+ # @param params [Hash] Request parameters
143
+ # @param headers [Hash] Request headers
144
+ def log_request(method, url, params, headers)
145
+ return unless @config.debug && @config.logger
146
+
147
+ @config.logger.debug "[TiktokBusinessApi] Request: #{method.upcase} #{url}"
148
+ @config.logger.debug "[TiktokBusinessApi] Parameters: #{params.inspect}"
149
+ @config.logger.debug "[TiktokBusinessApi] Headers: #{headers.inspect}"
150
+ end
151
+
152
+ # Log the response details
153
+ #
154
+ # @param response [Faraday::Response] HTTP response
155
+ def log_response(response)
156
+ return unless @config.debug && @config.logger
157
+
158
+ @config.logger.debug "[TiktokBusinessApi] Response Status: #{response.status}"
159
+ @config.logger.debug "[TiktokBusinessApi] Response Body: #{response.body}"
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ # Configuration class for TikTok Business API
5
+ class Config
6
+ # @return [String] TikTok API app ID
7
+ attr_accessor :app_id
8
+
9
+ # @return [String] TikTok API app secret
10
+ attr_accessor :secret
11
+
12
+ # @return [String] TikTok API access token
13
+ attr_accessor :access_token
14
+
15
+ # @return [String] Base URL for the TikTok API
16
+ attr_accessor :api_base_url
17
+
18
+ # @return [Boolean] Whether to enable debug logging
19
+ attr_accessor :debug
20
+
21
+ # @return [Object] Custom logger instance
22
+ attr_accessor :logger
23
+
24
+ # @return [Integer] Request timeout in seconds
25
+ attr_accessor :timeout
26
+
27
+ # @return [Integer] Open timeout in seconds
28
+ attr_accessor :open_timeout
29
+
30
+ # Initialize configuration with default values
31
+ def initialize
32
+ @api_base_url = 'https://business-api.tiktok.com/open_api/'
33
+ @debug = false
34
+ @timeout = 60
35
+ @open_timeout = 30
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ # Base error class for the TikTok Business API
5
+ class Error < StandardError
6
+ # @return [Integer] HTTP status code
7
+ attr_reader :status_code
8
+
9
+ # @return [Hash] Full response body
10
+ attr_reader :body
11
+
12
+ # @return [Hash] Request that caused the error
13
+ attr_reader :request
14
+
15
+ # Initialize a new error
16
+ #
17
+ # @param message [String] Error message
18
+ # @param status_code [Integer] HTTP status code
19
+ # @param body [Hash] Response body
20
+ # @param request [Hash] Request that caused the error
21
+ def initialize(message = nil, status_code = nil, body = nil, request = nil)
22
+ @status_code = status_code
23
+ @body = body
24
+ @request = request
25
+ super(message)
26
+ end
27
+ end
28
+
29
+ # Error raised when authentication fails
30
+ class AuthenticationError < Error; end
31
+
32
+ # Error raised when authorization fails
33
+ class AuthorizationError < Error; end
34
+
35
+ # Error raised for invalid requests
36
+ class InvalidRequestError < Error; end
37
+
38
+ # Error raised for API errors
39
+ class ApiError < Error; end
40
+
41
+ # Error raised for rate limit errors
42
+ class RateLimitError < Error; end
43
+
44
+ # Factory for creating the appropriate error object
45
+ class ErrorFactory
46
+ # Create an error object based on the response
47
+ #
48
+ # @param response [Faraday::Response] The HTTP response
49
+ # @param request [Hash] The request that caused the error
50
+ # @return [TiktokBusinessApi::Error] The appropriate error object
51
+ def self.from_response(response, request = nil)
52
+ status = response.status
53
+ body = response.body
54
+
55
+ # Parse TikTok API response which has its own error structure
56
+ error_code = body.is_a?(Hash) ? body['code'] : nil
57
+ error_message = body.is_a?(Hash) ? body['message'] : nil
58
+
59
+ # Determine the error class based on status and error code
60
+ klass = case status
61
+ when 401
62
+ AuthenticationError
63
+ when 403
64
+ AuthorizationError
65
+ when 429
66
+ RateLimitError
67
+ when 400..499
68
+ InvalidRequestError
69
+ when 500..599
70
+ ApiError
71
+ else
72
+ Error
73
+ end
74
+
75
+ # Create and return the error
76
+ klass.new(
77
+ error_message || "HTTP #{status}",
78
+ status,
79
+ body,
80
+ request
81
+ )
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
5
+ # Base class for all API resources
6
+ class BaseResource
7
+ # @return [TiktokBusinessApi::Client] Client instance
8
+ attr_reader :client
9
+
10
+ # Initialize a new resource
11
+ #
12
+ # @param client [TiktokBusinessApi::Client] Client instance
13
+ def initialize(client)
14
+ @client = client
15
+ end
16
+
17
+ # Get the resource name (used for endpoint paths)
18
+ #
19
+ # @return [String] Resource name
20
+ def resource_name
21
+ self.class.name.split('::').last.downcase
22
+ end
23
+
24
+ # Get the API version
25
+ #
26
+ # @return [String] API version
27
+ def api_version
28
+ 'v1.3'
29
+ end
30
+
31
+ # Get the base path for this resource
32
+ #
33
+ # @return [String] Base path
34
+ def base_path
35
+ "#{api_version}/#{resource_name}/"
36
+ end
37
+
38
+ # Make a GET request to the resource
39
+ #
40
+ # @param path [String] Path relative to the resource base path
41
+ # @param params [Hash] Query parameters
42
+ # @param headers [Hash] Custom headers
43
+ # @return [Hash] Response data
44
+ def get(path, params = {}, headers = {})
45
+ full_path = File.join(base_path, path)
46
+ client.request(:get, full_path, params, headers)
47
+ end
48
+
49
+ # Make a POST request to the resource
50
+ #
51
+ # @param path [String] Path relative to the resource base path
52
+ # @param params [Hash] Body parameters
53
+ # @param headers [Hash] Custom headers
54
+ # @return [Hash] Response data
55
+ def post(path, params = {}, headers = {})
56
+ full_path = File.join(base_path, path)
57
+ client.request(:post, full_path, params, headers)
58
+ end
59
+
60
+ # Handle pagination for list endpoints
61
+ #
62
+ # @param path [String] Path relative to the resource base path
63
+ # @param params [Hash] Query parameters
64
+ # @param headers [Hash] Custom headers
65
+ # @param data_key [String] Key in the response that contains the data array
66
+ # @yield [item] Block to process each item
67
+ # @yieldparam item [Hash] Item from the response
68
+ # @return [Array] All items if no block is given
69
+ def paginate(path, params = {}, headers = {}, data_key = 'data')
70
+ items = []
71
+ page = 1
72
+ page_size = params[:page_size] || 10
73
+ has_more = true
74
+
75
+ while has_more
76
+ params[:page] = page
77
+ params[:page_size] = page_size
78
+
79
+ response = get(path, params, headers)
80
+
81
+ # Extract data from the response
82
+ current_items = response.dig('data', data_key) || []
83
+
84
+ if block_given?
85
+ current_items.each { |item| yield(item) }
86
+ else
87
+ items.concat(current_items)
88
+ end
89
+
90
+ # Check if there are more pages
91
+ page_info = response.dig('data', 'page_info') || {}
92
+ has_more = page_info['has_more'] == true
93
+ page += 1
94
+ end
95
+
96
+ block_given? ? nil : items
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ module Resources
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)
15
+
16
+ response = post('create/', params)
17
+ response['data']
18
+ end
19
+
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 = get('get/', params)
30
+ response['data']
31
+ end
32
+
33
+ # Get a list of campaigns with pagination support
34
+ #
35
+ # @param advertiser_id [String] Advertiser ID
36
+ # @param params [Hash] Filter parameters
37
+ # @yield [campaign] Block to process each campaign
38
+ # @yieldparam campaign [Hash] Campaign from the response
39
+ # @return [Array] All campaigns if no block is given
40
+ def list_all(advertiser_id, params = {}, &block)
41
+ # Ensure advertiser_id is included in the params
42
+ params = params.merge(advertiser_id: advertiser_id)
43
+
44
+ paginate('get/', params, {}, 'list')
45
+ end
46
+
47
+ # Get a campaign by ID
48
+ #
49
+ # @param advertiser_id [String] Advertiser ID
50
+ # @param campaign_id [String] Campaign ID
51
+ # @return [Hash] Campaign data
52
+ def get(advertiser_id, campaign_id)
53
+ params = {
54
+ advertiser_id: advertiser_id,
55
+ campaign_ids: [campaign_id]
56
+ }
57
+
58
+ response = get('get/', params)
59
+ campaigns = response.dig('data', 'list') || []
60
+ campaigns.first
61
+ end
62
+
63
+ # Update a campaign
64
+ #
65
+ # @param advertiser_id [String] Advertiser ID
66
+ # @param campaign_id [String] Campaign ID
67
+ # @param params [Hash] Campaign parameters to update
68
+ # @return [Hash] Updated campaign data
69
+ def update(advertiser_id, campaign_id, params = {})
70
+ # Ensure required parameters are included
71
+ params = params.merge(
72
+ advertiser_id: advertiser_id,
73
+ campaign_id: campaign_id
74
+ )
75
+
76
+ response = post('update/', params)
77
+ response['data']
78
+ end
79
+
80
+ # Update campaign status (enable/disable)
81
+ #
82
+ # @param advertiser_id [String] Advertiser ID
83
+ # @param campaign_id [String] Campaign ID
84
+ # @param status [String] New status ('ENABLE' or 'DISABLE')
85
+ # @return [Hash] Result
86
+ def update_status(advertiser_id, campaign_id, status)
87
+ params = {
88
+ advertiser_id: advertiser_id,
89
+ campaign_ids: [campaign_id],
90
+ operation_status: status
91
+ }
92
+
93
+ response = post('status/update/', params)
94
+ response['data']
95
+ end
96
+
97
+ # Delete a campaign
98
+ #
99
+ # @param advertiser_id [String] Advertiser ID
100
+ # @param campaign_id [String] Campaign ID
101
+ # @return [Hash] Result
102
+ def delete(advertiser_id, campaign_id)
103
+ params = {
104
+ advertiser_id: advertiser_id,
105
+ campaign_ids: [campaign_id]
106
+ }
107
+
108
+ response = post('delete/', params)
109
+ response['data']
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TiktokBusinessApi
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'faraday/follow_redirects'
6
+ require 'json'
7
+
8
+ require_relative 'tiktok_business_api/version'
9
+ require_relative 'tiktok_business_api/config'
10
+ require_relative 'tiktok_business_api/errors'
11
+ require_relative 'tiktok_business_api/client'
12
+ require_relative 'tiktok_business_api/auth'
13
+
14
+ # Resources
15
+ require_relative 'tiktok_business_api/resources/base_resource'
16
+ require_relative 'tiktok_business_api/resources/campaign'
17
+
18
+ module TiktokBusinessApi
19
+ class << self
20
+ attr_accessor :config
21
+
22
+ # Configure the TikTok Business API client
23
+ #
24
+ # @yield [config] Configuration object that can be modified
25
+ # @return [TiktokBusinessApi::Config] The configuration object
26
+ def configure
27
+ self.config ||= Config.new
28
+ yield(config) if block_given?
29
+ config
30
+ end
31
+
32
+ # Create a new client instance
33
+ #
34
+ # @param options [Hash] Optional configuration overrides
35
+ # @return [TiktokBusinessApi::Client] A new client instance
36
+ def client(options = {})
37
+ TiktokBusinessApi::Client.new(options)
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tiktok_business_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Your Name
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-follow_redirects
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.14'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.14'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.21'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.21'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.9'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.9'
139
+ description: A Ruby interface to the TikTok Business API with support for campaigns,
140
+ ad groups, ads, and more
141
+ email:
142
+ - your.email@example.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - LICENSE.txt
148
+ - README.md
149
+ - bin/console
150
+ - bin/publish_gem
151
+ - lib/tiktok_ads_api.rb
152
+ - lib/tiktok_business_api.rb
153
+ - lib/tiktok_business_api/auth.rb
154
+ - lib/tiktok_business_api/client.rb
155
+ - lib/tiktok_business_api/config.rb
156
+ - lib/tiktok_business_api/errors.rb
157
+ - lib/tiktok_business_api/resources/base_resource.rb
158
+ - lib/tiktok_business_api/resources/campaign.rb
159
+ - lib/tiktok_business_api/version.rb
160
+ homepage: https://github.com/yourusername/tiktok_business_api
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 2.6.0
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubygems_version: 3.4.19
180
+ signing_key:
181
+ specification_version: 4
182
+ summary: Ruby client for the TikTok Business API
183
+ test_files: []