nonebot-adapter-qq 1.5.3__py3-none-any.whl → 1.6.1__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,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,20 +70,29 @@ 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
 
73
94
  async def startup(self) -> None:
74
- log(
75
- "DEBUG",
76
- ("QQ run in sandbox mode: " f"<y>{self.qq_config.qq_is_sandbox}</y>"),
77
- )
95
+ log("DEBUG", f"QQ run in sandbox mode: <y>{self.qq_config.qq_is_sandbox}</y>")
78
96
 
79
97
  try:
80
98
  api_base = self.get_api_base()
@@ -84,8 +102,36 @@ class Adapter(BaseAdapter):
84
102
 
85
103
  log("DEBUG", f"QQ api base url: <y>{escape_tag(str(api_base))}</y>")
86
104
 
105
+ if isinstance(self.driver, ASGIMixin):
106
+ self.setup_http_server(
107
+ HTTPServerSetup(
108
+ URL("/qq/"),
109
+ "POST",
110
+ f"{self.get_name()} Root Webhook",
111
+ self._handle_http,
112
+ ),
113
+ )
114
+ self.setup_http_server(
115
+ HTTPServerSetup(
116
+ URL("/qq/webhook"),
117
+ "POST",
118
+ f"{self.get_name()} Webhook",
119
+ self._handle_http,
120
+ ),
121
+ )
122
+ self.setup_http_server(
123
+ HTTPServerSetup(
124
+ URL("/qq/webhook/"),
125
+ "POST",
126
+ f"{self.get_name()} Webhook Slash",
127
+ self._handle_http,
128
+ ),
129
+ )
130
+
87
131
  for bot in self.qq_config.qq_bots:
88
- task = asyncio.create_task(self.run_bot(bot))
132
+ if not bot.use_websocket:
133
+ continue
134
+ task = asyncio.create_task(self.run_bot_websocket(bot))
89
135
  task.add_done_callback(self.tasks.discard)
90
136
  self.tasks.add(task)
91
137
 
@@ -99,7 +145,7 @@ class Adapter(BaseAdapter):
99
145
  return_exceptions=True,
100
146
  )
101
147
 
102
- async def run_bot(self, bot_info: BotInfo) -> None:
148
+ async def run_bot_websocket(self, bot_info: BotInfo) -> None:
103
149
  bot = Bot(self, bot_info.id, bot_info)
104
150
 
105
151
  # get sharded gateway url
@@ -347,20 +393,7 @@ class Adapter(BaseAdapter):
347
393
  f"Received payload: {escape_tag(repr(payload))}",
348
394
  )
349
395
  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)
396
+ self.dispatch_event(bot, payload)
364
397
  elif isinstance(payload, HeartbeatAck):
365
398
  log("TRACE", "Heartbeat ACK")
366
399
  continue
@@ -383,6 +416,133 @@ class Adapter(BaseAdapter):
383
416
  f"Unknown payload from server: {escape_tag(repr(payload))}",
384
417
  )
385
418
 
