aioqzone 1.9.0.dev1__tar.gz → 1.9.2.dev1__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.0.dev1 → aioqzone-1.9.2.dev1}/PKG-INFO +5 -5
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/README.md +1 -1
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/pyproject.toml +5 -3
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/feed.py +3 -13
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/profile.py +3 -4
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/response.py +26 -12
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/_model.py +6 -3
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/__init__.py +8 -5
- aioqzone-1.9.2.dev1/src/qqqr/up/captcha/_model.py +77 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/capsess.py +30 -12
- aioqzone-1.9.2.dev1/src/qqqr/up/captcha/click/__init__.py +3 -0
- aioqzone-1.9.2.dev1/src/qqqr/up/captcha/click/_types.py +8 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/select/_types.py +6 -8
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/slide/_types.py +26 -7
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/h5.py +2 -8
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/web.py +2 -8
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/jsjson.py +4 -3
- aioqzone-1.9.0.dev1/src/qqqr/up/captcha/_model.py +0 -84
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/LICENSE +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/h5/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/h5/model.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/login/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/login/_base.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/exception.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/message.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/request.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/config.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/entity.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/entity.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/regex.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/retry.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/time.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/base.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/constant.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/exception.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/message.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/py.typed +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/qr/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/qr/type.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/type.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/pil_utils.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/select/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/slide/__init__.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/encrypt.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/encrypt.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/iter.py +0 -0
- {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/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.dev1
|
|
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.dev1"
|
|
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"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import sys
|
|
2
1
|
import typing as t
|
|
3
2
|
from enum import IntEnum
|
|
4
3
|
|
|
@@ -15,15 +14,6 @@ from pydantic import (
|
|
|
15
14
|
|
|
16
15
|
__all__ = ["FeedData"]
|
|
17
16
|
|
|
18
|
-
if sys.version_info >= (3, 9):
|
|
19
|
-
removeprefix = str.removeprefix
|
|
20
|
-
else:
|
|
21
|
-
|
|
22
|
-
def removeprefix(self: str, prefix: str, /):
|
|
23
|
-
if self.startswith(prefix):
|
|
24
|
-
return self[len(prefix) :]
|
|
25
|
-
return self
|
|
26
|
-
|
|
27
17
|
|
|
28
18
|
class UgcRight(IntEnum):
|
|
29
19
|
unknown = 0
|
|
@@ -225,13 +215,13 @@ class Share(HasCommon):
|
|
|
225
215
|
|
|
226
216
|
class FeedOriginal(HasFid, HasCommon, HasUserInfo, HasSummary, HasMedia):
|
|
227
217
|
@model_validator(mode="before")
|
|
228
|
-
def remove_prefix(cls, v: dict):
|
|
229
|
-
return {removeprefix(
|
|
218
|
+
def remove_prefix(cls, v: dict[str, t.Any]):
|
|
219
|
+
return {k.removeprefix("cell_"): i for k, i in v.items()}
|
|
230
220
|
|
|
231
221
|
@field_validator("summary")
|
|
232
222
|
@classmethod
|
|
233
223
|
def remove_colon(cls, v: FeedSummary):
|
|
234
|
-
v.summary =
|
|
224
|
+
v.summary = v.summary.removeprefix(":")
|
|
235
225
|
return v
|
|
236
226
|
|
|
237
227
|
|
|
@@ -21,7 +21,6 @@ from .feed import (
|
|
|
21
21
|
Share,
|
|
22
22
|
ShareInfo,
|
|
23
23
|
UserInfo,
|
|
24
|
-
removeprefix,
|
|
25
24
|
)
|
|
26
25
|
|
|
27
26
|
|
|
@@ -89,13 +88,13 @@ class ProfileComment(FeedComment):
|
|
|
89
88
|
|
|
90
89
|
class ProfileFeedOriginal(HasFid, HasCommon, HasUserInfo, HasSummary, HasMedia):
|
|
91
90
|
@model_validator(mode="before")
|
|
92
|
-
def remove_prefix(cls, v: dict):
|
|
93
|
-
return {removeprefix(
|
|
91
|
+
def remove_prefix(cls, v: dict[str, t.Any]):
|
|
92
|
+
return {k.removeprefix("cell_"): i for k, i in v.items()}
|
|
94
93
|
|
|
95
94
|
@field_validator("summary")
|
|
96
95
|
@classmethod
|
|
97
96
|
def remove_colon(cls, v: FeedSummary):
|
|
98
|
-
v.summary =
|
|
97
|
+
v.summary = v.summary.removeprefix(":")
|
|
99
98
|
return v
|
|
100
99
|
|
|
101
100
|
|
|
@@ -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,11 @@ 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"]
|
|
160
|
+
assert isinstance(data["data"], dict)
|
|
161
|
+
data["data"]["qzonetoken"] = qzonetoken
|
|
148
162
|
|
|
149
163
|
return data
|
|
150
164
|
|
|
@@ -206,9 +220,9 @@ class ProfilePagePesp(QzoneResponse):
|
|
|
206
220
|
@classmethod
|
|
207
221
|
def from_response_object(cls, obj: "StrDict") -> Self:
|
|
208
222
|
return cls(
|
|
209
|
-
info=QzoneInfo.from_response_object(obj["info"]),
|
|
210
|
-
feedpage=ProfileResp.from_response_object(obj["feedpage"]),
|
|
211
|
-
qzonetoken=obj
|
|
223
|
+
info=QzoneInfo.from_response_object(validate_strdict(obj["info"])),
|
|
224
|
+
feedpage=ProfileResp.from_response_object(validate_strdict(obj["feedpage"])),
|
|
225
|
+
qzonetoken=validate_str(obj.get("qzonetoken", "")),
|
|
212
226
|
)
|
|
213
227
|
|
|
214
228
|
|
|
@@ -245,10 +259,10 @@ class UploadPicResponse(QzoneResponse):
|
|
|
245
259
|
filemd5: str
|
|
246
260
|
|
|
247
261
|
@classmethod
|
|
248
|
-
async def response_to_object(cls, response: ClientResponse):
|
|
262
|
+
async def response_to_object(cls, response: ClientResponse) -> StrDict:
|
|
249
263
|
m = response_callback.search(await response.text())
|
|
250
264
|
assert m
|
|
251
|
-
return json_loads(m.group(1))
|
|
265
|
+
return validate_strdict(json_loads(m.group(1)))
|
|
252
266
|
|
|
253
267
|
|
|
254
268
|
class PicInfo(QzoneResponse):
|
|
@@ -268,7 +282,7 @@ class PhotosPreuploadResponse(QzoneResponse):
|
|
|
268
282
|
photos: t.List[PicInfo] = Field(default_factory=list)
|
|
269
283
|
|
|
270
284
|
@classmethod
|
|
271
|
-
async def response_to_object(cls, response: ClientResponse):
|
|
285
|
+
async def response_to_object(cls, response: ClientResponse) -> StrDict:
|
|
272
286
|
m = response_callback.search(await response.text())
|
|
273
287
|
assert m
|
|
274
288
|
|
|
@@ -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,9 +16,10 @@ 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
|
-
from .
|
|
21
|
+
from .click import ClickCaptchaSession
|
|
22
|
+
from .select import SelectCaptchaSession
|
|
22
23
|
|
|
23
24
|
PREHANDLE_URL = "https://t.captcha.qq.com/cap_union_prehandle"
|
|
24
25
|
SHOW_NEW_URL = "https://t.captcha.qq.com/cap_union_new_show"
|
|
@@ -127,11 +128,13 @@ class Captcha(_CaptchaHookMixin):
|
|
|
127
128
|
m = re.search(CALLBACK + r"\((\{.*\})\)", await r.text("utf8"))
|
|
128
129
|
|
|
129
130
|
assert m
|
|
130
|
-
return
|
|
131
|
+
return PrehandleRespValidator.validate_json(m.group(1))
|
|
131
132
|
|
|
132
133
|
sess = TcaptchaSession.factory(sid, await retry_closure())
|
|
133
134
|
if isinstance(sess, SelectCaptchaSession):
|
|
134
135
|
sess.solve_captcha_hook = self.solve_select_captcha
|
|
136
|
+
elif isinstance(sess, ClickCaptchaSession):
|
|
137
|
+
raise NotImplementedError("“依次点击”类验证码正在施工")
|
|
135
138
|
else:
|
|
136
139
|
sess.solve_captcha_hook = self.solve_slide_captcha
|
|
137
140
|
return sess
|
|
@@ -178,9 +181,9 @@ class Captcha(_CaptchaHookMixin):
|
|
|
178
181
|
"collect": collect,
|
|
179
182
|
"tlg": len(collect),
|
|
180
183
|
"eks": info.strip("'"),
|
|
181
|
-
"sess": sess.prehandle
|
|
184
|
+
"sess": sess.prehandle["sess"],
|
|
182
185
|
"ans": json.dumps([ans]),
|
|
183
|
-
"pow_answer": hex_add(sess.conf
|
|
186
|
+
"pow_answer": hex_add(sess.conf["common"]["pow_cfg"]["prefix"], sess.pow_ans),
|
|
184
187
|
"pow_calc_time": sess.duration,
|
|
185
188
|
}
|
|
186
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)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
2
3
|
import typing as t
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from hashlib import md5
|
|
5
6
|
from time import time
|
|
6
7
|
|
|
8
|
+
from pydantic import ValidationError
|
|
7
9
|
from tylisten import HookSpec
|
|
8
10
|
from yarl import URL
|
|
9
11
|
|
|
10
12
|
from qqqr.utils.net import ClientAdapter
|
|
11
13
|
|
|
12
|
-
from ._model import PrehandleResp
|
|
14
|
+
from ._model import CommonRender, PrehandleResp
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class BaseTcaptchaSession(ABC):
|
|
16
|
-
data_type: str
|
|
20
|
+
data_type: str
|
|
17
21
|
mouse_track: "asyncio.Future[t.Optional[t.List[t.Tuple[int, int]]]]"
|
|
18
22
|
solve_captcha_hook: HookSpec
|
|
19
23
|
|
|
@@ -32,7 +36,8 @@ class BaseTcaptchaSession(ABC):
|
|
|
32
36
|
self.mouse_track = asyncio.get_event_loop().create_future()
|
|
33
37
|
|
|
34
38
|
def parse_captcha_data(self):
|
|
35
|
-
|
|
39
|
+
assert self.prehandle["captcha"]
|
|
40
|
+
self.conf = self.prehandle["captcha"]
|
|
36
41
|
|
|
37
42
|
def solve_workload(self, *, timeout: float = 30.0):
|
|
38
43
|
"""
|
|
@@ -44,9 +49,9 @@ class BaseTcaptchaSession(ABC):
|
|
|
44
49
|
:return: None
|
|
45
50
|
"""
|
|
46
51
|
|
|
47
|
-
pow_cfg = self.conf
|
|
48
|
-
nonce = str(pow_cfg
|
|
49
|
-
target = pow_cfg
|
|
52
|
+
pow_cfg = self.conf["common"]["pow_cfg"]
|
|
53
|
+
nonce = str(pow_cfg["prefix"]).encode()
|
|
54
|
+
target = pow_cfg["md5"].lower()
|
|
50
55
|
|
|
51
56
|
start = time()
|
|
52
57
|
cnt = 0
|
|
@@ -65,7 +70,9 @@ class BaseTcaptchaSession(ABC):
|
|
|
65
70
|
|
|
66
71
|
def _tdx_js_url(self):
|
|
67
72
|
assert self.conf
|
|
68
|
-
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
|
+
)
|
|
69
76
|
|
|
70
77
|
def _vmslide_js_url(self):
|
|
71
78
|
raise NotImplementedError
|
|
@@ -82,7 +89,7 @@ class BaseTcaptchaSession(ABC):
|
|
|
82
89
|
r.raise_for_status()
|
|
83
90
|
self.tdc = prepare(
|
|
84
91
|
await r.text("utf8"),
|
|
85
|
-
ip=ip or self.prehandle
|
|
92
|
+
ip=ip or self.prehandle["uip"],
|
|
86
93
|
ua=ua or client.headers["User-Agent"],
|
|
87
94
|
mouse_track=await self.mouse_track,
|
|
88
95
|
)
|
|
@@ -97,10 +104,21 @@ class BaseTcaptchaSession(ABC):
|
|
|
97
104
|
|
|
98
105
|
@classmethod
|
|
99
106
|
def factory(cls, session: str, prehandle: PrehandleResp):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
assert prehandle["captcha"]
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
render = CommonRender.model_validate(prehandle["captcha"]["render"])
|
|
111
|
+
except ValidationError:
|
|
112
|
+
log.error(prehandle["captcha"]["render"])
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
if render.bg.cfg["data_type"] == "DynAnswerType_UC":
|
|
116
|
+
from .select import SelectCaptchaSession as cls
|
|
117
|
+
elif render.bg.cfg["data_type"] == "DynAnswerType_POS":
|
|
118
|
+
log.error(prehandle["captcha"]["render"])
|
|
119
|
+
raise NotImplementedError("“依次点击”类验证码正在施工")
|
|
120
|
+
from .click import ClickCaptchaSession as cls
|
|
103
121
|
else:
|
|
104
|
-
from .slide
|
|
122
|
+
from .slide import SlideCaptchaSession as cls
|
|
105
123
|
|
|
106
124
|
return cls(session=session, prehandle=prehandle)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .._model import PrehandleResp
|
|
2
|
+
from ..capsess import BaseTcaptchaSession
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ClickCaptchaSession(BaseTcaptchaSession):
|
|
6
|
+
def __init__(self, session: str, prehandle: PrehandleResp) -> None:
|
|
7
|
+
super().__init__(session, prehandle)
|
|
8
|
+
self.mouse_track.set_result(None)
|
|
@@ -10,15 +10,14 @@ from qqqr.utils.iter import first
|
|
|
10
10
|
from qqqr.utils.jsjson import json_loads
|
|
11
11
|
from qqqr.utils.net import ClientAdapter
|
|
12
12
|
|
|
13
|
-
from .._model import
|
|
13
|
+
from .._model import CommonBgElmConf, CommonRender, PrehandleResp, Sprite
|
|
14
14
|
from ..capsess import BaseTcaptchaSession
|
|
15
15
|
from ..pil_utils import *
|
|
16
16
|
|
|
17
17
|
log = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class SelectBgElemCfg(Sprite):
|
|
21
|
-
click_cfg: ClickCfg
|
|
20
|
+
class SelectBgElemCfg(CommonBgElmConf, Sprite):
|
|
22
21
|
img_url: str
|
|
23
22
|
|
|
24
23
|
|
|
@@ -40,7 +39,7 @@ class SelectJsonPayload(BaseModel):
|
|
|
40
39
|
return len(self.picture_ids)
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class
|
|
42
|
+
class SelectRender(CommonRender):
|
|
44
43
|
instruction: str
|
|
45
44
|
bg: SelectBgElemCfg = Field(alias="bg_elem_cfg")
|
|
46
45
|
verify_trigger_cfg: dict
|
|
@@ -56,15 +55,14 @@ class SelectCaptchaDisplay(BaseModel):
|
|
|
56
55
|
class SelectCaptchaSession(BaseTcaptchaSession):
|
|
57
56
|
solve_captcha_hook: solve_select_captcha.TyInst
|
|
58
57
|
|
|
59
|
-
def __init__(self, session: str, prehandle: PrehandleResp) -> None:
|
|
58
|
+
def __init__(self, session: str, prehandle: "PrehandleResp") -> None:
|
|
60
59
|
super().__init__(session, prehandle)
|
|
61
60
|
self.mouse_track.set_result(None)
|
|
62
61
|
|
|
63
62
|
def parse_captcha_data(self):
|
|
64
63
|
super().parse_captcha_data()
|
|
65
|
-
self.render =
|
|
66
|
-
|
|
67
|
-
self.data_type = self.render.bg.click_cfg.data_type[0]
|
|
64
|
+
self.render = SelectRender.model_validate(self.conf["render"])
|
|
65
|
+
self.data_type = self.render.bg.cfg["data_type"]
|
|
68
66
|
|
|
69
67
|
async def get_captcha_problem(self, client: ClientAdapter):
|
|
70
68
|
async with client.get(self._cdn_join(self.render.bg.img_url)) as r:
|
|
@@ -4,13 +4,13 @@ import typing as t
|
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from random import choices, randint
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import AliasPath, BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from qqqr.message import solve_slide_captcha
|
|
10
10
|
from qqqr.utils.iter import first, firstn
|
|
11
11
|
from qqqr.utils.net import ClientAdapter
|
|
12
12
|
|
|
13
|
-
from .._model import
|
|
13
|
+
from .._model import CommonBgElmConf, CommonRender, Sprite
|
|
14
14
|
from ..capsess import BaseTcaptchaSession
|
|
15
15
|
from ..pil_utils import *
|
|
16
16
|
|
|
@@ -58,14 +58,33 @@ except ImportError:
|
|
|
58
58
|
return noise_x, noise_y
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class
|
|
61
|
+
class MoveCfg(BaseModel):
|
|
62
|
+
track_limit: str
|
|
63
|
+
move_factor: t.List[int]
|
|
64
|
+
data_type: str = Field(validation_alias=AliasPath("data_type", 0))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FgElemCfg(Sprite):
|
|
68
|
+
id: int
|
|
69
|
+
init_pos: t.List[int]
|
|
70
|
+
move_cfg: t.Optional[MoveCfg] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FgBindingCfg(BaseModel):
|
|
74
|
+
master: int
|
|
75
|
+
slave: int
|
|
76
|
+
bind_type: str
|
|
77
|
+
bind_factor: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SlideBgElemCfg(CommonBgElmConf, Sprite):
|
|
62
81
|
img_url: str
|
|
63
82
|
"""relative url to get jigsaw puzzle image (background with dimmed piece shape)."""
|
|
64
83
|
init_pos: t.List[int] = Field(default=[0, 0])
|
|
65
84
|
"""sprite init position on captcha (x, y)"""
|
|
66
85
|
|
|
67
86
|
|
|
68
|
-
class
|
|
87
|
+
class SlideRender(CommonRender):
|
|
69
88
|
bg: SlideBgElemCfg = Field(alias="bg_elem_cfg")
|
|
70
89
|
"""Background (puzzle)"""
|
|
71
90
|
fg_binding_list: t.List[FgBindingCfg] = Field(default=[])
|
|
@@ -80,7 +99,8 @@ class SlideCaptchaSession(BaseTcaptchaSession):
|
|
|
80
99
|
|
|
81
100
|
def parse_captcha_data(self):
|
|
82
101
|
super().parse_captcha_data()
|
|
83
|
-
self.render =
|
|
102
|
+
self.render = SlideRender.model_validate(self.conf["render"])
|
|
103
|
+
|
|
84
104
|
self.cdn_urls = (
|
|
85
105
|
self._cdn_join(self.render.bg.img_url),
|
|
86
106
|
self._cdn_join(self.render.sprite_url),
|
|
@@ -89,8 +109,7 @@ class SlideCaptchaSession(BaseTcaptchaSession):
|
|
|
89
109
|
|
|
90
110
|
self.piece_sprite = first(self.render.sprites, lambda s: s.move_cfg)
|
|
91
111
|
assert self.piece_sprite.move_cfg
|
|
92
|
-
|
|
93
|
-
self.data_type = self.piece_sprite.move_cfg.data_type[0]
|
|
112
|
+
self.data_type = self.piece_sprite.move_cfg.data_type
|
|
94
113
|
|
|
95
114
|
async def get_captcha_problem(self, client: ClientAdapter):
|
|
96
115
|
"""
|
|
@@ -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,84 +0,0 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
2
|
-
|
|
3
|
-
from pydantic import 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 Sprite(BaseModel):
|
|
19
|
-
"""Represents a sprite from a source material."""
|
|
20
|
-
|
|
21
|
-
size_2d: List[int]
|
|
22
|
-
"""sprite size (w, h)"""
|
|
23
|
-
sprite_pos: List[int]
|
|
24
|
-
"""sprite position on material (x, y)"""
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def height(self):
|
|
28
|
-
return self.size_2d[1]
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def width(self):
|
|
32
|
-
return self.size_2d[0]
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def box(self):
|
|
36
|
-
l, t = self.sprite_pos
|
|
37
|
-
return (l, t, l + self.width, l + self.height)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class ClickCfg(BaseModel):
|
|
41
|
-
mark_style: str
|
|
42
|
-
data_type: List[str]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class MoveCfg(BaseModel):
|
|
46
|
-
track_limit: str
|
|
47
|
-
move_factor: List[int]
|
|
48
|
-
data_type: Optional[List[str]] = None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class FgElemCfg(Sprite):
|
|
52
|
-
id: int
|
|
53
|
-
init_pos: List[int]
|
|
54
|
-
move_cfg: Optional[MoveCfg] = None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class FgBindingCfg(BaseModel):
|
|
58
|
-
master: int
|
|
59
|
-
slave: int
|
|
60
|
-
bind_type: str
|
|
61
|
-
bind_factor: int
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class CaptchaData(BaseModel):
|
|
65
|
-
common: CommonCaptchaConf = Field(alias="comm_captcha_cfg")
|
|
66
|
-
render: dict = Field(alias="dyn_show_info")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class PrehandleResp(BaseModel):
|
|
70
|
-
captcha: CaptchaData = Field(alias="data", default=None)
|
|
71
|
-
sess: str
|
|
72
|
-
|
|
73
|
-
capclass: int = 0
|
|
74
|
-
log_js: str = ""
|
|
75
|
-
randstr: str = ""
|
|
76
|
-
sid: str = ""
|
|
77
|
-
src_1: str = ""
|
|
78
|
-
src_2: str = ""
|
|
79
|
-
src_3: str = ""
|
|
80
|
-
state: int = 0
|
|
81
|
-
subcapclass: int = 0
|
|
82
|
-
ticket: str = ""
|
|
83
|
-
uip: str = ""
|
|
84
|
-
"""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
|