Class: AdsPowerClient

Inherits:
Object
  • Object
show all
Defined in:
lib/adspower-client.rb

Constant Summary collapse

CLOUD_API_BASE =
'https://api.adspower.com/v1'
LOCK_FILE =
'/tmp/adspower_api_lock'
@@drivers =

control over the drivers created, in order to not create the same driver twice and not generate memory leaks. reference: github.com/leandrosardi/adspower-client/issues/4

{}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(h = {}) ⇒ AdsPowerClient

Returns a new instance of AdsPowerClient.



22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/adspower-client.rb', line 22

def initialize(h={})
    self.key = h[:key] # mandatory
    self.port = h[:port] || '50325'
    self.server_log = h[:server_log] || '~/adspower-client.log'
    self.adspower_listener = h[:adspower_listener] || 'http://127.0.0.1'
    
    # DEPRECATED
    self.adspower_default_browser_version = h[:adspower_default_browser_version] || '116'

    # PENDING
    self.cloud_token = h[:cloud_token]
end

Instance Attribute Details

#adspower_default_browser_versionObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def adspower_default_browser_version
  @adspower_default_browser_version
end

#adspower_listenerObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def adspower_listener
  @adspower_listener
end

#cloud_tokenObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def cloud_token
  @cloud_token
end

#keyObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def key
  @key
end

#portObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def port
  @port
end

#server_logObject



14
15
16
# File 'lib/adspower-client.rb', line 14

def server_log
  @server_log
end

Instance Method Details

#acquire_lockObject

Acquire the lock



36
37
38
39
# File 'lib/adspower-client.rb', line 36

def acquire_lock
    @lockfile ||= File.open(LOCK_FILE, File::CREAT | File::RDWR)
    @lockfile.flock(File::LOCK_EX)
end

#check(id) ⇒ Object

Check if the browser session for the given user profile is active.



264
265
266
267
268
269
270
271
272
# File 'lib/adspower-client.rb', line 264

def check(id)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/browser/active?user_id=#{id}"
        uri = URI.parse(url)
        res = Net::HTTP.get(uri)
        return false if JSON.parse(res)['msg'] != 'success'
        JSON.parse(res)['data']['status'] == 'Active'
    end
end

#cloud_profile_quotaObject

Return a hash with:

• :limit     ⇒ total profile slots allowed (-1 = unlimited)
• :used      ⇒ number of profiles currently created
• :remaining ⇒ slots left (nil if unlimited)

Fetch your real profile quota from the Cloud API



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/adspower-client.rb', line 127

def cloud_profile_quota
    uri = URI("#{CLOUD_API_BASE}/account/get_info")
    req = Net::HTTP::Get.new(uri)
    req['Authorization'] = "Bearer #{self.cloud_token}"

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
        http.request(req)
    end
    data = JSON.parse(res.body)
    raise "Cloud API error: #{data['msg']}" unless data['code'] == 0

    allowed = data['data']['total_profiles_allowed'].to_i
    used    = data['data']['profiles_used'].to_i
    remaining = allowed < 0 ? nil : (allowed - used)

    { limit:     allowed,
    used:      used,
    remaining: remaining }
end

#createObject

Create a new user profile via API call and return the ID of the created user.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/adspower-client.rb', line 148

def create
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/user/create"
        body = {
            'group_id' => '0',
            'proxyid' => '1',
            'fingerprint_config' => {
                'browser_kernel_config' => {"version": self.adspower_default_browser_version, "type": "chrome"}
            }
        }
        # API call
        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret['data']['id']
    end
end

#create2(name:, proxy_config:, group_id: '0', browser_version: nil) ⇒ Object

Create a new desktop profile with custom name, proxy, and fingerprint settings

Parameters:

  • name (String)

    the profile’s display name

  • proxy_config (Hash)

    keys: :ip, :port, :user, :password, :proxy_soft (default ‘other’), :proxy_type (default ‘http’)

  • group_id (String) (defaults to: '0')

    which AdsPower group to assign (default ‘0’)

  • browser_version (String) (defaults to: nil)

    Chrome version to use (must match Chromedriver), defaults to adspower_default_browser_version

Returns:

  • String the new profile’s ID



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/adspower-client.rb', line 173

