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.
Files changed (55) hide show
  1. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/PKG-INFO +5 -5
  2. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/README.md +1 -1
  3. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/pyproject.toml +5 -3
  4. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/feed.py +3 -13
  5. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/profile.py +3 -4
  6. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/response.py +26 -12
  7. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/_model.py +6 -3
  8. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/__init__.py +8 -5
  9. aioqzone-1.9.2.dev1/src/qqqr/up/captcha/_model.py +77 -0
  10. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/capsess.py +30 -12
  11. aioqzone-1.9.2.dev1/src/qqqr/up/captcha/click/__init__.py +3 -0
  12. aioqzone-1.9.2.dev1/src/qqqr/up/captcha/click/_types.py +8 -0
  13. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/select/_types.py +6 -8
  14. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/slide/_types.py +26 -7
  15. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/h5.py +2 -8
  16. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/web.py +2 -8
  17. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/jsjson.py +4 -3
  18. aioqzone-1.9.0.dev1/src/qqqr/up/captcha/_model.py +0 -84
  19. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/LICENSE +0 -0
  20. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/__init__.py +0 -0
  21. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/__init__.py +0 -0
  22. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/h5/__init__.py +0 -0
  23. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/h5/model.py +0 -0
  24. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/login/__init__.py +0 -0
  25. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/api/login/_base.py +0 -0
  26. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/exception.py +0 -0
  27. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/message.py +0 -0
  28. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/__init__.py +0 -0
  29. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/__init__.py +0 -0
  30. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/api/request.py +0 -0
  31. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/__init__.py +0 -0
  32. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/config.py +0 -0
  33. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/model/protocol/entity.py +0 -0
  34. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/__init__.py +0 -0
  35. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/entity.py +0 -0
  36. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/regex.py +0 -0
  37. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/retry.py +0 -0
  38. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/aioqzone/utils/time.py +0 -0
  39. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/__init__.py +0 -0
  40. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/base.py +0 -0
  41. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/constant.py +0 -0
  42. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/exception.py +0 -0
  43. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/message.py +0 -0
  44. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/py.typed +0 -0
  45. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/qr/__init__.py +0 -0
  46. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/qr/type.py +0 -0
  47. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/type.py +0 -0
  48. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/__init__.py +0 -0
  49. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/pil_utils.py +0 -0
  50. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/select/__init__.py +0 -0
  51. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/captcha/slide/__init__.py +0 -0
  52. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/up/encrypt.py +0 -0
  53. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/encrypt.py +0 -0
  54. {aioqzone-1.9.0.dev1 → aioqzone-1.9.2.dev1}/src/qqqr/utils/iter.py +0 -0
  55. {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
1
+ Metadata-Version: 2.3
2
2
  Name: aioqzone
3
- Version: 1.9.0.dev1
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: zzzzss990315@gmail.com
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-2024 aioqzone.
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
@@ -60,7 +60,7 @@ __在做了:__
60
60
  ## 许可证
61
61
 
62
62
  ```
63
- Copyright (C) 2022-2024 aioqzone.
63
+ Copyright (C) 2022-2025 aioqzone.
64
64
 
65
65
  This program is free software: you can redistribute it and/or modify
66
66
  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.0.dev1"
3
+ version = "1.9.2.dev1"
4
4
  description = "A Python wrapper for Qzone login and H5 APIs."
5
- authors = ["aioqzone <zzzzss990315@gmail.com>"]
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.21.2"
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(k, "cell_"): i for k, i in v.items()}
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 = removeprefix(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(k, "cell_"): i for k, i in v.items()}
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 = removeprefix(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 AliasChoices, AliasPath, BaseModel, Field, HttpUrl, model_validator
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 JsonValue, json_loads
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, JsonValue]
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"]["qzonetoken"] = qzonetoken # type: ignore
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"]), # type: ignore
210
- feedpage=ProfileResp.from_response_object(obj["feedpage"]), # type: ignore
211
- qzonetoken=obj["qzonetoken"], # type: ignore
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(BaseModel):
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 = Field(alias="salt")
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 .select._types import SelectCaptchaSession
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 PrehandleResp.model_validate_json(m.group(1))
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.sess,
184
+ "sess": sess.prehandle["sess"],
182
185
  "ans": json.dumps([ans]),
183
- "pow_answer": hex_add(sess.conf.common.pow_cfg.prefix, sess.pow_ans),
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 = "DynAnswerType_UC"
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
- self.conf = self.prehandle.captcha
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.common.pow_cfg
48
- nonce = str(pow_cfg.prefix).encode()
49
- target = pow_cfg.md5.lower()
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(self.conf.common.tdc_path, encoded=True)
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.uip,
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
- render = prehandle.captcha.render
101
- if "json_payload" in render:
102
- from .select._types import SelectCaptchaSession as cls
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._types import SlideCaptchaSession as cls
122
+ from .slide import SlideCaptchaSession as cls
105
123
 
106
124
  return cls(session=session, prehandle=prehandle)
@@ -0,0 +1,3 @@
1
+ from ._types import ClickCaptchaSession
2
+
3
+ __all__ = ["ClickCaptchaSession"]
@@ -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 ClickCfg, PrehandleResp, Sprite
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 SelectCaptchaDisplay(BaseModel):
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 = SelectCaptchaDisplay.model_validate(self.conf.render)
66
- if self.render.bg.click_cfg.data_type:
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 FgBindingCfg, FgElemCfg, Sprite
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 SlideBgElemCfg(Sprite):
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 SlideCaptchaDisplay(BaseModel):
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 = SlideCaptchaDisplay.model_validate(self.conf.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
- if self.piece_sprite.move_cfg.data_type:
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 CheckResp
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
- rdict = dict(
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
- rdict = dict(
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
- JsonDict = Dict[Union[str, int], "JsonValue"]
7
+ BaseTypes = Union[int, float, bool, str]
8
+ JsonDict = Dict[BaseTypes, "JsonValue"]
8
9
  JsonList = List["JsonValue"]
9
- JsonValue = Union[bool, int, str, JsonDict, JsonList]
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.Str(s=node.id)
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