perk-pushplus-sdk 1.0.0__py3-none-any.whl

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 (44) hide show
  1. perk_pushplus/__init__.py +155 -0
  2. perk_pushplus/access_key.py +77 -0
  3. perk_pushplus/api/__init__.py +33 -0
  4. perk_pushplus/api/access_key.py +44 -0
  5. perk_pushplus/api/base.py +210 -0
  6. perk_pushplus/api/channel.py +47 -0
  7. perk_pushplus/api/clawbot.py +49 -0
  8. perk_pushplus/api/friend.py +58 -0
  9. perk_pushplus/api/message.py +151 -0
  10. perk_pushplus/api/message_token.py +52 -0
  11. perk_pushplus/api/open_message.py +51 -0
  12. perk_pushplus/api/pre.py +40 -0
  13. perk_pushplus/api/setting.py +90 -0
  14. perk_pushplus/api/topic.py +89 -0
  15. perk_pushplus/api/topic_user.py +35 -0
  16. perk_pushplus/api/user.py +34 -0
  17. perk_pushplus/api/webhook.py +35 -0
  18. perk_pushplus/callback_parser.py +51 -0
  19. perk_pushplus/client.py +169 -0
  20. perk_pushplus/config.py +85 -0
  21. perk_pushplus/enums.py +214 -0
  22. perk_pushplus/exceptions.py +61 -0
  23. perk_pushplus/http.py +158 -0
  24. perk_pushplus/models/__init__.py +90 -0
  25. perk_pushplus/models/base.py +246 -0
  26. perk_pushplus/models/callback.py +76 -0
  27. perk_pushplus/models/open_access.py +18 -0
  28. perk_pushplus/models/open_channel.py +57 -0
  29. perk_pushplus/models/open_clawbot.py +34 -0
  30. perk_pushplus/models/open_friend.py +31 -0
  31. perk_pushplus/models/open_message.py +36 -0
  32. perk_pushplus/models/open_pre.py +51 -0
  33. perk_pushplus/models/open_setting.py +46 -0
  34. perk_pushplus/models/open_token.py +49 -0
  35. perk_pushplus/models/open_topic.py +166 -0
  36. perk_pushplus/models/open_user.py +44 -0
  37. perk_pushplus/models/open_webhook.py +43 -0
  38. perk_pushplus/models/send.py +255 -0
  39. perk_pushplus/py.typed +0 -0
  40. perk_pushplus/rate_limit.py +124 -0
  41. perk_pushplus_sdk-1.0.0.dist-info/METADATA +233 -0
  42. perk_pushplus_sdk-1.0.0.dist-info/RECORD +44 -0
  43. perk_pushplus_sdk-1.0.0.dist-info/WHEEL +5 -0
  44. perk_pushplus_sdk-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,155 @@
