Skip to content

Commit 0404e25

Browse files
committed
Version 1.7.0 captcha solver
1 parent 119563c commit 0404e25

File tree

22 files changed

+1164
-489
lines changed

22 files changed

+1164
-489
lines changed

docs/twikit.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ Geo
131131
:show-inheritance:
132132
:member-order: bysource
133133

134+
Capsolver
135+
-------------------
136+
137+
.. automodule:: twikit._captcha.capsolver
138+
:members:
139+
:undoc-members:
140+
:show-inheritance:
141+
:member-order: bysource
142+
134143
Utils
135144
-------------------
136145

docs/twikit.twikit_async.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,16 @@ Notification
125125
Geo
126126
-------------------
127127

128-
.. automodule:: twikit.geo
128+
.. automodule:: twikit.twikit_async.geo
129+
:members:
130+
:undoc-members:
131+
:show-inheritance:
132+
:member-order: bysource
133+
134+
Capsolver
135+
-------------------
136+
137+
.. automodule:: twikit.twikit_async._captcha.capsolver
129138
:members:
130139
:undoc-members:
131140
:show-inheritance:

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
httpx
2-
fake_useragent
3-
filetype
2+
filetype
3+
beautifulsoup4

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
version=version,
1414
install_requires=[
1515
'httpx',
16-
'fake_useragent',
17-
'filetype'
16+
'filetype',
17+
'beautifulsoup4'
1818
],
1919
python_requires='>=3.10',
2020
description='Twitter API wrapper for python with **no API key required**.',

