itd-sdk 2.8.2__tar.gz → 2.9.1__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 (68) hide show
  1. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/PKG-INFO +6 -2
  2. itd_sdk-2.9.1/itd/api/auth.py +58 -0
  3. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/files.py +2 -2
  4. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/notifications.py +6 -5
  5. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/search.py +3 -3
  6. itd_sdk-2.9.1/itd/core/auth.py +233 -0
  7. itd_sdk-2.9.1/itd/core/captcha/__init__.py +41 -0
  8. itd_sdk-2.9.1/itd/core/captcha/base.py +43 -0
  9. itd_sdk-2.9.1/itd/core/captcha/cloudflare.py +96 -0
  10. itd_sdk-2.9.1/itd/core/captcha/itd.py +66 -0
  11. itd_sdk-2.9.1/itd/core/cli.py +11 -0
  12. itd_sdk-2.9.1/itd/core/client.py +221 -0
  13. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/config.py +3 -0
  14. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/default.py +17 -2
  15. itd_sdk-2.9.1/itd/core/logger.py +74 -0
  16. itd_sdk-2.9.1/itd/core/profile.py +102 -0
  17. itd_sdk-2.9.1/itd/core/qr.py +141 -0
  18. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/request.py +80 -89
  19. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/utils.py +4 -4
  20. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/enums.py +2 -2
  21. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/exceptions.py +19 -5
  22. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/announcement.py +5 -5
  23. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/file.py +1 -1
  24. itd_sdk-2.9.1/itd/models/search.py +38 -0
  25. itd_sdk-2.9.1/itd/py.typed +0 -0
  26. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/pyproject.toml +12 -2
  27. itd_sdk-2.8.2/itd/api/auth.py +0 -38
  28. itd_sdk-2.8.2/itd/core/client.py +0 -294
  29. itd_sdk-2.8.2/itd/core/logger.py +0 -50
  30. itd_sdk-2.8.2/itd/core/session_file.py +0 -27
  31. itd_sdk-2.8.2/itd/models/__init__.py +0 -4
  32. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/README.md +0 -0
  33. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/__init__.py +0 -0
  34. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/__init__.py +0 -0
  35. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/comments.py +0 -0
  36. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/dwell.py +0 -0
  37. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/hashtags.py +0 -0
  38. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/pins.py +0 -0
  39. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/platform.py +0 -0
  40. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/polls.py +0 -0
  41. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/portal.py +0 -0
  42. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/posts.py +0 -0
  43. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/reports.py +0 -0
  44. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/sessions.py +0 -0
  45. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/subscription.py +0 -0
  46. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/users.py +0 -0
  47. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/api/verification.py +0 -0
  48. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/__init__.py +0 -0
  49. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/base.py +0 -0
  50. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/dwell.py +0 -0
  51. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/limiter.py +0 -0
  52. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/timer.py +0 -0
  53. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/core/visibility.py +0 -0
  54. /itd_sdk-2.8.2/itd/py.typed → /itd_sdk-2.9.1/itd/models/__init__.py +0 -0
  55. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/clan.py +0 -0
  56. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/comment.py +0 -0
  57. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/hashtag.py +0 -0
  58. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/notification.py +0 -0
  59. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/pin.py +0 -0
  60. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/poll.py +0 -0
  61. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/portal.py +0 -0
  62. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/post.py +0 -0
  63. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/report.py +0 -0
  64. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/session.py +0 -0
  65. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/span.py +0 -0
  66. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/user.py +0 -0
  67. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/utils.py +0 -0
  68. {itd_sdk-2.8.2 → itd_sdk-2.9.1}/itd/models/version.py +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.1
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
@@ -15,8 +15,9 @@ def get_notifications(client: Client, limit: int = 20, offset: int = 0):
15
15
  return {'limit': limit, 'offset': offset}
16
16
 
17
17
 
18
- @endpoint('post', 'notifications/{id}/read', NotFoundError('Notification', json_check=lambda json: json.get('success') is False))
19
- def mark_as_read(client: Client, id: UUID): ...
18
+ @endpoint('post', 'notifications/read-batch', NotFoundError('Notification', json_check=lambda json: json.get('success') is False))
19
+ def mark_as_read(client: Client, id: UUID):
20
+ return {'ids': [str(id)]}
20
21
 
21
22
 
22
23
  @endpoint('post', 'notifications/read-all')
@@ -36,5 +37,5 @@ def update_notifications_settings(client: Client, settings: dict):
36
37
  return settings
37
38
 
38
39
 
