aioqzone 0.13.0.dev1__tar.gz → 0.14.1.dev8__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 (64) hide show
  1. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/PKG-INFO +4 -2
  2. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/pyproject.toml +13 -12
  3. aioqzone-0.14.1.dev8/src/aioqzone/api/__init__.py +12 -0
  4. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/model.py +2 -2
  5. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/raw.py +17 -11
  6. aioqzone-0.14.1.dev8/src/aioqzone/api/login/__init__.py +227 -0
  7. {aioqzone-0.13.0.dev1/src/aioqzone/api/loginman → aioqzone-0.14.1.dev8/src/aioqzone/api/login}/_base.py +9 -8
  8. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/model.py +3 -3
  9. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/raw.py +31 -19
  10. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/exception.py +19 -12
  11. aioqzone-0.14.1.dev8/src/aioqzone/message.py +26 -0
  12. {aioqzone-0.13.0.dev1/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model}/__init__.py +2 -3
  13. aioqzone-0.13.0.dev1/src/aioqzone/type/internal.py → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol/__init__.py +3 -1
  14. aioqzone-0.14.1.dev8/src/aioqzone/model/protocol/config.py +25 -0
  15. aioqzone-0.14.1.dev8/src/aioqzone/model/response/__init__.py +1 -0
  16. {aioqzone-0.13.0.dev1/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/h5.py +12 -12
  17. {aioqzone-0.13.0.dev1/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/web.py +1 -1
  18. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/entity.py +1 -1
  19. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/html.py +10 -10
  20. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/base.py +11 -5
  21. aioqzone-0.14.1.dev8/src/qqqr/exception.py +34 -0
  22. aioqzone-0.14.1.dev8/src/qqqr/message.py +35 -0
  23. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/qr/__init__.py +52 -16
  24. aioqzone-0.14.1.dev8/src/qqqr/up/__init__.py +4 -0
  25. aioqzone-0.13.0.dev1/src/qqqr/up/type.py → aioqzone-0.14.1.dev8/src/qqqr/up/_model.py +1 -1
  26. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/__init__.py +8 -10
  27. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/encrypt.py +3 -4
  28. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/h5.py +5 -5
  29. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/web.py +31 -21
  30. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/iter.py +5 -1
  31. aioqzone-0.13.0.dev1/src/aioqzone/api/__init__.py +0 -5
  32. aioqzone-0.13.0.dev1/src/aioqzone/api/loginman/__init__.py +0 -333
  33. aioqzone-0.13.0.dev1/src/aioqzone/event/__init__.py +0 -7
  34. aioqzone-0.13.0.dev1/src/aioqzone/event/login.py +0 -42
  35. aioqzone-0.13.0.dev1/src/aioqzone/py.typed +0 -0
  36. aioqzone-0.13.0.dev1/src/aioqzone/type/resp/__init__.py +0 -1
  37. aioqzone-0.13.0.dev1/src/qqqr/event/__init__.py +0 -11
  38. aioqzone-0.13.0.dev1/src/qqqr/event/evt.py +0 -168
  39. aioqzone-0.13.0.dev1/src/qqqr/event/evtmgr.py +0 -104
  40. aioqzone-0.13.0.dev1/src/qqqr/event/login.py +0 -42
  41. aioqzone-0.13.0.dev1/src/qqqr/exception.py +0 -59
  42. aioqzone-0.13.0.dev1/src/qqqr/ssl.py +0 -30
  43. aioqzone-0.13.0.dev1/src/qqqr/up/__init__.py +0 -4
  44. aioqzone-0.13.0.dev1/src/qqqr/utils/daug.py +0 -25
  45. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/LICENSE +0 -0
  46. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/README.md +0 -0
  47. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/__init__.py +0 -0
  48. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/__init__.py +0 -0
  49. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/__init__.py +0 -0
  50. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/constant.py +0 -0
  51. {aioqzone-0.13.0.dev1/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol}/entity.py +0 -0
  52. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/__init__.py +0 -0
  53. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/catch.py +0 -0
  54. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/regex.py +0 -0
  55. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/time.py +0 -0
  56. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/__init__.py +0 -0
  57. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/constant.py +0 -0
  58. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/py.typed +0 -0
  59. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/qr/type.py +0 -0
  60. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/type.py +0 -0
  61. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/jigsaw.py +0 -0
  62. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/encrypt.py +0 -0
  63. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/jsjson.py +0 -0
  64. {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/net.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioqzone
3
- Version: 0.13.0.dev1
3
+ Version: 0.14.1.dev8
4
4
  Summary: Python wrapper for Qzone login and Qzone HTTP APIs.
5
5
  Home-page: https://github.com/aioqzone/aioqzone
6
6
  License: AGPL-3.0
@@ -26,8 +26,10 @@ Requires-Dist: lxml (>=4.9.1,<5.0.0)
26
26
  Requires-Dist: numpy (>=1.22.3,<2.0.0) ; extra == "captcha"
27
27
  Requires-Dist: pillow (>=9.4.0,<10.0.0) ; extra == "captcha"
28
28
  Requires-Dist: pychaosvm (>=0.2.3,<0.3.0) ; extra == "captcha"
29
- Requires-Dist: pydantic (>=1.10.4,<2.0.0)
29
+ Requires-Dist: pydantic (>=2.0.3,<3.0.0)
30
+ Requires-Dist: pydantic-settings (>=2.0.2,<3.0.0)
30
31
  Requires-Dist: rsa (>=4.8,<5.0)
32
+ Requires-Dist: tylisten (>=0.5.1,<0.6.0)
31
33
  Project-URL: Bug Tracker, https://github.com/aioqzone/aioqzone/issues
32
34
  Project-URL: Documentation, https://aioqzone.github.io/aioqzone
33
35
  Project-URL: Discussion, https://t.me/aioqzone_chatroom
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aioqzone"
3
- version = "0.13.0.dev1"
3
+ version = "0.14.1.dev8"
4
4
  description = "Python wrapper for Qzone login and Qzone HTTP APIs."
5
5
  authors = ["aioqzone <zzzzss990315@gmail.com>"]
6
6
  license = "AGPL-3.0"
@@ -29,15 +29,17 @@ exclude = ["*.js"]
29
29
  [tool.poetry.dependencies]
30
30
  python = "^3.8"
31
31
  httpx = "^0.24.0"
32
- pydantic = "^1.10.4"
32
+ pydantic = "^2.0.3"
33
+ pydantic-settings = "^2.0.2"
33
34
  rsa = "^4.8"
34
35
  lxml = "^4.9.1"
35
36
  cssselect = "^1.1.0"
36
- exceptiongroup = { version = "^1.1.1", python = "<3.11", source = "PyPI" }
37
+ tylisten = { version = "~0.5.1", source = "aioqzone-index" }
38
+ exceptiongroup = { version = "^1.1.1", python = "<3.11"}
37
39
 
38
- numpy = { version = "^1.22.3", optional = true, source = "PyPI" }
39
- pillow = { version = "^9.4.0", optional = true, source = "PyPI" }
40
- pychaosvm = { version = "^0.2.3", optional = true, source = "aioqzone-index" }
40
+ numpy = { version = "^1.22.3", optional = true}
41
+ pillow = { version = "^9.4.0", optional = true}
42
+ pychaosvm = { version = "~0.2.3", optional = true, source = "aioqzone-index" }
41
43
 
42
44
  [tool.poetry.extras]
43
45
  captcha = ["numpy", "pillow", "pychaosvm"]
@@ -47,7 +49,7 @@ captcha = ["numpy", "pillow", "pychaosvm"]
47
49
  optional = false
48
50
 
49
51
  [tool.poetry.group.test.dependencies]
50
- pytest = "^7.2.1"
52
+ pytest = "^7.4.0"
51
53
  pytest-asyncio = "~0.21.0"
52
54
 
53
55
  [tool.poetry.group.dev]
@@ -62,17 +64,16 @@ isort = "*"
62
64
  optional = true
63
65
 
64
66
  [tool.poetry.group.docs.dependencies]
65
- Sphinx = "^6.1.3"
66
- autodoc-pydantic = "*"
67
+ Sphinx = "^7.0.1"
68
+ autodoc-pydantic = "^2.0.1" # autodoc-pydantic/#146 #162
67
69
  sphinx-autodoc-typehints = "^1.19.5"
68
- sphinx-rtd-theme = "*"
70
+ furo = "*"
69
71
  sphinx-intl = "*"
70
72
 
71
73
  [[tool.poetry.source]]
72
74
  name = "aioqzone-index"
73
75
  url = "https://aioqzone.github.io/aioqzone-index/simple"
74
- priority = "primary"
75
-
76
+ priority = "supplemental"
76
77
 
77
78
  [[tool.poetry.source]]
78
79
  name = "PyPI"
@@ -0,0 +1,12 @@
1
+ from .h5 import QzoneH5API
2
+ from .login import *
3
+ from .login._base import Loginable
4
+
5
+ __all__ = [
6
+ "UnifiedLoginManager",
7
+ "LoginMethod",
8
+ "QrLoginConfig",
9
+ "UpLoginConfig",
10
+ "Loginable",
11
+ "QzoneH5API",
12
+ ]
@@ -3,7 +3,7 @@ from typing import Type, TypeVar
3
3
 
4
4
  from pydantic import BaseModel, ValidationError
5
5
 
6
- from aioqzone.type.resp.h5 import FeedCount, FeedPageResp, GetMoreResp
6
+ from aioqzone.model import FeedCount, FeedPageResp, GetMoreResp
7
7
 
8
8
  from .raw import QzoneH5RawAPI
9
9
 
@@ -14,7 +14,7 @@ log = logging.getLogger(__name__)
14
14
 
15
15
  def _parse_obj(model: Type[_model], o: object) -> _model:
16
16
  try:
17
- return model.parse_obj(o)
17
+ return model.model_validate(o)
18
18
  except ValidationError:
19
19
  log.debug(o)
20
20
  raise
@@ -5,10 +5,11 @@ from typing import Callable, Dict, List, Optional, Tuple, Union
5
5
 
6
6
  from lxml.html import fromstring
7
7
 
8
- from aioqzone.api.loginman import Loginable
8
+ from aioqzone.api.login import Loginable
9
9
  from aioqzone.exception import QzoneError
10
10
  from aioqzone.utils.catch import HTTPStatusErrorDispatch, QzoneErrorDispatch
11
11
  from aioqzone.utils.regex import entire_closing, response_callback
12
+ from qqqr.utils.iter import firstn
12
13
  from qqqr.utils.jsjson import JsonValue, json_loads
13
14
  from qqqr.utils.net import ClientAdapter
14
15
 
@@ -98,11 +99,16 @@ class QzoneH5RawAPI:
98
99
  The exceptions this wrapper may raise depends on the login manager you passed in.
99
100
  Any exceptions irrelevent to "login needed" will be passed through w/o any change.
100
101
 
101
- :raises: All error that may be raised from :meth:`.login.new_cookie`, which depends on the login manager you passed in.
102
+ :raises:
103
+ All errors that may be raised from :meth:`Loginable.new_cookie`, which depends on
104
+ the login manager you are using. If you use our built-in manager,
105
+ :class:`aioqzone.api.loginman.UnifiedLoginManager`,
106
+ :exc:`SkipLoginInterrupt` and :exc:`LoginError` may be raised.
102
107
  """
103
108
 
104
109
  with QzoneErrorDispatch() as qze, HTTPStatusErrorDispatch() as hse:
105
- qze.dispatch(-3000, suppress=lambda e: "登录" in e.msg) # -3000: 请先登录
110
+ # NOTE: 尽管只有“-3000: 请先登录”明确要求重新登录,但似乎任何原因的-3000错误值都意味着cookie过期。因此移除了对message的校验
111
+ qze.dispatch(-3000)
106
112
  qze.dispatch(-10000)
107
113
  hse.dispatch(302, 403)
108
114
  return await func(*args, **kwds)
@@ -143,20 +149,20 @@ class QzoneH5RawAPI:
143
149
  if cb:
144
150
  match = response_callback.search(robj)
145
151
  assert match
146
- robj = match.group(1)
152
+ robj = str(match.group(1))
147
153
  r = json_loads(robj)
148
154
  else:
149
155
  r = robj
150
156
 
151
157
  assert isinstance(r, dict)
152
158
 
153
- err = next(filter(lambda i: i is not None, (r.get(i) for i in errno_key)), None)
159
+ err = firstn((r.get(i) for i in errno_key), lambda i: i is not None)
154
160
  assert err is not None, f"no {errno_key} in {r.keys()}"
155
161
  assert isinstance(err, (int, str))
156
162
  err = int(err)
157
163
 
158
164
  if err != 0:
159
- msg = next(filter(None, (r.get(i) for i in msg_key)), None)
165
+ msg = firstn((r.get(i) for i in msg_key), lambda i: i is not None)
160
166
  if msg:
161
167
  raise QzoneError(err, msg, rdict=r)
162
168
  else:
@@ -167,7 +173,7 @@ class QzoneH5RawAPI:
167
173
  async def index(self) -> StrDict:
168
174
  """This api is the redirect page after h5 login, which is also the landing (main) page of h5 qzone.
169
175
 
170
- :raise RuntimeError: if any failure occurs in data parsing.
176
+ :raise `RuntimeError`: if any failure occurs in data parsing.
171
177
  """
172
178
 
173
179
  @self._relogin_retry
@@ -183,7 +189,7 @@ class QzoneH5RawAPI:
183
189
  raise RuntimeError("script tag not found")
184
190
 
185
191
  texts: List[str] = [s.text for s in scripts]
186
- script = next(filter(lambda s: "shine0callback" in s, texts), None)
192
+ script = firstn(texts, lambda s: "shine0callback" in s)
187
193
  if not script:
188
194
  raise RuntimeError("data script not found")
189
195
 
@@ -201,8 +207,8 @@ class QzoneH5RawAPI:
201
207
  return self._rtext_handler(data, cb=False, errno_key=("code", "ret"), data_key="data")
202
208
 
203
209
  async def get_active_feeds(self, attach_info: str) -> StrDict:
204
- """Get next page. If :obj:`.qzonetoken` is not parsed or `attach_info` is empty,
205
- it will call :meth:`index` and return its response.
210
+ """Get next page. If :obj:`.qzonetoken` is not parsed or :obj:`attach_info` is empty,
211
+ it will call :meth:`.index` and return its response.
206
212
 
207
213
  :param attach_info: The ``attach_info`` field from last call.
208
214
  Pass an empty string equals to call :meth:`.index`.
@@ -233,7 +239,7 @@ class QzoneH5RawAPI:
233
239
  async def shuoshuo(self, fid: str, hostuin: int, appid=311, busi_param: str = ""):
234
240
  """This can be used to get the detailed summary of a feed.
235
241
 
236
- :param fid: aka. cellid
242
+ :param fid: aka. ``cellid``
237
243
  :param hostuin: uin of the owner of the given feed
238
244
  :param appid: appid of the given feed, default as 311
239
245
  :param busi_param: optional encoded params
@@ -0,0 +1,227 @@
1
+ """
2
+ Collect some built-in login manager without persistant cookie.
3
+ Users can inherit these managers and implement their own persistance logic.
4
+
5
+ .. versionchanged:: 0.14.0
6
+
7
+ Removed ``UPLoginMan`` and ``QRLoginMan``. Renamed ``MixedLoginMan`` to :class:`UnifiedLoginManager`.
8
+ For the removed managers, use :class:`.UnifiedLoginManager` instead.
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+ from typing import Dict, List, Optional, Sequence, Union
14
+
15
+ from httpx import ConnectError, HTTPError
16
+ from tylisten.futstore import FutureStore
17
+
18
+ from aioqzone.exception import LoginError, SkipLoginInterrupt
19
+ from aioqzone.message import LoginMethod
20
+ from aioqzone.model import QrLoginConfig, UpLoginConfig
21
+ from qqqr.exception import TencentLoginError, UserBreak
22
+ from qqqr.qr import QrLogin
23
+ from qqqr.up import UpH5Login
24
+ from qqqr.utils.net import ClientAdapter
25
+
26
+ from ._base import Loginable
27
+
28
+ log = logging.getLogger(__name__)
29
+
30
+ __all__ = ["ConstLoginMan", "UnifiedLoginManager", "LoginMethod", "QrLoginConfig", "UpLoginConfig"]
31
+
32
+
33
+ class ConstLoginMan(Loginable):
34
+ """A basic login manager which uses external provided cookie."""
35
+
36
+ def __init__(self, uin: int, cookie: Dict[str, str]) -> None:
37
+ super().__init__(uin)
38
+ self._cookie = cookie
39
+
40
+ @Loginable.cookie.setter
41
+ def cookie(self, v: Dict[str, str]):
42
+ self._cookie = v
43
+
44
+ async def _new_cookie(self) -> Dict[str, str]:
45
+ return self._cookie
46
+
47
+
48
+ class UnifiedLoginManager(Loginable):
49
+ """A login manager that will try methods according to the given :obj:`.order`.
50
+
51
+ .. versionchanged:: 0.14.0
52
+
53
+ Renamed to ``UnifiedLoginManager``.
54
+ """
55
+
56
+ _order: List[LoginMethod]
57
+
58
+ def __init__(
59
+ self,
60
+ client: ClientAdapter,
61
+ up_config: Optional[UpLoginConfig] = None,
62
+ qr_config: Optional[QrLoginConfig] = None,
63
+ *,
64
+ h5=True,
65
+ ) -> None:
66
+ self.up_config = up_config or UpLoginConfig()
67
+ self.qr_config = qr_config or QrLoginConfig()
68
+ super().__init__(self.up_config.uin or self.qr_config.uin)
69
+
70
+ self._order = []
71
+ self.client = client
72
+ self.channel = FutureStore()
73
+
74
+ self.h5(h5, clear_cookie=False) # init uplogin and qrlogin
75
+ self.sms_code_required = self.uplogin.sms_code_required
76
+ self.sms_code_input = self.uplogin.sms_code_input
77
+ if self.up_config.uin > 0:
78
+ self._order.append("up")
79
+
80
+ self.refresh_times = self.qr_config.max_refresh_times
81
+ self.poll_freq = self.qr_config.poll_freq
82
+ self.qr_fetched = self.qrlogin.qr_fetched
83
+ self.qr_cancelled = self.qrlogin.qr_cancelled
84
+ self.cancel_qr = self.qrlogin.cancel
85
+ self.refresh_qr = self.qrlogin.refresh
86
+ if self.qr_config.uin > 0:
87
+ self._order.append("qr")
88
+
89
+ @property
90
+ def order(self):
91
+ """Returns order of :obj:`LoginMethod`. Assign a :obj:`LoginMethod` :obj:`Sequence` to this to
92
+ change the order of :obj:`LoginMethod`."""
93
+ return self._order
94
+
95
+ @order.setter
96
+ def order(self, v: Sequence[LoginMethod]):
97
+ v = list(v)
98
+ if "qr" in v and self.qr_config.uin <= 0:
99
+ raise ValueError(self.qr_config)
100
+ if "up" in v and self.up_config.uin <= 0:
101
+ raise ValueError(self.up_config)
102
+ self._order = v
103
+
104
+ async def _try_up_login(self) -> Union[Dict[str, str], str]:
105
+ """
106
+ :raises:
107
+ Exceptions except for :exc:`TencentLoginError`, :exc:`NotImplementedError`,
108
+ :exc:`GeneratorExit`, :exc:`httpx.ConnectError`, :exc:`httpx.HTTPError`
109
+
110
+ .. versionchanged:: 0.12.9
111
+
112
+ Do not raise :exc:`SystemExit` any more. Any unexpected error will be reraised.
113
+
114
+ :return: cookie dict
115
+ """
116
+ try:
117
+ cookie = await self.uplogin.login()
118
+ except TencentLoginError as e:
119
+ log.warning(e := str(e))
120
+ return e
121
+ except NotImplementedError as e:
122
+ log.warning(str(e))
123
+ return "10009:需要手机验证"
124
+ except (GeneratorExit, ConnectError, HTTPError) as e:
125
+ omit_exc_info = isinstance(e, (GeneratorExit, ConnectError))
126
+ log.warning(f"{type(e).__name__} captured, continue.", exc_info=not omit_exc_info)
127
+ log.debug(e.args, extra=e.__dict__)
128
+ return str(e)
129
+ except:
130
+ log.fatal("密码登录抛出未捕获的异常.", exc_info=True)
131
+ raise
132
+ return "密码登录期间出现奇怪的错误😰请检查日志以便寻求帮助."
133
+
134
+ return cookie
135
+
136
+ async def _try_qr_login(self) -> Union[Dict[str, str], str]:
137
+ """
138
+ :raises:
139
+ Exceptions except for :exc:`UserBreak`, :exc:`KeyboardInterrupt`, :exc:`asyncio.CancelledError`,
140
+ :exc:`asyncio.TimeoutError`, :exc:`GeneratorExit`, :exc:`httpx.ConnectError`, :exc:`httpx.HTTPError`
141
+
142
+ .. versionchanged:: 0.12.9
143
+
144
+ Do not raise :exc:`SystemExit` any more. Any unexpected error will be reraised.
145
+
146
+ :return: cookie dict
147
+ """
148
+
149
+ try:
150
+ cookie = await self.qrlogin.login(
151
+ refresh_times=self.refresh_times, poll_freq=self.poll_freq
152
+ )
153
+ except (UserBreak, KeyboardInterrupt, asyncio.CancelledError) as e:
154
+ return "用户取消了登录"
155
+ except (asyncio.TimeoutError, GeneratorExit, ConnectError, HTTPError) as e:
156
+ omit_exc_info = isinstance(e, (ConnectError, GeneratorExit, asyncio.TimeoutError))
157
+ log.warning(f"{type(e).__name__} captured, continue.", exc_info=not omit_exc_info)
158
+ log.debug(e.args, extra=e.__dict__)
159
+ return str(e)
160
+ except:
161
+ log.fatal("Unexpected error in QR login.", exc_info=True)
162
+ raise
163
+ return "二维码登录期间出现奇怪的错误😰请检查日志以便寻求帮助."
164
+
165
+ return cookie
166
+
167
+ async def _new_cookie(self) -> Dict[str, str]:
168
+ """
169
+ :meta public:
170
+ :raise `aioqzone.exception.SkipLoginInterrupt`: if :obj:`.order` returns an empty list.
171
+ :raise `aioqzone.exception.LoginError`: if all login methods failed.
172
+
173
+ :return: cookie dict
174
+ """
175
+ methods = self.order.copy()
176
+ if not methods:
177
+ log.info("No method selected for this login, raise SkipLoginInterrupt.")
178
+ raise SkipLoginInterrupt
179
+
180
+ log.info(f"Methods selected for this login: {methods}")
181
+ loginables = dict(up=self._try_up_login, qr=self._try_qr_login)
182
+
183
+ reasons: Dict[LoginMethod, str] = {}
184
+ fail_with = lambda meth, msg: self.channel.add_awaitable(
185
+ self.login_failed.emit(uin=self.uin, method=meth, exc=str(msg))
186
+ )
187
+
188
+ for m in methods:
189
+ try:
190
+ result = await loginables[m]()
191
+ except BaseException as e:
192
+ fail_with(m, e)
193
+ break
194
+
195
+ if isinstance(result, str):
196
+ fail_with(m, result)
197
+ reasons[m] = result
198
+ else:
199
+ return result
200
+
201
+ raise LoginError(reasons=reasons)
202
+
203
+ def h5(self, enable=True, clear_cookie=True):
204
+ """Change :obj:`.qrlogin` and :obj:`.uplogin` to h5 login proxy.
205
+
206
+ :param enable: use h5 mode or not
207
+ :param clear_cookie: remove existing login cookie in :obj:`~Loginable.cookie`!
208
+
209
+ .. versionchanged:: 0.14.1
210
+
211
+ Allow user to switch h5 back; Allow to skip clearing cookie.
212
+ """
213
+ if clear_cookie:
214
+ self._cookie.clear()
215
+ self.client.client.cookies.clear()
216
+
217
+ if enable:
218
+ from qqqr.up import UpH5Login as cls
219
+ else:
220
+ from qqqr.up.web import UpWebLogin as cls
221
+ self.uplogin = cls(
222
+ client=self.client,
223
+ uin=self.up_config.uin,
224
+ pwd=self.up_config.pwd.get_secret_value(),
225
+ h5=enable,
226
+ )
227
+ self.qrlogin = QrLogin(client=self.client, h5=enable)
@@ -3,13 +3,14 @@ from abc import ABC, abstractmethod
3
3
  from time import time
4
4
  from typing import Dict
5
5
 
6
+ from tylisten import Emitter
7
+
8
+ import aioqzone.message as MT
6
9
  from qqqr.utils.encrypt import gtk
7
10
 
8
11
 
9
12
  class Loginable(ABC):
10
- """Abstract class represents a login manager.
11
- It is a :class:`Emittable` class which can emit :class:`LoginEvent`.
12
- """
13
+ """Abstract class represents a login manager."""
13
14
 
14
15
  last_login: float = 0
15
16
  """Last login time stamp. 0 represents no login since created."""
@@ -20,12 +21,12 @@ class Loginable(ABC):
20
21
  self._cookie = {}
21
22
  self.lock = asyncio.Lock()
22
23
 
24
+ self.login_success = Emitter(MT.login_success)
25
+ self.login_failed = Emitter(MT.login_failed)
26
+
23
27
  @property
24
28
  def cookie(self) -> Dict[str, str]:
25
- """Get a cookie dict using any method. Allows cached cookie.
26
-
27
- :return: cookie. Cached cookie is preferable.
28
- """
29
+ """Cached cookie."""
29
30
  return self._cookie
30
31
 
31
32
  @abstractmethod
@@ -60,7 +61,7 @@ class Loginable(ABC):
60
61
 
61
62
  @property
62
63
  def gtk(self) -> int:
63
- """Calculate ``gtk`` using ``pskey`` filed in the cookie.
64
+ """Calculate ``gtk`` using ``pskey`` field in the cookie.
64
65
 
65
66
  :return: gtk
66
67
 
@@ -7,8 +7,8 @@ from typing import List, Optional, Type, TypeVar
7
7
 
8
8
  from pydantic import ValidationError
9
9
 
10
- from aioqzone.type.internal import AlbumData
11
- from aioqzone.type.resp import *
10
+ from aioqzone.model import AlbumData
11
+ from aioqzone.model.response.web import *
12
12
 
13
13
  from .raw import QzoneWebRawAPI
14
14
 
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
19
19
 
20
20
  def _parse_obj(model: Type[_model], o: object) -> _model:
21
21
  try:
22
- return model.parse_obj(o)
22
+ return model.model_validate(o)
23
23
  except ValidationError:
24
24
  log.debug(o)
25
25
  raise
@@ -11,13 +11,13 @@ from urllib.parse import parse_qs
11
11
  from httpx import HTTPStatusError
12
12
 
13
13
  import aioqzone.api.web.constant as const
14
- from aioqzone.api.loginman import Loginable
14
+ from aioqzone.api.login import Loginable
15
15
  from aioqzone.exception import CorruptError, QzoneError
16
- from aioqzone.type.internal import AlbumData, LikeData
16
+ from aioqzone.model import AlbumData, LikeData
17
17
  from aioqzone.utils.catch import HTTPStatusErrorDispatch, QzoneErrorDispatch
18
18
  from aioqzone.utils.regex import response_callback
19
19
  from aioqzone.utils.time import time_ms
20
- from qqqr.utils.daug import du
20
+ from qqqr.utils.iter import firstn
21
21
  from qqqr.utils.jsjson import JsonValue, json_loads
22
22
  from qqqr.utils.net import ClientAdapter, raise_for_status
23
23
 
@@ -156,13 +156,13 @@ class QzoneWebRawAPI:
156
156
  r = json_loads(rtext)
157
157
  assert isinstance(r, dict)
158
158
 
159
- err = next(filter(lambda i: i is not None, (r.get(i) for i in errno_key)), None)
159
+ err = firstn((r.get(i) for i in errno_key), lambda i: i is not None)
160
160
  assert err is not None, f"no {errno_key} in {r.keys()}"
161
161
  assert isinstance(err, (int, str))
162
162
  err = int(err)
163
163
 
164
164
  if err != 0:
165
- msg = next(filter(None, (r.get(i) for i in msg_key)), None)
165
+ msg = firstn((r.get(i) for i in msg_key), lambda i: i is not None)
166
166
  if msg:
167
167
  raise QzoneError(err, msg, rdict=r)
168
168
  else:
@@ -238,10 +238,11 @@ class QzoneWebRawAPI:
238
238
  "usertime": time_ms(),
239
239
  "externparam": external,
240
240
  }
241
+ query.update(default)
241
242
 
242
243
  @self._relogin_retry
243
244
  async def retry_closure():
244
- async with self.host_get(const.feeds3_html_more, du(default, query)) as r:
245
+ async with self.host_get(const.feeds3_html_more, query) as r:
245
246
  r.raise_for_status()
246
247
  rtext = r.text
247
248
 
@@ -288,11 +289,12 @@ class QzoneWebRawAPI:
288
289
  "tid": tid,
289
290
  "feedsType": feedstype,
290
291
  }
291
- logger.debug("emotion_getcomments post data:", body)
292
+ logger.debug(f"emotion_getcomments post data: {body}")
293
+ body.update(default)
292
294
 
293
295
  @self._relogin_retry
294
296
  async def retry_closure():
295
- async with self.host_post(const.emotion_getcomments, data=du(default, body)) as r:
297
+ async with self.host_post(const.emotion_getcomments, data=body) as r:
296
298
  r.raise_for_status()
297
299
  rtext = r.text
298
300
 
@@ -326,10 +328,11 @@ class QzoneWebRawAPI:
326
328
  "need_private_comment": 1,
327
329
  }
328
330
  query = {"uin": owner, "tid": fid}
331
+ query.update(default)
329
332
 
330
333
  @self._relogin_retry
331
334
  async def retry_closure():
332
- async with self.host_get(const.emotion_msgdetail, params=du(default, query)) as r:
335
+ async with self.host_get(const.emotion_msgdetail, params=query) as r:
333
336
  r.raise_for_status()
334
337
  return self._rtext_handler(r.text)
335
338
 
@@ -387,12 +390,14 @@ class QzoneWebRawAPI:
387
390
  "abstime": likedata.abstime,
388
391
  "fid": likedata.fid,
389
392
  }
390
- logger.debug("like_app post data:", body)
393
+ logger.debug(f"like_app post data: {body}")
394
+
395
+ body.update(default)
391
396
  url = const.internal_dolike_app if like else const.internal_unlike_app
392
397
 
393
398
  @self._relogin_retry
394
399
  async def retry_closure():
395
- async with self.host_post(url, data=du(default, body)) as r:
400
+ async with self.host_post(url, data=body) as r:
396
401
  r.raise_for_status()
397
402
  return self._rtext_handler(r.text, errno_key=("code", "ret"))
398
403
 
@@ -454,10 +459,11 @@ class QzoneWebRawAPI:
454
459
  "t": randint(int(1e8), int(1e9 - 1))
455
460
  # The distribution is not consistent with photo.js; but the format is.
456
461
  }
462
+ query.update(default)
457
463
 
458
464
  @self._relogin_retry
459
465
  async def retry_closure():
460
- async with self.host_get(const.floatview_photo_list, du(default, query)) as r:
466
+ async with self.host_get(const.floatview_photo_list, query) as r:
461
467
  r.raise_for_status()
462
468
  return self._rtext_handler(r.text)
463
469
 
@@ -506,9 +512,12 @@ class QzoneWebRawAPI:
506
512
  "need_private_comment": 1,
507
513
  }
508
514
 
515
+ if pos:
516
+ param.update(add)
517
+
509
518
  @self._relogin_retry
510
519
  async def retry_closure():
511
- async with self.host_get(const.emotion_msglist, du(param, add) if pos else param) as r:
520
+ async with self.host_get(const.emotion_msglist, param) as r:
512
521
  r.raise_for_status()
513
522
  rtext = r.text
514
523
  return self._rtext_handler(rtext)
@@ -551,11 +560,12 @@ class QzoneWebRawAPI:
551
560
  "feedversion": 1,
552
561
  "hostuin": self.login.uin,
553
562
  }
554
- logger.debug("emotion_publish post data:", body)
563
+ logger.debug(f"emotion_publish post data: {body}")
564
+ body.update(default)
555
565
 
556
566
  @self._relogin_retry
557
567
  async def retry_closure():
558
- async with self.host_post(const.emotion_publish, data=du(default, body)) as r:
568
+ async with self.host_post(const.emotion_publish, data=body) as r:
559
569
  r.raise_for_status()
560
570
  return self._rtext_handler(r.text)
561
571
 
@@ -649,11 +659,12 @@ class QzoneWebRawAPI:
649
659
  "hostuin": uin or self.login.uin,
650
660
  # 'pic_bo': ''
651
661
  }
652
- logger.debug("emotion_update post data:", body)
662
+ logger.debug(f"emotion_update post data: {body}")
663
+ body.update(default)
653
664
 
654
665
  @self._relogin_retry
655
666
  async def retry_closure():
656
- async with self.host_post(const.emotion_update, data=du(default, body)) as r:
667
+ async with self.host_post(const.emotion_update, data=body) as r:
657
668
  r.raise_for_status()
658
669
  return self._rtext_handler(r.text)
659
670
 
@@ -706,11 +717,12 @@ class QzoneWebRawAPI:
706
717
  private=int(is_private),
707
718
  paramstr=1,
708
719
  )
709
- logger.debug("emotion_re_feeds post data:", data)
720
+ logger.debug(f"emotion_re_feeds post data: {data}")
721
+ data.update(default)
710
722
 
711
723
  @self._relogin_retry
712
724
  async def retry_closure():
713
- async with self.host_post(const.emotion_re_feeds, data=du(default, data)) as r:
725
+ async with self.host_post(const.emotion_re_feeds, data=data) as r:
714
726
  r.raise_for_status()
715
727
  return self._rtext_handler(r.text)
716
728