419
+ async def receive_payload(self, bot: Bot, ws: WebSocket) -> Payload:
420
+ return self.data_to_payload(bot, await ws.receive())
421
+
422
+ async def _handle_http(self, request: Request) -> Response:
423
+ bot_id = request.headers.get("X-Bot-Appid")
424
+ if not bot_id:
425
+ log("WARNING", "Missing X-Bot-Appid header in request")
426
+ return Response(403, content="Missing X-Bot-Appid header")
427
+ elif bot_id in self.bots:
428
+ bot = cast(Bot, self.bots[bot_id])
429
+ elif bot_info := next(
430
+ (bot_info for bot_info in self.qq_config.qq_bots if bot_info.id == bot_id),
431
+ None,
432
+ ):
433
+ bot = Bot(self, bot_id, bot_info)
434
+ else:
435
+ log("ERROR", f"Bot {bot_id} not found")
436
+ return Response(403, content="Bot not found")
437
+
438
+ if request.content is None:
439
+ return Response(400, content="Missing request content")
440
+
441
+ try:
442
+ payload = self.data_to_payload(bot, request.content)
443
+ except Exception as e:
444
+ log(
445
+ "ERROR",
446
+ "<r><bg #f8bbd0>Error while parsing data from webhook</bg #f8bbd0></r>",
447
+ e,
448
+ )
449
+ return Response(400, content="Invalid request content")
450
+
451
+ log(
452
+ "TRACE",
453
+ f"Received payload: {escape_tag(repr(payload))}",
454
+ )
455
+ if isinstance(payload, WebhookVerify):
456
+ log("INFO", "Received qq webhook verify request")
457
+ return self._webhook_verify(bot, payload)
458
+
459
+ if self.qq_config.qq_verify_webhook and (
460
+ response := self._check_signature(bot, request)
461
+ ):
462
+ return response
463
+
464
+ # ensure bot self info
465
+ if not bot._self_info:
466
+ bot.self_info = await bot.me()
467
+
468
+ if bot.self_id not in self.bots:
469
+ self.bot_connect(bot)
470
+
471
+ if isinstance(payload, Dispatch):
472
+ self.dispatch_event(bot, payload)
473
+
474
+ return Response(200)
475
+
476
+ def _get_ed25519_key(self, bot: Bot) -> Ed25519PrivateKey:
477
+ secret = bot.bot_info.secret.encode()
478
+ seed = secret
479
+ while len(seed) < 32:
480
+ seed += secret
481
+ seed = seed[:32]
482
+ return Ed25519PrivateKey.from_private_bytes(seed)
483
+
484
+ def _webhook_verify(self, bot: Bot, payload: WebhookVerify) -> Response:
485
+ plain_token = payload.data.plain_token
486
+ event_ts = payload.data.event_ts
487
+
488
+ try:
489
+ private_key = self._get_ed25519_key(bot)
490
+ except Exception as e:
491
+ log("ERROR", "Failed to create private key", e)
492
+ return Response(500, content="Failed to create private key")
493
+
494
+ msg = f"{event_ts}{plain_token}".encode()
495
+ try:
496
+ signature = private_key.sign(msg)
497
+ signature_hex = binascii.hexlify(signature).decode()
498
+ except Exception as e:
499
+ log("ERROR", "Failed to sign message", e)
500
+ return Response(500, content="Failed to sign message")
501
+
502
+ return Response(
503
+ 200,
504
+ content=json.dumps(
505
+ {"plain_token": plain_token, "signature": signature_hex}
506
+ ),
507
+ )
508
+
509
+ def _check_signature(self, bot: Bot, request: Request) -> Optional[Response]:
510
+ signature = request.headers.get("X-Signature-Ed25519")
511
+ timestamp = request.headers.get("X-Signature-Timestamp")
512
+ if not signature or not timestamp:
513
+ log("WARNING", "Missing signature or timestamp in request")
514
+ return Response(403, content="Missing signature or timestamp")
515
+
516
+ if request.content is None:
517
+ return Response(400, content="Missing request content")
518
+
519
+ try:
520
+ private_key = self._get_ed25519_key(bot)
521
+ public_key = private_key.public_key()
522
+ except Exception as e:
523
+ log("ERROR", "Failed to create public key", e)
524
+ return Response(500, content="Failed to create public key")
525
+
526
+ signature = binascii.unhexlify(signature)
527
+ if len(signature) != 64 or signature[63] & 224 != 0:
528
+ log("WARNING", "Invalid signature in request")
529
+ return Response(403, content="Invalid signature")
530
+
531
+ body = (
532
+ request.content.encode()
533
+ if isinstance(request.content, str)
534
+ else request.content
535
+ )
536
+ msg = timestamp.encode() + body
537
+ try:
538
+ public_key.verify(signature, msg)
539
+ except InvalidSignature:
540
+ log("WARNING", "Invalid signature in request")
541
+ return Response(403, content="Invalid signature")
542
+ except Exception as e:
543
+ log("ERROR", "Failed to verify signature", e)
544
+ return Response(403, content="Failed to verify signature")
545
+
386
546
  def get_auth_base(self) -> URL:
387
547
  return URL(str(self.qq_config.qq_auth_base))
388
548
 
@@ -393,8 +553,8 @@ class Adapter(BaseAdapter):
393
553
  return URL(str(self.qq_config.qq_api_base))
394
554
 
395
555
  @staticmethod
396
- async def receive_payload(bot: Bot, ws: WebSocket) -> Payload:
397
- payload = type_validate_json(PayloadType, await ws.receive())
556
+ def data_to_payload(bot: Bot, data: Union[str, bytes]) -> Payload:
557
+ payload = type_validate_json(PayloadType, data)
398
558
  if isinstance(payload, Dispatch):
399
559
  bot.on_dispatch(payload)
