aioqzone 1.9.1.dev3__tar.gz → 1.9.2.dev2__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.
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/PKG-INFO +5 -5
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/README.md +1 -1
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/pyproject.toml +5 -3
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/api/feed.py +0 -1
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/api/response.py +25 -12
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/_model.py +6 -3
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/__init__.py +4 -4
- aioqzone-1.9.2.dev2/src/qqqr/up/captcha/_model.py +77 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/capsess.py +15 -13
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/select/_types.py +3 -3
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/slide/_types.py +1 -1
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/h5.py +2 -8
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/web.py +2 -8
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/utils/jsjson.py +4 -3
- aioqzone-1.9.1.dev3/src/qqqr/up/captcha/_model.py +0 -73
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/LICENSE +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/api/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/api/h5/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/api/h5/model.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/api/login/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/api/login/_base.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/exception.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/message.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/api/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/api/profile.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/api/request.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/protocol/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/protocol/config.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/model/protocol/entity.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/utils/entity.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/utils/regex.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/utils/retry.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/aioqzone/utils/time.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/base.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/constant.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/exception.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/message.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/py.typed +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/qr/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/qr/type.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/type.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/click/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/click/_types.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/pil_utils.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/select/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/captcha/slide/__init__.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/up/encrypt.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/utils/encrypt.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/utils/iter.py +0 -0
- {aioqzone-1.9.1.dev3 → aioqzone-1.9.2.dev2}/src/qqqr/utils/net.py +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: aioqzone
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2.dev2
|
|
4
4
|
Summary: A Python wrapper for Qzone login and H5 APIs.
|
|
5
|
-
Home-page: https://github.com/aioqzone/aioqzone
|
|
6
5
|
License: AGPL-3.0
|
|
7
6
|
Keywords: qzone-api,autologin,asyncio-spider
|
|
8
7
|
Author: aioqzone
|
|
9
|
-
Author-email:
|
|
8
|
+
Author-email: 22952836+JamzumSum@users.noreply.github.com
|
|
10
9
|
Requires-Python: >=3.9,<4.0
|
|
11
10
|
Classifier: Development Status :: 4 - Beta
|
|
12
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -33,6 +32,7 @@ Requires-Dist: tylisten (>=2.1.4,<3.0.0)
|
|
|
33
32
|
Project-URL: Bug Tracker, https://github.com/aioqzone/aioqzone/issues
|
|
34
33
|
Project-URL: Documentation, https://aioqzone.github.io/aioqzone
|
|
35
34
|
Project-URL: Discussion, https://t.me/aioqzone_chatroom
|
|
35
|
+
Project-URL: Homepage, https://github.com/aioqzone/aioqzone
|
|
36
36
|
Project-URL: Repository, https://github.com/aioqzone/aioqzone
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
@@ -98,7 +98,7 @@ __在做了:__
|
|
|
98
98
|
## 许可证
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
Copyright (C) 2022-
|
|
101
|
+
Copyright (C) 2022-2025 aioqzone.
|
|
102
102
|
|
|
103
103
|
This program is free software: you can redistribute it and/or modify
|
|
104
104
|
it under the terms of the GNU Affero General Public License as published
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "aioqzone"
|
|
3
|
-
version = "1.9.
|
|
3
|
+
version = "1.9.2.dev2"
|
|
4
4
|
description = "A Python wrapper for Qzone login and H5 APIs."
|
|
5
|
-
authors = ["aioqzone <
|
|
5
|
+
authors = ["aioqzone <22952836+JamzumSum@users.noreply.github.com>"]
|
|
6
6
|
license = "AGPL-3.0"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
homepage = "https://github.com/aioqzone/aioqzone"
|
|
@@ -48,7 +48,7 @@ optional = false
|
|
|
48
48
|
|
|
49
49
|
[tool.poetry.group.test.dependencies]
|
|
50
50
|
pytest = "^8.2.0"
|
|
51
|
-
pytest-asyncio = "~0.
|
|
51
|
+
pytest-asyncio = "~0.25.2"
|
|
52
52
|
|
|
53
53
|
[tool.poetry.group.dev]
|
|
54
54
|
optional = true
|
|
@@ -87,6 +87,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
87
87
|
pythonpath = 'src'
|
|
88
88
|
log_cli = 1
|
|
89
89
|
log_cli_level = 'WARNING'
|
|
90
|
+
asyncio_default_fixture_loop_scope = "module"
|
|
90
91
|
|
|
91
92
|
[tool.isort]
|
|
92
93
|
profile = "black"
|
|
@@ -99,3 +100,4 @@ target-version = ['py39']
|
|
|
99
100
|
[tool.pyright]
|
|
100
101
|
pythonVersion = "3.9"
|
|
101
102
|
pythonPlatform = "All"
|
|
103
|
+
reportIncompatibleVariableOverride = "information"
|
|
@@ -4,19 +4,28 @@ from contextlib import suppress
|
|
|
4
4
|
|
|
5
5
|
from aiohttp import ClientResponse
|
|
6
6
|
from lxml.html import HtmlElement, document_fromstring
|
|
7
|
-
from pydantic import
|
|
7
|
+
from pydantic import (
|
|
8
|
+
AliasChoices,
|
|
9
|
+
AliasPath,
|
|
10
|
+
BaseModel,
|
|
11
|
+
Field,
|
|
12
|
+
HttpUrl,
|
|
13
|
+
TypeAdapter,
|
|
14
|
+
model_validator,
|
|
15
|
+
)
|
|
8
16
|
from tenacity import TryAgain
|
|
9
17
|
from typing_extensions import Self
|
|
10
18
|
|
|
11
19
|
from aioqzone.exception import QzoneError
|
|
12
20
|
from aioqzone.utils.regex import entire_closing, response_callback
|
|
13
21
|
from qqqr.utils.iter import firstn
|
|
14
|
-
from qqqr.utils.jsjson import
|
|
22
|
+
from qqqr.utils.jsjson import json_loads
|
|
15
23
|
|
|
16
24
|
from .feed import FeedData
|
|
17
25
|
from .profile import ProfileFeedData, QzoneProfile
|
|
18
26
|
|
|
19
|
-
StrDict = t.Dict[str,
|
|
27
|
+
StrDict = t.Dict[str, t.Any]
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
__all__ = [
|
|
22
31
|
"QzoneResponse",
|
|
@@ -37,6 +46,9 @@ __all__ = [
|
|
|
37
46
|
"ProfileFeedData",
|
|
38
47
|
]
|
|
39
48
|
|
|
49
|
+
validate_strdict = TypeAdapter(StrDict).validate_python
|
|
50
|
+
validate_str = TypeAdapter(str).validate_python
|
|
51
|
+
|
|
40
52
|
|
|
41
53
|
class QzoneResponse(BaseModel):
|
|
42
54
|
_errno_key: t.ClassVar[t.Union[str, AliasPath, AliasChoices, None]] = AliasChoices(
|
|
@@ -120,7 +132,7 @@ class IndexPageResp(FeedPageResp):
|
|
|
120
132
|
qzonetoken: str = ""
|
|
121
133
|
|
|
122
134
|
@classmethod
|
|
123
|
-
async def response_to_object(cls, response: ClientResponse):
|
|
135
|
+
async def response_to_object(cls, response: ClientResponse) -> StrDict:
|
|
124
136
|
html = await response.text()
|
|
125
137
|
scripts: t.List[HtmlElement] = document_fromstring(html).xpath(
|
|
126
138
|
'body/script[@type="application/javascript"]'
|
|
@@ -142,9 +154,10 @@ class IndexPageResp(FeedPageResp):
|
|
|
142
154
|
if m is None:
|
|
143
155
|
raise TryAgain("page data not found")
|
|
144
156
|
data = script[m.end() - 1 : m.end() + entire_closing(script[m.end() - 1 :])]
|
|
145
|
-
data = json_loads(data)
|
|
157
|
+
data = validate_strdict(json_loads(data))
|
|
158
|
+
|
|
146
159
|
with suppress(TypeError):
|
|
147
|
-
data["data"]["qzonetoken"] = qzonetoken
|
|
160
|
+
data["data"]["qzonetoken"] = qzonetoken
|
|
148
161
|
|
|
149
162
|
return data
|
|
150
163
|
|
|
@@ -206,9 +219,9 @@ class ProfilePagePesp(QzoneResponse):
|
|
|
206
219
|
@classmethod
|
|
207
220
|
def from_response_object(cls, obj: "StrDict") -> Self:
|
|
208
221
|
return cls(
|
|
209
|
-
info=QzoneInfo.from_response_object(obj["info"]),
|
|
210
|
-
feedpage=ProfileResp.from_response_object(obj["feedpage"]),
|
|
211
|
-
qzonetoken=obj
|
|
222
|
+
info=QzoneInfo.from_response_object(validate_strdict(obj["info"])),
|
|
223
|
+
feedpage=ProfileResp.from_response_object(validate_strdict(obj["feedpage"])),
|
|
224
|
+
qzonetoken=validate_str(obj.get("qzonetoken", "")),
|
|
212
225
|
)
|
|
213
226
|
|
|
214
227
|
|
|
@@ -245,10 +258,10 @@ class UploadPicResponse(QzoneResponse):
|
|
|
245
258
|
filemd5: str
|
|
246
259
|
|
|
247
260
|
@classmethod
|
|
248
|
-
async def response_to_object(cls, response: ClientResponse):
|
|
261
|
+
async def response_to_object(cls, response: ClientResponse) -> StrDict:
|
|
249
262
|
m = response_callback.search(await response.text())
|
|
250
263
|
assert m
|
|
251
|
-
return json_loads(m.group(1))
|
|
264
|
+
return validate_strdict(json_loads(m.group(1)))
|
|
252
265
|
|
|
253
266
|
|
|
254
267
|
class PicInfo(QzoneResponse):
|
|
@@ -268,7 +281,7 @@ class PhotosPreuploadResponse(QzoneResponse):
|
|
|
268
281
|
photos: t.List[PicInfo] = Field(default_factory=list)
|
|
269
282
|
|
|
270
283
|
@classmethod
|
|
271
|
-
async def response_to_object(cls, response: ClientResponse):
|
|
284
|
+
async def response_to_object(cls, response: ClientResponse) -> StrDict:
|
|
272
285
|
m = response_callback.search(await response.text())
|
|
273
286
|
assert m
|
|
274
287
|
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import typing as t
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, HttpUrl
|
|
5
|
+
from pydantic import BaseModel, Field, HttpUrl, TypeAdapter
|
|
6
6
|
|
|
7
7
|
from qqqr.type import RedirectCookies
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class CheckResp(
|
|
10
|
+
class CheckResp(t.NamedTuple):
|
|
11
11
|
code: int
|
|
12
12
|
"""code = 0/2/3 hideVC; code = 1 showVC
|
|
13
13
|
"""
|
|
14
14
|
verifycode: str
|
|
15
|
-
salt_repr: str
|
|
15
|
+
salt_repr: t.Annotated[str, Field(alias="salt")]
|
|
16
16
|
verifysession: str
|
|
17
17
|
isRandSalt: int
|
|
18
18
|
ptdrvs: str
|
|
@@ -40,3 +40,6 @@ class VerifyResp(BaseModel):
|
|
|
40
40
|
ticket: str
|
|
41
41
|
errMessage: str
|
|
42
42
|
sess: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
CheckRespValidator = TypeAdapter(CheckResp)
|
|
@@ -16,7 +16,7 @@ from qqqr.message import solve_select_captcha, solve_slide_captcha
|
|
|
16
16
|
|
|
17
17
|
from ...utils.net import ClientAdapter
|
|
18
18
|
from .._model import VerifyResp
|
|
19
|
-
from ._model import PrehandleResp
|
|
19
|
+
from ._model import PrehandleResp, PrehandleRespValidator
|
|
20
20
|
from .capsess import BaseTcaptchaSession as TcaptchaSession
|
|
21
21
|
from .click import ClickCaptchaSession
|
|
22
22
|
from .select import SelectCaptchaSession
|
|
@@ -128,7 +128,7 @@ class Captcha(_CaptchaHookMixin):
|
|
|
128
128
|
m = re.search(CALLBACK + r"\((\{.*\})\)", await r.text("utf8"))
|
|
129
129
|
|
|
130
130
|
assert m
|
|
131
|
-
return
|
|
131
|
+
return PrehandleRespValidator.validate_json(m.group(1))
|
|
132
132
|
|
|
133
133
|
sess = TcaptchaSession.factory(sid, await retry_closure())
|
|
134
134
|
if isinstance(sess, SelectCaptchaSession):
|
|
@@ -181,9 +181,9 @@ class Captcha(_CaptchaHookMixin):
|
|
|
181
181
|
"collect": collect,
|
|
182
182
|
"tlg": len(collect),
|
|
183
183
|
"eks": info.strip("'"),
|
|
184
|
-
"sess": sess.prehandle
|
|
184
|
+
"sess": sess.prehandle["sess"],
|
|
185
185
|
"ans": json.dumps([ans]),
|
|
186
|
-
"pow_answer": hex_add(sess.conf
|
|
186
|
+
"pow_answer": hex_add(sess.conf["common"]["pow_cfg"]["prefix"], sess.pow_ans),
|
|
187
187
|
"pow_calc_time": sess.duration,
|
|
188
188
|
}
|
|
189
189
|
log.debug(f"verify post data: {data}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasPath, BaseModel, Field, TypeAdapter
|
|
4
|
+
from typing_extensions import TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PowCfg(TypedDict):
|
|
8
|
+
prefix: str
|
|
9
|
+
md5: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommonCaptchaConf(TypedDict):
|
|
13
|
+
pow_cfg: PowCfg
|
|
14
|
+
"""Ians, duration = match_md5(pow_cfg)"""
|
|
15
|
+
tdc_path: str
|
|
16
|
+
"""relative path to get tdc.js"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommonClickConf(TypedDict):
|
|
20
|
+
data_type: t.Annotated[str, Field(validation_alias=AliasPath("data_type", 0))]
|
|
21
|
+
mark_style: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CommonBgElmConf(BaseModel):
|
|
25
|
+
cfg: CommonClickConf = Field(validation_alias="click_cfg")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CommonRender(BaseModel):
|
|
29
|
+
bg: CommonBgElmConf = Field(validation_alias="bg_elem_cfg")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Sprite(BaseModel):
|
|
33
|
+
"""Represents a sprite from a source material."""
|
|
34
|
+
|
|
35
|
+
size_2d: t.List[int]
|
|
36
|
+
"""sprite size (w, h)"""
|
|
37
|
+
sprite_pos: t.List[int]
|
|
38
|
+
"""sprite position on material (x, y)"""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def height(self):
|
|
42
|
+
return self.size_2d[1]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def width(self):
|
|
46
|
+
return self.size_2d[0]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def box(self):
|
|
50
|
+
l, t = self.sprite_pos
|
|
51
|
+
return (l, t, l + self.width, l + self.height)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CaptchaData(TypedDict):
|
|
55
|
+
common: t.Annotated[CommonCaptchaConf, Field(alias="comm_captcha_cfg")]
|
|
56
|
+
render: t.Annotated[dict[str, t.Any], Field(alias="dyn_show_info")]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PrehandleResp(TypedDict):
|
|
60
|
+
captcha: t.Annotated[t.Optional[CaptchaData], Field(alias="data", default=None)]
|
|
61
|
+
sess: str
|
|
62
|
+
|
|
63
|
+
capclass: t.Annotated[int, Field(default=0)]
|
|
64
|
+
log_js: t.Annotated[str, Field(default="")]
|
|
65
|
+
randstr: t.Annotated[str, Field(default="")]
|
|
66
|
+
sid: t.Annotated[str, Field(default="")]
|
|
67
|
+
src_1: t.Annotated[str, Field(default="")]
|
|
68
|
+
src_2: t.Annotated[str, Field(default="")]
|
|
69
|
+
src_3: t.Annotated[str, Field(default="")]
|
|
70
|
+
state: t.Annotated[int, Field(default=0)]
|
|
71
|
+
subcapclass: t.Annotated[int, Field(default=0)]
|
|
72
|
+
ticket: t.Annotated[str, Field(default="")]
|
|
73
|
+
uip: t.Annotated[str, Field(default="")]
|
|
74
|
+
"""ipv4 / ipv6"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
PrehandleRespValidator = TypeAdapter(PrehandleResp)
|
|
@@ -36,8 +36,8 @@ class BaseTcaptchaSession(ABC):
|
|
|
36
36
|
self.mouse_track = asyncio.get_event_loop().create_future()
|
|
37
37
|
|
|
38
38
|
def parse_captcha_data(self):
|
|
39
|
-
assert self.prehandle
|
|
40
|
-
self.conf = self.prehandle
|
|
39
|
+
assert self.prehandle["captcha"]
|
|
40
|
+
self.conf = self.prehandle["captcha"]
|
|
41
41
|
|
|
42
42
|
def solve_workload(self, *, timeout: float = 30.0):
|
|
43
43
|
"""
|
|
@@ -49,9 +49,9 @@ class BaseTcaptchaSession(ABC):
|
|
|
49
49
|
:return: None
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
pow_cfg = self.conf
|
|
53
|
-
nonce = str(pow_cfg
|
|
54
|
-
target = pow_cfg
|
|
52
|
+
pow_cfg = self.conf["common"]["pow_cfg"]
|
|
53
|
+
nonce = str(pow_cfg["prefix"]).encode()
|
|
54
|
+
target = pow_cfg["md5"].lower()
|
|
55
55
|
|
|
56
56
|
start = time()
|
|
57
57
|
cnt = 0
|
|
@@ -70,7 +70,9 @@ class BaseTcaptchaSession(ABC):
|
|
|
70
70
|
|
|
71
71
|
def _tdx_js_url(self):
|
|
72
72
|
assert self.conf
|
|
73
|
-
return URL("https://t.captcha.qq.com").with_path(
|
|
73
|
+
return URL("https://t.captcha.qq.com").with_path(
|
|
74
|
+
self.conf["common"]["tdc_path"], encoded=True
|
|
75
|
+
)
|
|
74
76
|
|
|
75
77
|
def _vmslide_js_url(self):
|
|
76
78
|
raise NotImplementedError
|
|
@@ -87,7 +89,7 @@ class BaseTcaptchaSession(ABC):
|
|
|
87
89
|
r.raise_for_status()
|
|
88
90
|
self.tdc = prepare(
|
|
89
91
|
await r.text("utf8"),
|
|
90
|
-
ip=ip or self.prehandle
|
|
92
|
+
ip=ip or self.prehandle["uip"],
|
|
91
93
|
ua=ua or client.headers["User-Agent"],
|
|
92
94
|
mouse_track=await self.mouse_track,
|
|
93
95
|
)
|
|
@@ -102,18 +104,18 @@ class BaseTcaptchaSession(ABC):
|
|
|
102
104
|
|
|
103
105
|
@classmethod
|
|
104
106
|
def factory(cls, session: str, prehandle: PrehandleResp):
|
|
105
|
-
assert prehandle
|
|
107
|
+
assert prehandle["captcha"]
|
|
106
108
|
|
|
107
109
|
try:
|
|
108
|
-
render = CommonRender.model_validate(prehandle
|
|
110
|
+
render = CommonRender.model_validate(prehandle["captcha"]["render"])
|
|
109
111
|
except ValidationError:
|
|
110
|
-
log.error(prehandle
|
|
112
|
+
log.error(prehandle["captcha"]["render"])
|
|
111
113
|
raise
|
|
112
114
|
|
|
113
|
-
if render.bg.cfg
|
|
115
|
+
if render.bg.cfg["data_type"] == "DynAnswerType_UC":
|
|
114
116
|
from .select import SelectCaptchaSession as cls
|
|
115
|
-
elif render.bg.cfg
|
|
116
|
-
log.error(prehandle
|
|
117
|
+
elif render.bg.cfg["data_type"] == "DynAnswerType_POS":
|
|
118
|
+
log.error(prehandle["captcha"]["render"])
|
|
117
119
|
raise NotImplementedError("“依次点击”类验证码正在施工")
|
|
118
120
|
from .click import ClickCaptchaSession as cls
|
|
119
121
|
else:
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import typing as t
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
|
|
6
|
-
from pydantic import AliasPath, BaseModel, Field,
|
|
6
|
+
from pydantic import AliasPath, BaseModel, Field, model_validator
|
|
7
7
|
|
|
8
8
|
from qqqr.message import solve_select_captcha
|
|
9
9
|
from qqqr.utils.iter import first
|
|
@@ -61,8 +61,8 @@ class SelectCaptchaSession(BaseTcaptchaSession):
|
|
|
61
61
|
|
|
62
62
|
def parse_captcha_data(self):
|
|
63
63
|
super().parse_captcha_data()
|
|
64
|
-
self.render = SelectRender.model_validate(self.conf
|
|
65
|
-
self.data_type = self.render.bg.cfg
|
|
64
|
+
self.render = SelectRender.model_validate(self.conf["render"])
|
|
65
|
+
self.data_type = self.render.bg.cfg["data_type"]
|
|
66
66
|
|
|
67
67
|
async def get_captcha_problem(self, client: ClientAdapter):
|
|
68
68
|
async with client.get(self._cdn_join(self.render.bg.img_url)) as r:
|
|
@@ -99,7 +99,7 @@ class SlideCaptchaSession(BaseTcaptchaSession):
|
|
|
99
99
|
|
|
100
100
|
def parse_captcha_data(self):
|
|
101
101
|
super().parse_captcha_data()
|
|
102
|
-
self.render = SlideRender.model_validate(self.conf
|
|
102
|
+
self.render = SlideRender.model_validate(self.conf["render"])
|
|
103
103
|
|
|
104
104
|
self.cdn_urls = (
|
|
105
105
|
self._cdn_join(self.render.bg.img_url),
|
|
@@ -4,7 +4,7 @@ from random import random
|
|
|
4
4
|
|
|
5
5
|
from yarl import URL
|
|
6
6
|
|
|
7
|
-
from ._model import
|
|
7
|
+
from ._model import CheckRespValidator
|
|
8
8
|
from .web import UpWebLogin, UpWebSession
|
|
9
9
|
|
|
10
10
|
log = logging.getLogger(__name__)
|
|
@@ -43,13 +43,7 @@ class UpH5Login(UpWebLogin):
|
|
|
43
43
|
r.raise_for_status()
|
|
44
44
|
rl = re.findall(r"'(.*?)'[,\)]", await r.text())
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
zip(
|
|
48
|
-
["code", "verifycode", "salt", "verifysession", "isRandSalt", "ptdrvs", "session"],
|
|
49
|
-
rl,
|
|
50
|
-
)
|
|
51
|
-
)
|
|
52
|
-
sess.set_check_result(CheckResp.model_validate(rdict))
|
|
46
|
+
sess.set_check_result(CheckRespValidator.validate_python(rl))
|
|
53
47
|
|
|
54
48
|
async def _make_login_param(self, sess: UpWebSession):
|
|
55
49
|
const = {
|
|
@@ -16,7 +16,7 @@ from qqqr.type import APPID, PT_QR_APP, Proxy
|
|
|
16
16
|
from qqqr.utils.iter import firstn
|
|
17
17
|
from qqqr.utils.net import ClientAdapter, get_all_cookie
|
|
18
18
|
|
|
19
|
-
from ._model import CheckResp, LoginResp, RedirectCookies, VerifyResp
|
|
19
|
+
from ._model import CheckResp, CheckRespValidator, LoginResp, RedirectCookies, VerifyResp
|
|
20
20
|
from .captcha import Captcha
|
|
21
21
|
from .encrypt import PasswdEncoder, TeaEncoder
|
|
22
22
|
|
|
@@ -160,13 +160,7 @@ class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
|
|
|
160
160
|
r.raise_for_status()
|
|
161
161
|
rl = re.findall(r"'(.*?)'[,\)]", await r.text())
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
zip(
|
|
165
|
-
["code", "verifycode", "salt", "verifysession", "isRandSalt", "ptdrvs", "session"],
|
|
166
|
-
rl,
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
sess.set_check_result(CheckResp.model_validate(rdict))
|
|
163
|
+
sess.set_check_result(CheckRespValidator.validate_python(rl))
|
|
170
164
|
|
|
171
165
|
async def send_sms_code(self, sess: UpWebSession):
|
|
172
166
|
"""Send verify sms (to get dynamic code)
|
|
@@ -4,9 +4,10 @@ from textwrap import dedent
|
|
|
4
4
|
from typing import Dict, List, Union
|
|
5
5
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
7
|
+
BaseTypes = Union[int, float, bool, str]
|
|
8
|
+
JsonDict = Dict[BaseTypes, "JsonValue"]
|
|
8
9
|
JsonList = List["JsonValue"]
|
|
9
|
-
JsonValue = Union[
|
|
10
|
+
JsonValue = Union[BaseTypes, JsonDict, JsonList]
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class AstLoader:
|
|
@@ -27,7 +28,7 @@ class AstLoader:
|
|
|
27
28
|
def visit_Name(self, node: ast.Name):
|
|
28
29
|
if node.id in self.const:
|
|
29
30
|
return self.const[node.id]
|
|
30
|
-
return ast.
|
|
31
|
+
return ast.Constant(value=node.id)
|
|
31
32
|
|
|
32
33
|
@classmethod
|
|
33
34
|
def json_loads(cls, js: str, filename: str = "stdin") -> JsonValue:
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import typing as t
|
|
2
|
-
|
|
3
|
-
from pydantic import AliasPath, BaseModel, Field
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class PowCfg(BaseModel):
|
|
7
|
-
prefix: str
|
|
8
|
-
md5: str
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CommonCaptchaConf(BaseModel):
|
|
12
|
-
pow_cfg: PowCfg
|
|
13
|
-
"""Ians, duration = match_md5(pow_cfg)"""
|
|
14
|
-
tdc_path: str
|
|
15
|
-
"""relative path to get tdc.js"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CommonClickConf(BaseModel):
|
|
19
|
-
data_type: str = Field(validation_alias=AliasPath("data_type", 0))
|
|
20
|
-
mark_style: str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class CommonBgElmConf(BaseModel):
|
|
24
|
-
cfg: CommonClickConf = Field(validation_alias="click_cfg")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class CommonRender(BaseModel):
|
|
28
|
-
bg: CommonBgElmConf = Field(validation_alias="bg_elem_cfg")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class Sprite(BaseModel):
|
|
32
|
-
"""Represents a sprite from a source material."""
|
|
33
|
-
|
|
34
|
-
size_2d: t.List[int]
|
|
35
|
-
"""sprite size (w, h)"""
|
|
36
|
-
sprite_pos: t.List[int]
|
|
37
|
-
"""sprite position on material (x, y)"""
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def height(self):
|
|
41
|
-
return self.size_2d[1]
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def width(self):
|
|
45
|
-
return self.size_2d[0]
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def box(self):
|
|
49
|
-
l, t = self.sprite_pos
|
|
50
|
-
return (l, t, l + self.width, l + self.height)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class CaptchaData(BaseModel):
|
|
54
|
-
common: CommonCaptchaConf = Field(alias="comm_captcha_cfg")
|
|
55
|
-
render: dict[str, t.Any] = Field(alias="dyn_show_info")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class PrehandleResp(BaseModel):
|
|
59
|
-
captcha: t.Optional[CaptchaData] = Field(alias="data", default=None)
|
|
60
|
-
sess: str
|
|
61
|
-
|
|
62
|
-
capclass: int = 0
|
|
63
|
-
log_js: str = ""
|
|
64
|
-
randstr: str = ""
|
|
65
|
-
sid: str = ""
|
|
66
|
-
src_1: str = ""
|
|
67
|
-
src_2: str = ""
|
|
68
|
-
src_3: str = ""
|
|
69
|
-
state: int = 0
|
|
70
|
-
subcapclass: int = 0
|
|
71
|
-
ticket: str = ""
|
|
72
|
-
uip: str = ""
|
|
73
|
-
"""ipv4 / ipv6"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|