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.
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/PKG-INFO +4 -2
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/pyproject.toml +13 -12
- aioqzone-0.14.1.dev8/src/aioqzone/api/__init__.py +12 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/model.py +2 -2
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/raw.py +17 -11
- aioqzone-0.14.1.dev8/src/aioqzone/api/login/__init__.py +227 -0
- {aioqzone-0.13.0.dev1/src/aioqzone/api/loginman → aioqzone-0.14.1.dev8/src/aioqzone/api/login}/_base.py +9 -8
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/model.py +3 -3
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/raw.py +31 -19
- {aioqzone-0.13.0.dev1 → 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.dev1/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model}/__init__.py +2 -3
- aioqzone-0.13.0.dev1/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.dev1/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/h5.py +12 -12
- {aioqzone-0.13.0.dev1/src/aioqzone/type/resp → aioqzone-0.14.1.dev8/src/aioqzone/model/response}/web.py +1 -1
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/entity.py +1 -1
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/html.py +10 -10
- {aioqzone-0.13.0.dev1 → 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.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/qr/__init__.py +52 -16
- aioqzone-0.14.1.dev8/src/qqqr/up/__init__.py +4 -0
- aioqzone-0.13.0.dev1/src/qqqr/up/type.py → aioqzone-0.14.1.dev8/src/qqqr/up/_model.py +1 -1
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/__init__.py +8 -10
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/encrypt.py +3 -4
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/h5.py +5 -5
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/web.py +31 -21
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/iter.py +5 -1
- aioqzone-0.13.0.dev1/src/aioqzone/api/__init__.py +0 -5
- aioqzone-0.13.0.dev1/src/aioqzone/api/loginman/__init__.py +0 -333
- aioqzone-0.13.0.dev1/src/aioqzone/event/__init__.py +0 -7
- aioqzone-0.13.0.dev1/src/aioqzone/event/login.py +0 -42
- aioqzone-0.13.0.dev1/src/aioqzone/py.typed +0 -0
- aioqzone-0.13.0.dev1/src/aioqzone/type/resp/__init__.py +0 -1
- aioqzone-0.13.0.dev1/src/qqqr/event/__init__.py +0 -11
- aioqzone-0.13.0.dev1/src/qqqr/event/evt.py +0 -168
- aioqzone-0.13.0.dev1/src/qqqr/event/evtmgr.py +0 -104
- aioqzone-0.13.0.dev1/src/qqqr/event/login.py +0 -42
- aioqzone-0.13.0.dev1/src/qqqr/exception.py +0 -59
- aioqzone-0.13.0.dev1/src/qqqr/ssl.py +0 -30
- aioqzone-0.13.0.dev1/src/qqqr/up/__init__.py +0 -4
- aioqzone-0.13.0.dev1/src/qqqr/utils/daug.py +0 -25
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/LICENSE +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/README.md +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/__init__.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/h5/__init__.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/__init__.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/api/web/constant.py +0 -0
- {aioqzone-0.13.0.dev1/src/aioqzone/type → aioqzone-0.14.1.dev8/src/aioqzone/model/protocol}/entity.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/catch.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/regex.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/aioqzone/utils/time.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/__init__.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/constant.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/py.typed +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/qr/type.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/type.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/up/captcha/jigsaw.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/encrypt.py +0 -0
- {aioqzone-0.13.0.dev1 → aioqzone-0.14.1.dev8}/src/qqqr/utils/jsjson.py +0 -0
- {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.
|
|
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"
|
|
36
|
-
|
|
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
|
|
39
|
-
pillow = { version = "^9.4.0", optional = true
|
|
40
|
-
pychaosvm = { version = "
|
|
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.
|
|
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 = "^
|
|
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
|
-
|
|
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 = "
|
|
75
|
-
|
|
76
|
+
priority = "supplemental"
|
|
76
77
|
|
|
77
78
|
[[tool.poetry.source]]
|
|
78
79
|
name = "PyPI"
|
|
@@ -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,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.
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
205
|
-
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.
|
|
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
|
-
"""
|
|
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,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.
|
|
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
|
|
20
|
-
from qqqr.utils.
|
|
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 =
|
|
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 =
|
|
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,
|
|
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:
|
|
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=
|
|
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=
|
|
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:
|
|
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=
|
|
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,
|
|
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,
|
|
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:
|
|
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=
|
|
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:
|
|
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=
|
|
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:
|
|
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=
|
|
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
|
|