nonebot-adapter-qq 1.5.2__tar.gz → 1.6.0__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.
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/PKG-INFO +19 -7
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/README.md +16 -6
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/adapter.py +208 -27
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/bot.py +5 -10
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/compat.py +3 -3
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/config.py +5 -13
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/__init__.py +1 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/payload.py +20 -2
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/permission.py +2 -2
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/pyproject.toml +6 -10
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/LICENSE +0 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/__init__.py +0 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/event.py +54 -54
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/exception.py +0 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/message.py +0 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/common.py +17 -17
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/guild.py +52 -52
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/qq.py +6 -6
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/store.py +0 -0
- {nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: nonebot-adapter-qq
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.6.0
|
4
4
|
Summary: QQ adapter for nonebot2
|
5
5
|
Home-page: https://github.com/nonebot/adapter-qq
|
6
6
|
License: MIT
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
|
+
Requires-Dist: cryptography (>=43.0.3,<45.0.0)
|
21
23
|
Requires-Dist: nonebot2 (>=2.2.1,<3.0.0)
|
22
24
|
Requires-Dist: pydantic (>=1.10.0,<3.0.0,!=2.5.0,!=2.5.1)
|
23
25
|
Requires-Dist: typing-extensions (>=4.4.0,<5.0.0)
|
@@ -44,15 +46,19 @@ _✨ QQ 协议适配 ✨_
|
|
44
46
|
|
45
47
|
### Driver
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
如:
|
49
|
+
如果使用 WebSocket 连接方式,请参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。如:
|
50
50
|
|
51
51
|
```dotenv
|
52
52
|
DRIVER=~httpx+~websockets
|
53
53
|
DRIVER=~aiohttp
|
54
54
|
```
|
55
55
|
|
56
|
+
如果使用 WebHook 连接方式,则添加 `ASGIServer` 支持。如:
|
57
|
+
|
58
|
+
```dotenv
|
59
|
+
DRIVER=~fastapi
|
60
|
+
```
|
61
|
+
|
56
62
|
### QQ_IS_SANDBOX
|
57
63
|
|
58
64
|
是否为沙盒模式,默认为 `False`。
|
@@ -65,9 +71,13 @@ QQ_IS_SANDBOX=true
|
|
65
71
|
|
66
72
|
配置机器人帐号 `id` `token` `secret`,intent 需要根据机器人类型以及需要的事件进行配置。
|
67
73
|
|
74
|
+
#### Webhook / WebSocket
|
75
|
+
|
76
|
+
通过配置项 `use_websocket` 来选择是否启用 WebSocket 连接,当前默认为 `True`。如果关闭 WebSocket 连接方式,则可以通过 WebHook 方式来连接机器人,请在 QQ 开放平台中配置机器人回调地址:`https://host:port/qq/webhook`。
|
77
|
+
|
68
78
|
#### Intent
|
69
79
|
|
70
|
-
|
80
|
+
Intent 仅对 WebSocket 连接方式生效。以下为所有 Intent 配置项以及默认值:
|
71
81
|
|
72
82
|
```json
|
73
83
|
{
|
@@ -101,7 +111,8 @@ QQ_BOTS='
|
|
101
111
|
"intent": {
|
102
112
|
"guild_messages": true,
|
103
113
|
"at_messages": false
|
104
|
-
}
|
114
|
+
},
|
115
|
+
"use_websocket": false
|
105
116
|
}
|
106
117
|
]
|
107
118
|
'
|
@@ -118,7 +129,8 @@ QQ_BOTS='
|
|
118
129
|
"secret": "xxx",
|
119
130
|
"intent": {
|
120
131
|
"c2c_group_at_messages": true
|
121
|
-
}
|
132
|
+
},
|
133
|
+
"use_websocket": false
|
122
134
|
}
|
123
135
|
]
|
124
136
|
'
|
@@ -16,15 +16,19 @@ _✨ QQ 协议适配 ✨_
|
|
16
16
|
|
17
17
|
### Driver
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
如:
|
19
|
+
如果使用 WebSocket 连接方式,请参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。如:
|
22
20
|
|
23
21
|
```dotenv
|
24
22
|
DRIVER=~httpx+~websockets
|
25
23
|
DRIVER=~aiohttp
|
26
24
|
```
|
27
25
|
|
26
|
+
如果使用 WebHook 连接方式,则添加 `ASGIServer` 支持。如:
|
27
|
+
|
28
|
+
```dotenv
|
29
|
+
DRIVER=~fastapi
|
30
|
+
```
|
31
|
+
|
28
32
|
### QQ_IS_SANDBOX
|
29
33
|
|
30
34
|
是否为沙盒模式,默认为 `False`。
|
@@ -37,9 +41,13 @@ QQ_IS_SANDBOX=true
|
|
37
41
|
|
38
42
|
配置机器人帐号 `id` `token` `secret`,intent 需要根据机器人类型以及需要的事件进行配置。
|
39
43
|
|
44
|
+
#### Webhook / WebSocket
|
45
|
+
|
46
|
+
通过配置项 `use_websocket` 来选择是否启用 WebSocket 连接,当前默认为 `True`。如果关闭 WebSocket 连接方式,则可以通过 WebHook 方式来连接机器人,请在 QQ 开放平台中配置机器人回调地址:`https://host:port/qq/webhook`。
|
47
|
+
|
40
48
|
#### Intent
|
41
49
|
|
42
|
-
|
50
|
+
Intent 仅对 WebSocket 连接方式生效。以下为所有 Intent 配置项以及默认值:
|
43
51
|
|
44
52
|
```json
|
45
53
|
{
|
@@ -73,7 +81,8 @@ QQ_BOTS='
|
|
73
81
|
"intent": {
|
74
82
|
"guild_messages": true,
|
75
83
|
"at_messages": false
|
76
|
-
}
|
84
|
+
},
|
85
|
+
"use_websocket": false
|
77
86
|
}
|
78
87
|
]
|
79
88
|
'
|
@@ -90,7 +99,8 @@ QQ_BOTS='
|
|
90
99
|
"secret": "xxx",
|
91
100
|
"intent": {
|
92
101
|
"c2c_group_at_messages": true
|
93
|
-
}
|
102
|
+
},
|
103
|
+
"use_websocket": false
|
94
104
|
}
|
95
105
|
]
|
96
106
|
'
|
@@ -1,20 +1,28 @@
|
|
1
1
|
import sys
|
2
|
+
import json
|
2
3
|
import asyncio
|
4
|
+
import binascii
|
3
5
|
from typing_extensions import override
|
4
|
-
from typing import Any, Literal, Optional
|
6
|
+
from typing import Any, Union, Literal, Optional, cast
|
5
7
|
|
6
8
|
from nonebot.utils import escape_tag
|
7
9
|
from nonebot.exception import WebSocketClosed
|
10
|
+
from cryptography.exceptions import InvalidSignature
|
11
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
8
12
|
from nonebot.compat import PYDANTIC_V2, type_validate_json, type_validate_python
|
9
13
|
from nonebot.drivers import (
|
10
14
|
URL,
|
11
15
|
Driver,
|
12
16
|
Request,
|
17
|
+
Response,
|
18
|
+
ASGIMixin,
|
13
19
|
WebSocket,
|
14
20
|
HTTPClientMixin,
|
21
|
+
HTTPServerSetup,
|
15
22
|
WebSocketClientMixin,
|
16
23
|
)
|
17
24
|
|
25
|
+
from nonebot import get_plugin_config
|
18
26
|
from nonebot.adapters import Adapter as BaseAdapter
|
19
27
|
|
20
28
|
from .bot import Bot
|
@@ -33,6 +41,7 @@ from .models import (
|
|
33
41
|
Reconnect,
|
34
42
|
PayloadType,
|
35
43
|
HeartbeatAck,
|
44
|
+
WebhookVerify,
|
36
45
|
InvalidSession,
|
37
46
|
)
|
38
47
|
|
@@ -44,9 +53,9 @@ class Adapter(BaseAdapter):
|
|
44
53
|
def __init__(self, driver: Driver, **kwargs: Any):
|
45
54
|
super().__init__(driver, **kwargs)
|
46
55
|
|
47
|
-
self.qq_config: Config = Config
|
56
|
+
self.qq_config: Config = get_plugin_config(Config)
|
48
57
|
|
49
|
-
self.tasks:
|
58
|
+
self.tasks: set["asyncio.Task"] = set()
|
50
59
|
self.setup()
|
51
60
|
|
52
61
|
@classmethod
|
@@ -61,12 +70,24 @@ class Adapter(BaseAdapter):
|
|
61
70
|
"http client requests! "
|
62
71
|
"QQ Adapter need a HTTPClient Driver to work."
|
63
72
|
)
|
64
|
-
|
73
|
+
|
74
|
+
if any(bot.use_websocket for bot in self.qq_config.qq_bots) and not isinstance(
|
75
|
+
self.driver, WebSocketClientMixin
|
76
|
+
):
|
65
77
|
raise RuntimeError(
|
66
78
|
f"Current driver {self.config.driver} does not support "
|
67
79
|
"websocket client! "
|
68
80
|
"QQ Adapter need a WebSocketClient Driver to work."
|
69
81
|
)
|
82
|
+
|
83
|
+
if not all(
|
84
|
+
bot.use_websocket for bot in self.qq_config.qq_bots
|
85
|
+
) and not isinstance(self.driver, ASGIMixin):
|
86
|
+
raise RuntimeError(
|
87
|
+
f"Current driver {self.config.driver} does not support "
|
88
|
+
"ASGI server! "
|
89
|
+
"QQ Adapter need a ASGI Driver to receive webhook."
|
90
|
+
)
|
70
91
|
self.on_ready(self.startup)
|
71
92
|
self.driver.on_shutdown(self.shutdown)
|
72
93
|
|
@@ -84,8 +105,38 @@ class Adapter(BaseAdapter):
|
|
84
105
|
|
85
106
|
log("DEBUG", f"QQ api base url: <y>{escape_tag(str(api_base))}</y>")
|
86
107
|
|
108
|
+
if isinstance(self.driver, ASGIMixin):
|
109
|
+
self.setup_http_server(
|
110
|
+
HTTPServerSetup(
|
111
|
+
URL("/qq/"),
|
112
|
+
"POST",
|
113
|
+
f"{self.get_name()} Root Webhook",
|
114
|
+
self._handle_http,
|
115
|
+
),
|
116
|
+
)
|
117
|
+
self.setup_http_server(
|
118
|
+
HTTPServerSetup(
|
119
|
+
URL("/qq/webhook"),
|
120
|
+
"POST",
|
121
|
+
f"{self.get_name()} Webhook",
|
122
|
+
self._handle_http,
|
123
|
+
),
|
124
|
+
)
|
125
|
+
self.setup_http_server(
|
126
|
+
HTTPServerSetup(
|
127
|
+
URL("/qq/webhook/"),
|
128
|
+
"POST",
|
129
|
+
f"{self.get_name()} Webhook Slash",
|
130
|
+
self._handle_http,
|
131
|
+
),
|
132
|
+
)
|
133
|
+
|
87
134
|
for bot in self.qq_config.qq_bots:
|
88
|
-
|
135
|
+
if not bot.use_websocket:
|
136
|
+
continue
|
137
|
+
task = asyncio.create_task(self.run_bot_websocket(bot))
|
138
|
+
task.add_done_callback(self.tasks.discard)
|
139
|
+
self.tasks.add(task)
|
89
140
|
|
90
141
|
async def shutdown(self) -> None:
|
91
142
|
for task in self.tasks:
|
@@ -97,7 +148,7 @@ class Adapter(BaseAdapter):
|
|
97
148
|
return_exceptions=True,
|
98
149
|
)
|
99
150
|
|
100
|
-
async def
|
151
|
+
async def run_bot_websocket(self, bot_info: BotInfo) -> None:
|
101
152
|
bot = Bot(self, bot_info.id, bot_info)
|
102
153
|
|
103
154
|
# get sharded gateway url
|
@@ -123,17 +174,17 @@ class Adapter(BaseAdapter):
|
|
123
174
|
|
124
175
|
# start connection in single shard mode
|
125
176
|
if bot_info.shard is not None:
|
126
|
-
self.
|
127
|
-
|
128
|
-
)
|
177
|
+
task = asyncio.create_task(self._forward_ws(bot, ws_url, bot_info.shard))
|
178
|
+
task.add_done_callback(self.tasks.discard)
|
179
|
+
self.tasks.add(task)
|
129
180
|
return
|
130
181
|
|
131
182
|
# start connection in sharding mode
|
132
183
|
shards = gateway_info.shards or 1
|
133
184
|
for i in range(shards):
|
134
|
-
self.
|
135
|
-
|
136
|
-
)
|
185
|
+
task = asyncio.create_task(self._forward_ws(bot, ws_url, (i, shards)))
|
186
|
+
task.add_done_callback(self.tasks.discard)
|
187
|
+
self.tasks.add(task)
|
137
188
|
# wait for session start concurrency limit
|
138
189
|
await asyncio.sleep(gateway_info.session_start_limit.max_concurrency or 1)
|
139
190
|
|
@@ -318,7 +369,9 @@ class Adapter(BaseAdapter):
|
|
318
369
|
)
|
319
370
|
|
320
371
|
if ready_event:
|
321
|
-
asyncio.create_task(bot.handle_event(ready_event))
|
372
|
+
task = asyncio.create_task(bot.handle_event(ready_event))
|
373
|
+
task.add_done_callback(self.tasks.discard)
|
374
|
+
self.tasks.add(task)
|
322
375
|
|
323
376
|
return True
|
324
377
|
|
@@ -343,18 +396,7 @@ class Adapter(BaseAdapter):
|
|
343
396
|
f"Received payload: {escape_tag(repr(payload))}",
|
344
397
|
)
|
345
398
|
if isinstance(payload, Dispatch):
|
346
|
-
|
347
|
-
event = self.payload_to_event(payload)
|
348
|
-
except Exception as e:
|
349
|
-
log(
|
350
|
-
"WARNING",
|
351
|
-
f"Failed to parse event {escape_tag(repr(payload))}",
|
352
|
-
e,
|
353
|
-
)
|
354
|
-
else:
|
355
|
-
if isinstance(event, MessageAuditEvent):
|
356
|
-
audit_result.add_result(event)
|
357
|
-
asyncio.create_task(bot.handle_event(event)) # noqa: RUF006
|
399
|
+
self.dispatch_event(bot, payload)
|
358
400
|
elif isinstance(payload, HeartbeatAck):
|
359
401
|
log("TRACE", "Heartbeat ACK")
|
360
402
|
continue
|
@@ -377,6 +419,129 @@ class Adapter(BaseAdapter):
|
|
377
419
|
f"Unknown payload from server: {escape_tag(repr(payload))}",
|
378
420
|
)
|
379
421
|
|
422
|
+
async def receive_payload(self, bot: Bot, ws: WebSocket) -> Payload:
|
423
|
+
return self.data_to_payload(bot, await ws.receive())
|
424
|
+
|
425
|
+
async def _handle_http(self, request: Request) -> Response:
|
426
|
+
bot_id = request.headers.get("X-Bot-Appid")
|
427
|
+
if not bot_id:
|
428
|
+
log("WARNING", "Missing X-Bot-Appid header in request")
|
429
|
+
return Response(403, content="Missing X-Bot-Appid header")
|
430
|
+
elif bot_id in self.bots:
|
431
|
+
bot = cast(Bot, self.bots[bot_id])
|
432
|
+
elif bot_info := next(
|
433
|
+
(bot_info for bot_info in self.qq_config.qq_bots if bot_info.id == bot_id),
|
434
|
+
None,
|
435
|
+
):
|
436
|
+
bot = Bot(self, bot_id, bot_info)
|
437
|
+
else:
|
438
|
+
log("ERROR", f"Bot {bot_id} not found")
|
439
|
+
return Response(403, content="Bot not found")
|
440
|
+
|
441
|
+
if request.content is None:
|
442
|
+
return Response(400, content="Missing request content")
|
443
|
+
|
444
|
+
try:
|
445
|
+
payload = self.data_to_payload(bot, request.content)
|
446
|
+
except Exception as e:
|
447
|
+
log(
|
448
|
+
"ERROR",
|
449
|
+
"<r><bg #f8bbd0>Error while parsing data from webhook</bg #f8bbd0></r>",
|
450
|
+
e,
|
451
|
+
)
|
452
|
+
return Response(400, content="Invalid request content")
|
453
|
+
|
454
|
+
log(
|
455
|
+
"TRACE",
|
456
|
+
f"Received payload: {escape_tag(repr(payload))}",
|
457
|
+
)
|
458
|
+
if isinstance(payload, WebhookVerify):
|
459
|
+
log("INFO", "Received qq webhook verify request")
|
460
|
+
return self._webhook_verify(bot, payload)
|
461
|
+
|
462
|
+
if self.qq_config.qq_verify_webhook and (
|
463
|
+
response := self._check_signature(bot, request)
|
464
|
+
):
|
465
|
+
return response
|
466
|
+
|
467
|
+
if bot.self_id not in self.bots:
|
468
|
+
self.bot_connect(bot)
|
469
|
+
|
470
|
+
if isinstance(payload, Dispatch):
|
471
|
+
self.dispatch_event(bot, payload)
|
472
|
+
|
473
|
+
return Response(200)
|
474
|
+
|
475
|
+
def _get_ed25519_key(self, bot: Bot) -> Ed25519PrivateKey:
|
476
|
+
secret = bot.bot_info.secret.encode()
|
477
|
+
seed = secret
|
478
|
+
while len(seed) < 32:
|
479
|
+
seed += secret
|
480
|
+
seed = seed[:32]
|
481
|
+
return Ed25519PrivateKey.from_private_bytes(seed)
|
482
|
+
|
483
|
+
def _webhook_verify(self, bot: Bot, payload: WebhookVerify) -> Response:
|
484
|
+
plain_token = payload.data.plain_token
|
485
|
+
event_ts = payload.data.event_ts
|
486
|
+
|
487
|
+
try:
|
488
|
+
private_key = self._get_ed25519_key(bot)
|
489
|
+
except Exception as e:
|
490
|
+
log("ERROR", "Failed to create private key", e)
|
491
|
+
return Response(500, content="Failed to create private key")
|
492
|
+
|
493
|
+
msg = f"{event_ts}{plain_token}".encode()
|
494
|
+
try:
|
495
|
+
signature = private_key.sign(msg)
|
496
|
+
signature_hex = binascii.hexlify(signature).decode()
|
497
|
+
except Exception as e:
|
498
|
+
log("ERROR", "Failed to sign message", e)
|
499
|
+
return Response(500, content="Failed to sign message")
|
500
|
+
|
501
|
+
return Response(
|
502
|
+
200,
|
503
|
+
content=json.dumps(
|
504
|
+
{"plain_token": plain_token, "signature": signature_hex}
|
505
|
+
),
|
506
|
+
)
|
507
|
+
|
508
|
+
def _check_signature(self, bot: Bot, request: Request) -> Optional[Response]:
|
509
|
+
signature = request.headers.get("X-Signature-Ed25519")
|
510
|
+
timestamp = request.headers.get("X-Signature-Timestamp")
|
511
|
+
if not signature or not timestamp:
|
512
|
+
log("WARNING", "Missing signature or timestamp in request")
|
513
|
+
return Response(403, content="Missing signature or timestamp")
|
514
|
+
|
515
|
+
if request.content is None:
|
516
|
+
return Response(400, content="Missing request content")
|
517
|
+
|
518
|
+
try:
|
519
|
+
private_key = self._get_ed25519_key(bot)
|
520
|
+
public_key = private_key.public_key()
|
521
|
+
except Exception as e:
|
522
|
+
log("ERROR", "Failed to create public key", e)
|
523
|
+
return Response(500, content="Failed to create public key")
|
524
|
+
|
525
|
+
signature = binascii.unhexlify(signature)
|
526
|
+
if len(signature) != 64 or signature[63] & 224 != 0:
|
527
|
+
log("WARNING", "Invalid signature in request")
|
528
|
+
return Response(403, content="Invalid signature")
|
529
|
+
|
530
|
+
body = (
|
531
|
+
request.content.encode()
|
532
|
+
if isinstance(request.content, str)
|
533
|
+
else request.content
|
534
|
+
)
|
535
|
+
msg = timestamp.encode() + body
|
536
|
+
try:
|
537
|
+
public_key.verify(signature, msg)
|
538
|
+
except InvalidSignature:
|
539
|
+
log("WARNING", "Invalid signature in request")
|
540
|
+
return Response(403, content="Invalid signature")
|
541
|
+
except Exception as e:
|
542
|
+
log("ERROR", "Failed to verify signature", e)
|
543
|
+
return Response(403, content="Failed to verify signature")
|
544
|
+
|
380
545
|
def get_auth_base(self) -> URL:
|
381
546
|
return URL(str(self.qq_config.qq_auth_base))
|
382
547
|
|
@@ -387,8 +552,8 @@ class Adapter(BaseAdapter):
|
|
387
552
|
return URL(str(self.qq_config.qq_api_base))
|
388
553
|
|
389
554
|
@staticmethod
|
390
|
-
|
391
|
-
payload = type_validate_json(PayloadType,
|
555
|
+
def data_to_payload(bot: Bot, data: Union[str, bytes]) -> Payload:
|
556
|
+
payload = type_validate_json(PayloadType, data)
|
392
557
|
if isinstance(payload, Dispatch):
|
393
558
|
bot.on_dispatch(payload)
|
394
559
|
return payload
|
@@ -400,6 +565,22 @@ class Adapter(BaseAdapter):
|
|
400
565
|
|
401
566
|
return payload.json(by_alias=True)
|
402
567
|
|
568
|
+
def dispatch_event(self, bot: Bot, payload: Dispatch):
|
569
|
+
try:
|
570
|
+
event = self.payload_to_event(payload)
|
571
|
+
except Exception as e:
|
572
|
+
log(
|
573
|
+
"WARNING",
|
574
|
+
f"Failed to parse event {escape_tag(repr(payload))}",
|
575
|
+
e,
|
576
|
+
)
|
577
|
+
else:
|
578
|
+
if isinstance(event, MessageAuditEvent):
|
579
|
+
audit_result.add_result(event)
|
580
|
+
task = asyncio.create_task(bot.handle_event(event))
|
581
|
+
task.add_done_callback(self.tasks.discard)
|
582
|
+
self.tasks.add(task)
|
583
|
+
|
403
584
|
@staticmethod
|
404
585
|
def payload_to_event(payload: Dispatch) -> Event:
|
405
586
|
EventClass = EVENT_CLASSES.get(payload.type, None)
|
@@ -259,16 +259,14 @@ class Bot(BaseBot):
|
|
259
259
|
|
260
260
|
async def _get_authorization_header(self) -> str:
|
261
261
|
"""获取当前 Bot 的鉴权信息"""
|
262
|
-
|
263
|
-
return f"QQBot {await self.get_access_token()}"
|
264
|
-
return f"Bot {self.bot_info.id}.{self.bot_info.token}"
|
262
|
+
return f"QQBot {await self.get_access_token()}"
|
265
263
|
|
266
264
|
async def get_authorization_header(self) -> dict[str, str]:
|
267
265
|
"""获取当前 Bot 的鉴权信息"""
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
266
|
+
return {
|
267
|
+
"Authorization": await self._get_authorization_header(),
|
268
|
+
"X-Union-Appid": self.bot_info.id,
|
269
|
+
}
|
272
270
|
|
273
271
|
async def handle_event(self, event: Event) -> None:
|
274
272
|
if isinstance(event, (GuildMessageEvent, QQMessageEvent)):
|
@@ -566,9 +564,6 @@ class Bot(BaseBot):
|
|
566
564
|
try:
|
567
565
|
return self._handle_response(response)
|
568
566
|
except UnauthorizedException as e:
|
569
|
-
if not self.bot_info.is_group_bot:
|
570
|
-
raise
|
571
|
-
|
572
567
|
log("DEBUG", "Access token expired, try to refresh it.")
|
573
568
|
|
574
569
|
# try to refresh access token
|
@@ -2,7 +2,7 @@ from typing import Literal, overload
|
|
2
2
|
|
3
3
|
from nonebot.compat import PYDANTIC_V2
|
4
4
|
|
5
|
-
__all__ = ("
|
5
|
+
__all__ = ("field_validator", "model_validator")
|
6
6
|
|
7
7
|
if PYDANTIC_V2:
|
8
8
|
from pydantic import field_validator as field_validator
|
@@ -19,5 +19,5 @@ else:
|
|
19
19
|
def model_validator(*, mode: Literal["before", "after"]):
|
20
20
|
return root_validator(pre=mode == "before", allow_reuse=True)
|
21
21
|
|
22
|
-
def field_validator(
|
23
|
-
return validator(
|
22
|
+
def field_validator(field, /, *fields, mode: Literal["before", "after"] = "after"):
|
23
|
+
return validator(field, *fields, pre=mode == "before", allow_reuse=True)
|
@@ -43,11 +43,6 @@ class Intents(BaseModel):
|
|
43
43
|
| self.at_messages << 30
|
44
44
|
)
|
45
45
|
|
46
|
-
@property
|
47
|
-
def is_group_enabled(self) -> bool:
|
48
|
-
"""是否开启群聊功能"""
|
49
|
-
return self.c2c_group_at_messages is True
|
50
|
-
|
51
46
|
|
52
47
|
class BotInfo(BaseModel):
|
53
48
|
id: str = Field(alias="id")
|
@@ -55,16 +50,13 @@ class BotInfo(BaseModel):
|
|
55
50
|
secret: str = Field(alias="secret")
|
56
51
|
shard: Optional[tuple[int, int]] = None
|
57
52
|
intent: Intents = Field(default_factory=Intents)
|
58
|
-
|
59
|
-
@property
|
60
|
-
def is_group_bot(self) -> bool:
|
61
|
-
"""是否为群机器人"""
|
62
|
-
return self.intent.is_group_enabled
|
53
|
+
use_websocket: bool = True
|
63
54
|
|
64
55
|
|
65
56
|
class Config(BaseModel):
|
66
57
|
qq_is_sandbox: bool = False
|
67
|
-
qq_api_base: HttpUrl = Field("https://api.sgroup.qq.com/")
|
68
|
-
qq_sandbox_api_base: HttpUrl = Field("https://sandbox.api.sgroup.qq.com")
|
69
|
-
qq_auth_base: HttpUrl = Field("https://bots.qq.com/app/getAppAccessToken")
|
58
|
+
qq_api_base: HttpUrl = Field("https://api.sgroup.qq.com/") # type: ignore
|
59
|
+
qq_sandbox_api_base: HttpUrl = Field("https://sandbox.api.sgroup.qq.com") # type: ignore
|
60
|
+
qq_auth_base: HttpUrl = Field("https://bots.qq.com/app/getAppAccessToken") # type: ignore
|
61
|
+
qq_verify_webhook: bool = True
|
70
62
|
qq_bots: list[BotInfo] = Field(default_factory=list)
|
{nonebot_adapter_qq-1.5.2 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/__init__.py
RENAMED
@@ -11,4 +11,5 @@ from .payload import Heartbeat as Heartbeat
|
|
11
11
|
from .payload import Reconnect as Reconnect
|
12
12
|
from .payload import PayloadType as PayloadType
|
13
13
|
from .payload import HeartbeatAck as HeartbeatAck
|
14
|
+
from .payload import WebhookVerify as WebhookVerify
|
14
15
|
from .payload import InvalidSession as InvalidSession
|
@@ -18,6 +18,7 @@ class Opcode(IntEnum):
|
|
18
18
|
HELLO = 10
|
19
19
|
HEARTBEAT_ACK = 11
|
20
20
|
HTTP_CALLBACK_ACK = 12
|
21
|
+
WEBHOOK_VERIFY = 13
|
21
22
|
|
22
23
|
|
23
24
|
class Payload(BaseModel):
|
@@ -41,7 +42,7 @@ class Payload(BaseModel):
|
|
41
42
|
class Dispatch(Payload):
|
42
43
|
opcode: Literal[Opcode.DISPATCH] = Field(Opcode.DISPATCH)
|
43
44
|
data: dict
|
44
|
-
sequence: int
|
45
|
+
sequence: Optional[int] = None
|
45
46
|
type: str
|
46
47
|
id: Optional[str] = None
|
47
48
|
|
@@ -121,9 +122,26 @@ class HTTPCallbackAck(Payload):
|
|
121
122
|
data: int
|
122
123
|
|
123
124
|
|
125
|
+
class WebhookVerifyData(BaseModel):
|
126
|
+
plain_token: str
|
127
|
+
event_ts: str
|
128
|
+
|
129
|
+
if PYDANTIC_V2:
|
130
|
+
model_config: ConfigDict = ConfigDict(extra="allow")
|
131
|
+
else:
|
132
|
+
|
133
|
+
class Config:
|
134
|
+
extra = "allow"
|
135
|
+
|
136
|
+
|
137
|
+
class WebhookVerify(Payload):
|
138
|
+
opcode: Literal[Opcode.WEBHOOK_VERIFY] = Field(Opcode.WEBHOOK_VERIFY)
|
139
|
+
data: WebhookVerifyData
|
140
|
+
|
141
|
+
|
124
142
|
PayloadType = Union[
|
125
143
|
Annotated[
|
126
|
-
Union[Dispatch, Reconnect, InvalidSession, Hello, HeartbeatAck],
|
144
|
+
Union[Dispatch, Reconnect, InvalidSession, Hello, HeartbeatAck, WebhookVerify],
|
127
145
|
Field(discriminator="opcode"),
|
128
146
|
],
|
129
147
|
Payload,
|
@@ -6,7 +6,7 @@ from .event import MessageCreateEvent, AtMessageCreateEvent
|
|
6
6
|
|
7
7
|
|
8
8
|
async def _guild_channel_admin(
|
9
|
-
event: Union[AtMessageCreateEvent, MessageCreateEvent]
|
9
|
+
event: Union[AtMessageCreateEvent, MessageCreateEvent],
|
10
10
|
) -> bool:
|
11
11
|
return "5" in getattr(event.member, "roles", ())
|
12
12
|
|
@@ -27,7 +27,7 @@ GUILD_OWNER: Permission = Permission(_guild_owner)
|
|
27
27
|
"""匹配任意频道群主群消息类型事件"""
|
28
28
|
|
29
29
|
__all__ = [
|
30
|
-
"GUILD_CHANNEL_ADMIN",
|
31
30
|
"GUILD_ADMIN",
|
31
|
+
"GUILD_CHANNEL_ADMIN",
|
32
32
|
"GUILD_OWNER",
|
33
33
|
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "nonebot-adapter-qq"
|
3
|
-
version = "1.
|
3
|
+
version = "1.6.0"
|
4
4
|
description = "QQ adapter for nonebot2"
|
5
5
|
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
6
6
|
license = "MIT"
|
@@ -22,21 +22,15 @@ packages = [{ include = "nonebot" }]
|
|
22
22
|
python = "^3.9"
|
23
23
|
yarl = "^1.9.0"
|
24
24
|
nonebot2 = "^2.2.1"
|
25
|
+
cryptography = ">=43.0.3, <45.0.0"
|
25
26
|
typing-extensions = ">=4.4.0, <5.0.0"
|
26
27
|
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1"
|
27
28
|
|
28
29
|
[tool.poetry.group.dev.dependencies]
|
29
|
-
ruff = "^0.
|
30
|
+
ruff = "^0.8.0"
|
30
31
|
isort = "^5.10.1"
|
31
|
-
black = "^24.0.0"
|
32
32
|
nonemoji = "^0.1.3"
|
33
|
-
pre-commit = "^
|
34
|
-
|
35
|
-
[tool.black]
|
36
|
-
line-length = 88
|
37
|
-
include = '\.pyi?$'
|
38
|
-
extend-exclude = '''
|
39
|
-
'''
|
33
|
+
pre-commit = "^4.0.0"
|
40
34
|
|
41
35
|
[tool.isort]
|
42
36
|
profile = "black"
|
@@ -85,6 +79,8 @@ defineConstant = { PYDANTIC_V2 = true }
|
|
85
79
|
typeCheckingMode = "standard"
|
86
80
|
reportShadowedImports = false
|
87
81
|
disableBytesTypePromotions = true
|
82
|
+
reportIncompatibleMethodOverride = false
|
83
|
+
reportIncompatibleVariableOverride = false
|
88
84
|
|
89
85
|
[build-system]
|
90
86
|
requires = ["poetry-core>=1.0.0"]
|
File without changes
|
File without changes
|
@@ -680,75 +680,75 @@ class GroupMsgReceiveEvent(GroupRobotEvent):
|
|
680
680
|
|
681
681
|
__all__ = [
|
682
682
|
"EVENT_CLASSES",
|
683
|
-
"
|
683
|
+
"AtMessageCreateEvent",
|
684
|
+
"AudioEvent",
|
685
|
+
"AudioFinishEvent",
|
686
|
+
"AudioOffMicEvent",
|
687
|
+
"AudioOnMicEvent",
|
688
|
+
"AudioStartEvent",
|
689
|
+
"C2CMessageCreateEvent",
|
690
|
+
"C2CMsgReceiveEvent",
|
691
|
+
"C2CMsgRejectEvent",
|
692
|
+
"ChannelCreateEvent",
|
693
|
+
"ChannelDeleteEvent",
|
694
|
+
"ChannelEvent",
|
695
|
+
"ChannelUpdateEvent",
|
696
|
+
"DirectMessageCreateEvent",
|
697
|
+
"DirectMessageDeleteEvent",
|
684
698
|
"Event",
|
685
|
-
"
|
686
|
-
"
|
687
|
-
"
|
688
|
-
"
|
689
|
-
"
|
699
|
+
"EventType",
|
700
|
+
"ForumEvent",
|
701
|
+
"ForumPostCreateEvent",
|
702
|
+
"ForumPostDeleteEvent",
|
703
|
+
"ForumPostEvent",
|
704
|
+
"ForumPublishAuditResult",
|
705
|
+
"ForumReplyCreateEvent",
|
706
|
+
"ForumReplyDeleteEvent",
|
707
|
+
"ForumReplyEvent",
|
708
|
+
"ForumThreadCreateEvent",
|
709
|
+
"ForumThreadDeleteEvent",
|
710
|
+
"ForumThreadEvent",
|
711
|
+
"ForumThreadUpdateEvent",
|
712
|
+
"FriendAddEvent",
|
713
|
+
"FriendDelEvent",
|
714
|
+
"FriendRobotEvent",
|
715
|
+
"GroupAddRobotEvent",
|
716
|
+
"GroupAtMessageCreateEvent",
|
717
|
+
"GroupDelRobotEvent",
|
718
|
+
"GroupMsgReceiveEvent",
|
719
|
+
"GroupMsgRejectEvent",
|
720
|
+
"GroupRobotEvent",
|
690
721
|
"GuildCreateEvent",
|
691
|
-
"GuildUpdateEvent",
|
692
722
|
"GuildDeleteEvent",
|
693
|
-
"
|
694
|
-
"ChannelCreateEvent",
|
695
|
-
"ChannelUpdateEvent",
|
696
|
-
"ChannelDeleteEvent",
|
697
|
-
"GuildMemberEvent",
|
723
|
+
"GuildEvent",
|
698
724
|
"GuildMemberAddEvent",
|
699
|
-
"
|
725
|
+
"GuildMemberEvent",
|
700
726
|
"GuildMemberRemoveEvent",
|
701
|
-
"
|
727
|
+
"GuildMemberUpdateEvent",
|
702
728
|
"GuildMessageEvent",
|
703
|
-
"
|
704
|
-
"MessageDeleteEvent",
|
705
|
-
"AtMessageCreateEvent",
|
706
|
-
"PublicMessageDeleteEvent",
|
707
|
-
"DirectMessageCreateEvent",
|
708
|
-
"DirectMessageDeleteEvent",
|
709
|
-
"QQMessageEvent",
|
710
|
-
"C2CMessageCreateEvent",
|
711
|
-
"GroupAtMessageCreateEvent",
|
729
|
+
"GuildUpdateEvent",
|
712
730
|
"InteractionCreateEvent",
|
713
731
|
"MessageAuditEvent",
|
714
732
|
"MessageAuditPassEvent",
|
715
733
|
"MessageAuditRejectEvent",
|
716
|
-
"
|
734
|
+
"MessageCreateEvent",
|
735
|
+
"MessageDeleteEvent",
|
736
|
+
"MessageEvent",
|
717
737
|
"MessageReactionAddEvent",
|
738
|
+
"MessageReactionEvent",
|
718
739
|
"MessageReactionRemoveEvent",
|
719
|
-
"
|
720
|
-
"
|
721
|
-
"AudioFinishEvent",
|
722
|
-
"AudioOnMicEvent",
|
723
|
-
"AudioOffMicEvent",
|
724
|
-
"ForumEvent",
|
725
|
-
"ForumThreadEvent",
|
726
|
-
"ForumThreadCreateEvent",
|
727
|
-
"ForumThreadUpdateEvent",
|
728
|
-
"ForumThreadDeleteEvent",
|
729
|
-
"ForumPostEvent",
|
730
|
-
"ForumPostCreateEvent",
|
731
|
-
"ForumPostDeleteEvent",
|
732
|
-
"ForumReplyEvent",
|
733
|
-
"ForumReplyCreateEvent",
|
734
|
-
"ForumReplyDeleteEvent",
|
735
|
-
"ForumPublishAuditResult",
|
740
|
+
"MetaEvent",
|
741
|
+
"NoticeEvent",
|
736
742
|
"OpenForumEvent",
|
737
|
-
"OpenForumThreadCreateEvent",
|
738
|
-
"OpenForumThreadUpdateEvent",
|
739
|
-
"OpenForumThreadDeleteEvent",
|
740
743
|
"OpenForumPostCreateEvent",
|
741
744
|
"OpenForumPostDeleteEvent",
|
742
745
|
"OpenForumReplyCreateEvent",
|
743
746
|
"OpenForumReplyDeleteEvent",
|
744
|
-
"
|
745
|
-
"
|
746
|
-
"
|
747
|
-
"
|
748
|
-
"
|
749
|
-
"
|
750
|
-
"
|
751
|
-
"GroupDelRobotEvent",
|
752
|
-
"GroupMsgRejectEvent",
|
753
|
-
"GroupMsgReceiveEvent",
|
747
|
+
"OpenForumThreadCreateEvent",
|
748
|
+
"OpenForumThreadDeleteEvent",
|
749
|
+
"OpenForumThreadUpdateEvent",
|
750
|
+
"PublicMessageDeleteEvent",
|
751
|
+
"QQMessageEvent",
|
752
|
+
"ReadyEvent",
|
753
|
+
"ResumedEvent",
|
754
754
|
]
|
File without changes
|
File without changes
|
@@ -165,26 +165,26 @@ class ButtonInteraction(BaseModel):
|
|
165
165
|
|
166
166
|
|
167
167
|
__all__ = [
|
168
|
+
"Action",
|
169
|
+
"Button",
|
170
|
+
"ButtonInteraction",
|
171
|
+
"ButtonInteractionContent",
|
172
|
+
"ButtonInteractionData",
|
173
|
+
"InlineKeyboard",
|
174
|
+
"InlineKeyboardRow",
|
175
|
+
"MessageArk",
|
176
|
+
"MessageArkKv",
|
177
|
+
"MessageArkObj",
|
178
|
+
"MessageArkObjKv",
|
168
179
|
"MessageAttachment",
|
169
|
-
"
|
170
|
-
"MessageEmbedField",
|
180
|
+
"MessageAudited",
|
171
181
|
"MessageEmbed",
|
172
|
-
"
|
173
|
-
"
|
174
|
-
"
|
175
|
-
"MessageArk",
|
176
|
-
"MessageReference",
|
177
|
-
"MessageMarkdownParams",
|
182
|
+
"MessageEmbedField",
|
183
|
+
"MessageEmbedThumbnail",
|
184
|
+
"MessageKeyboard",
|
178
185
|
"MessageMarkdown",
|
186
|
+
"MessageMarkdownParams",
|
187
|
+
"MessageReference",
|
179
188
|
"Permission",
|
180
|
-
"Action",
|
181
189
|
"RenderData",
|
182
|
-
"Button",
|
183
|
-
"InlineKeyboardRow",
|
184
|
-
"InlineKeyboard",
|
185
|
-
"MessageKeyboard",
|
186
|
-
"MessageAudited",
|
187
|
-
"ButtonInteractionContent",
|
188
|
-
"ButtonInteractionData",
|
189
|
-
"ButtonInteraction",
|
190
190
|
]
|
@@ -498,67 +498,67 @@ class ShardUrlGetReturn(BaseModel):
|
|
498
498
|
|
499
499
|
|
500
500
|
__all__ = [
|
501
|
-
"
|
502
|
-
"
|
503
|
-
"
|
504
|
-
"
|
505
|
-
"
|
506
|
-
"
|
501
|
+
"DMS",
|
502
|
+
"APIPermission",
|
503
|
+
"APIPermissionDemand",
|
504
|
+
"APIPermissionDemandIdentify",
|
505
|
+
"Alignment",
|
506
|
+
"Announces",
|
507
|
+
"AudioAction",
|
508
|
+
"AudioControl",
|
509
|
+
"AudioStatus",
|
507
510
|
"Channel",
|
508
|
-
"Member",
|
509
|
-
"GetRoleMembersReturn",
|
510
|
-
"Role",
|
511
|
-
"GetGuildRolesReturn",
|
512
|
-
"PostGuildRoleReturn",
|
513
|
-
"PatchGuildRoleReturn",
|
514
511
|
"ChannelPermissions",
|
512
|
+
"ChannelSubType",
|
513
|
+
"ChannelType",
|
514
|
+
"Elem",
|
515
|
+
"ElemType",
|
516
|
+
"Emoji",
|
517
|
+
"EmojiType",
|
518
|
+
"ForumAuditResult",
|
519
|
+
"ForumAuditType",
|
520
|
+
"ForumSourceInfo",
|
521
|
+
"GetGuildAPIPermissionReturn",
|
522
|
+
"GetGuildRolesReturn",
|
523
|
+
"GetReactionUsersReturn",
|
524
|
+
"GetRoleMembersReturn",
|
525
|
+
"GetThreadReturn",
|
526
|
+
"GetThreadsListReturn",
|
527
|
+
"Guild",
|
528
|
+
"ImageElem",
|
529
|
+
"Member",
|
515
530
|
"Message",
|
516
531
|
"MessageDelete",
|
532
|
+
"MessageReaction",
|
517
533
|
"MessageSetting",
|
518
|
-
"
|
519
|
-
"
|
520
|
-
"
|
534
|
+
"Paragraph",
|
535
|
+
"ParagraphProps",
|
536
|
+
"PatchGuildRoleReturn",
|
521
537
|
"PinsMessage",
|
538
|
+
"Post",
|
539
|
+
"PostGuildRoleReturn",
|
540
|
+
"PostInfo",
|
541
|
+
"PrivateType",
|
542
|
+
"PutThreadReturn",
|
543
|
+
"ReactionTarget",
|
544
|
+
"ReactionTargetType",
|
545
|
+
"RecommendChannel",
|
522
546
|
"RemindType",
|
547
|
+
"Reply",
|
548
|
+
"ReplyInfo",
|
549
|
+
"RichText",
|
550
|
+
"Role",
|
523
551
|
"Schedule",
|
524
|
-
"
|
525
|
-
"
|
526
|
-
"
|
527
|
-
"ReactionTarget",
|
528
|
-
"MessageReaction",
|
529
|
-
"GetReactionUsersReturn",
|
530
|
-
"AudioStatus",
|
531
|
-
"AudioControl",
|
532
|
-
"AudioAction",
|
533
|
-
"ElemType",
|
534
|
-
"TextProps",
|
552
|
+
"SessionStartLimit",
|
553
|
+
"ShardUrlGetReturn",
|
554
|
+
"SpeakPermission",
|
535
555
|
"TextElem",
|
536
|
-
"
|
537
|
-
"VideoElem",
|
538
|
-
"URLElem",
|
539
|
-
"Elem",
|
540
|
-
"Alignment",
|
541
|
-
"ParagraphProps",
|
542
|
-
"Paragraph",
|
543
|
-
"RichText",
|
544
|
-
"ThreadObjectInfo",
|
545
|
-
"ThreadInfo",
|
546
|
-
"ForumSourceInfo",
|
556
|
+
"TextProps",
|
547
557
|
"Thread",
|
548
|
-
"
|
549
|
-
"
|
550
|
-
"
|
551
|
-
"Reply",
|
552
|
-
"ForumAuditType",
|
553
|
-
"ForumAuditResult",
|
554
|
-
"GetThreadsListReturn",
|
555
|
-
"GetThreadReturn",
|
556
|
-
"PutThreadReturn",
|
557
|
-
"APIPermission",
|
558
|
-
"APIPermissionDemandIdentify",
|
559
|
-
"APIPermissionDemand",
|
560
|
-
"GetGuildAPIPermissionReturn",
|
558
|
+
"ThreadInfo",
|
559
|
+
"ThreadObjectInfo",
|
560
|
+
"URLElem",
|
561
561
|
"UrlGetReturn",
|
562
|
-
"
|
563
|
-
"
|
562
|
+
"User",
|
563
|
+
"VideoElem",
|
564
564
|
]
|
@@ -76,15 +76,15 @@ class PostGroupMembersReturn(BaseModel):
|
|
76
76
|
|
77
77
|
|
78
78
|
__all__ = [
|
79
|
+
"Attachment",
|
79
80
|
"FriendAuthor",
|
81
|
+
"GroupMember",
|
80
82
|
"GroupMemberAuthor",
|
81
|
-
"Attachment",
|
82
83
|
"Media",
|
83
|
-
"QQMessage",
|
84
|
-
"PostC2CMessagesReturn",
|
85
|
-
"PostGroupMessagesReturn",
|
86
84
|
"PostC2CFilesReturn",
|
87
|
-
"
|
88
|
-
"PostGroupMembersReturn",
|
85
|
+
"PostC2CMessagesReturn",
|
89
86
|
"PostGroupFilesReturn",
|
87
|
+
"PostGroupMembersReturn",
|
88
|
+
"PostGroupMessagesReturn",
|
89
|
+
"QQMessage",
|
90
90
|
]
|
File without changes
|
File without changes
|