itd-sdk 2.8.2__tar.gz → 2.9.0__tar.gz

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.
Files changed (66) hide show
  1. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/PKG-INFO +6 -2
  2. itd_sdk-2.9.0/itd/api/auth.py +58 -0
  3. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/files.py +2 -2
  4. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/notifications.py +3 -3
  5. itd_sdk-2.9.0/itd/core/auth.py +198 -0
  6. itd_sdk-2.9.0/itd/core/captcha/__init__.py +41 -0
  7. itd_sdk-2.9.0/itd/core/captcha/base.py +43 -0
  8. itd_sdk-2.9.0/itd/core/captcha/cloudflare.py +96 -0
  9. itd_sdk-2.9.0/itd/core/captcha/itd.py +66 -0
  10. itd_sdk-2.9.0/itd/core/cli.py +11 -0
  11. itd_sdk-2.9.0/itd/core/client.py +216 -0
  12. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/config.py +3 -0
  13. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/default.py +17 -2
  14. itd_sdk-2.9.0/itd/core/logger.py +74 -0
  15. itd_sdk-2.9.0/itd/core/profile.py +102 -0
  16. itd_sdk-2.9.0/itd/core/qr.py +112 -0
  17. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/request.py +80 -89
  18. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/utils.py +4 -4
  19. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/enums.py +2 -2
  20. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/exceptions.py +19 -5
  21. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/announcement.py +5 -5
  22. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/file.py +1 -1
  23. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/pyproject.toml +12 -2
  24. itd_sdk-2.8.2/itd/api/auth.py +0 -38
  25. itd_sdk-2.8.2/itd/core/client.py +0 -294
  26. itd_sdk-2.8.2/itd/core/logger.py +0 -50
  27. itd_sdk-2.8.2/itd/core/session_file.py +0 -27
  28. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/README.md +0 -0
  29. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/__init__.py +0 -0
  30. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/__init__.py +0 -0
  31. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/comments.py +0 -0
  32. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/dwell.py +0 -0
  33. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/hashtags.py +0 -0
  34. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/pins.py +0 -0
  35. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/platform.py +0 -0
  36. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/polls.py +0 -0
  37. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/portal.py +0 -0
  38. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/posts.py +0 -0
  39. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/reports.py +0 -0
  40. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/search.py +0 -0
  41. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/sessions.py +0 -0
  42. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/subscription.py +0 -0
  43. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/users.py +0 -0
  44. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/api/verification.py +0 -0
  45. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/__init__.py +0 -0
  46. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/base.py +0 -0
  47. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/dwell.py +0 -0
  48. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/limiter.py +0 -0
  49. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/timer.py +0 -0
  50. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/core/visibility.py +0 -0
  51. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/__init__.py +0 -0
  52. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/clan.py +0 -0
  53. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/comment.py +0 -0
  54. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/hashtag.py +0 -0
  55. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/notification.py +0 -0
  56. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/pin.py +0 -0
  57. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/poll.py +0 -0
  58. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/portal.py +0 -0
  59. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/post.py +0 -0
  60. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/report.py +0 -0
  61. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/session.py +0 -0
  62. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/span.py +0 -0
  63. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/user.py +0 -0
  64. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/utils.py +0 -0
  65. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/models/version.py +0 -0
  66. {itd_sdk-2.8.2 → itd_sdk-2.9.0}/itd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.8.2
3
+ Version: 2.9.0
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: fi-res
@@ -17,16 +17,20 @@ Requires-Dist: sseclient-py~=1.9.0
17
17
  Requires-Dist: telegramify-markdown>=0.5.4
18
18
  Requires-Dist: lxml>=6.1.1
19
19
  Requires-Dist: platformdirs>=4.10.0
20
- Requires-Dist: websockets>=16.1.1
20
+ Requires-Dist: camoufox[geoip]>=0.4.11 ; extra == 'captcha'
21
+ Requires-Dist: playwright>=1.62.0 ; extra == 'captcha'
21
22
  Requires-Dist: dateparser>=1.2.2 ; extra == 'date'
22
23
  Requires-Dist: filetype>=1.2.0 ; extra == 'filetype'
23
24
  Requires-Dist: rich>=15.0.0 ; extra == 'logs'
25
+ Requires-Dist: qrcode>=8.2 ; extra == 'qrcode'
24
26
  Requires-Python: >=3.10