400
560
  return payload
@@ -406,6 +566,22 @@ class Adapter(BaseAdapter):
406
566
 
407
567
  return payload.json(by_alias=True)
408
568
 
569
+ def dispatch_event(self, bot: Bot, payload: Dispatch):
570
+ try:
571
+ event = self.payload_to_event(payload)
572
+ except Exception as e:
573
+ log(
574
+ "WARNING",
575
+ f"Failed to parse event {escape_tag(repr(payload))}",
576
+ e,
577
+ )
578
+ else:
579
+ if isinstance(event, MessageAuditEvent):
580
+ audit_result.add_result(event)
581
+ task = asyncio.create_task(bot.handle_event(event))
582
+ task.add_done_callback(self.tasks.discard)
583
+ self.tasks.add(task)
584
+
409
585
  @staticmethod
410
586
  def payload_to_event(payload: Dispatch) -> Event:
411
587
  EventClass = EVENT_CLASSES.get(payload.type, None)
@@ -200,6 +200,10 @@ class Bot(BaseBot):
200
200
  raise RuntimeError(f"Bot {self.bot_info} is not connected!")
201
201
  return self._self_info
202
202
 
203
+ @self_info.setter
204
+ def self_info(self, info: User):
205
+ self._self_info = info
206
+
203
207
  @property
204
208
  def ready(self) -> bool:
205
209
  """Bot 是否已经准备就绪"""
@@ -259,16 +263,14 @@ class Bot(BaseBot):
259
263
 
260
264
  async def _get_authorization_header(self) -> str:
261
265
  """获取当前 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}"
266
+ return f"QQBot {await self.get_access_token()}"
265
267
 
266
268
  async def get_authorization_header(self) -> dict[str, str]:
267
269
  """获取当前 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
270
+ return {
271
+ "Authorization": await self._get_authorization_header(),
272
+ "X-Union-Appid": self.bot_info.id,
273
+ }
272
274
 
273
275
  async def handle_event(self, event: Event) -> None:
274
276
  if isinstance(event, (GuildMessageEvent, QQMessageEvent)):
@@ -283,9 +285,11 @@ class Bot(BaseBot):
283
285
  return _message
284
286
 
285
287
  @staticmethod
286
- def _extract_send_message(message: Message) -> dict[str, Any]:
288
+ def _extract_send_message(
289
+ message: Message, escape_text: bool = True
290
+ ) -> dict[str, Any]:
287
291
  kwargs = {}
288
- content = message.extract_content() or None
292
+ content = message.extract_content(escape_text) or None
289
293
  kwargs["content"] = content
290
294
  if embed := (message["embed"] or None):
291
295
  kwargs["embed"] = embed[-1].data["embed"]
@@ -349,7 +353,7 @@ class Bot(BaseBot):
349
353
  guild_id=guild_id,
350
354
  msg_id=msg_id,
351
355
  event_id=event_id,
352
- **self._extract_send_message(message=message),
356
+ **self._extract_send_message(message=message, escape_text=True),
353
357
  **self._extract_guild_image(message=message),
354
358
  )
355
359
 
@@ -365,7 +369,7 @@ class Bot(BaseBot):
365
369
  channel_id=channel_id,
366
370
  msg_id=msg_id,
367
371
  event_id=event_id,
368
- **self._extract_send_message(message=message),
372
+ **self._extract_send_message(message=message, escape_text=True),
369
373
  **self._extract_guild_image(message=message),
370
374
  )
371
375
 
@@ -378,7 +382,7 @@ class Bot(BaseBot):
378
382
  event_id: Optional[str] = None,
379
383
  ) -> Union[PostC2CMessagesReturn, PostC2CFilesReturn]:
380
384
  message = self._prepare_message(message)
381
- kwargs = self._extract_send_message(message=message)
385
+ kwargs = self._extract_send_message(message=message, escape_text=False)
382
386
  if kwargs.get("embed"):
383
387
  msg_type = 4
384
388
  elif kwargs.get("ark"):
@@ -427,7 +431,7 @@ class Bot(BaseBot):
427
431
  event_id: Optional[str] = None,
428
432
  ) -> Union[PostGroupMessagesReturn, PostGroupFilesReturn]:
429
433
  message = self._prepare_message(message)
430
- kwargs = self._extract_send_message(message=message)
434
+ kwargs = self._extract_send_message(message=message, escape_text=False)
431
435
  if kwargs.get("embed"):
