nonebot-adapter-qq 1.5.3__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.
Files changed (20) hide show
  1. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/PKG-INFO +18 -7
  2. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/README.md +16 -6
  3. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/adapter.py +196 -21
  4. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/bot.py +5 -10
  5. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/compat.py +3 -3
  6. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/config.py +5 -13
  7. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/__init__.py +1 -0
  8. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/payload.py +20 -2
  9. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/permission.py +2 -2
  10. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/pyproject.toml +3 -9
  11. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/LICENSE +0 -0
  12. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/__init__.py +0 -0
  13. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/event.py +54 -54
  14. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/exception.py +0 -0
  15. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/message.py +0 -0
  16. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/common.py +17 -17
  17. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/guild.py +52 -52
  18. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/models/qq.py +6 -6
  19. {nonebot_adapter_qq-1.5.3 → nonebot_adapter_qq-1.6.0}/nonebot/adapters/qq/store.py +0 -0
  20. {nonebot_adapter_qq-1.5.3 → 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.5.3
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
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Dist: cryptography (>=43.0.3,<45.0.0)
22
23
  Requires-Dist: nonebot2 (>=2.2.1,<3.0.0)
23
24
  Requires-Dist: pydantic (>=1.10.0,<3.0.0,!=2.5.0,!=2.5.1)
24
25
  Requires-Dist: typing-extensions (>=4.4.0,<5.0.0)
@@ -45,15 +46,19 @@ _✨ QQ 协议适配 ✨_
45
46
 
46
47
  ### Driver
47
48
 
48
- 参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。
49
-
50
- 如:
49
+ 如果使用 WebSocket 连接方式,请参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。如:
51
50
 
52
51
  ```dotenv
53
52
  DRIVER=~httpx+~websockets
54
53
  DRIVER=~aiohttp
55
54
  ```
56
55
 
56
+ 如果使用 WebHook 连接方式,则添加 `ASGIServer` 支持。如:
57
+
58
+ ```dotenv
59
+ DRIVER=~fastapi
60
+ ```
61
+
57
62
  ### QQ_IS_SANDBOX
58
63
 
59
64
  是否为沙盒模式,默认为 `False`。
@@ -66,9 +71,13 @@ QQ_IS_SANDBOX=true
66
71
 
67
72
  配置机器人帐号 `id` `token` `secret`,intent 需要根据机器人类型以及需要的事件进行配置。
68
73
 
74
+ #### Webhook / WebSocket
75
+
76
+ 通过配置项 `use_websocket` 来选择是否启用 WebSocket 连接,当前默认为 `True`。如果关闭 WebSocket 连接方式,则可以通过 WebHook 方式来连接机器人,请在 QQ 开放平台中配置机器人回调地址:`https://host:port/qq/webhook`。
77
+
69
78
  #### Intent
70
79
 
71
- 以下为所有 Intent 配置项以及默认值:
80
+ Intent 仅对 WebSocket 连接方式生效。以下为所有 Intent 配置项以及默认值:
72
81
 
73
82
  ```json
74
83
  {
@@ -102,7 +111,8 @@ QQ_BOTS='
102
111
  "intent": {
103
112
  "guild_messages": true,
104
113
  "at_messages": false
105
- }
114
+ },
115
+ "use_websocket": false
106
116
  }
107
117
  ]
108
118
  '
@@ -119,7 +129,8 @@ QQ_BOTS='
119
129
  "secret": "xxx",
120
130
  "intent": {
121
131
  "c2c_group_at_messages": true
122
- }
132
+ },
133
+ "use_websocket": false
123
134
  }
124
135
  ]
125
136
  '
@@ -16,15 +16,19 @@ _✨ QQ 协议适配 ✨_
16
16
 
17
17
  ### Driver
18
18
 
19
- 参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。
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
- 以下为所有 Intent 配置项以及默认值:
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,7 +53,7 @@ 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(**self.config.dict())
56
+ self.qq_config: Config = get_plugin_config(Config)
48
57
 
49
58
  self.tasks: set["asyncio.Task"] = set()
50
59
  self.setup()
@@ -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
- if not isinstance(self.driver, WebSocketClientMixin):
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,36 @@ 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
- task = asyncio.create_task(self.run_bot(bot))
135
+ if not bot.use_websocket:
136
+ continue
137
+ task = asyncio.create_task(self.run_bot_websocket(bot))
89
138
  task.add_done_callback(self.tasks.discard)
90
139
  self.tasks.add(task)
91
140
 
@@ -99,7 +148,7 @@ class Adapter(BaseAdapter):
99
148
  return_exceptions=True,
100
149
  )
101
150
 
102
- async def run_bot(self, bot_info: BotInfo) -> None:
151
+ async def run_bot_websocket(self, bot_info: BotInfo) -> None:
103
152
  bot = Bot(self, bot_info.id, bot_info)
104
153
 
105
154
  # get sharded gateway url
@@ -347,20 +396,7 @@ class Adapter(BaseAdapter):
347
396
  f"Received payload: {escape_tag(repr(payload))}",
348
397
  )
349
398
  if isinstance(payload, Dispatch):
350
- try:
351
- event = self.payload_to_event(payload)
352
- except Exception as e:
353
- log(
354
- "WARNING",
355
- f"Failed to parse event {escape_tag(repr(payload))}",
356
- e,
357
- )
358
- else:
359
- if isinstance(event, MessageAuditEvent):
360
- audit_result.add_result(event)
361
- task = asyncio.create_task(bot.handle_event(event))
362
- task.add_done_callback(self.tasks.discard)
363
- self.tasks.add(task)
399
+ self.dispatch_event(bot, payload)
364
400
  elif isinstance(payload, HeartbeatAck):
365
401
  log("TRACE", "Heartbeat ACK")
366
402
  continue
@@ -383,6 +419,129 @@ class Adapter(BaseAdapter):
383
419
  f"Unknown payload from server: {escape_tag(repr(payload))}",
384
420
  )
385
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
+
386
545
  def get_auth_base(self) -> URL:
387
546
  return URL(str(self.qq_config.qq_auth_base))
388
547
 
@@ -393,8 +552,8 @@ class Adapter(BaseAdapter):
393
552
  return URL(str(self.qq_config.qq_api_base))
394
553
 
395
554
  @staticmethod
396
- async def receive_payload(bot: Bot, ws: WebSocket) -> Payload:
397
- payload = type_validate_json(PayloadType, await ws.receive())
555
+ def data_to_payload(bot: Bot, data: Union[str, bytes]) -> Payload:
556
+ payload = type_validate_json(PayloadType, data)
398
557
  if isinstance(payload, Dispatch):
399
558
  bot.on_dispatch(payload)
400
559
  return payload
@@ -406,6 +565,22 @@ class Adapter(BaseAdapter):
406
565
 
407
566
  return payload.json(by_alias=True)
408
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
+
409
584
  @staticmethod
410
585
  def payload_to_event(payload: Dispatch) -> Event:
411
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
- if self.bot_info.is_group_bot:
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
- headers = {"Authorization": await self._get_authorization_header()}
269
- if self.bot_info.is_group_bot:
270
- headers["X-Union-Appid"] = self.bot_info.id
271
- return headers
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__ = ("model_validator", "field_validator")
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(__field, *fields, mode: Literal["before", "after"] = "after"):
23
- return validator(__field, *fields, pre=mode == "before", allow_reuse=True)
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)
@@ -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.5.3"
3
+ version = "1.6.0"
4
4
  description = "QQ adapter for nonebot2"
5
5
  authors = ["yanyongyu <yyy@nonebot.dev>"]
6
6
  license = "MIT"
@@ -22,22 +22,16 @@ 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.7.0"
30
+ ruff = "^0.8.0"
30
31
  isort = "^5.10.1"
31
- black = "^24.0.0"
32
32
  nonemoji = "^0.1.3"
33
33
  pre-commit = "^4.0.0"
34
34
 
35
- [tool.black]
36
- line-length = 88
37
- include = '\.pyi?$'
38
- extend-exclude = '''
39
- '''
40
-
41
35
  [tool.isort]
42
36
  profile = "black"
43
37
  line_length = 88
@@ -680,75 +680,75 @@ class GroupMsgReceiveEvent(GroupRobotEvent):
680
680
 
681
681
  __all__ = [
682
682
  "EVENT_CLASSES",
683
- "EventType",
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
- "MetaEvent",
686
- "ReadyEvent",
687
- "ResumedEvent",
688
- "NoticeEvent",
689
- "GuildEvent",
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
- "ChannelEvent",
694
- "ChannelCreateEvent",
695
- "ChannelUpdateEvent",
696
- "ChannelDeleteEvent",
697
- "GuildMemberEvent",
723
+ "GuildEvent",
698
724
  "GuildMemberAddEvent",
699
- "GuildMemberUpdateEvent",
725
+ "GuildMemberEvent",
700
726
  "GuildMemberRemoveEvent",
701
- "MessageEvent",
727
+ "GuildMemberUpdateEvent",
702
728
  "GuildMessageEvent",
703
- "MessageCreateEvent",
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
- "MessageReactionEvent",
734
+ "MessageCreateEvent",
735
+ "MessageDeleteEvent",
736
+ "MessageEvent",
717
737
  "MessageReactionAddEvent",
738
+ "MessageReactionEvent",
718
739
  "MessageReactionRemoveEvent",
719
- "AudioEvent",
720
- "AudioStartEvent",
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
- "FriendRobotEvent",
745
- "FriendAddEvent",
746
- "FriendDelEvent",
747
- "C2CMsgRejectEvent",
748
- "C2CMsgReceiveEvent",
749
- "GroupRobotEvent",
750
- "GroupAddRobotEvent",
751
- "GroupDelRobotEvent",
752
- "GroupMsgRejectEvent",
753
- "GroupMsgReceiveEvent",
747
+ "OpenForumThreadCreateEvent",
748
+ "OpenForumThreadDeleteEvent",
749
+ "OpenForumThreadUpdateEvent",
750
+ "PublicMessageDeleteEvent",
751
+ "QQMessageEvent",
752
+ "ReadyEvent",
753
+ "ResumedEvent",
754
754
  ]
@@ -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
- "MessageEmbedThumbnail",
170
- "MessageEmbedField",
180
+ "MessageAudited",
171
181
  "MessageEmbed",
172
- "MessageArkObjKv",
173
- "MessageArkObj",
174
- "MessageArkKv",
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
- "Guild",
502
- "User",
503
- "ChannelType",
504
- "ChannelSubType",
505
- "PrivateType",
506
- "SpeakPermission",
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
- "DMS",
519
- "RecommendChannel",
520
- "Announces",
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
- "EmojiType",
525
- "Emoji",
526
- "ReactionTargetType",
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
- "ImageElem",
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
- "PostInfo",
549
- "Post",
550
- "ReplyInfo",
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
- "SessionStartLimit",
563
- "ShardUrlGetReturn",
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
- "GroupMember",
88
- "PostGroupMembersReturn",
85
+ "PostC2CMessagesReturn",
89
86
  "PostGroupFilesReturn",
87
+ "PostGroupMembersReturn",
88
+ "PostGroupMessagesReturn",
89
+ "QQMessage",
90
90
  ]