25
27
  Project-URL: Homepage, https://itdsdk.qzz.io
26
28
  Project-URL: Documentation, https://itdsdk.qzz.io/docs
29
+ Provides-Extra: captcha
27
30
  Provides-Extra: date
28
31
  Provides-Extra: filetype
29
32
  Provides-Extra: logs
33
+ Provides-Extra: qrcode
30
34
  Description-Content-Type: text/markdown
31
35
 
32
36
  # itd-sdk
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from uuid import UUID
5
+
6
+ from itd.core.request import endpoint
7
+ from itd.enums import AuthLevel
8
+ from itd.exceptions import (
9
+ CaptchaFailedError,
10
+ EmailDomainNotAllowedError,
11
+ InvalidCredentialsError,
12
+ InvalidEmailError,
13
+ InvalidOldPasswordError,
14
+ InvalidPasswordError,
15
+ SamePasswordError,
16
+ SessionExpiredError,
17
+ SessionNotFoundError,
18
+ SessionRevokedError
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from itd.core.client import Client
23
+
24
+
25
+ @endpoint('post', 'v1/auth/refresh', SessionExpiredError(), SessionNotFoundError(), SessionRevokedError(), level=AuthLevel.REFRESH)
26
+ def refresh_token(client: Client): ...
27
+
28
+
29
+ @endpoint('post', 'v1/auth/change-password', InvalidPasswordError(), SamePasswordError(), InvalidOldPasswordError())
30
+ def change_password(client: Client, old: str, new: str):
31
+ return {'newPassword': new, 'oldPassword': old}
32
+
33
+
34
+ @endpoint('post', 'v1/auth/logout', level=AuthLevel.REFRESH)
35
+ def logout(client: Client): ...
36
+
37
+
38
+ @endpoint('get', 'v1/auth/captcha/provider', level=AuthLevel.NO)
39
+ def get_captcha_provider(client: Client): ...
40
+
41
+
42
+ @endpoint('post', 'v1/auth/sign-in', InvalidCredentialsError(), CaptchaFailedError(), EmailDomainNotAllowedError(), InvalidEmailError(), level=AuthLevel.NO)
43
+ def sign_in(client: Client, email: str, password: str, token_field: str, token: str): # token field: turnstileToken for клодфлер token for итд
44
+ return {'email': email, 'password': password, token_field: token}
45
+
46
+
47
+ @endpoint('post', 'v1/auth/qr/start', level=AuthLevel.NO)
48
+ def qr_start(cleint: Client): ...
49
+
50
+
51
+ @endpoint('post', 'v1/auth/qr/claim', level=AuthLevel.NO)
52
+ def qr_claim(client: Client, qr_id: UUID, claim_token: str):
53
+ return {'qrId': str(qr_id), 'claimToken': claim_token}
54
+
55
+
56
+ @endpoint('post', 'v1/auth/qr/stream', sse=True, level=AuthLevel.NO)
57
+ def qr_stream(client: Client, qr_id: UUID, claim_token: str):
58
+ return {'qrId': str(qr_id), 'claimToken': claim_token}
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from _io import BufferedReader
3
+ from io import BufferedReader
4
4
  from typing import TYPE_CHECKING
5
5
  from uuid import UUID
6
6
 
7
- from itd.exceptions import InvalidFileTypeError, ModerationFailedError, TooLargeError, UploadError
8
7
  from itd.core.request import Payload, endpoint
8
+ from itd.exceptions import InvalidFileTypeError, ModerationFailedError, TooLargeError, UploadError
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from itd.core.client import Client
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
  from uuid import UUID
5
5
 
6
- from itd.exceptions import NotFoundError
7
6
  from itd.core.request import endpoint
7
+ from itd.exceptions import NotFoundError
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from itd.core.client import Client
@@ -36,5 +36,5 @@ def update_notifications_settings(client: Client, settings: dict):
36
36
  return settings
37
37
 
38
38
 
39
- def stream_notifications(client: Client):
40
- return client.request_sse('notifications/stream')
39
+ @endpoint('get', 'notifications/stream', sse=True)
40
+ def stream_notifications(client: Client): ...
@@ -0,0 +1,198 @@
1
+ from os import getenv
2
+ from typing import TYPE_CHECKING
3
+
4
+ from itd.api.auth import sign_in
5
+ from itd.core.captcha import get_turnstile
6
+ from itd.core.logger import RICH_AVAILABLE, get_logger, iprint, rich_input
7
+ from itd.core.qr import auth_qr
8
+ from itd.core.utils import shorten_token
9
+ from itd.exceptions import (
10
+ AccessTokenExpiredError,
11
+ CaptchaFailedError,
12
+ EmailDomainNotAllowedError,
13
+ InvalidAccessTokenError,
14
+ InvalidCredentialsError,
15
+ InvalidEmailError,
16
+ JWTAlgorithmUnsupportedError,
17
+ SessionExpiredError,
18
+ SessionNotFoundError,
19
+ SessionRevokedError
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from itd.core.client import Client
24
+
25
+ if RICH_AVAILABLE:
26
+ from rich.status import Status
27
+
28
+ l = get_logger('auth')
29
+
30
+
31
+ def _auth_login(client: 'Client', email: str, password: str):
32
+ client._profile.email = email
33
+ client._profile.password = password
34
+ client._profile.creds_valid = True
35
+ client._set_from_profile()
36
+
37
+ def _sign_in(turnstile):
38
+ try:
39
+ res = sign_in(client, client._profile.email, client._profile.password, 'turnstileToken', turnstile)
40
+ except CaptchaFailedError:
41
+ l.error('captcha verify failed. Please fill issue at https://github.com/itd-sdk/itd-sdk/issues/new')
42
+ except InvalidCredentialsError:
43
+ l.error('invalid email or password')
44
+ except EmailDomainNotAllowedError:
45
+ l.error('email domain not allowed')
46
+ except InvalidEmailError:
47
+ l.error('invalid email format')
48
+ else:
49
+ client._profile.set_refresh(res.cookies['refresh_token'], set_expire=True)
50
+ client._profile.set_access(res.json()['accessToken'])
51
+ client._profile.creds_valid = True
52
+ return True
53
+
54
+ if RICH_AVAILABLE:
55
+ with Status('Solving captcha..') as status:
56
+ turnstile = get_turnstile(client, status=status)
57
+ status.update('Verifying..')
58
+ if _sign_in(turnstile):
59
+ iprint(l, 'accepted')
60
+ return True
61
+ else:
62
+ iprint(l, 'solving captcha..')
63
+ if _sign_in(get_turnstile(client)):
64
+ iprint(l, 'accepted')
65
+ return True
66
+
67
+ client._profile.creds_valid = False
68
+
69
+
70
+ def _auth_refresh(client: 'Client', refresh: str):
71
+ client._profile.set_refresh(refresh, set_expire=True)
72
+ client._set_from_profile()
73
+
74
+ def _verify():
75
+ try:
76
+ client.refresh_auth()
77
+ except SessionExpiredError:
78
+ l.error('refresh token expired')
79
+ except SessionNotFoundError:
80
+ l.error('invalid refresh token')
81
+ except SessionRevokedError:
82
+ l.error('refresh token revoked')
83
+ else:
84
+ return True
85
+
86
+ if RICH_AVAILABLE:
87
+ with Status('Verifying..'):
88
+ if _verify():
89
+ iprint(l, 'accepted')
90
+ return True
91
+ else:
92
+ if _verify():
93
+ iprint(l, 'accepted')
94
+ return True
95
+
96
+ client._profile.refresh_valid = False
97
+
98
+
99
+ def _auth_access(client: 'Client', access: str):
100
+ client._profile.set_access(access)
101
+ client._set_from_profile()
102
+
103
+ def _verify():
104
+ try:
105
+ client.user.refresh()
106
+ except AccessTokenExpiredError:
107
+ l.error('access token expired')
108
+ except InvalidAccessTokenError:
109
+ l.error('invalid access token')
110
+ except JWTAlgorithmUnsupportedError:
111
+ l.error('jwt algorithm unsupported')
112
+ else:
113
+ return True
114
+
115
+ if RICH_AVAILABLE:
116
+ with Status('Verifying..'):
117
+ if _verify():
118
+ iprint(l, 'accepted')
119
+ return True
120
+ else:
121
+ if _verify():
122
+ iprint(l, 'accepted')
123
+ return True
124
+
125
+ client._profile.access_valid = False
126
+
127
+
128
+ def interactive_auth(client: 'Client') -> bool:
129
+ if client._profile._file is None:
130
+ l.debug('create new profile')
131
+
132
+ if client._profile.creds_valid or client._profile.refresh_valid or client._profile.access_valid:
133
+ # if not client._profile.access_valid:
134
+ # client._profile.access = None
135
+ # if not client._profile.refresh_valid:
136
+ # client._profile.refresh = None
137
+ # if not client._profile.creds_valid:
138
+ # client._profile.email = None
139
+ # client._profile.password = None
140
+
141
+ return True
142
+ client._credtest = True
143
+
144
+ if getenv('ITD_AUTH_METHOD'):
145
+ match getenv('ITD_AUTH_METHOD'):
146
+ case 'login':
147
+ if _auth_login(client, getenv('ITD_LOGIN', ''), getenv('ITD_PASSWORD', '')):
148
+ return True
149
+ case 'refresh':
150
+ if _auth_refresh(client, getenv('ITD_REFRESH', '')):
151
+ return True
152
+ case 'access':
153
+ if _auth_access(client, getenv('ITD_ACCESS', '')):
154
+ return True
155
+ case _:
156
+ l.error('unknown option')
157
+ quit()
158
+
159
+ l.error('auth failed')
160
+ quit()
161
+
162
+ if not client._profile.refresh_valid and client._profile.refresh:
163
+ l.warning('session file refresh token is not valid')
164
+
165
+ if not client._profile.creds_valid and client._profile.email:
166
+ l.warning('session file credentials is not valid')
167
+
168
+ iprint(
169
+ l, f'session file data: access={shorten_token(client._profile.access)} refresh={shorten_token(client._profile.refresh)} email={client._profile.email}'
170
+ )
171
+ iprint(l, 'select auth option:')
172
+ iprint(l, '[1] Login using credentials')
173
+ iprint(l, '[2] Login via QR code')
174
+ iprint(l, '[3] Manually auth using refresh token')
175
+ iprint(l, '[4] Manually auth using access token')
176
+ iprint(l, '[5] Quit')
177
+
178
+ while True:
179
+ option = rich_input('option', 'magenta')
180
+
181
+ match option:
182
+ case '1':
183
+ if _auth_login(client, rich_input('email', 'green'), rich_input('password', 'green', password=True)):
184
+ return True
185
+ case '2':
186
+ if auth_qr(client):
187
+ return True
188
+ case '3':
189
+ if _auth_refresh(client, rich_input('refresh token', 'cyan', password=True)):
190
+ return True
191
+ case '4':
192
+ l.info('note: authorization will work only for ~15min')
193
+ if _auth_access(client, rich_input('access token', 'cyan')):
194
+ return True
195
+ case '5' | 'q' | 'quit':
196
+ quit()
197
+ case _:
198
+ l.error('unknown option')
@@ -0,0 +1,41 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ # from itd.api.auth import get_captcha_provider
4
+ # from itd.core.captcha import cloudflare, itd # required to they fill "providers" value # noqa
5
+ # from itd.core.captcha.base import providers
6
+ from itd.core.captcha.base import CAPTCHA_AVAILABLE
7
+ from itd.core.captcha.cloudflare import CloudflareProvider
8
+ from itd.core.logger import get_logger, iprint, rich_input
9
+
10
+ if TYPE_CHECKING:
11
+ from itd.core.client import Client
12
+
13
+ l = get_logger('captcha')
14
+
15
+
16
+ def get_turnstile(client: 'Client | None' = None, status=None) -> str: # tuple[str, str]:
17
+ # provider_data = {'provider': 'cloudflare', 'token': 'turnstileToken'} # get_captcha_provider(client)
18
+ # if provider_data['provider'] not in providers:
19
+ # raise RuntimeError(f'Unknown provider: {provider_data["provider"]}')
20
+
21
+ # provider = providers[provider_data['provider']]()
22
+ if client and client.config.captcha_solve:
23
+ l.warning('captcha solving disabled')
24
+ iprint(l, 'solve captcha on other devices via `uv run itd captcha`, then paste result here')
25
+ iprint(l, 'enter `continue` to force solve captcha on this device')
26
+ if status:
27
+ status.stop()
28
+ turnstile = rich_input('turnstile', 'yellow')
29
+ if turnstile != 'continue':
30
+ return turnstile
31
+ if status:
32
+ status.start()
33
+
34
+ if not CAPTCHA_AVAILABLE:
35
+ l.error('captcha libraries not installed; install via `uv add itd-sdk[captcha]`')
36
+ provider = CloudflareProvider(False if not client else client.config.captcha_headless)
37
+ provider.launch()
38
+ turnstile = provider.solve()
39
+ provider.close()
40
+
41
+ return turnstile # provider_data.get('token', 'token'), turnstile
@@ -0,0 +1,43 @@
1
+ from typing import Literal, cast
2
+
3
+ try:
4
+ from camoufox.sync_api import Camoufox
5
+ from playwright.sync_api import Browser, ViewportSize
6
+ except ImportError:
7
+ CAPTCHA_AVAILABLE = False
8
+ else:
9
+ CAPTCHA_AVAILABLE = True
10
+
11
+
12
+ class BaseProvider:
13
+ url: str
14
+ _init_script: str
15
+
16
+ def __init__(self, headless: bool | Literal['virtual'] = 'virtual'):
17
+ self.camoufox = Camoufox(headless=headless, geoip=True, humanize=True)
18
+ self.browser = None
19
+ self.context = None
20
+
21
+ def launch(self):
22
+ self.browser = cast(Browser, self.camoufox.__enter__())
23
+ self.context = self.browser.new_context(
24
+ viewport=ViewportSize(width=1920, height=1080),
25
+ screen=ViewportSize(width=1920, height=1080)
26
+ # locale='ru',
27
+ # user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0'
28
+ )
29
+ self.context.add_init_script(self._init_script)
30
+ self.page = self.context.new_page()
31
+ self.page.goto(self.url, wait_until='domcontentloaded', timeout=25000)
32
+
33
+ def solve(self): ...
34
+
35
+ def close(self):
36
+ if self.context:
37
+ self.context.close()
38
+ if self.browser:
39
+ self.browser.close()
40
+ self.camoufox.__exit__(None, None, None)
41
+
42
+
43
+ # providers: dict[str, type[BaseProvider]] = {}
@@ -0,0 +1,96 @@
1
+ # credits: @itdStatus
2
+
3
+ from time import sleep, time
4
+
5
+ from itd.core.captcha.base import BaseProvider # , providers
6
+
7
+
8
+ class CloudflareProvider(BaseProvider):
9
+ url = 'https://xn--d1ah4a.com/turnstile.html?theme=dark'
10
+ _init_script = """
11
+ () => {
12
+ if (window.__t_patched) return;
13
+ window.__t_patched = true;
14
+ window.__t__ = null;
15
+
16
+ window.onSuccess = tk => { window.__t__ = tk; };
17
+
18
+ window.addEventListener('message', ev => {
19
+ try {
20
+ const data = (typeof ev.data === 'string') ? JSON.parse(ev.data) : ev.data;
21
+ if (!data) return;
22
+ const tk = data.token || data['cf-turnstile-response'] || data.cfToken;
23
+ if (tk && typeof tk === 'string' && tk.length > 20) window.__t__ = tk;
24
+ } catch (_) {}
25
+ }, true);
26
+
27
+ let attempts = 0;
28
+ const iv = setInterval(() => {
29
+ if (typeof window.turnstile !== 'undefined') {
30
+ clearInterval(iv);
31
+ const orig = window.turnstile.render.bind(window.turnstile);
32
+ window.turnstile.render = (container, opts) => {
33
+ const cb = opts.callback;
34
+ opts.callback = tk => {
35
+ window.__t__ = tk;
36
+ if (typeof cb === 'function') cb(tk);
37
+ };
38
+ return orig(container, opts);
39
+ };
40
+ } else if (++attempts > 150) {
41
+ clearInterval(iv);
42
+ }
43
+ }, 100);
44
+
45
+ const origFetch = window.fetch;
46
+ window.fetch = async (...args) => {
47
+ const res = await origFetch(...args);
48
+ try {
49
+ const clone = res.clone();
50
+ const text = await clone.text();
51
+ const json = JSON.parse(text);
52
+ const tk = json.token || json['cf-turnstile-response'];
53
+ if (tk && tk.length > 20) window.__t__ = tk;
54
+ } catch (_) {}
55
+ return res;
56
+ };
57
+
58
+ const observer = new MutationObserver(() => {
59
+ const inp = document.querySelector(
60
+ 'input[name="cf-turnstile-response"], input[name="cf_turnstile_response"]'
61
+ );
62
+ if (inp && inp.value && inp.value.length > 20) window.__t__ = inp.value;
63
+ });
64
+ observer.observe(document.documentElement, { subtree: true, attributes: true, childList: true });
65
+ }
66
+ """
67
+
68
+ def solve(self):
69
+ sleep(5)
70
+ self.page.mouse.move(20, 30)
71
+ sleep(0.15)
72
+ self.page.mouse.click(20, 30)
73
+
74
+ start = time()
75
+ while start + 30 > time():
76
+ token = self.page.evaluate("""
77
+ (() => {
78
+ if (window.__t__ && window.__t__.length > 20) return window.__t__;
79
+ const inp = document.querySelector(
80
+ 'input[name="cf-turnstile-response"], input[name="cf_turnstile_response"], textarea[name="cf-turnstile-response"]'
81
+ );
82
+ if (inp && inp.value && inp.value.length > 20) return inp.value;
83
+ return null;
84
+ })()
85
+ """)
86
+ if token:
87
+ break
88
+ sleep(0.3)
89
+
90
+ if not token:
91
+ raise RuntimeError('Cloudflare token timeout')
92
+
93
+ return token
94
+
95
+
96
+ # providers['cloudflare'] = CloudflareProvider
@@ -0,0 +1,66 @@
1
+ from time import sleep
2
+
3
+ from itd.core.captcha.base import BaseProvider # , providers
4
+
5
+ SITEKEY = 'sk_44d64cf7bf8bc8377f5b'
6
+
7
+
8
+ class ItdProvider(BaseProvider):
9
+ url = f'https://captcha.xn--d1ah4a.com/widget.html?sitekey={SITEKEY}'
10
+ _init_script = """
11
+ const plugins = [new Object(), new Object(), new Object(), new Object(), new Object()];
12
+ plugins.item = (i) => plugins[i];
13
+ Object.defineProperty(navigator, 'plugins', { get: () => plugins});
14
+ window.Plugin = Object;
15
+
16
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
17
+ Object.defineProperty(navigator, 'languages', { get: () => ['ru-RU', 'ru', 'en-US', 'en']});
18
+ Object.defineProperty(navigator, 'platform', { get: () => 'Windows 11'});
19
+ Object.defineProperty(navigator, 'mimeTypes', { get: () => [0, 0]});
20
+
21
+ matchMedia = (a) => ({matches: a.includes('dark')}) ;
22
+
23
+ const _origOwnProp = Object.getOwnPropertyDescriptor;
24
+ Object.defineProperty(Object, 'getOwnPropertyDescriptor', { value: (o, p) => o == navigator && p == "webdriver"? undefined : _origOwnProp(o, p)});
25
+
26
+ const _origAttach = Element.prototype.attachShadow;
27
+ Object.defineProperty(Element.prototype, 'attachShadow', {
28
+ configurable: true,
29
+ writable: true,
30
+ value: function (init) {
31
+ const root = _origAttach.call(this, { ...init, mode: 'open' });
32
+ (window.__roots ??= []).push({ host: this, root });
33
+ return root;
34
+ },
35
+ });
36
+
37
+ window.turnstile = null;
38
+ const _origFetch = window.fetch;
39
+ window.fetch = async function (u, p) {
40
+ console.log(`invoke headers ${u}`);
41
+ p.headers = {
42
+ 'Accept': 'application/json',
43
+ 'Content-Type': 'application/json',
44
+ 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
45
+ 'Cache-Control': 'no-cache',
46
+ 'Pragma': 'no-cache'
47
+ };
48
+ const res = await _origFetch(u, p);
49
+ if (u.includes('/api/v1/c')) {
50
+ const json = await res.json();
51
+ window.turnstile = json.t;
52
+ console.log(`fetched turnstile ${window.turnstile}`);
53
+ return new Response(JSON.stringify(json), {status: res.status, statusText: res.statusText, headers: res.headers});
54
+ }
55
+ return res;
56
+ }
57
+ """
58
+
59
+ def solve(self):
60
+ sleep(0.5)
61
+ self.page.locator('#c').evaluate('(c) => c.shadowRoot.querySelector(".cb").click()')
62
+ sleep(3)
63
+ return self.page.evaluate('() => window.turnstile')
64
+
65
+
66
+ # providers['itd'] = ItdProvider
@@ -0,0 +1,11 @@
1
+ from sys import argv
2
+
3
+ from itd.core.captcha import get_turnstile
4
+
5
+
6
+ def main():
7
+ if argv[-1] == 'captcha':
8
+ print(get_turnstile())
9
+
10
+ else:
11
+ print('unknown command')