1
+ """perk-pushplus-python-sdk: PushPlus 推送加官方接口的 Python SDK。
2
+
3
+ 提供消息接口(``/send``、``/batchSend``)以及全部开放接口的封装,
4
+ 内置 AccessKey 自动管理与本地限流守卫。
5
+
6
+ 参考文档:
7
+ - 消息接口:https://www.pushplus.plus/doc/guide/api.html
8
+ - 开放接口:https://www.pushplus.plus/doc/guide/openApi.html
9
+
10
+ 快速使用::
11
+
12
+ from perk_pushplus import PushPlusClient
13
+
14
+ client = PushPlusClient.builder() \
15
+ .token("your_user_token") \
16
+ .secret_key("your_secret_key") \
17
+ .build()
18
+
19
+ short_code = client.send_simple("标题", "内容")
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from . import callback_parser
24
+ from .access_key import AccessKeyManager
25
+ from .client import PushPlusClient, PushPlusClientBuilder
26
+ from .config import DEFAULT_BASE_URL, PushPlusConfig
27
+ from .enums import (
28
+ CallbackEvent,
29
+ Channel,
30
+ ErrorCode,
31
+ SendStatus,
32
+ Template,
33
+ WebhookType,
34
+ )
35
+ from .exceptions import PushPlusError, PushPlusException
36
+ from .http import HttpRequester, HttpResponse, RequestsHttpRequester
37
+ from .models import (
38
+ AccessKeyResult,
39
+ ApiResponse,
40
+ BatchSendRequest,
41
+ BatchSendResult,
42
+ CallbackFriendInfo,
43
+ CallbackPayload,
44
+ ClawBotInfo,
45
+ ClawBotMessage,
46
+ ClawBotQrCode,
47
+ CpItem,
48
+ FriendItem,
49
+ FriendQrCode,
50
+ MailDetail,
51
+ MailItem,
52
+ MessageCompleteInfo,
53
+ MessageItem,
54
+ MessageTokenAddRequest,
55
+ MessageTokenEditRequest,
56
+ MessageTokenItem,
57
+ MessageTokenOption,
58
+ MpItem,
59
+ PageQuery,
60
+ PageResult,
61
+ PreDetail,
62
+ PreItem,
63
+ PreSaveRequest,
64
+ PreTestRequest,
65
+ SendCount,
66
+ SendMessageResult,
67
+ SendRequest,
68
+ TopicAddRequest,
69
+ TopicDetail,
70
+ TopicEditRequest,
71
+ TopicItem,
72
+ TopicListQuery,
73
+ TopicQrCode,
74
+ TopicUserInfo,
75
+ TopicUserItem,
76
+ TopicUserListQuery,
77
+ UserDefaultDetail,
78
+ UserDefaultItem,
79
+ UserDefaultSaveRequest,
80
+ UserInfo,
81
+ UserLimitTime,
82
+ WebhookItem,
83
+ WebhookSaveRequest,
84
+ )
85
+ from .rate_limit import RateLimitGuard
86
+
87
+ __version__ = "1.0.0"
88
+
89
+ __all__ = [
90
+ "__version__",
91
+ "PushPlusClient",
92
+ "PushPlusClientBuilder",
93
+ "PushPlusConfig",
94
+ "DEFAULT_BASE_URL",
95
+ "PushPlusError",
96
+ "PushPlusException",
97
+ "AccessKeyManager",
98
+ "RateLimitGuard",
99
+ "HttpRequester",
100
+ "HttpResponse",
101
+ "RequestsHttpRequester",
102
+ "callback_parser",
103
+ "Channel",
104
+ "Template",
105
+ "CallbackEvent",
106
+ "SendStatus",
107
+ "WebhookType",
108
+ "ErrorCode",
109
+ "ApiResponse",
110
+ "PageQuery",
111
+ "PageResult",
112
+ "SendRequest",
113
+ "BatchSendRequest",
114
+ "BatchSendResult",
115
+ "CallbackPayload",
116
+ "MessageCompleteInfo",
117
+ "TopicUserInfo",
118
+ "CallbackFriendInfo",
119
+ "AccessKeyResult",
120
+ "MessageItem",
121
+ "SendMessageResult",
122
+ "UserInfo",
123
+ "UserLimitTime",
124
+ "SendCount",
125
+ "MessageTokenItem",
126
+ "MessageTokenOption",
127
+ "MessageTokenAddRequest",
128
+ "MessageTokenEditRequest",
129
+ "TopicItem",
130
+ "TopicDetail",
131
+ "TopicQrCode",
132
+ "TopicListQuery",
133
+ "TopicUserListQuery",
134
+ "TopicUserItem",
135
+ "TopicAddRequest",
136
+ "TopicEditRequest",
137
+ "FriendItem",
138
+ "FriendQrCode",
139
+ "WebhookItem",
140
+ "WebhookSaveRequest",
141
+ "MpItem",
142
+ "CpItem",
143
+ "MailItem",
144
+ "MailDetail",
145
+ "ClawBotQrCode",
146
+ "ClawBotInfo",
147
+ "ClawBotMessage",
148
+ "UserDefaultItem",
149
+ "UserDefaultDetail",
150
+ "UserDefaultSaveRequest",
151
+ "PreItem",
152
+ "PreDetail",
153
+ "PreSaveRequest",
154
+ "PreTestRequest",
155
+ ]
@@ -0,0 +1,77 @@
1
+ """AccessKey 管理器:缓存 + 过期前自动刷新 + 多线程安全。"""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import threading
6
+ from datetime import datetime, timedelta
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ from .config import PushPlusConfig
10
+ from .exceptions import PushPlusError
11
+
12
+ if TYPE_CHECKING:
13
+ from .api.access_key import AccessKeyApi
14
+
15
+
16
+ _logger = logging.getLogger("perk_pushplus")
17
+
18
+
19
+ class AccessKeyManager:
20
+ """提供线程安全的 AccessKey 缓存与过期前自动刷新能力。
21
+
22
+ 在调用任意需要 ``access-key`` 的开放接口前,:class:`OpenApi` 基类会
23
+ 自动通过本类拿到一个有效的 AccessKey。
24
+
25
+ 刷新策略:在 ``expiresIn`` 到期前 :attr:`PushPlusConfig.access_key_refresh_ahead_seconds`
26
+ 秒视为过期。文档说明老 key 在新 key 生成后 5 分钟内仍可用,因此默认 300 秒提前量足够安全。
27
+
28
+ 线程安全:多线程并发调用 :meth:`get_access_key` 时仅会触发一次刷新。
29
+ """
30
+
31
+ def __init__(self, config: PushPlusConfig, access_key_api: "AccessKeyApi") -> None:
32
+ self._config = config
33
+ self._api = access_key_api
34
+ self._lock = threading.Lock()
35
+ self._cached_key: Optional[str] = None
36
+ self._expire_at: datetime = datetime.min
37
+
38
+ def get_access_key(self) -> str:
39
+ """获取有效的 AccessKey。如已缓存且未过期则直接返回;否则触发刷新。"""
40
+
41
+ if self._is_valid():
42
+ return self._cached_key # type: ignore[return-value]
43
+ return self.refresh()
44
+
45
+ def refresh(self) -> str:
46
+ """强制刷新。多线程并发调用时仅会真正发起一次刷新请求。"""
47
+
48
+ with self._lock:
49
+ if self._is_valid():
50
+ return self._cached_key # type: ignore[return-value]
51
+ result = self._api.get_access_key()
52
+ if result is None or not result.accessKey:
53
+ raise PushPlusError("获取 AccessKey 失败:返回为空")
54
+ ttl_seconds = int(result.expiresIn) if result.expiresIn is not None else 7200
55
+ ahead = max(0, int(self._config.access_key_refresh_ahead_seconds))
56
+ effective_ttl = max(1, ttl_seconds - ahead)
57
+ self._cached_key = result.accessKey
58
+ self._expire_at = datetime.now() + timedelta(seconds=effective_ttl)
59
+ _logger.debug(
60
+ "[pushplus] AccessKey refreshed, ttl=%ss, refresh in <= %ss",
61
+ ttl_seconds,
62
+ effective_ttl,
63
+ )
64
+ return self._cached_key
65
+
66
+ def invalidate(self) -> None:
67
+ """失效缓存。下次调用 :meth:`get_access_key` 时会重新拉取。"""
68
+
69
+ with self._lock:
70
+ self._cached_key = None
71
+ self._expire_at = datetime.min
72
+
73
+ def _is_valid(self) -> bool:
74
+ return self._cached_key is not None and datetime.now() < self._expire_at
75
+
76
+
77
+ __all__ = ["AccessKeyManager"]
@@ -0,0 +1,33 @@
1
+ """API 子包入口。"""
2
+ from .access_key import AccessKeyApi
3
+ from .base import AbstractApi, OpenAbstractApi
4
+ from .channel import ChannelApi
5
+ from .clawbot import ClawBotApi
6
+ from .friend import FriendApi
7
+ from .message import MessageApi
8
+ from .message_token import MessageTokenApi
9
+ from .open_message import OpenMessageApi
10
+ from .pre import PreApi
11
+ from .setting import SettingApi
12
+ from .topic import TopicApi
13
+ from .topic_user import TopicUserApi
14
+ from .user import UserApi
15
+ from .webhook import WebhookApi
16
+
17
+ __all__ = [
18
+ "AbstractApi",
19
+ "OpenAbstractApi",
20
+ "AccessKeyApi",
21
+ "MessageApi",
22
+ "OpenMessageApi",
23
+ "UserApi",
24
+ "MessageTokenApi",
25
+ "TopicApi",
26
+ "TopicUserApi",
27
+ "FriendApi",
28
+ "WebhookApi",
29
+ "ChannelApi",
30
+ "ClawBotApi",
31
+ "SettingApi",
32
+ "PreApi",
33
+ ]
@@ -0,0 +1,44 @@
1
+ """开放接口 - 获取 AccessKey。"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ from ..config import PushPlusConfig
7
+ from ..exceptions import PushPlusError
8
+ from ..http import HttpRequester
9
+ from ..models import AccessKeyResult
10
+ from .base import AbstractApi
11
+
12
+
13
+ class AccessKeyApi(AbstractApi):
14
+ """对应文档「一. 获取 AccessKey」。"""
15
+
16
+ def __init__(self, config: PushPlusConfig, http: HttpRequester) -> None:
17
+ super().__init__(config, http)
18
+
19
+ def get_access_key(
20
+ self,
21
+ token: Optional[str] = None,
22
+ secret_key: Optional[str] = None,
23
+ ) -> AccessKeyResult:
24
+ """获取 AccessKey。
25
+
26
+ ``token`` / ``secret_key`` 不传时使用 :class:`PushPlusConfig` 中的配置。
27
+ """
28
+
29
+ token = token or self.config.token
30
+ secret_key = secret_key or self.config.secret_key
31
+ if not token:
32
+ raise PushPlusError("获取 AccessKey 需要 token")
33
+ if not secret_key:
34
+ raise PushPlusError("获取 AccessKey 需要 secretKey")
35
+ body = {"token": token, "secretKey": secret_key}
36
+ result = self.execute_for_data(
37
+ "POST", "/api/common/openApi/getAccessKey", None, body, AccessKeyResult
38
+ )
39
+ if result is None:
40
+ raise PushPlusError("获取 AccessKey 失败:返回为空")
41
+ return result
42
+
43
+
44
+ __all__ = ["AccessKeyApi"]
@@ -0,0 +1,210 @@
1
+ """API 基类:提供请求执行、统一错误处理、access-key 自动注入与重试。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Type, TypeVar
6
+ from urllib.parse import quote
7
+
8
+ from ..config import PushPlusConfig
9
+ from ..exceptions import PushPlusError
10
+ from ..http import HttpRequester, to_json
11
+ from ..models.base import ApiResponse, model_from_dict, model_to_dict
12
+
13
+ if TYPE_CHECKING:
14
+ from ..access_key import AccessKeyManager
15
+
16
+
17
+ T = TypeVar("T")
18
+ HEADER_ACCESS_KEY = "access-key"
19
+ _CODE_ACCESS_KEY_INVALID = 401
20
+
21
+
22
+ class AbstractApi:
23
+ """API 基类。"""
24
+
25
+ def __init__(self, config: PushPlusConfig, http: HttpRequester) -> None:
26
+ self.config = config
27
+ self.http = http
28
+
29
+ def resolve_url(self, path: str) -> str:
30
+ """拼接绝对 URL。"""
31
+
32
+ if path.startswith("http://") or path.startswith("https://"):
33
+ return path
34
+ base = self.config.resolve_base_url()
35
+ return base + (path if path.startswith("/") else "/" + path)
36
+
37
+ @staticmethod
38
+ def build_query(params: Optional[Mapping[str, Any]]) -> str:
39
+ """把 mapping 拼成 query string;忽略 ``None`` 值。"""
40
+
41
+ if not params:
42
+ return ""
43
+ parts = []
44
+ for key, value in params.items():
45
+ if value is None:
46
+ continue
47
+ parts.append(f"{quote(str(key), safe='')}={quote(str(value), safe='')}")
48
+ return "&".join(parts)
49
+
50
+ @classmethod
51
+ def append_query(cls, path: str, params: Optional[Mapping[str, Any]]) -> str:
52
+ """在 path 上追加 query string。"""
53
+
54
+ q = cls.build_query(params)
55
+ if not q:
56
+ return path
57
+ sep = "&" if "?" in path else "?"
58
+ return f"{path}{sep}{q}"
59
+
60
+ def execute(
61
+ self,
62
+ method: str,
63
+ path: str,
64
+ headers: Optional[Dict[str, str]],
65
+ body: Any,
66
+ data_type: Optional[Type[T]] = None,
67
+ ) -> ApiResponse[T]:
68
+ """执行请求并返回原始 :class:`ApiResponse`(不进行 code 校验)。"""
69
+
70
+ url = self.resolve_url(path)
71
+ body_json: Optional[str]
72
+ if body is None:
73
+ body_json = None
74
+ elif isinstance(body, str):
75
+ body_json = body
76
+ else:
77
+ body_json = to_json(model_to_dict(body))
78
+ resp = self.http.execute(method, url, headers, body_json)
79
+ if not resp.is_successful:
80
+ raise PushPlusError(
81
+ f"PushPlus 接口 HTTP 调用失败: status={resp.status_code}, body={resp.body}",
82
+ code=resp.status_code,
83
+ )
84
+ return self._parse_api_response(resp.body, data_type)
85
+
86
+ def execute_for_data(
87
+ self,
88
+ method: str,
89
+ path: str,
90
+ headers: Optional[Dict[str, str]],
91
+ body: Any,
92
+ data_type: Optional[Type[T]] = None,
93
+ ) -> Optional[T]:
94
+ """执行请求并直接返回 ``data``;非 200 抛出异常。"""
95
+
96
+ resp = self.execute(method, path, headers, body, data_type)
97
+ if not resp.is_success():
98
+ raise PushPlusError(
99
+ self._build_business_error_message("PushPlus 接口业务失败", resp),
100
+ code=resp.code if resp.code is not None else -1,
101
+ )
102
+ return resp.data
103
+
104
+ @staticmethod
105
+ def _parse_api_response(
106
+ response_body: str, data_type: Optional[Type[T]] = None
107
+ ) -> ApiResponse[T]:
108
+ if response_body is None or response_body == "":
109
+ raise PushPlusError("PushPlus 接口返回为空")
110
+ try:
111
+ payload = json.loads(response_body)
112
+ except (TypeError, ValueError) as exc:
113
+ raise PushPlusError(
114
+ f"解析 PushPlus 响应 JSON 失败: {exc}, payload={response_body}", cause=exc
115
+ ) from exc
116
+ if not isinstance(payload, dict):
117
+ raise PushPlusError(f"PushPlus 接口返回非 JSON 对象: {response_body}")
118
+
119
+ result: ApiResponse[T] = ApiResponse(
120
+ code=payload.get("code"), msg=payload.get("msg"), data=None
121
+ )
122
+ data_node = payload.get("data")
123
+ if result.is_success():
124
+ if data_node is None:
125
+ result.data = None
126
+ return result
127
+ try:
128
+ if data_type is None:
129
+ result.data = data_node
130
+ else:
131
+ result.data = model_from_dict(data_type, data_node)
132
+ except Exception as exc: # noqa: BLE001
133
+ raise PushPlusError(
134
+ f"解析 PushPlus 响应 data 字段失败: {exc}, payload={response_body}",
135
+ cause=exc,
136
+ ) from exc
137
+ return result
138
+ # 业务失败:把 data 中的字符串/简单值附加到 msg。
139
+ data_text = AbstractApi._extract_data_text(data_node)
140
+ if data_text:
141
+ if not result.msg:
142
+ result.msg = data_text
143
+ elif data_text not in result.msg:
144
+ result.msg = f"{result.msg}: {data_text}"
145
+ return result
146
+
147
+ @staticmethod
148
+ def _extract_data_text(data_node: Any) -> Optional[str]:
149
+ if data_node is None:
150
+ return None
151
+ if isinstance(data_node, (str, int, float, bool)):
152
+ return str(data_node)
153
+ try:
154
+ return json.dumps(data_node, ensure_ascii=False)
155
+ except Exception: # noqa: BLE001
156
+ return None
157
+
158
+ @staticmethod
159
+ def _build_business_error_message(prefix: str, resp: ApiResponse) -> str:
160
+ return f"{prefix}: code={resp.code}, msg={resp.msg}"
161
+
162
+
163
+ class OpenAbstractApi(AbstractApi):
164
+ """开放接口基类。
165
+
166
+ 会自动在 header 中带上 ``access-key``,并在收到 ``code=401`` 时尝试刷新 AccessKey 后重试一次。
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ config: PushPlusConfig,
172
+ http: HttpRequester,
173
+ access_key_manager: "AccessKeyManager",
174
+ ) -> None:
175
+ super().__init__(config, http)
176
+ self.access_key_manager = access_key_manager
177
+
178
+ def _headers_with_access_key(self) -> Dict[str, str]:
179
+ return {HEADER_ACCESS_KEY: self.access_key_manager.get_access_key()}
180
+
181
+ def execute_open(
182
+ self,
183
+ method: str,
184
+ path: str,
185
+ body: Any = None,
186
+ data_type: Optional[Type[T]] = None,
187
+ ) -> Optional[T]:
188
+ """执行带 ``access-key`` 的请求;当返回 ``code=401`` 时自动刷新 key 并重试一次。"""
189
+
190
+ headers = self._headers_with_access_key()
191
+ resp = self.execute(method, path, headers, body, data_type)
192
+ if resp.is_success():
193
+ return resp.data
194
+ if resp.code is not None and int(resp.code) == _CODE_ACCESS_KEY_INVALID:
195
+ self.access_key_manager.invalidate()
196
+ retry_headers = self._headers_with_access_key()
197
+ retry = self.execute(method, path, retry_headers, body, data_type)
198
+ if retry.is_success():
199
+ return retry.data
200
+ raise PushPlusError(
201
+ f"PushPlus 开放接口业务失败(重试后): code={retry.code}, msg={retry.msg}",
202
+ code=retry.code if retry.code is not None else -1,
203
+ )
204
+ raise PushPlusError(
205
+ f"PushPlus 开放接口业务失败: code={resp.code}, msg={resp.msg}",
206
+ code=resp.code if resp.code is not None else -1,
207
+ )
208
+
209
+
210
+ __all__ = ["AbstractApi", "OpenAbstractApi", "HEADER_ACCESS_KEY"]
@@ -0,0 +1,47 @@
1
+ """开放接口 - 微信公众号/企业微信/邮箱渠道列表(文档「七. 渠道配置接口」 5-8)。"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ from ..models import CpItem, MailDetail, MailItem, MpItem, PageQuery, PageResult
7
+ from .base import OpenAbstractApi
8
+
9
+
10
+ class ChannelApi(OpenAbstractApi):
11
+ """微信公众号 / 企业微信 / 邮箱渠道列表。"""
12
+
13
+ def mp_list(self, query: Optional[PageQuery] = None) -> PageResult[MpItem]:
14
+ """微信公众号渠道列表。"""
15
+
16
+ body = query if query is not None else PageQuery()
17
+ result = self.execute_open(
18
+ "POST", "/api/open/mp/list", body, PageResult[MpItem]
19
+ )
20
+ return result or PageResult(list=[])
21
+
22
+ def cp_list(self, query: Optional[PageQuery] = None) -> PageResult[CpItem]:
23
+ """企业微信应用渠道列表。"""
24
+
25
+ body = query if query is not None else PageQuery()
26
+ result = self.execute_open(
27
+ "POST", "/api/open/cp/list", body, PageResult[CpItem]
28
+ )
29
+ return result or PageResult(list=[])
30
+
31
+ def mail_list(self, query: Optional[PageQuery] = None) -> PageResult[MailItem]:
32
+ """邮箱渠道列表。"""
33
+
34
+ body = query if query is not None else PageQuery()
35
+ result = self.execute_open(
36
+ "POST", "/api/open/mail/list", body, PageResult[MailItem]
37
+ )
38
+ return result or PageResult(list=[])
39
+
40
+ def mail_detail(self, mail_id: int) -> MailDetail:
41
+ """邮箱渠道详情。"""
42
+
43
+ path = self.append_query("/api/open/mail/detail", {"mailId": int(mail_id)})
44
+ return self.execute_open("GET", path, None, MailDetail)
45
+
46
+
47
+ __all__ = ["ChannelApi"]
@@ -0,0 +1,49 @@
1
+ """开放接口 - 微信 ClawBot(文档「八. 微信ClawBot接口」)。"""
2
+ from __future__ import annotations
3
+
4
+ from typing import List
5
+
6
+ from ..models import ClawBotInfo, ClawBotMessage, ClawBotQrCode
7
+ from .base import OpenAbstractApi
8
+
9
+
10
+ class ClawBotApi(OpenAbstractApi):
11
+ """微信 ClawBot 渠道相关接口。"""
12
+
13
+ def get_bot_qrcode(self) -> ClawBotQrCode:
14
+ """获取二维码。"""
15
+
16
+ return self.execute_open(
17
+ "GET", "/api/open/clawBot/getBotQrcode", None, ClawBotQrCode
18
+ )
19
+
20
+ def get_qrcode_status(self, qrcode: str) -> None:
21
+ """扫码结果查询。"""
22
+
23
+ path = self.append_query(
24
+ "/api/open/clawBot/getQrcodeStatus", {"getQrcodeStatus": qrcode}
25
+ )
26
+ self.execute_open("GET", path, None, None)
27
+
28
+ def bot_info(self) -> ClawBotInfo:
29
+ """绑定详情。"""
30
+
31
+ return self.execute_open(
32
+ "GET", "/api/open/clawBot/botInfo", None, ClawBotInfo
33
+ )
34
+
35
+ def unbind(self) -> None:
36
+ """解绑。"""
37
+
38
+ self.execute_open("GET", "/api/open/clawBot/unbind", None, None)
39
+
40
+ def get_msg(self) -> List[ClawBotMessage]:
41
+ """获取发送消息。"""
42
+
43
+ result = self.execute_open(
44
+ "GET", "/api/open/clawBot/getMsg", None, List[ClawBotMessage]
45
+ )
46
+ return result or []
47
+
48
+
49
+ __all__ = ["ClawBotApi"]
@@ -0,0 +1,58 @@
1
+ """开放接口 - 好友功能(文档「十. 好友功能接口」)。"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ from ..models import FriendItem, FriendQrCode, PageQuery, PageResult
7
+ from .base import OpenAbstractApi
8
+
9
+
10
+ class FriendApi(OpenAbstractApi):
11
+ """好友相关接口。"""
12
+
13
+ def get_qr_code(
14
+ self,
15
+ app_id: Optional[str] = None,
16
+ content: Optional[str] = None,
17
+ second: Optional[int] = None,
18
+ scan_count: Optional[int] = None,
19
+ ) -> FriendQrCode:
20
+ """获取个人二维码。"""
21
+
22
+ params = {}
23
+ if app_id is not None:
24
+ params["appId"] = app_id
25
+ if content is not None:
26
+ params["content"] = content
27
+ if second is not None:
28
+ params["second"] = int(second)
29
+ if scan_count is not None:
30
+ params["scanCount"] = int(scan_count)
31
+ path = self.append_query("/api/open/friend/getQrCode", params)
32
+ return self.execute_open("GET", path, None, FriendQrCode)
33
+
34
+ def list(self, query: Optional[PageQuery] = None) -> PageResult[FriendItem]:
35
+ """获取好友列表。"""
36
+
37
+ body = query if query is not None else PageQuery()
38
+ result = self.execute_open(
39
+ "POST", "/api/open/friend/list", body, PageResult[FriendItem]
40
+ )
41
+ return result or PageResult(list=[])
42
+
43
+ def delete(self, friend_id: int) -> None:
44
+ """删除好友。"""
45
+
46
+ path = self.append_query(
47
+ "/api/open/friend/deleteFriend", {"friendId": int(friend_id)}
48
+ )
49
+ self.execute_open("GET", path, None, None)
50
+
51
+ def edit_remark(self, id_: int, remark: str) -> None:
52
+ """修改好友备注。"""
53
+
54
+ body = {"id": int(id_), "remark": remark}
55
+ self.execute_open("POST", "/api/open/friend/editRemark", body, None)
56
+
57
+
58
+ __all__ = ["FriendApi"]