def create2(name:, proxy_config:, group_id: '0', browser_version: nil)
    browser_version ||= adspower_default_browser_version

    with_lock do
        url = "#{adspower_listener}:#{port}/api/v2/browser-profile/create"
        body = {
            'name'            => name,
            'group_id'        => group_id,
            'user_proxy_config' => {
            'proxy_soft'     => proxy_config[:proxy_soft]     || 'other',
            'proxy_type'     => proxy_config[:proxy_type]     || 'http',
            'proxy_host'     => proxy_config[:ip],
            'proxy_port'     => proxy_config[:port].to_s,
            'proxy_user'     => proxy_config[:user],
            'proxy_password' => proxy_config[:password]
            },
            'fingerprint_config' => {
                # 1) Chrome kernel version → must match your Chromedriver
                'browser_kernel_config' => {
                    'version' => browser_version,
                    'type'    => 'chrome'
                },
                # 2) Auto‐detect timezone (and locale) from proxy IP
                'automatic_timezone' => '1',
                'timezone'           => '',
                'language'           => [],
                # 3) Force desktop UA (no mobile): empty random_ua & default UA settings
                'ua' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{browser_version}.0.0.0 Safari/537.36",
                'ua_category' => 'desktop',
                #'screen_resolution' => '1920*1080',
                'is_mobile' => false,
                # standard desktop fingerprints
                'webrtc'  => 'disabled',  # hide real IP via WebRTC
                'flash'   => 'allow',
                'fonts'   => [],          # default fonts
            }
        }

        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error creating profile: #{ret['msg']}" unless ret['code'] == 0

        ret['data']['profile_id']
    end
end

#delete(id) ⇒ Object

Delete a user profile via API call.



221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/adspower-client.rb', line 221

def delete(id)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/user/delete"
        body = {
            'api_key' => self.key,
            'user_ids' => [id],
        }
        # API call
        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
    end
end

#driver(id, headless = false) ⇒ Object

Attach to the existing browser session with Selenium WebDriver.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/adspower-client.rb', line 275

def driver(id, headless=false)
    # Return the existing driver if it's still active.
    old = @@drivers[id]
    return old if old

    # Otherwise, start the driver
    ret = self.start(id, headless)

    # Attach test execution to the existing browser
    url = ret['data']['ws']['selenium']
    opts = Selenium::WebDriver::Chrome::Options.new
    opts.add_option("debuggerAddress", url)

    # Connect to the existing browser
    driver = Selenium::WebDriver.for(:chrome, options: opts)

    # Save the driver
    @@drivers[id] = driver

    # Return the driver
    driver
end

#driver2(id, headless: false, read_timeout: 180) ⇒ Object

Attach to the existing browser session with Selenium WebDriver.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/adspower-client.rb', line 299

def driver2(id, headless: false, read_timeout: 180)
    # Return the existing driver if it's still active.
    old = @@drivers[id]
    return old if old

    # Otherwise, start the driver
    ret = self.start(id, headless)

    # Attach test execution to the existing browser
    url = ret['data']['ws']['selenium']
    opts = Selenium::WebDriver::Chrome::Options.new
    opts.add_option("debuggerAddress", url)

    # Set up the custom HTTP client with a longer timeout
    client = Selenium::WebDriver::Remote::Http::Default.new
    client.read_timeout = read_timeout # Set this to the desired timeout in seconds

    # Connect to the existing browser
    driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: client)

    # Save the driver
    @@drivers[id] = driver

    # Return the driver
    driver
end

#html(url) ⇒ Object

DEPRECATED - Use Zyte instead of this method.

Create a new profile, start the browser, visit a page, grab the HTML, and clean up.



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/adspower-client.rb', line 329

def html(url)
    ret = {
        :profile_id => nil,
        :html => nil,
        :status => 'success',
    }
    id = nil
    html = nil
    begin
        # Create the profile
        sleep(1)
        id = self.create

        # Update the result
        ret[:profile_id] = id

        # Start the profile and attach the driver
        driver = self.driver(id)

        # Get HTML
        driver.get(url)
        html = driver.page_source

        # Update the result
        ret[:html] = html

        # Stop the profile
        sleep(1)
        driver.quit
        self.stop(id)

        # Delete the profile
        sleep(1)
        self.delete(id)

        # Reset ID
        id = nil
    rescue => e
        # Stop and delete current profile if an error occurs
        if id
            sleep(1)
            self.stop(id)
            sleep(1)
            driver.quit if driver
            self.delete(id) if id
        end
        # Inform the exception
        ret[:status] = e.to_s