39
- def stream_notifications(client: Client):
40
- return client.request_sse('notifications/stream')
40
+ @endpoint('get', 'notifications/stream', sse=True)
41
+ def stream_notifications(client: Client): ...
@@ -2,14 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from itd.core.request import endpoint
5
6
  from itd.enums import AuthLevel
6
7
  from itd.exceptions import ValidationError
7
- from itd.core.request import endpoint
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from itd.core.client import Client
11
11
 
12
12
 
13
13
  @endpoint('get', 'search', ValidationError(), level=AuthLevel.NO)
14
- def search(client: Client, query: str, user_limit: int = 5, hashtag_limit: int = 5):
15
- return {'userLimit': user_limit, 'hashtagLimit': hashtag_limit, 'q': query}
14
+ def search(client: Client, query: str, users_limit: int = 10, hashtags_limit: int = 10):
15
+ return {'userLimit': users_limit, 'hashtagLimit': hashtags_limit, 'q': query}
@@ -0,0 +1,233 @@
1
+ from contextlib import contextmanager
2
+ from dataclasses import dataclass
3
+ from os import getenv
4
+ from typing import TYPE_CHECKING
5
+
6
+ from itd.api.auth import sign_in
7
+ from itd.core.captcha import get_turnstile
8
+ from itd.core.logger import RICH_AVAILABLE, get_logger, iprint, rich_input
9
+ from itd.core.qr import auth_qr
10
+ from itd.core.utils import shorten_token
11
+ from itd.exceptions import (
12
+ AccessTokenExpiredError,
13
+ CaptchaFailedError,
14
+ EmailDomainNotAllowedError,
15
+ InvalidAccessTokenError,
16
+ InvalidCredentialsError,
17
+ InvalidEmailError,
18
+ JWTAlgorithmUnsupportedError,
19
+ SessionExpiredError,
20
+ SessionNotFoundError,
21
+ SessionRevokedError
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from itd.core.client import Client
26
+
27
+ if RICH_AVAILABLE:
28
+ from rich.status import Status
29
+
30
+ l = get_logger('auth')
31
+
32
+
33
+ @contextmanager
34
+ def _status(text: str):
35
+ if not RICH_AVAILABLE:
36
+ iprint(l, text)
37
+ yield None
38
+ return
39
+ with Status(text) as status:
40
+ yield status
41
+
42
+
43
+ def _auth_login(client: 'Client', email: str, password: str) -> bool:
44
+ with _status('Solving captcha..') as status:
45
+ turnstile = get_turnstile(client, status=status)
46
+ if status:
47
+ status.update('Verifying..')
48
+ try:
49
+ apply_auth(client, CredentialsAuth(email, password, turnstile))
50
+ except CaptchaFailedError:
51
+ l.error('captcha verify failed; please fill issue at https://github.com/itd-sdk/itd-sdk/issues/new')
52
+ except InvalidCredentialsError:
53
+ l.error('invalid email or password')
54
+ except EmailDomainNotAllowedError:
55
+ l.error('email domain not allowed')
56
+ except InvalidEmailError:
57
+ l.error('invalid email format')
58
+ else:
59
+ iprint(l, 'accepted')
60
+ return True
61
+ return False
62
+
63
+
64
+ def _auth_refresh(client: 'Client', refresh: str) -> bool:
65
+ with _status('Verifying..'):
66
+ try:
67
+ apply_auth(client, RefreshAuth(refresh))
68
+ except SessionExpiredError:
69
+ l.error('refresh token expired')
70
+ except SessionNotFoundError:
71
+ l.error('invalid refresh token')
72
+ except SessionRevokedError:
73
+ l.error('refresh token revoked')
74
+ else:
75
+ iprint(l, 'accepted')
76
+ return True
77
+ return False
78
+
79
+
80
+ def _auth_access(client: 'Client', access: str) -> bool:
81
+ with _status('Verifying..'):
82
+ try:
83
+ apply_auth(client, AccessAuth(access))
84
+ except AccessTokenExpiredError:
85
+ l.error('access token expired')
86
+ except InvalidAccessTokenError:
87
+ l.error('invalid access token')
88
+ except JWTAlgorithmUnsupportedError:
89
+ l.error('jwt algorithm unsupported')
90
+ else:
91
+ iprint(l, 'accepted')
92
+ return True
93
+ return False
94
+
95
+
96
+ @dataclass
97
+ class CredentialsAuth:
98
+ email: str
99
+ password: str
100
+ turnstile: str | None = None
101
+
102
+
103
+ @dataclass
104
+ class RefreshAuth:
105
+ refresh: str
106
+ access: str | None = None
107
+
108
+
109
+ @dataclass
110
+ class AccessAuth:
111
+ access: str
112
+
113
+
114
+ AuthMethod = CredentialsAuth | RefreshAuth | AccessAuth
115
+
116
+
117
+ def apply_auth(client: 'Client', auth: AuthMethod) -> None:
118
+ match auth:
119
+ case CredentialsAuth():
120
+ client._profile.email = auth.email
121
+ client._profile.password = auth.password
122
+ client._profile.creds_valid = True
123
+ client._set_from_profile()
124
+ try:
125
+ res = sign_in(client, auth.email, auth.password, 'turnstileToken', auth.turnstile or get_turnstile(client)[1])
126
+ except Exception:
127
+ client._profile.email = None
128
+ client._profile.password = None
129
+ client._profile.creds_valid = False
130
+ client._set_from_profile()
131
+ raise
132
+ client._profile.set_refresh(res.cookies['refresh_token'], set_expire=True)
133
+ client._profile.set_access(res.json()['accessToken'])
134
+
135
+ case RefreshAuth():
136
+ client._profile.set_refresh(auth.refresh, set_expire=True)
137
+ if auth.access:
138
+ client._profile.set_access(auth.access)
139
+ client._set_from_profile()
140
+ try:
141
+ client.refresh_auth()
142
+ except Exception:
143
+ client._profile.refresh_valid = False
144
+ client._set_from_profile()
145
+ raise
146
+
147
+ case AccessAuth():
148
+ client._profile.set_access(auth.access)
149
+ client._set_from_profile()
150
+ try:
151
+ client.user.refresh()
152
+ except Exception:
153
+ client._profile.access_valid = False
154
+ client._set_from_profile()
155
+ raise
156
+
157
+ client._profile.flush()
158
+
159
+
160
+ def env_auth(client: 'Client') -> bool:
161
+ """Auth from ITD_* environment variables. Returns False if none set."""
162
+ method = getenv('ITD_AUTH_METHOD')
163
+ if not method:
164
+ return False
165
+
166
+ match method:
167
+ case 'login':
168
+ auth = CredentialsAuth(getenv('ITD_LOGIN', ''), getenv('ITD_PASSWORD', ''))
169
+ case 'refresh':
170
+ auth = RefreshAuth(getenv('ITD_REFRESH', ''))
171
+ case 'access':
172
+ auth = AccessAuth(getenv('ITD_ACCESS', ''))
173
+ case _:
174
+ raise ValueError(f'unknown env auth method {method}')
175
+
176
+ apply_auth(client, auth)
177
+ l.info('authorized env method=%s', method)
178
+ return True
179
+
180
+
181
+ def interactive_auth(client: 'Client') -> bool:
182
+ if client._profile._file is None:
183
+ l.debug('create new profile')
184
+
185
+ if client._profile.creds_valid or client._profile.refresh_valid or client._profile.access_valid:
186
+ # if not client._profile.access_valid:
187
+ # client._profile.access = None
188
+ # if not client._profile.refresh_valid:
189
+ # client._profile.refresh = None
190
+ # if not client._profile.creds_valid:
191
+ # client._profile.email = None
192
+ # client._profile.password = None
193
+
194
+ return True
195
+ client._credtest = True
196
+
197
+ if not client._profile.refresh_valid and client._profile.refresh:
198
+ l.warning('session file refresh token is not valid')
199
+
200
+ if not client._profile.creds_valid and client._profile.email:
201
+ l.warning('session file credentials is not valid')
202
+
203
+ iprint(
204
+ l, f'session file data: access={shorten_token(client._profile.access)} refresh={shorten_token(client._profile.refresh)} email={client._profile.email}'
205
+ )
206
+ iprint(l, 'select auth option:')
207
+ iprint(l, '[1] Login using credentials')
208
+ iprint(l, '[2] Login via QR code')
209
+ iprint(l, '[3] Manually auth using refresh token')
210
+ iprint(l, '[4] Manually auth using access token')
211
+ iprint(l, '[5] Quit')
212
+
213
+ while True:
214
+ option = rich_input('option', 'magenta')
215
+
216
+ match option:
217
+ case '1':
218
+ if _auth_login(client, rich_input('email', 'green'), rich_input('password', 'green', password=True)):
219
+ return True
220
+ case '2':
221
+ if auth_qr(client):
222
+ return True
223
+ case '3':
224
+ if _auth_refresh(client, rich_input('refresh token', 'cyan', password=True)):
225
+ return True
226
+ case '4':
227
+ l.info('note: authorization will work only for ~15min')
228
+ if _auth_access(client, rich_input('access token', 'cyan')):
229
+ return True
230
+ case '5' | 'q' | 'quit':
231
+ quit()
232
+ case _:
233
+ 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 not 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(r'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')