aioqzone 1.9.6.dev6__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.
Files changed (60) hide show
  1. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/PKG-INFO +2 -2
  2. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/pyproject.toml +2 -2
  3. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/api/__init__.py +6 -0
  4. aioqzone-1.9.8.dev1/src/aioqzone/api/h5/__init__.py +8 -0
  5. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/api/h5/model.py +47 -5
  6. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/api/login/__init__.py +42 -9
  7. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/api/login/_base.py +24 -5
  8. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/exception.py +8 -1
  9. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/message.py +6 -0
  10. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/__init__.py +44 -0
  11. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/feed.py +7 -0
  12. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/request.py +16 -0
  13. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/response.py +19 -6
  14. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/__init__.py +10 -4
  15. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/config.py +7 -0
  16. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/protocol/entity.py +4 -0
  17. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/entity.py +17 -0
  18. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/regex.py +7 -1
  19. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/retry.py +28 -1
  20. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/time.py +8 -1
  21. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/base.py +31 -3
  22. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/exception.py +7 -0
  23. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/message.py +6 -0
  24. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/qr/__init__.py +15 -2
  25. aioqzone-1.9.8.dev1/src/qqqr/qr/type.py +37 -0
  26. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/type.py +6 -0
  27. aioqzone-1.9.8.dev1/src/qqqr/up/__init__.py +9 -0
  28. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/_model.py +18 -5
  29. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/__init__.py +22 -10
  30. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/_model.py +21 -0
  31. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/capsess.py +40 -3
  32. aioqzone-1.9.8.dev1/src/qqqr/up/captcha/click/__init__.py +8 -0
  33. aioqzone-1.9.8.dev1/src/qqqr/up/captcha/click/_types.py +21 -0
  34. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/pil_utils.py +8 -0
  35. aioqzone-1.9.8.dev1/src/qqqr/up/captcha/select/__init__.py +8 -0
  36. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/select/_types.py +31 -1
  37. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/slide/__init__.py +5 -0
  38. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/captcha/slide/_types.py +19 -6
  39. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/h5.py +17 -0
  40. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/web.py +38 -7
  41. aioqzone-1.9.8.dev1/src/qqqr/utils/encrypt.py +26 -0
  42. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/utils/iter.py +17 -0
  43. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/utils/net.py +7 -0
  44. aioqzone-1.9.6.dev6/src/aioqzone/api/h5/__init__.py +0 -3
  45. aioqzone-1.9.6.dev6/src/qqqr/qr/type.py +0 -24
  46. aioqzone-1.9.6.dev6/src/qqqr/up/__init__.py +0 -4
  47. aioqzone-1.9.6.dev6/src/qqqr/up/captcha/click/__init__.py +0 -3
  48. aioqzone-1.9.6.dev6/src/qqqr/up/captcha/click/_types.py +0 -8
  49. aioqzone-1.9.6.dev6/src/qqqr/up/captcha/select/__init__.py +0 -3
  50. aioqzone-1.9.6.dev6/src/qqqr/utils/encrypt.py +0 -9
  51. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/README.md +0 -0
  52. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/__init__.py +0 -0
  53. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/__init__.py +0 -0
  54. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/model/api/profile.py +0 -0
  55. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/aioqzone/utils/__init__.py +0 -0
  56. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/__init__.py +0 -0
  57. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/constant.py +0 -0
  58. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/py.typed +0 -0
  59. {aioqzone-1.9.6.dev6 → aioqzone-1.9.8.dev1}/src/qqqr/up/encrypt.py +0 -0
  60. {aioqzone-1.9.6.dev6 → 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.6.dev6
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.13
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.6.dev6"
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.13",
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
+ """aioqzone API package.
2
+
3
+ Exports :class:`QrLoginConfig`, :class:`UpLoginConfig`, :class:`Loginable`,
4
+ and :class:`QzoneH5API`.
5
+ """
6
+
1
7
  from .h5 import QzoneH5API
2
8
  from .login import *
3
9
  from .login._base import Loginable
@@ -0,0 +1,8 @@
1
+ """H5 API subpackage.
2
+
3
+ Exports :class:`QzoneH5API`.
4
+ """
5
+
6
+ from .model import QzoneH5API
7
+
8
+ __all__ = ["QzoneH5API"]
@@ -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 = {}
@@ -89,7 +115,7 @@ class QzoneH5API:
89
115
  log.debug(f"Error when validating {obj}, {api=}")
90
116
  raise
91
117
  else:
92
- raise AssertionError
118
+ raise RuntimeError("max retry exceeded")
93
119
 
94
120
  async def _retry_sleep(self, *_) -> None:
95
121
  await self.login.new_cookie()
@@ -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 any failure occurs in data parsing.
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 ``data`` field of the response.
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 ``data`` field of the response.
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(locals(), from_attributes=True))
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(
@@ -101,6 +120,7 @@ class UpLoginManager(Loginable):
101
120
  raise TryAgain from e
102
121
  raise
103
122
  except NotImplementedError:
123
+ # intended: forwarded as NeedSmsVerify
104
124
  from qqqr.constant import StatusCode
105
125
 
106
126
  raise TencentLoginError(StatusCode.NeedSmsVerify, "需要手机验证")
@@ -108,7 +128,7 @@ class UpLoginManager(Loginable):
108
128
  log.warning(f"密码登录:{type(e).__name__},重试", exc_info=True)
109
129
  log.debug(e.args, extra=e.__dict__)
110
130
  raise TryAgain from e
111
- except BaseException as e:
131
+ except Exception as e:
112
132
  log.fatal("密码登录异常", exc_info=True)
113
133
  raise UnexpectedLoginError from e
114
134
 
@@ -142,17 +162,30 @@ class UpLoginManager(Loginable):
142
162
 
143
163
 
144
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
+
145
171
  def __init__(
146
172
  self,
147
173
  client: ClientAdapter,
148
174
  config: QrLoginConfig,
149
175
  *,
150
- h5=True,
176
+ h5: bool = True,
151
177
  ch_login_notify: t.Optional[FutureStore] = None,
152
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
+ """
153
186
  super().__init__(config.uin, ch_login_notify=ch_login_notify)
154
- self.client = client
155
- self.config = config
187
+ self.client = client #: HTTP client
188
+ self.config = config #: Login config instance
156
189
  self.h5(h5, clear_cookie=False) # init uplogin
157
190
 
158
191
  async def _new_cookie(self) -> t.Dict[str, str]:
@@ -178,8 +211,8 @@ class QrLoginManager(Loginable):
178
211
  except (GeneratorExit, ClientError) as e:
179
212
  log.warning(f"二维码登录:{type(e).__name__},重试", exc_info=True)
180
213
  log.debug(e.args, extra=e.__dict__)
181
- raise TryAgain
182
- except BaseException as e:
214
+ raise TryAgain from e
215
+ except Exception as e:
183
216
  log.fatal("二维码登录异常", exc_info=True)
184
217
  raise UnexpectedLoginError from e
185
218
 
@@ -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 class represents a login manager."""
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: cookie. Shouldn't be a cached one.
68
+ :return: True if login succeeded, False otherwise.
50
69
  """
51
70
  if self.lock.locked():
52
71
  last_gtk = self.gtk
@@ -57,7 +76,7 @@ class Loginable(ABC):
57
76
  async with self.lock:
58
77
  try:
59
78
  self.cookie = await self._new_cookie()
60
- except BaseException as e:
79
+ except Exception as e:
61
80
  self.ch_login_notify.add_awaitable(self.login_failed.emit(self.uin, e))
62
81
  return False
63
82
  else:
@@ -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
 
@@ -8,7 +15,7 @@ class QzoneError(RuntimeError):
8
15
  self.robj = robj
9
16
  if len(args) > 0 and isinstance(args[0], str):
10
17
  self.msg = args[0]
11
- super().__init__(self, *args)
18
+ super().__init__(*args)
12
19
 
13
20
  def __str__(self) -> str:
14
21
  return f"QzoneCode {self.code}: {self.msg}"
@@ -1,3 +1,9 @@
1
+ """tylisten hook definitions for aioqzone events.
2
+
3
+ Defines :func:`login_success` and :func:`login_failed` hooks, and
4
+ re-exports QR/UP/captcha hooks from :mod:`qqqr.message`.
5
+ """
6
+
1
7
  import typing as t
2
8
 
3
9
  from tylisten import hookdef
@@ -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
@@ -212,7 +219,8 @@ class ProfilePagePesp(QzoneResponse):
212
219
  data = script[m.end() - 1 : m.end() + entire_closing(script[m.end() - 1 :], "[")]
213
220
  data = re.sub(r",,\]$", "]", data)
214
221
  data = json_loads(data)
215
- assert isinstance(data, list)
222
+ if not isinstance(data, list):
223
+ raise TryAgain("ProfilePageResponse: profile not returned")
216
224
  if len(data) < 2:
217
225
  raise TryAgain("ProfilePageResponse: profile not returned")
218
226
 
@@ -261,7 +269,8 @@ class AddCommentLegacyResp(QzoneResponse):
261
269
  raise TryAgain("AddCommentLegacyResponse: script tag not found")
262
270
 
263
271
  m = response_callback.search(script)
264
- assert m
272
+ if not m:
273
+ raise TryAgain("AddCommentLegacyResponse: callback not found")
265
274
  return validate_strdict(json_loads(m.group(1)))
266
275
 
267
276
 
@@ -280,7 +289,8 @@ class DeleteCommentResp(QzoneResponse):
280
289
  raise TryAgain("DeleteCommentResponse: script tag not found")
281
290
 
282
291
  m = response_callback.search(script)
283
- assert m
292
+ if not m:
293
+ raise TryAgain("DeleteCommentResponse: callback not found")
284
294
  return validate_strdict(json_loads(m.group(1)))
285
295
 
286
296
 
@@ -306,7 +316,8 @@ class UploadPicResponse(QzoneResponse):
306
316
  @classmethod
307
317
  async def response_to_object(cls, response: ClientResponse) -> StrDict:
308
318
  m = response_callback.search(await response.text())
309
- assert m
319
+ if not m:
320
+ raise TryAgain("UploadPicResponse: callback not found")
310
321
  return validate_strdict(json_loads(m.group(1)))
311
322
 
312
323
 
@@ -331,10 +342,12 @@ class PhotosPreuploadResponse(QzoneResponse):
331
342
  @classmethod
332
343
  async def response_to_object(cls, response: ClientResponse) -> StrDict:
333
344
  m = response_callback.search(await response.text())
334
- assert m
345
+ if not m:
346
+ raise TryAgain("PhotosPreuploadResponse: callback not found")
335
347
 
336
348
  picinfos = json_loads(m.group(1))
337
- assert isinstance(picinfos, list)
349
+ if not isinstance(picinfos, list):
350
+ raise TryAgain("PhotosPreuploadResponse: picinfos not a list")
338
351
  return dict(photos=[PicInfo.from_response_object(info["picinfo"]) for info in picinfos]) # type: ignore
339
352
 
340
353