twikit/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
A Python library for interacting with the Twitter API.
88
"""
99

10-
__version__ = '1.6.4'
10+
__version__ = '1.7.0'
1111

12+
from ._captcha import Capsolver
1213
from .bookmark import BookmarkFolder
1314
from .client import Client
1415
from .community import (Community, CommunityCreator, CommunityMember,

twikit/_captcha/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .base import CaptchaSolver
2+
from .capsolver import Capsolver

twikit/_captcha/base.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import TYPE_CHECKING, NamedTuple
5+
6+
from bs4 import BeautifulSoup
7+
from httpx import Response
8+
9+
from twikit.utils import urlencode
10+
11+
if TYPE_CHECKING:
12+
from ..client import Client
13+
14+
15+
class UnlockHTML(NamedTuple):
16+
authenticity_token: str
17+
assignment_token: str
18+
needs_unlock: bool
19+
start_button: bool
20+
finish_button: bool
21+
delete_button: bool
22+
blob: str
23+
24+
25+
class CaptchaSolver:
26+
client: Client
27+
max_attempts: int
28+
29+
CAPTCHA_URL = 'https://twitter.com/account/access'
30+
CAPTCHA_SITE_KEY = '0152B4EB-D2DC-460A-89A1-629838B529C9'
31+
32+
def get_unlock_html(self) -> tuple[Response, UnlockHTML]:
33+
headers = {
34+
'X-Twitter-Client-Language': 'en-US',
35+
'User-Agent': self.client._user_agent,
36+
'Upgrade-Insecure-Requests': '1'
37+
}
38+
_, response = self.client.get(
39+
self.CAPTCHA_URL, headers=headers
40+
)
41+
return response, parse_unlock_html(response.text)
42+
43+
def ui_metrix(self) -> str:
44+
js, _ = self.client.get(
45+
'https://twitter.com/i/js_inst?c_name=ui_metrics'
46+
)
47+
return re.findall(r'return ({.*?});', js, re.DOTALL)[0]
48+
49+
def confirm_unlock(
50+
self,
51+
authenticity_token: str,
52+
assignment_token: str,
53+
verification_string: str = None,
54+
ui_metrics: bool = False
55+
) -> tuple[Response, UnlockHTML]:
56+
data = {
57+
'authenticity_token': authenticity_token,
58+
'assignment_token': assignment_token,
59+
'lang': 'en',
60+
'flow': '',
61+
}
62+
params = {}
63+
if verification_string:
64+
data['verification_string'] = verification_string
65+
data['language_code'] = 'en'
66+
params['lang'] = 'en'
67+
if ui_metrics:
68+
data['ui_metrics'] = self.ui_metrix()
69+
data = urlencode(data)
70+
headers = {
71+
'Content-Type': 'application/x-www-form-urlencoded',
72+
'Upgrade-Insecure-Requests': '1',
73+
'Referer': self.CAPTCHA_URL
74+
}
75+
_, response = self.client.post(
76+
self.CAPTCHA_URL, params=params, data=data, headers=headers
77+
)
78+
return response, parse_unlock_html(response.text)
79+
80+
81+
def parse_unlock_html(html: str) -> UnlockHTML:
82+
soup = BeautifulSoup(html, 'lxml')
83+
84+
authenticity_token = None
85+
authenticity_token_element = soup.find(
86+
'input', {'name': 'authenticity_token'}
87+
)
88+
if authenticity_token_element is not None:
89+
authenticity_token: str = authenticity_token_element.get('value')
90+
91+
assignment_token = None
92+
assignment_token_element = soup.find('input', {'name': 'assignment_token'})
93+
if assignment_token_element is not None:
94+
assignment_token = assignment_token_element.get('value')
95+
96+
verification_string = soup.find('input', id='verification_string')
97+
needs_unlock = bool(verification_string)
98+
start_button = bool(soup.find('input', value='Start'))
99+
finish_button = bool(soup.find('input', value='Continue to X'))
100+
delete_button = bool(soup.find('input', value='Delete'))
101+
102+
iframe = soup.find(id='arkose_iframe')
103+
blob = re.findall(r'data=(.+)', iframe['src'])[0] if iframe else None
104+
105+
return UnlockHTML(
106+
authenticity_token,
107+
assignment_token,
108+
needs_unlock,
109+
start_button,
110+
finish_button,
111+
delete_button,
112+
blob
113+
)

twikit/_captcha/capsolver.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
from time import sleep
4+
5+
import httpx
6+
7+
from .base import CaptchaSolver
8+
9+
10+
class Capsolver(CaptchaSolver):
11+
"""
12+
You can automatically unlock the account by passing the `captcha_solver`
13+
argument when initialising the :class:`.Client`.
14+
15+
First, visit https://capsolver.com and obtain your Capsolver API key.
16+
Next, pass the Capsolver instance to the client as shown in the example.
17+
18+
.. code-block:: python
19+
20+
from twikit.twikit_async import Capsolver, Client
21+
solver = Capsolver(
22+
api_key='your_api_key',
23+
max_attempts=10
24+
)
25+
client = Client(captcha_solver=solver)
26+
27+
Parameters
28+
----------
29+
api_key : :class:`str`
30+
Capsolver API key.
31+
max_attempts : :class:`int`, default=3
32+
The maximum number of attempts to solve the captcha.
33+
get_result_interval : :class:`float`, default=1.0
34+
35+
use_blob_data : :class:`bool`, default=False
36+
"""
37+
def __init__(
38+
self,
39+
api_key: str,
40+
max_attempts: int = 3,
41+
get_result_interval: float = 1.0,
42+
use_blob_data: bool = False
43+
) -> None:
44+
self.api_key = api_key
45+
self.get_result_interval = get_result_interval
46+
self.max_attempts = max_attempts
47+
self.use_blob_data = use_blob_data
48+
49+
def create_task(self, task_data: dict) -> dict:
50+
data = {
51+
'clientKey': self.api_key,
52+
'task': task_data
53+
}
54+
response = httpx.post(
55+
'https://api.capsolver.com/createTask',
56+
json=data,
57+
headers={'content-type': 'application/json'}
58+
).json()
59+
return response
60+
61+
def get_task_result(self, task_id: str) -> dict:
62+
data = {
63+
'clientKey': self.api_key,
64+
'taskId': task_id
65+
}
66+
response = httpx.post(
67+
'https://api.capsolver.com/getTaskResult',
68+
json=data,
69+
headers={'content-type': 'application/json'}
70+
).json()
71+
return response
72+
73+
def solve_funcaptcha(self, blob: str) -> dict:
74+
if self.client.proxy is None:
75+
captcha_type = 'FunCaptchaTaskProxyLess'
76+
else:
77+
captcha_type = 'FunCaptchaTask'
78+
79+
task_data = {
80+
'type': captcha_type,
81+
'websiteURL': 'https://iframe.arkoselabs.com',
82+
'websitePublicKey': self.CAPTCHA_SITE_KEY,
83+
'funcaptchaApiJSSubdomain': 'https://client-api.arkoselabs.com',
84+
}
85+
if self.use_blob_data:
86+
task_data['data'] = '{"blob":"%s"}' % blob
87+
task_data['userAgent'] = self.client._user_agent
88+
task = self.create_task(task_data)
89+
while True:
90+
sleep(self.get_result_interval)
91+
result = self.get_task_result(task['taskId'])
92+
if result['status'] in ('ready', 'failed'):
93+
return result

0 commit comments

Comments
 (0)