432
436
  msg_type = 4
433
437
  elif kwargs.get("ark"):
@@ -566,9 +570,6 @@ class Bot(BaseBot):
566
570
  try:
567
571
  return self._handle_response(response)
568
572
  except UnauthorizedException as e:
569
- if not self.bot_info.is_group_bot:
570
- raise
571
-
572
573
  log("DEBUG", "Access token expired, try to refresh it.")
573
574
 
574
575
  # 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
  ]
@@ -532,9 +532,9 @@ class Message(BaseMessage[MessageSegment]):
532
532
  )
533
533
  return msg
534
534
 
535
- def extract_content(self) -> str:
535
+ def extract_content(self, escape_text: bool = True) -> str:
536
536
  return "".join(
537
- str(seg)
537
+ seg.data["text"] if not escape_text and seg.type == "text" else str(seg)
538
538
  for seg in self
539
539
  if seg.type
540
540
  in ("text", "emoji", "mention_user", "mention_everyone", "mention_channel")
@@ -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,
@@ -10,11 +10,13 @@ from nonebot.adapters.qq.compat import field_validator
10
10
  class FriendAuthor(BaseModel):
11
11
  id: str
12
12
  user_openid: str
13
+ union_openid: Optional[str] = None
13
14
 
14
15
 
15
16
  class GroupMemberAuthor(BaseModel):
16
17
  id: str
17
18
  member_openid: str
19
+ union_openid: Optional[str] = None
18
20
 
19
21
 
20
22
  class Attachment(BaseModel):
@@ -76,15 +78,15 @@ class PostGroupMembersReturn(BaseModel):
76
78
 
77
79
 
78
80
  __all__ = [
81
+ "Attachment",
79
82
  "FriendAuthor",
83
+ "GroupMember",
80
84
  "GroupMemberAuthor",
81
- "Attachment",
82
85
  "Media",
83
- "QQMessage",
84
- "PostC2CMessagesReturn",
85
- "PostGroupMessagesReturn",
86
86
  "PostC2CFilesReturn",
87
- "GroupMember",
88
- "PostGroupMembersReturn",
87
+ "PostC2CMessagesReturn",
89
88
  "PostGroupFilesReturn",
89
+ "PostGroupMembersReturn",
90
+ "PostGroupMessagesReturn",
91
+ "QQMessage",
90
92
  ]
@@ -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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: nonebot-adapter-qq
3
- Version: 1.5.3
3
+ Version: 1.6.1
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
  '
@@ -0,0 +1,20 @@
1
+ nonebot/adapters/qq/__init__.py,sha256=jm1QxHfhcYIrWhkXxF9gE4G3lkagrLCWsmVqKBTQSOw,815
2
+ nonebot/adapters/qq/adapter.py,sha256=UwAjO168oWmvb3frjay4CTC54xq3hsCiVbeFxRwaGxI,21243
3
+ nonebot/adapters/qq/bot.py,sha256=eYW4j4Vc0IBGmpZATal-zpq1BVjWrwL-Ff9Fy2tTWIM,61383
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=74H7L3FTpZu5GAm4j3wnVYf7wJQ_kP9HHjqaXn3uRTQ,15616
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=JH-1tC7ujwHpuJtPefGOJh2NQjrxAL7L-aRyu7xqplU,1931
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.1.dist-info/LICENSE,sha256=4EZBnkZPQYBvtO2KFGaUQHTO8nZ26pGeaCFchdBq_AE,1064
18
+ nonebot_adapter_qq-1.6.1.dist-info/METADATA,sha256=nAY5TQGs53DbKIJTI9dO-A0k4bh6hfyQ8UJjfaEi7ss,3383
19
+ nonebot_adapter_qq-1.6.1.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
20
+ nonebot_adapter_qq-1.6.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.0
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=mK_lTLADDyV-rXDlsgb4noYeJNnxlHxf-iWZBvxU9uY,14935
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.3.dist-info/LICENSE,sha256=4EZBnkZPQYBvtO2KFGaUQHTO8nZ26pGeaCFchdBq_AE,1064
18
- nonebot_adapter_qq-1.5.3.dist-info/METADATA,sha256=VriHr0yg5zp4oD6wVkuN0oRgs5XhY3OJry1uNxRlIts,2786
19
- nonebot_adapter_qq-1.5.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
20
- nonebot_adapter_qq-1.5.3.dist-info/RECORD,,