nonebot-adapter-qq 1.5.2__py3-none-any.whl → 1.6.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.
@@ -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(**self.config.dict())
56
+ self.qq_config: Config = get_plugin_config(Config)
48
57
 
49
- self.tasks: list["asyncio.Task"] = []
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
- 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,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
- self.tasks.append(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))
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 run_bot(self, bot_info: BotInfo) -> None:
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.tasks.append(
127
- asyncio.create_task(self._forward_ws(bot, ws_url, bot_info.shard))
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.tasks.append(
135
- asyncio.create_task(self._forward_ws(bot, ws_url, (i, shards)))
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)) # noqa: RUF006
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
- try:
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
- async def receive_payload(bot: Bot, ws: WebSocket) -> Payload:
391
- 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)
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
- 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)
@@ -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
  ]
@@ -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
@@ -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
  ]
@@ -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,
@@ -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
  ]
@@ -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
  Metadata-Version: 2.1
2
2
  Name: nonebot-adapter-qq
3
- Version: 1.5.2
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
- 参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `HTTPClient` 和 `WebSocketClient` 支持。
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
- 以下为所有 Intent 配置项以及默认值:
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
  '
@@ -0,0 +1,20 @@
1
+ nonebot/adapters/qq/__init__.py,sha256=jm1QxHfhcYIrWhkXxF9gE4G3lkagrLCWsmVqKBTQSOw,815
2
+ nonebot/adapters/qq/adapter.py,sha256=-wtvqeK-LmqhTkWtTwwjdh6K84GGEAmLrbUNfnbXMRY,21177
3
+ nonebot/adapters/qq/bot.py,sha256=A3nrkdgXj_AtzN1kebgkhfOjmAyI-s6f0ShQxJVu3wc,61167
4
+ nonebot/adapters/qq/compat.py,sha256=Fr96IgBHHiEqGm8DVmlTekuaNdI5EPxti7J7HDwOHwg,767
5
+ nonebot/adapters/qq/config.py,sha256=YRVpGNvslGcNHNbbFN9LOjZ98-yon5x9GMF98FVJ2xo,1974
6
+ nonebot/adapters/qq/event.py,sha256=3A78iz-G6wC-KsW5gYQM16veqgCtjWWsb8AysXhDOK4,18802
7
+ nonebot/adapters/qq/exception.py,sha256=8Xa9NwYyO7Ih0owBAC_zmEiIRAA8GjgQcoJcF5ogo0g,2701
8
+ nonebot/adapters/qq/message.py,sha256=q4dhlpuQ-Fyiqj2_cL1TCaZutdF41fcyGXpiI6RucVg,15526
9
+ nonebot/adapters/qq/models/__init__.py,sha256=q2OhopcTn0UD2wuep_86dXMVhF_wQBzBDhyGLN1q0OE,631
10
+ nonebot/adapters/qq/models/common.py,sha256=Hoot6Qr3PGj13G4Cz2Eyr-liaLiJXBpxpeM59IXBri0,4539
11
+ nonebot/adapters/qq/models/guild.py,sha256=uKvSijWFMJ0x26Gd04b8QbUr8IOIn3U3m1d-I6xTrcI,11686
12
+ nonebot/adapters/qq/models/payload.py,sha256=IhAx2W5rN8ihg2NchQ9rJMfcxDecwnnJ-5_N-ky8deM,3261
13
+ nonebot/adapters/qq/models/qq.py,sha256=giLeCJ17YDoP8tdjlL99jr5WUguvRJVabQPriV8kmFc,1853
14
+ nonebot/adapters/qq/permission.py,sha256=Jw00btklCMviP001MBlLQmqThb9OsjJwHO1nTEDFRWM,989
15
+ nonebot/adapters/qq/store.py,sha256=GKVbto6K7jI-oK9tSbw_xLxDiSA6Y9QnNQStKJU7mEY,868
16
+ nonebot/adapters/qq/utils.py,sha256=bzJwm1zWyHjFILr-bHrBKpKlkiveph9S4pLAXMcW9AY,1538
17
+ nonebot_adapter_qq-1.6.0.dist-info/LICENSE,sha256=4EZBnkZPQYBvtO2KFGaUQHTO8nZ26pGeaCFchdBq_AE,1064
18
+ nonebot_adapter_qq-1.6.0.dist-info/METADATA,sha256=IqQhCethVtQJt4_AuEyX1gHfs0cdWPX-bwx-pDh8ZHw,3383
19
+ nonebot_adapter_qq-1.6.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
20
+ nonebot_adapter_qq-1.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,20 +0,0 @@
1
- nonebot/adapters/qq/__init__.py,sha256=jm1QxHfhcYIrWhkXxF9gE4G3lkagrLCWsmVqKBTQSOw,815
2
- nonebot/adapters/qq/adapter.py,sha256=Fa1dVykpC_-mIVgN7b37k45Ecu3cddt2KWwsZSJqrp4,14591
3
- nonebot/adapters/qq/bot.py,sha256=AA2eGpTZxHRTj3Z7mzCa3K77QdKSL6O-wTcEJ4F7bmE,61394
4
- nonebot/adapters/qq/compat.py,sha256=7g5QvRzWREesZ7MSEhNRh53UZwEyhQlxE3e_KpqQ0pw,768
5
- nonebot/adapters/qq/config.py,sha256=vvTZ4JRJEQ60PyLh92IEnQkOltdUs41lGKZuYXxq6yA,2135
6
- nonebot/adapters/qq/event.py,sha256=9URqXn-U0K2JwYR1TJkNjejWB1C_-LnflLZPsTNn32I,18802
7
- nonebot/adapters/qq/exception.py,sha256=8Xa9NwYyO7Ih0owBAC_zmEiIRAA8GjgQcoJcF5ogo0g,2701
8
- nonebot/adapters/qq/message.py,sha256=q4dhlpuQ-Fyiqj2_cL1TCaZutdF41fcyGXpiI6RucVg,15526
9
- nonebot/adapters/qq/models/__init__.py,sha256=VsLh2rceTl9fK4hReYXBosOPZj6E1x7lDUdk7py_R8A,579
10
- nonebot/adapters/qq/models/common.py,sha256=oQDNaPUNUO64-_bgQdHobQyZLdtdDNnkykzGjbRdAUM,4539
11
- nonebot/adapters/qq/models/guild.py,sha256=8xyFLwXXzIk-LxqzSlkfkuJfPgxxIkLYWsrPRcX4lsM,11686
12
- nonebot/adapters/qq/models/payload.py,sha256=_Q-4yHlSlzb5Lm2MLv8QxQ_ADVbjWJt3SCWN3_niH-Q,2851
13
- nonebot/adapters/qq/models/qq.py,sha256=9gPQZBuuZEOvAsk7a6-w0Gh0Scf4qQL9y08pubdfk1o,1853
14
- nonebot/adapters/qq/permission.py,sha256=0aedypl6xymhAV3UkW0vlvHTBkJeDmOef3SxG2tPcHQ,988
15
- nonebot/adapters/qq/store.py,sha256=GKVbto6K7jI-oK9tSbw_xLxDiSA6Y9QnNQStKJU7mEY,868
16
- nonebot/adapters/qq/utils.py,sha256=bzJwm1zWyHjFILr-bHrBKpKlkiveph9S4pLAXMcW9AY,1538
17
- nonebot_adapter_qq-1.5.2.dist-info/LICENSE,sha256=4EZBnkZPQYBvtO2KFGaUQHTO8nZ26pGeaCFchdBq_AE,1064
18
- nonebot_adapter_qq-1.5.2.dist-info/METADATA,sha256=8qgJba66rQN3Cd3OEp_btLfeSIck14KQrMAWKwwE1eg,2735
19
- nonebot_adapter_qq-1.5.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
- nonebot_adapter_qq-1.5.2.dist-info/RECORD,,