aioqzone 0.13.0.dev2__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 (63) hide show
  1. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/PKG-INFO +4 -2
  2. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/pyproject.toml +6 -4
  3. aioqzone-0.14.1.dev8/src/aioqzone/api/__init__.py +12 -0
  4. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/model.py +2 -2
  5. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/raw.py +11 -7
  6. aioqzone-0.14.1.dev8/src/aioqzone/api/login/__init__.py +227 -0
  7. {aioqzone-0.13.0.dev2/src/aioqzone/api/loginman → aioqzone-0.14.1.dev8/src/aioqzone/api/login}/_base.py +9 -8
  8. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/model.py +3 -3
  9. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/raw.py +2 -2
  10. {aioqzone-0.13.0.dev2 → 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.dev2/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model}/__init__.py +2 -3
  13. aioqzone-0.13.0.dev2/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.dev2/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/h5.py +12 -12
  17. {aioqzone-0.13.0.dev2/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/web.py +1 -1
  18. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/entity.py +1 -1
  19. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/html.py +3 -4
  20. {aioqzone-0.13.0.dev2 → 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.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/qr/__init__.py +51 -14
  24. aioqzone-0.14.1.dev8/src/qqqr/up/__init__.py +4 -0
  25. aioqzone-0.13.0.dev2/src/qqqr/up/type.py → aioqzone-0.14.1.dev8/src/qqqr/up/_model.py +1 -1
  26. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/__init__.py +5 -5
  27. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/encrypt.py +1 -1
  28. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/h5.py +3 -3
  29. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/web.py +29 -19
  30. aioqzone-0.13.0.dev2/src/aioqzone/api/__init__.py +0 -5
  31. aioqzone-0.13.0.dev2/src/aioqzone/api/loginman/__init__.py +0 -333
  32. aioqzone-0.13.0.dev2/src/aioqzone/event/__init__.py +0 -7
  33. aioqzone-0.13.0.dev2/src/aioqzone/event/login.py +0 -42
  34. aioqzone-0.13.0.dev2/src/aioqzone/py.typed +0 -0
  35. aioqzone-0.13.0.dev2/src/aioqzone/type/resp/__init__.py +0 -1
  36. aioqzone-0.13.0.dev2/src/qqqr/event/__init__.py +0 -11
  37. aioqzone-0.13.0.dev2/src/qqqr/event/evt.py +0 -168
  38. aioqzone-0.13.0.dev2/src/qqqr/event/evtmgr.py +0 -104
  39. aioqzone-0.13.0.dev2/src/qqqr/event/login.py +0 -42
  40. aioqzone-0.13.0.dev2/src/qqqr/exception.py +0 -59
  41. aioqzone-0.13.0.dev2/src/qqqr/ssl.py +0 -30
  42. aioqzone-0.13.0.dev2/src/qqqr/up/__init__.py +0 -4
  43. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/LICENSE +0 -0
  44. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/README.md +0 -0
  45. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/__init__.py +0 -0
  46. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/__init__.py +0 -0
  47. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/__init__.py +0 -0
  48. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/constant.py +0 -0
  49. {aioqzone-0.13.0.dev2/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol}/entity.py +0 -0
  50. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/__init__.py +0 -0
  51. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/catch.py +0 -0
  52. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/regex.py +0 -0
  53. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/time.py +0 -0
  54. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/__init__.py +0 -0
  55. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/constant.py +0 -0
  56. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/py.typed +0 -0
  57. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/qr/type.py +0 -0
  58. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/type.py +0 -0
  59. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/jigsaw.py +0 -0
  60. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/encrypt.py +0 -0
  61. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/iter.py +0 -0
  62. {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/jsjson.py +0 -0
  63. {aioqzone-0.13.0.dev2 → 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.dev2
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.dev2"
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"
37
+ tylisten = { version = "~0.5.1", source = "aioqzone-index" }
36
38
  exceptiongroup = { version = "^1.1.1", python = "<3.11"}
37
39
 
38
40
  numpy = { version = "^1.22.3", optional = true}
39
41
  pillow = { version = "^9.4.0", optional = true}
40
- pychaosvm = { version = "^0.2.3", optional = true, source = "aioqzone-index" }
42
+ pychaosvm = { version = "~0.2.3", optional = true, source = "aioqzone-index" }
41
43
 
42
44
  [tool.poetry.extras]
43
45
  captcha = ["numpy", "pillow", "pychaosvm"]
@@ -63,7 +65,7 @@ optional = true
63
65
 
64
66
  [tool.poetry.group.docs.dependencies]
65
67
  Sphinx = "^7.0.1"
66
- autodoc-pydantic = "*"
68
+ autodoc-pydantic = "^2.0.1" # autodoc-pydantic/#146 #162
67
69
  sphinx-autodoc-typehints = "^1.19.5"
68
70
  furo = "*"
69
71
  sphinx-intl = "*"
@@ -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,7 +5,7 @@ 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
@@ -99,7 +99,11 @@ class QzoneH5RawAPI:
99
99
  The exceptions this wrapper may raise depends on the login manager you passed in.
100
100
  Any exceptions irrelevent to "login needed" will be passed through w/o any change.
101
101
 
102
- :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.
103
107
  """
104
108
 
105
109
  with QzoneErrorDispatch() as qze, HTTPStatusErrorDispatch() as hse:
@@ -145,7 +149,7 @@ class QzoneH5RawAPI:
145
149
  if cb:
146
150
  match = response_callback.search(robj)
147
151
  assert match
148
- robj = match.group(1)
152
+ robj = str(match.group(1))
149
153
  r = json_loads(robj)
150
154
  else:
151
155
  r = robj
@@ -169,7 +173,7 @@ class QzoneH5RawAPI:
169
173
  async def index(self) -> StrDict:
170
174
  """This api is the redirect page after h5 login, which is also the landing (main) page of h5 qzone.
171
175
 
172
- :raise RuntimeError: if any failure occurs in data parsing.
176
+ :raise `RuntimeError`: if any failure occurs in data parsing.
173
177
  """
174
178
 
175
179
  @self._relogin_retry
@@ -203,8 +207,8 @@ class QzoneH5RawAPI:
203
207
  return self._rtext_handler(data, cb=False, errno_key=("code", "ret"), data_key="data")
204
208
 
205
209
  async def get_active_feeds(self, attach_info: str) -> StrDict:
206
- """Get next page. If :obj:`.qzonetoken` is not parsed or `attach_info` is empty,
207
- 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.
208
212
 
209
213
  :param attach_info: The ``attach_info`` field from last call.
210
214
  Pass an empty string equals to call :meth:`.index`.
@@ -235,7 +239,7 @@ class QzoneH5RawAPI:
235
239
  async def shuoshuo(self, fid: str, hostuin: int, appid=311, busi_param: str = ""):
236
240
  """This can be used to get the detailed summary of a feed.
237
241
 
238
- :param fid: aka. cellid
242
+ :param fid: aka. ``cellid``
239
243
  :param hostuin: uin of the owner of the given feed
240
244
  :param appid: appid of the given feed, default as 311
241
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,9 +11,9 @@ 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
@@ -1,4 +1,8 @@
1
- from typing import Sequence
1
+ from typing import Mapping
2
+
3
+ from aioqzone.model import LoginMethod
4
+
5
+ _meth_name = dict(up="密码登录", qr="二维码登录") # type: dict[LoginMethod, str]
2
6
 
3
7
 
4
8
  class QzoneError(RuntimeError):
@@ -22,24 +26,27 @@ class LoginError(RuntimeError):
22
26
 
23
27
  .. versionchanged:: 0.12.9
24
28
 
25
- :.obj:`.methods_tried` is not optional.
26
- """
29
+ ``methods_tried`` is not optional.
27
30
 
28
- methods_tried: tuple
29
- """Login methods that have been tried in this login."""
31
+ .. versionchanged:: 0.14.1
32
+
33
+ ``msg`` and ``methods_tried`` is merged to a single parameter :obj:`reasons`.
34
+ """
30
35
 
31
36
  def __init__(
32
37
  self,
33
- msg: str,
34
- methods_tried: Sequence,
38
+ reasons: Mapping[LoginMethod, str],
35
39
  ) -> None:
36
- msg = "登陆失败: " + msg
37
- super().__init__(msg, methods_tried)
38
- self.msg = msg
39
- self.methods_tried = tuple(methods_tried or ())
40
+ super().__init__(reasons)
41
+ self.reasons = reasons
42
+
43
+ @property
44
+ def methods_tried(self):
45
+ """Login methods that have been tried in this login."""
46
+ return tuple(self.reasons.keys())
40
47
 
41
48
  def __str__(self) -> str:
42
- return f"{self.msg} (tried={self.methods_tried})"
49
+ return ";".join(f"{_meth_name[k]}:{v}" for k, v in self.reasons.items())
43
50
 
44
51
 
45
52
  class CorruptError(ValueError):
@@ -0,0 +1,26 @@
1
+ from pydantic import BaseModel
2
+ from tylisten import BaseMessage
3
+
4
+ from aioqzone.model import LoginMethod
5
+ from qqqr.message import *
6
+
7
+ __all__ = [
8
+ "qr_cancelled",
9
+ "qr_fetched",
10
+ "qr_refresh",
11
+ "sms_code_input",
12
+ "sms_code_required",
13
+ "login_success",
14
+ "login_failed",
15
+ ]
16
+
17
+
18
+ class login_success(BaseModel, BaseMessage):
19
+ uin: int
20
+ method: LoginMethod
21
+
22
+
23
+ class login_failed(BaseModel, BaseMessage):
24
+ uin: int
25
+ method: LoginMethod
26
+ exc: str
@@ -1,6 +1,5 @@
1
1
  """aioqzone typing system. Aims at making full use of pydantic type-validation
2
2
  and python typing support after py36."""
3
3
 
4
- from .entity import *
5
- from .internal import *
6
- from .resp import *
4
+ from .protocol import *
5
+ from .response import *
@@ -1,9 +1,11 @@
1
1
  """This module defines types that are used internally by aioqzone and its plugins.
2
2
  These types are not designed to represent responses from Qzone.
3
3
  """
4
-
5
4
  from pydantic import BaseModel
6
5
 
6
+ from .config import *
7
+ from .entity import *
8
+
7
9
 
8
10
  class PersudoCurkey(str):
9
11
  def __new__(cls, uin: int, abstime: int):
@@ -0,0 +1,25 @@
1
+ from typing import Literal, Union
2
+
3
+ from pydantic import Field, SecretStr
4
+ from pydantic_settings import BaseSettings
5
+
6
+ LoginMethod = Union[Literal["qr"], Literal["up"]]
7
+
8
+
9
+ class LoginConfig(BaseSettings):
10
+ uin: int = 0
11
+ """Login uin (qq)."""
12
+ min_login_interval: float = 1800
13
+ """Minimum login interval, in second."""
14
+
15
+
16
+ class UpLoginConfig(LoginConfig):
17
+ pwd: SecretStr = Field(default="")
18
+ """User password."""
19
+
20
+
21
+ class QrLoginConfig(LoginConfig):
22
+ max_refresh_times: int = 6
23
+ """Maximum QR code refresh times."""
24
+ poll_freq: float = 3
25
+ """QR status polling interval."""
@@ -0,0 +1 @@
1
+ from .h5 import *
@@ -1,6 +1,6 @@
1
1
  from typing import List, Optional, Set, Union
2
2
 
3
- from pydantic import BaseModel, Field, root_validator
3
+ from pydantic import BaseModel, Field, model_validator
4
4
  from pydantic.networks import HttpUrl
5
5
 
6
6
 
@@ -38,7 +38,7 @@ class UserInfo(BaseModel):
38
38
  nickname: str
39
39
  uin: int
40
40
 
41
- @root_validator(pre=True)
41
+ @model_validator(mode="before")
42
42
  def unpack_user(cls, v: dict):
43
43
  if "user" in v:
44
44
  return v["user"]
@@ -53,7 +53,7 @@ class FeedSummary(BaseModel):
53
53
  summary: str = ""
54
54
  hasmore: bool = False
55
55
 
56
- @root_validator(pre=True)
56
+ @model_validator(mode="before")
57
57
  def add_hasmore(cls, v: dict):
58
58
  if "hasmore" not in v:
59
59
  if len(v.get("summary", "")) >= 499:
@@ -91,7 +91,7 @@ class PhotoUrl(BaseModel):
91
91
  class PhotoUrls(BaseModel):
92
92
  urls: Set[PhotoUrl]
93
93
 
94
- @root_validator(pre=True)
94
+ @model_validator(mode="before")
95
95
  def unpack_urls(cls, v: dict):
96
96
  return dict(urls=list(v.values()))
97
97
 
@@ -129,7 +129,7 @@ class PicData(BaseModel):
129
129
  origin_width: int
130
130
  origin_phototype: int = 0
131
131
 
132
- @root_validator(pre=True)
132
+ @model_validator(mode="before")
133
133
  def remove_useless_data(cls, v: dict):
134
134
  if "videodata" in v:
135
135
  if not v["videodata"]["videourl"]:
@@ -181,8 +181,8 @@ class HasSummary(BaseModel):
181
181
 
182
182
 
183
183
  class HasMedia(BaseModel):
184
- pic: Optional[FeedPic]
185
- video: Optional[FeedVideo]
184
+ pic: Optional[FeedPic] = None
185
+ video: Optional[FeedVideo] = None
186
186
 
187
187
 
188
188
  class HasFid(BaseModel):
@@ -198,7 +198,7 @@ class ShareInfo(BaseModel):
198
198
  title: str = ""
199
199
  photourl: Optional[PhotoUrls] = None
200
200
 
201
- @root_validator(pre=True)
201
+ @model_validator(mode="before")
202
202
  def remove_empty_photourl(cls, v: dict):
203
203
  if not v.get("photourl"):
204
204
  v["photourl"] = None
@@ -212,7 +212,7 @@ class Share(HasCommon):
212
212
  class FeedOriginal(HasFid, HasCommon, HasUserInfo, HasSummary, HasMedia):
213
213
  common: FeedCommon = Field(alias="comm")
214
214
 
215
- @root_validator(pre=True)
215
+ @model_validator(mode="before")
216
216
  def remove_prefix(cls, v: dict):
217
217
  return {k[5:] if str.startswith(k, "cell_") else k: i for k, i in v.items()}
218
218
 
@@ -222,10 +222,10 @@ class FeedData(HasFid, HasCommon, HasSummary, HasMedia, HasUserInfo):
222
222
  like: LikeInfo = Field(default_factory=LikeInfo)
223
223
 
224
224
  comment: FeedComment = Field(default_factory=FeedComment)
225
- original: Union[FeedOriginal, Share, None]
225
+ original: Union[FeedOriginal, Share, None] = None
226
226
  share_info: ShareInfo = Field(default_factory=ShareInfo)
227
227
 
228
- @root_validator(pre=True)
228
+ @model_validator(mode="before")
229
229
  def unpack_share_info(cls, v: dict):
230
230
  if "operation" in v:
231
231
  v["share_info"] = v["operation"].get("share_info", {})
@@ -236,7 +236,7 @@ class GetMoreResp(FeedData):
236
236
  hasmore: bool = False
237
237
  attach_info: str = ""
238
238
 
239
- @root_validator(pre=True)
239
+ @model_validator(mode="before")
240
240
  def remove_prefix(cls, v: dict):
241
241
  return {k[5:] if str.startswith(k, "cell_") else k: i for k, i in v.items()}
242
242