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.
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/PKG-INFO +4 -2
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/pyproject.toml +6 -4
- aioqzone-0.14.1.dev8/src/aioqzone/api/__init__.py +12 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/model.py +2 -2
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/raw.py +11 -7
- aioqzone-0.14.1.dev8/src/aioqzone/api/login/__init__.py +227 -0
- {aioqzone-0.13.0.dev2/src/aioqzone/api/loginman → aioqzone-0.14.1.dev8/src/aioqzone/api/login}/_base.py +9 -8
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/model.py +3 -3
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/raw.py +2 -2
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/exception.py +19 -12
- aioqzone-0.14.1.dev8/src/aioqzone/message.py +26 -0
- {aioqzone-0.13.0.dev2/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model}/__init__.py +2 -3
- aioqzone-0.13.0.dev2/src/aioqzone/type/internal.py → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol/__init__.py +3 -1
- aioqzone-0.14.1.dev8/src/aioqzone/model/protocol/config.py +25 -0
- aioqzone-0.14.1.dev8/src/aioqzone/model/response/__init__.py +1 -0
- {aioqzone-0.13.0.dev2/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/h5.py +12 -12
- {aioqzone-0.13.0.dev2/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/web.py +1 -1
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/entity.py +1 -1
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/html.py +3 -4
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/base.py +11 -5
- aioqzone-0.14.1.dev8/src/qqqr/exception.py +34 -0
- aioqzone-0.14.1.dev8/src/qqqr/message.py +35 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/qr/__init__.py +51 -14
- aioqzone-0.14.1.dev8/src/qqqr/up/__init__.py +4 -0
- aioqzone-0.13.0.dev2/src/qqqr/up/type.py → aioqzone-0.14.1.dev8/src/qqqr/up/_model.py +1 -1
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/__init__.py +5 -5
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/encrypt.py +1 -1
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/h5.py +3 -3
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/web.py +29 -19
- aioqzone-0.13.0.dev2/src/aioqzone/api/__init__.py +0 -5
- aioqzone-0.13.0.dev2/src/aioqzone/api/loginman/__init__.py +0 -333
- aioqzone-0.13.0.dev2/src/aioqzone/event/__init__.py +0 -7
- aioqzone-0.13.0.dev2/src/aioqzone/event/login.py +0 -42
- aioqzone-0.13.0.dev2/src/aioqzone/py.typed +0 -0
- aioqzone-0.13.0.dev2/src/aioqzone/type/resp/__init__.py +0 -1
- aioqzone-0.13.0.dev2/src/qqqr/event/__init__.py +0 -11
- aioqzone-0.13.0.dev2/src/qqqr/event/evt.py +0 -168
- aioqzone-0.13.0.dev2/src/qqqr/event/evtmgr.py +0 -104
- aioqzone-0.13.0.dev2/src/qqqr/event/login.py +0 -42
- aioqzone-0.13.0.dev2/src/qqqr/exception.py +0 -59
- aioqzone-0.13.0.dev2/src/qqqr/ssl.py +0 -30
- aioqzone-0.13.0.dev2/src/qqqr/up/__init__.py +0 -4
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/LICENSE +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/README.md +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/__init__.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/__init__.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/__init__.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/constant.py +0 -0
- {aioqzone-0.13.0.dev2/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol}/entity.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/catch.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/regex.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/time.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/__init__.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/constant.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/py.typed +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/qr/type.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/type.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/jigsaw.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/encrypt.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/iter.py +0 -0
- {aioqzone-0.13.0.dev2 → aioqzone-0.14.1.dev8}/src/qqqr/utils/jsjson.py +0 -0
- {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.
|
|
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 (>=
|
|
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.
|
|
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 = "^
|
|
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 = "
|
|
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 = "*"
|
|
@@ -3,7 +3,7 @@ from typing import Type, TypeVar
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, ValidationError
|
|
5
5
|
|
|
6
|
-
from aioqzone.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
207
|
-
it will call :meth
|
|
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
|
-
"""
|
|
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``
|
|
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.
|
|
11
|
-
from aioqzone.
|
|
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.
|
|
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.
|
|
14
|
+
from aioqzone.api.login import Loginable
|
|
15
15
|
from aioqzone.exception import CorruptError, QzoneError
|
|
16
|
-
from aioqzone.
|
|
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
|
|
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
|
-
|
|
26
|
-
"""
|
|
29
|
+
``methods_tried`` is not optional.
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
methods_tried: Sequence,
|
|
38
|
+
reasons: Mapping[LoginMethod, str],
|
|
35
39
|
) -> None:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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"{
|
|
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,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,
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
|