#        # process interruption
#        rescue SignalException, SystemExit, Interrupt => e 
#            if id
#                sleep(1) # Avoid the "Too many request per second" error
#                self.stop(id)
#                sleep(1) # Avoid the "Too many request per second" error
#                driver.quit
#                self.delete(id) if id
#            end # if id
    end
    # Return
    ret
end

#online?Boolean

Send a GET request to “#url/status” and return true if it responded successfully.

Returns:

  • (Boolean)


86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/adspower-client.rb', line 86

def online?
    with_lock do
        begin
            url = "#{self.adspower_listener}:#{port}/status"
            uri = URI.parse(url)
            res = Net::HTTP.get(uri)
            return JSON.parse(res)['msg'] == 'success'
        rescue => e
            return false
        end
    end
end

#profile_count(group_id: nil) ⇒ Object

Count current profiles (optionally filtered by group)



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/adspower-client.rb', line 100

def profile_count(group_id: nil)
    count = 0
    page  = 1

    loop do
    params = { page: page, limit: 100 }
    params[:group_id] = group_id if group_id
    url   = "#{adspower_listener}:#{port}/api/v2/browser-profile/list"
    res   = BlackStack::Netting.call_post(url, params)
    data  = JSON.parse(res.body)
    raise "Error listing profiles: #{data['msg']}" unless data['code'] == 0

    list = data['data']['list']
    count += list.size
    break if list.size < 100

    page += 1
    end

    count
end

#release_lockObject

Release the lock



42
43
44
# File 'lib/adspower-client.rb', line 42

def release_lock
    @lockfile.flock(File::LOCK_UN) if @lockfile
end

#server_pidsObject

Return an array of PIDs of all the adspower_global processes running on the local computer.



55
56
57
# File 'lib/adspower-client.rb', line 55

def server_pids
    `ps aux | grep "adspower_global" | grep -v grep | awk '{print $2}'`.split("\n")
end

#server_start(timeout = 30) ⇒ Object

Run async command to start AdsPower server in headless mode. Wait up to 10 seconds to start the server, or raise an exception.



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/adspower-client.rb', line 61

def server_start(timeout=30)
    `xvfb-run --auto-servernum --server-args='-screen 0 1024x768x24' /usr/bin/adspower_global --headless=true --api-key=#{self.key.to_s} --api-port=#{self.port.to_s} > #{self.server_log} 2>&1 &`
    # wait up to 10 seconds to start the server
    timeout.times do
        break if self.online?
        sleep(1)
    end
    # add a delay of 5 more seconds
    sleep(5)
    # raise an exception if the server is not running
    raise "Error: the server is not running" if self.online? == false
    return
end

#server_stopObject

Kill all the adspower_global processes running on the local computer.



76
77
78
79
80
81
82
83
# File 'lib/adspower-client.rb', line 76

def server_stop
    with_lock do
        self.server_pids.each { |pid|
            `kill -9 #{pid}`
        }
    end
    return
end

#start(id, headless = false) ⇒ Object

Start the browser with the given user profile and return the connection details.



236
237
238
239
240
241
242
243
244
245
# File 'lib/adspower-client.rb', line 236

def start(id, headless=false)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/browser/start?user_id=#{id}&headless=#{headless ? '1' : '0'}"
        uri = URI.parse(url)
        res = Net::HTTP.get(uri)
        ret = JSON.parse(res)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret
    end
end

#stop(id) ⇒ Object

Stop the browser session for the given user profile.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/adspower-client.rb', line 248

def stop(id)
    with_lock do
        if @@drivers[id] && self.check(id)
            @@drivers[id].quit
            @@drivers[id] = nil
        end

        uri = URI.parse("#{self.adspower_listener}:#{port}/api/v1/browser/stop?user_id=#{id}")
        res = Net::HTTP.get(uri)
        ret = JSON.parse(res)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret
    end
end

#with_lockObject

Wrapper method for critical sections



47
48
49
50
51
52
# File 'lib/adspower-client.rb', line 47

def with_lock
    acquire_lock
    yield
ensure
    release_lock
end