aioqzone 1.9.7.dev1__tar.gz → 1.9.8.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/PKG-INFO +2 -2
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/pyproject.toml +2 -2
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/api/__init__.py +6 -0
- aioqzone-1.9.8.dev1/src/aioqzone/api/h5/__init__.py +8 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/api/h5/model.py +46 -4
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/api/login/__init__.py +38 -6
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/api/login/_base.py +23 -4
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/exception.py +7 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/message.py +6 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/__init__.py +44 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/feed.py +7 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/request.py +16 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/response.py +7 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/__init__.py +10 -4
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/config.py +7 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/entity.py +4 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/entity.py +17 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/regex.py +7 -1
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/retry.py +28 -1
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/time.py +8 -1
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/base.py +27 -1
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/exception.py +7 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/message.py +6 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/qr/__init__.py +11 -0
- aioqzone-1.9.8.dev1/src/qqqr/qr/type.py +37 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/type.py +6 -0
- aioqzone-1.9.8.dev1/src/qqqr/up/__init__.py +9 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/_model.py +18 -5
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/__init__.py +9 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/_model.py +21 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/capsess.py +40 -3
- aioqzone-1.9.8.dev1/src/qqqr/up/captcha/click/__init__.py +8 -0
- aioqzone-1.9.8.dev1/src/qqqr/up/captcha/click/_types.py +21 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/pil_utils.py +8 -0
- aioqzone-1.9.8.dev1/src/qqqr/up/captcha/select/__init__.py +8 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/select/_types.py +31 -1
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/slide/__init__.py +5 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/slide/_types.py +19 -6
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/h5.py +17 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/web.py +31 -1
- aioqzone-1.9.8.dev1/src/qqqr/utils/encrypt.py +26 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/utils/iter.py +17 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/utils/net.py +7 -0
- aioqzone-1.9.7.dev1/src/aioqzone/api/h5/__init__.py +0 -3
- aioqzone-1.9.7.dev1/src/qqqr/qr/type.py +0 -24
- aioqzone-1.9.7.dev1/src/qqqr/up/__init__.py +0 -4
- aioqzone-1.9.7.dev1/src/qqqr/up/captcha/click/__init__.py +0 -3
- aioqzone-1.9.7.dev1/src/qqqr/up/captcha/click/_types.py +0 -8
- aioqzone-1.9.7.dev1/src/qqqr/up/captcha/select/__init__.py +0 -3
- aioqzone-1.9.7.dev1/src/qqqr/utils/encrypt.py +0 -9
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/README.md +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/__init__.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/__init__.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/profile.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/__init__.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/constant.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/py.typed +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/up/encrypt.py +0 -0
- {aioqzone-1.9.7.dev1 → aioqzone-1.9.8.dev1}/src/qqqr/utils/jsjson.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aioqzone
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.8.dev1
|
|
4
4
|
Summary: A Python wrapper for Qzone login and H5 APIs.
|
|
5
5
|
Keywords: qzone-api,autologin,asyncio-spider
|
|
6
6
|
Author: aioqzone
|
|
@@ -9,7 +9,7 @@ Classifier: Development Status :: 4 - Beta
|
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
10
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
11
|
Classifier: Typing :: Typed
|
|
12
|
-
Requires-Dist: aiohttp~=3.
|
|
12
|
+
Requires-Dist: aiohttp~=3.14
|
|
13
13
|
Requires-Dist: pydantic~=2.12
|
|
14
14
|
Requires-Dist: pydantic-settings>=2.9.0
|
|
15
15
|
Requires-Dist: rsa>=4.9.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aioqzone"
|
|
3
|
-
version = "1.9.
|
|
3
|
+
version = "1.9.8.dev1"
|
|
4
4
|
description = "A Python wrapper for Qzone login and H5 APIs."
|
|
5
5
|
authors = [{ name = "aioqzone" }]
|
|
6
6
|
maintainers = [
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
dependencies = [
|
|
21
|
-
"aiohttp~=3.
|
|
21
|
+
"aiohttp~=3.14",
|
|
22
22
|
"pydantic~=2.12",
|
|
23
23
|
"pydantic-settings>=2.9.0",
|
|
24
24
|
"rsa>=4.9.1",
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""Qzone H5 API client.
|
|
2
|
+
|
|
3
|
+
Defines :class:`QzoneH5API` — the main API entry point for all Qzone H5
|
|
4
|
+
operations including feed fetching, commenting, liking, and photo upload.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import logging
|
|
2
8
|
import typing as t
|
|
3
9
|
from os import PathLike
|
|
@@ -14,12 +20,22 @@ log = logging.getLogger(__name__)
|
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class QzoneH5API:
|
|
23
|
+
"""Main Qzone H5 API client.
|
|
24
|
+
|
|
25
|
+
Wraps Qzone HTTP endpoints behind typed API methods. Handles login
|
|
26
|
+
state, automatic token injection, and retry-on-login-expire logic.
|
|
27
|
+
All methods delegate to :meth:`call` for actual HTTP dispatch.
|
|
28
|
+
"""
|
|
29
|
+
|
|
17
30
|
qzone_tokens: t.Dict[int, str]
|
|
31
|
+
"""Cached qzonetokens keyed by host uin."""
|
|
18
32
|
|
|
19
33
|
def __init__(
|
|
20
34
|
self, client: ClientAdapter, loginman: Loginable, *, retry_if_login_expire: bool = True
|
|
21
35
|
) -> None:
|
|
22
36
|
"""
|
|
37
|
+
:param client: network client
|
|
38
|
+
:param loginman: login manager
|
|
23
39
|
:param retry_if_login_expire: if we should retry if login expired.
|
|
24
40
|
"""
|
|
25
41
|
super().__init__()
|
|
@@ -47,6 +63,16 @@ class QzoneH5API:
|
|
|
47
63
|
self.qzone_tokens = {}
|
|
48
64
|
|
|
49
65
|
async def call(self, api: QzoneApi[TyRequest, TyResponse]) -> TyResponse:
|
|
66
|
+
"""Execute an API call with full lifecycle management.
|
|
67
|
+
|
|
68
|
+
Builds params, injects authentication (g_tk, qzonetoken, cookies),
|
|
69
|
+
dispatches HTTP request, parses response, and handles retry on
|
|
70
|
+
login expiry.
|
|
71
|
+
|
|
72
|
+
:param api: the API definition to call
|
|
73
|
+
:returns: typed response object
|
|
74
|
+
:raises RuntimeError: if max retry exceeded
|
|
75
|
+
"""
|
|
50
76
|
params: t.Dict[str, t.Any] = api.params.build_params(self.login.uin)
|
|
51
77
|
if api.http_method == "GET":
|
|
52
78
|
data = {}
|
|
@@ -97,7 +123,7 @@ class QzoneH5API:
|
|
|
97
123
|
async def index(self) -> IndexPageResp:
|
|
98
124
|
"""This api is the redirect page after h5 login, which is also the landing (main) page of h5 qzone.
|
|
99
125
|
|
|
100
|
-
:raise `RuntimeError`: if
|
|
126
|
+
:raise `RuntimeError`: if max retry exceeded.
|
|
101
127
|
"""
|
|
102
128
|
|
|
103
129
|
r = await self.call(IndexPageApi())
|
|
@@ -122,7 +148,7 @@ class QzoneH5API:
|
|
|
122
148
|
|
|
123
149
|
:param attach_info: The ``attach_info`` field from last call.
|
|
124
150
|
Pass an empty string equals to call :meth:`.index`.
|
|
125
|
-
:return: If success, the
|
|
151
|
+
:return: If success, the response.
|
|
126
152
|
"""
|
|
127
153
|
if not self.qzone_tokens.get(self.login.uin) or not attach_info:
|
|
128
154
|
return await self.index()
|
|
@@ -137,7 +163,7 @@ class QzoneH5API:
|
|
|
137
163
|
:param hostuin: uin of the user
|
|
138
164
|
:param attach_info: The ``attach_info`` field from last call.
|
|
139
165
|
Pass an empty string equals to call :meth:`.index`.
|
|
140
|
-
:return: If success, the
|
|
166
|
+
:return: If success, the response.
|
|
141
167
|
"""
|
|
142
168
|
if not self.qzone_tokens.get(hostuin) or not attach_info:
|
|
143
169
|
return (await self.profile(hostuin)).feedpage
|
|
@@ -213,6 +239,7 @@ class QzoneH5API:
|
|
|
213
239
|
:param content: comment content
|
|
214
240
|
:param photos: photos to be attached, usually returned by :meth:`.preupload_photos`
|
|
215
241
|
:param busi_param: optional encoded params from :obj:`FeedOperation.operation.busi_param`
|
|
242
|
+
:param feedsType: feed type
|
|
216
243
|
:param abstime: required if `appid != 311`
|
|
217
244
|
:param private: is private comment
|
|
218
245
|
|
|
@@ -265,6 +292,8 @@ class QzoneH5API:
|
|
|
265
292
|
photos: t.Optional[t.Sequence[t.Union[PhotoData, PicInfo]]] = None,
|
|
266
293
|
sync_weibo=False,
|
|
267
294
|
ugc_right: UgcRight = UgcRight.all,
|
|
295
|
+
allow_uins: t.Optional[t.Sequence[int]] = None,
|
|
296
|
+
deny_uins: t.Optional[t.Sequence[int]] = None,
|
|
268
297
|
) -> PublishMoodResp:
|
|
269
298
|
"""Publish a feed.
|
|
270
299
|
|
|
@@ -272,12 +301,18 @@ class QzoneH5API:
|
|
|
272
301
|
:param photos: photos to be attached, usually returned by :meth:`.preupload_photos`
|
|
273
302
|
:param sync_weibo: sync to weibo, default to false
|
|
274
303
|
:param ugc_right: access right, default to "Available to Everyone".
|
|
304
|
+
:param allow_uins: when ``ugc_right=part``, only these QQ users can see this feed.
|
|
305
|
+
:param deny_uins: when ``ugc_right=blacklist``, these QQ users cannot see this feed.
|
|
275
306
|
|
|
276
307
|
.. seealso:: :meth:`.preupload_photos`, :meth:`.upload_pic`
|
|
277
308
|
"""
|
|
278
309
|
photos = photos or []
|
|
310
|
+
kw = dict(locals())
|
|
311
|
+
kw.pop("self")
|
|
312
|
+
kw["allow_uins"] = kw.get("allow_uins") or []
|
|
313
|
+
kw["deny_uins"] = kw.get("deny_uins") or []
|
|
279
314
|
return await self.call(
|
|
280
|
-
PublishMoodApi(params=PublishMoodParams.model_validate(
|
|
315
|
+
PublishMoodApi(params=PublishMoodParams.model_validate(kw, from_attributes=True))
|
|
281
316
|
)
|
|
282
317
|
|
|
283
318
|
async def upload_pic(
|
|
@@ -288,6 +323,11 @@ class QzoneH5API:
|
|
|
288
323
|
quality: t.Union[int, float] = 70,
|
|
289
324
|
) -> UploadPicResponse:
|
|
290
325
|
"""
|
|
326
|
+
:param picture: image data or path
|
|
327
|
+
:param width: image width, if provided with height, the image will not be compressed
|
|
328
|
+
:param height: image height, if provided with width, the image will not be compressed
|
|
329
|
+
:param quality: compression quality, default to 70
|
|
330
|
+
|
|
291
331
|
.. versionchanged:: 1.8.5
|
|
292
332
|
|
|
293
333
|
In version <= 1.8.4, user is responsible for compressing a image and this api
|
|
@@ -324,6 +364,8 @@ class QzoneH5API:
|
|
|
324
364
|
"""Preupload photos before publishing a feed.
|
|
325
365
|
|
|
326
366
|
:param upload_pics: List of :obj:`.UploadPicResponse`, usually returned by :meth:`.upload_pic`
|
|
367
|
+
:param cur_num: current photo number, default to 0
|
|
368
|
+
:param upload_hd: whether to upload HD version, default to False
|
|
327
369
|
|
|
328
370
|
.. seealso:: :meth:`.upload_pic`
|
|
329
371
|
"""
|
|
@@ -42,6 +42,12 @@ class ConstLoginMan(Loginable):
|
|
|
42
42
|
*,
|
|
43
43
|
ch_login_notify: t.Optional[FutureStore] = None,
|
|
44
44
|
) -> None:
|
|
45
|
+
"""Initialize with a pre-existing cookie dict.
|
|
46
|
+
|
|
47
|
+
:param uin: QQ number
|
|
48
|
+
:param cookie: pre-existing cookie dict
|
|
49
|
+
:param ch_login_notify: whether to enable login event emission
|
|
50
|
+
"""
|
|
45
51
|
super().__init__(uin, ch_login_notify=ch_login_notify)
|
|
46
52
|
self.cookie = {} if cookie is None else cookie
|
|
47
53
|
|
|
@@ -63,17 +69,30 @@ class ConstLoginMan(Loginable):
|
|
|
63
69
|
|
|
64
70
|
|
|
65
71
|
class UpLoginManager(Loginable):
|
|
72
|
+
"""Login manager using uin-password authentication.
|
|
73
|
+
|
|
74
|
+
Wraps :class:`~qqqr.up.UpWebLogin` (or :class:`~qqqr.up.UpH5Login`)
|
|
75
|
+
and provides cookie refresh, captcha/SMS handling via hooks.
|
|
76
|
+
"""
|
|
77
|
+
|
|
66
78
|
def __init__(
|
|
67
79
|
self,
|
|
68
80
|
client: ClientAdapter,
|
|
69
81
|
config: UpLoginConfig,
|
|
70
82
|
*,
|
|
71
|
-
h5=True,
|
|
83
|
+
h5: bool = True,
|
|
72
84
|
ch_login_notify: t.Optional[FutureStore] = None,
|
|
73
85
|
) -> None:
|
|
86
|
+
"""Initialize UP (uin-password) login manager.
|
|
87
|
+
|
|
88
|
+
:param client: HTTP client
|
|
89
|
+
:param config: UP login config including uin and password
|
|
90
|
+
:param h5: use H5 endpoint (default True)
|
|
91
|
+
:param ch_login_notify: enable login event emission
|
|
92
|
+
"""
|
|
74
93
|
super().__init__(config.uin, ch_login_notify=ch_login_notify)
|
|
75
|
-
self.client = client
|
|
76
|
-
self.config = config
|
|
94
|
+
self.client = client #: HTTP client
|
|
95
|
+
self.config = config #: Login config instance
|
|
77
96
|
self.h5(h5, clear_cookie=False) # init uplogin
|
|
78
97
|
|
|
79
98
|
@retry(
|
|
@@ -143,17 +162,30 @@ class UpLoginManager(Loginable):
|
|
|
143
162
|
|
|
144
163
|
|
|
145
164
|
class QrLoginManager(Loginable):
|
|
165
|
+
"""Login manager using QR code authentication.
|
|
166
|
+
|
|
167
|
+
Wraps :class:`~qqqr.qr.QrLogin` and emits QR image via the
|
|
168
|
+
``qr_fetched`` hook for downstream display.
|
|
169
|
+
"""
|
|
170
|
+
|
|
146
171
|
def __init__(
|
|
147
172
|
self,
|
|
148
173
|
client: ClientAdapter,
|
|
149
174
|
config: QrLoginConfig,
|
|
150
175
|
*,
|
|
151
|
-
h5=True,
|
|
176
|
+
h5: bool = True,
|
|
152
177
|
ch_login_notify: t.Optional[FutureStore] = None,
|
|
153
178
|
) -> None:
|
|
179
|
+
"""Initialize QR code login manager.
|
|
180
|
+
|
|
181
|
+
:param client: HTTP client
|
|
182
|
+
:param config: QR login config including uin
|
|
183
|
+
:param h5: use H5 endpoint (default True)
|
|
184
|
+
:param ch_login_notify: enable login event emission
|
|
185
|
+
"""
|
|
154
186
|
super().__init__(config.uin, ch_login_notify=ch_login_notify)
|
|
155
|
-
self.client = client
|
|
156
|
-
self.config = config
|
|
187
|
+
self.client = client #: HTTP client
|
|
188
|
+
self.config = config #: Login config instance
|
|
157
189
|
self.h5(h5, clear_cookie=False) # init uplogin
|
|
158
190
|
|
|
159
191
|
async def _new_cookie(self) -> t.Dict[str, str]:
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Abstract login manager base.
|
|
2
|
+
|
|
3
|
+
Defines :class:`Loginable` ABC — the interface all login managers
|
|
4
|
+
implement. Provides cookie caching, gtk computation, and login event
|
|
5
|
+
emission.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import asyncio
|
|
2
9
|
from abc import ABC, abstractmethod
|
|
3
10
|
from time import time
|
|
@@ -10,7 +17,17 @@ from qqqr.utils.encrypt import gtk
|
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
class Loginable(ABC):
|
|
13
|
-
"""Abstract
|
|
20
|
+
"""Abstract login manager interface.
|
|
21
|
+
|
|
22
|
+
Subclasses implement :meth:`_new_cookie` to provide the actual login
|
|
23
|
+
flow (QR, UP, or static cookie). This base class handles cookie
|
|
24
|
+
caching, gtk computation, and login event emission.
|
|
25
|
+
|
|
26
|
+
.. seealso::
|
|
27
|
+
:class:`~aioqzone.api.login.ConstLoginMan`,
|
|
28
|
+
:class:`~aioqzone.api.login.UpLoginManager`,
|
|
29
|
+
:class:`~aioqzone.api.login.QrLoginManager`
|
|
30
|
+
"""
|
|
14
31
|
|
|
15
32
|
last_login: float = 0
|
|
16
33
|
"""Last login time stamp. 0 represents no login since created."""
|
|
@@ -21,13 +38,15 @@ class Loginable(ABC):
|
|
|
21
38
|
|
|
22
39
|
def __init__(self, uin: int, ch_login_notify: Optional[FutureStore] = None) -> None:
|
|
23
40
|
super().__init__()
|
|
24
|
-
self.uin = uin
|
|
41
|
+
self.uin = uin #: Login QQ number
|
|
25
42
|
self.cookie: Dict[str, str] = {}
|
|
26
|
-
self.lock = asyncio.Lock()
|
|
43
|
+
self.lock = asyncio.Lock() #: Lock to serialize concurrent cookie refresh attempts
|
|
27
44
|
self.ch_login_notify = ch_login_notify or FutureStore()
|
|
28
45
|
|
|
29
46
|
self.login_success = MT.login_success()
|
|
47
|
+
"""Emitted after a successful login."""
|
|
30
48
|
self.login_failed = MT.login_failed()
|
|
49
|
+
"""Emitted after a failed login attempt."""
|
|
31
50
|
|
|
32
51
|
@abstractmethod
|
|
33
52
|
async def _new_cookie(self) -> Dict[str, str]:
|
|
@@ -46,7 +65,7 @@ class Loginable(ABC):
|
|
|
46
65
|
an actual login at the same time, other requests will block until the first is complete
|
|
47
66
|
and share the cookie from this single login.
|
|
48
67
|
|
|
49
|
-
:return:
|
|
68
|
+
:return: True if login succeeded, False otherwise.
|
|
50
69
|
"""
|
|
51
70
|
if self.lock.locked():
|
|
52
71
|
last_gtk = self.gtk
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""aioqzone exception hierarchy.
|
|
2
|
+
|
|
3
|
+
Defines :class:`QzoneError` (HTTP OK but Qzone returned an error code),
|
|
4
|
+
:class:`UnexpectedLoginError`, and :class:`CorruptError`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
class QzoneError(RuntimeError):
|
|
2
9
|
"""HTTP OK, but Qzone returns an error code."""
|
|
3
10
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
"""Qzone API endpoint definitions.
|
|
2
|
+
|
|
3
|
+
Each class in this module defines a Qzone H5 API endpoint — its URL,
|
|
4
|
+
HTTP method, request params type, and response type. The :class:`QzoneApi`
|
|
5
|
+
base class provides common infrastructure; concrete subclasses wire up
|
|
6
|
+
specific endpoints.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import typing as t
|
|
2
10
|
|
|
3
11
|
from pydantic import BaseModel, Field
|
|
@@ -33,6 +41,8 @@ class QzoneApi(BaseModel, t.Generic[TyRequest, TyResponse]):
|
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
class IndexPageApi(QzoneApi[QzoneRequestParams, IndexPageResp]):
|
|
44
|
+
"""H5 Qzone landing page API. GET ``/mqzone/index``. Returns :class:`IndexPageResp` with qzonetoken."""
|
|
45
|
+
|
|
36
46
|
response: t.ClassVar = IndexPageResp
|
|
37
47
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
38
48
|
path: t.ClassVar[str] = "/mqzone/index"
|
|
@@ -41,6 +51,8 @@ class IndexPageApi(QzoneApi[QzoneRequestParams, IndexPageResp]):
|
|
|
41
51
|
|
|
42
52
|
|
|
43
53
|
class UserProfileApi(QzoneApi[ProfileParams, ProfilePagePesp]):
|
|
54
|
+
"""User profile page API. GET ``/mqzone/profile``. Returns :class:`ProfilePagePesp`."""
|
|
55
|
+
|
|
44
56
|
response: t.ClassVar = ProfilePagePesp
|
|
45
57
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
46
58
|
path: t.ClassVar[str] = "/mqzone/profile"
|
|
@@ -49,18 +61,24 @@ class UserProfileApi(QzoneApi[ProfileParams, ProfilePagePesp]):
|
|
|
49
61
|
|
|
50
62
|
|
|
51
63
|
class FeedPageApi(QzoneApi[ActiveFeedsParams, FeedPageResp]):
|
|
64
|
+
"""Active feeds pagination API. GET ``/mqzone_feeds/getActiveFeeds``. Returns :class:`FeedPageResp`."""
|
|
65
|
+
|
|
52
66
|
response: t.ClassVar = FeedPageResp
|
|
53
67
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
54
68
|
path: t.ClassVar[str] = "/webapp/json/mqzone_feeds/getActiveFeeds"
|
|
55
69
|
|
|
56
70
|
|
|
57
71
|
class ShuoshuoApi(QzoneApi[ShuoshuoParams, DetailResp]):
|
|
72
|
+
"""Feed detail (shuoshuo) API. GET ``/mqzone_detail/shuoshuo``. Returns :class:`DetailResp`."""
|
|
73
|
+
|
|
58
74
|
response: t.ClassVar = DetailResp
|
|
59
75
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
60
76
|
path: t.ClassVar[str] = "/webapp/json/mqzone_detail/shuoshuo"
|
|
61
77
|
|
|
62
78
|
|
|
63
79
|
class GetFeedsApi(QzoneApi[GetFeedsParams, ProfileResp]):
|
|
80
|
+
"""User feeds pagination API. GET ``/get_feeds``. Returns :class:`ProfileResp`."""
|
|
81
|
+
|
|
64
82
|
response: t.ClassVar = ProfileResp
|
|
65
83
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
66
84
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
@@ -68,6 +86,8 @@ class GetFeedsApi(QzoneApi[GetFeedsParams, ProfileResp]):
|
|
|
68
86
|
|
|
69
87
|
|
|
70
88
|
class GetCountApi(QzoneApi[GetCountParams, FeedCount]):
|
|
89
|
+
"""New feeds count / keep-alive API. GET ``/feeds/mfeeds_get_count``. Returns :class:`FeedCount`."""
|
|
90
|
+
|
|
71
91
|
response: t.ClassVar = FeedCount
|
|
72
92
|
params: GetCountParams = Field(default_factory=GetCountParams)
|
|
73
93
|
|
|
@@ -78,16 +98,22 @@ class GetCountApi(QzoneApi[GetCountParams, FeedCount]):
|
|
|
78
98
|
|
|
79
99
|
|
|
80
100
|
class LikeApi(QzoneApi[DolikeParam, SingleReturnResp]):
|
|
101
|
+
"""Like a feed. POST ``/cgi-bin/likes/internal_dolike_app``. Returns :class:`SingleReturnResp`."""
|
|
102
|
+
|
|
81
103
|
response: t.ClassVar = SingleReturnResp
|
|
82
104
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
83
105
|
path: t.ClassVar[str] = "/proxy/domain/w.qzone.qq.com/cgi-bin/likes/internal_dolike_app"
|
|
84
106
|
|
|
85
107
|
|
|
86
108
|
class UnlikeApi(LikeApi):
|
|
109
|
+
"""Unlike a feed. POST ``/cgi-bin/likes/internal_unlike_app``. Extends :class:`LikeApi`."""
|
|
110
|
+
|
|
87
111
|
path: t.ClassVar[str] = "/proxy/domain/w.qzone.qq.com/cgi-bin/likes/internal_unlike_app"
|
|
88
112
|
|
|
89
113
|
|
|
90
114
|
class AddCommentApi(QzoneApi[AddCommentParams, AddCommentResp]):
|
|
115
|
+
"""Add comment (JSON API). POST ``/qzoneOperation/addComment``. Returns :class:`AddCommentResp`."""
|
|
116
|
+
|
|
91
117
|
response: t.ClassVar = AddCommentResp
|
|
92
118
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
93
119
|
path: t.ClassVar[str] = "/webapp/json/qzoneOperation/addComment"
|
|
@@ -95,24 +121,32 @@ class AddCommentApi(QzoneApi[AddCommentParams, AddCommentResp]):
|
|
|
95
121
|
|
|
96
122
|
|
|
97
123
|
class AddCommentApiLegacy(QzoneApi[AddCommentParamsLegacy, AddCommentLegacyResp]):
|
|
124
|
+
"""Add comment with photos (legacy API). POST ``/cgi-bin/emotion_cgi_re_feeds``. Returns :class:`AddCommentLegacyResp`."""
|
|
125
|
+
|
|
98
126
|
response: t.ClassVar = AddCommentLegacyResp
|
|
99
127
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
100
128
|
path: t.ClassVar[str] = "/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_re_feeds"
|
|
101
129
|
|
|
102
130
|
|
|
103
131
|
class DeleteCommentApi(QzoneApi[DeleteCommentParams, DeleteCommentResp]):
|
|
132
|
+
"""Delete a comment. POST ``/cgi-bin/emotion_cgi_delcomment_ugc``. Returns :class:`DeleteCommentResp`."""
|
|
133
|
+
|
|
104
134
|
response: t.ClassVar = DeleteCommentResp
|
|
105
135
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
106
136
|
path: t.ClassVar[str] = "/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_delcomment_ugc"
|
|
107
137
|
|
|
108
138
|
|
|
109
139
|
class ListFriendApi(QzoneApi):
|
|
140
|
+
"""List friends (reserved, not yet wired). GET ``/friend/mfriend_list``."""
|
|
141
|
+
|
|
110
142
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
111
143
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
112
144
|
path: t.ClassVar[str] = "/friend/mfriend_list"
|
|
113
145
|
|
|
114
146
|
|
|
115
147
|
class PublishMoodApi(QzoneApi[PublishMoodParams, PublishMoodResp]):
|
|
148
|
+
"""Publish a mood feed. POST ``/mood/publish_mood``. Returns :class:`PublishMoodResp`."""
|
|
149
|
+
|
|
116
150
|
response: t.ClassVar = PublishMoodResp
|
|
117
151
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
118
152
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
@@ -120,6 +154,8 @@ class PublishMoodApi(QzoneApi[PublishMoodParams, PublishMoodResp]):
|
|
|
120
154
|
|
|
121
155
|
|
|
122
156
|
class AddOperationApi(QzoneApi):
|
|
157
|
+
"""Generic operation API. POST ``/operation/operation_add``."""
|
|
158
|
+
|
|
123
159
|
response: t.Type[QzoneResponse]
|
|
124
160
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
125
161
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
@@ -127,6 +163,8 @@ class AddOperationApi(QzoneApi):
|
|
|
127
163
|
|
|
128
164
|
|
|
129
165
|
class UploadPicApi(QzoneApi[UploadPicParams, UploadPicResponse]):
|
|
166
|
+
"""Upload image. POST ``/cgi-bin/upload/cgi_upload_pic_v2``. Returns :class:`UploadPicResponse`."""
|
|
167
|
+
|
|
130
168
|
response: t.ClassVar = UploadPicResponse
|
|
131
169
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
132
170
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
@@ -134,6 +172,8 @@ class UploadPicApi(QzoneApi[UploadPicParams, UploadPicResponse]):
|
|
|
134
172
|
|
|
135
173
|
|
|
136
174
|
class PhotosPreuploadApi(QzoneApi[PhotosPreuploadParams, PhotosPreuploadResponse]):
|
|
175
|
+
"""Preupload photos before publishing. POST ``/cgi-bin/upload/cgi_upload_pic_v2``. Returns :class:`PhotosPreuploadResponse`."""
|
|
176
|
+
|
|
137
177
|
response: t.ClassVar = PhotosPreuploadResponse
|
|
138
178
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
139
179
|
host: t.ClassVar[str] = "https://mobile.qzone.qq.com"
|
|
@@ -141,6 +181,8 @@ class PhotosPreuploadApi(QzoneApi[PhotosPreuploadParams, PhotosPreuploadResponse
|
|
|
141
181
|
|
|
142
182
|
|
|
143
183
|
class AvatarApi(QzoneApi[AvatarParams, AvatarResponse]):
|
|
184
|
+
"""Get avatar by uin (no login required). GET ``/qzone/{hostuin}/{hostuin}/{size}``. Returns :class:`AvatarResponse`."""
|
|
185
|
+
|
|
144
186
|
response: t.ClassVar = AvatarResponse
|
|
145
187
|
login_required: t.ClassVar[bool] = False
|
|
146
188
|
http_method: t.ClassVar[TyHttpMethod] = "GET"
|
|
@@ -149,6 +191,8 @@ class AvatarApi(QzoneApi[AvatarParams, AvatarResponse]):
|
|
|
149
191
|
|
|
150
192
|
|
|
151
193
|
class SetTopApi(QzoneApi[SetTopParams, SingleReturnResp]):
|
|
194
|
+
"""Set/unset feed as top. POST ``/cgi-bin/feeds/cgi_settopfeed``. Returns :class:`SingleReturnResp`."""
|
|
195
|
+
|
|
152
196
|
response: t.ClassVar = SingleReturnResp
|
|
153
197
|
http_method: t.ClassVar[TyHttpMethod] = "POST"
|
|
154
198
|
host: t.ClassVar[str] = "user.qzone.qq.com"
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Feed data models.
|
|
2
|
+
|
|
3
|
+
Defines the core feed data structures including :class:`FeedData`,
|
|
4
|
+
:class:`FeedCommon`, :class:`UserInfo`, feed media types, and related
|
|
5
|
+
enums. These models represent the Qzone feed JSON structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import typing as t
|
|
2
9
|
from enum import IntEnum
|
|
3
10
|
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Qzone API request parameter models.
|
|
2
|
+
|
|
3
|
+
Defines :class:`QzoneRequestParams` and its subclasses for each API
|
|
4
|
+
endpoint. Each subclass carries fields specific to that endpoint and
|
|
5
|
+
provides :meth:`build_params` for serialization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import typing as t
|
|
2
9
|
from base64 import b64encode
|
|
3
10
|
from io import BytesIO
|
|
@@ -190,6 +197,11 @@ class PublishMoodParams(QzoneRequestParams):
|
|
|
190
197
|
)
|
|
191
198
|
ugc_right: UgcRight = UgcRight.all
|
|
192
199
|
|
|
200
|
+
allow_uins: t.List[int] = Field(default_factory=list)
|
|
201
|
+
"""When ugc_right=part (16), only these QQ users can see this shuoshuo."""
|
|
202
|
+
deny_uins: t.List[int] = Field(default_factory=list)
|
|
203
|
+
"""When ugc_right=blacklist (128), these QQ users cannot see this shuoshuo."""
|
|
204
|
+
|
|
193
205
|
opr_type: str = "publish_shuoshuo"
|
|
194
206
|
format: str = "json"
|
|
195
207
|
# lat: int
|
|
@@ -200,6 +212,10 @@ class PublishMoodParams(QzoneRequestParams):
|
|
|
200
212
|
def richval(self, photos: t.List[PhotoData]):
|
|
201
213
|
return " ".join(i.to_richval() for i in photos)
|
|
202
214
|
|
|
215
|
+
@field_serializer("allow_uins", "deny_uins")
|
|
216
|
+
def uinlist_to_str(self, uins: t.List[int]) -> str:
|
|
217
|
+
return ",".join(str(u) for u in uins) if uins else ""
|
|
218
|
+
|
|
203
219
|
|
|
204
220
|
class DeleteUgcParams(QzoneRequestParams):
|
|
205
221
|
uin_fields = ("res_uin",)
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Qzone API response models.
|
|
2
|
+
|
|
3
|
+
Defines :class:`QzoneResponse` base class and its subclasses for each
|
|
4
|
+
API endpoint. Includes factory methods :meth:`~QzoneResponse.from_response_object`
|
|
5
|
+
and :meth:`~QzoneResponse.response_to_object` for HTTP response parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import re
|
|
2
9
|
import typing as t
|
|
3
10
|
from contextlib import suppress
|
|
@@ -8,7 +8,9 @@ from .config import *
|
|
|
8
8
|
from .entity import *
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class PseudoCurkey(str):
|
|
12
|
+
"""Pseudo curkey string helper."""
|
|
13
|
+
|
|
12
14
|
def __new__(cls, uin: int, abstime: int):
|
|
13
15
|
return str.__new__(cls, cls.build(uin, abstime))
|
|
14
16
|
|
|
@@ -31,6 +33,8 @@ class PersudoCurkey(str):
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class AlbumData(BaseModel):
|
|
36
|
+
"""Album data model."""
|
|
37
|
+
|
|
34
38
|
topicid: str
|
|
35
39
|
pickey: str
|
|
36
40
|
hostuin: int
|
|
@@ -38,6 +42,8 @@ class AlbumData(BaseModel):
|
|
|
38
42
|
|
|
39
43
|
# LikeData is not a response of any API. It's just a type def.
|
|
40
44
|
class LikeData(BaseModel):
|
|
45
|
+
"""Like data model with helper methods for key construction."""
|
|
46
|
+
|
|
41
47
|
unikey: str
|
|
42
48
|
curkey: str
|
|
43
49
|
appid: int
|
|
@@ -46,11 +52,11 @@ class LikeData(BaseModel):
|
|
|
46
52
|
abstime: int
|
|
47
53
|
|
|
48
54
|
@staticmethod
|
|
49
|
-
def
|
|
50
|
-
return str(
|
|
55
|
+
def pseudo_curkey(uin: int, abstime: int):
|
|
56
|
+
return str(PseudoCurkey(uin, abstime))
|
|
51
57
|
|
|
52
58
|
@staticmethod
|
|
53
|
-
def
|
|
59
|
+
def pseudo_unikey(appid: int, uin: int, fid: str):
|
|
54
60
|
if appid == 311:
|
|
55
61
|
return f"https://user.qzone.qq.com/{uin}/mood/{fid}"
|
|
56
62
|
|
|
@@ -10,6 +10,8 @@ from pydantic import BaseModel, Field, HttpUrl
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class HasContent(BaseModel):
|
|
13
|
+
"""Mixin for models that contain content text."""
|
|
14
|
+
|
|
13
15
|
content: str = ""
|
|
14
16
|
|
|
15
17
|
|
|
@@ -67,4 +69,6 @@ class LinkEntity(ConEntity):
|
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
class HasConEntity(HasContent):
|
|
72
|
+
"""Mixin for models that contain content entities."""
|
|
73
|
+
|
|
70
74
|
entities: Optional[List[ConEntity]] = Field(default=None, alias="conlist")
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""Content entity parsing utilities.
|
|
2
|
+
|
|
3
|
+
Provides :func:`finfo_box_entities` and :func:`split_entities` for
|
|
4
|
+
extracting structured entities from feed content HTML.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import re
|
|
2
8
|
from typing import List
|
|
3
9
|
|
|
@@ -38,6 +44,12 @@ RE_ENTITY = re.compile(entity_rules, re.VERBOSE)
|
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def finfo_box_entities(finfo: HtmlElement) -> List[ConEntity]:
|
|
47
|
+
"""Extract entities from a feed info box HTML element.
|
|
48
|
+
|
|
49
|
+
:param finfo: parsed HTML element of the feed info box
|
|
50
|
+
:returns: list of entity dicts
|
|
51
|
+
"""
|
|
52
|
+
|
|
41
53
|
def yield_children(elm: HtmlElement):
|
|
42
54
|
if elm.text is not None:
|
|
43
55
|
yield elm.text
|
|
@@ -72,6 +84,11 @@ def finfo_box_entities(finfo: HtmlElement) -> List[ConEntity]:
|
|
|
72
84
|
|
|
73
85
|
|
|
74
86
|
def split_entities(s: str) -> List[ConEntity]:
|
|
87
|
+
"""Split a content string into text and entity segments.
|
|
88
|
+
|
|
89
|
+
:param s: raw content string with entity markup
|
|
90
|
+
:returns: list of text/entity segment dicts
|
|
91
|
+
"""
|
|
75
92
|
entities: List[ConEntity] = []
|
|
76
93
|
pos = 0
|
|
77
94
|
for m in RE_ENTITY.finditer(s):
|