nonebot-adapter-qq 1.5.1__tar.gz → 1.5.2__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.1 → nonebot_adapter_qq-1.5.2}/PKG-INFO +1 -1
  2. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/__init__.py +2 -2
  3. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/adapter.py +6 -6
  4. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/bot.py +45 -29
  5. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/config.py +3 -3
  6. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/event.py +4 -4
  7. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/message.py +114 -3
  8. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/models/__init__.py +3 -3
  9. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/models/common.py +14 -12
  10. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/models/guild.py +15 -15
  11. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/models/payload.py +3 -3
  12. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/models/qq.py +3 -3
  13. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/store.py +2 -2
  14. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/utils.py +7 -17
  15. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/pyproject.toml +32 -5
  16. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/LICENSE +0 -0
  17. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/README.md +0 -0
  18. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/compat.py +0 -0
  19. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/exception.py +0 -0
  20. {nonebot_adapter_qq-1.5.1 → nonebot_adapter_qq-1.5.2}/nonebot/adapters/qq/permission.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-adapter-qq
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: QQ adapter for nonebot2
5
5
  Home-page: https://github.com/nonebot/adapter-qq
6
6
  License: MIT
@@ -1,10 +1,10 @@
1
- from .event import *
2
- from .permission import *
3
1
  from .bot import Bot as Bot
4
2
  from .utils import log as log
3
+ from .event import * # noqa: F403
5
4
  from .utils import escape as escape
6
5
  from .adapter import Adapter as Adapter
7
6
  from .message import Message as Message
7
+ from .permission import * # noqa: F403
8
8
  from .utils import unescape as unescape
9
9
  from .exception import ActionFailed as ActionFailed
10
10
  from .exception import NetworkError as NetworkError
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  import asyncio
3
3
  from typing_extensions import override
4
- from typing import Any, List, Tuple, Literal, Optional
4
+ from typing import Any, Literal, Optional
5
5
 
6
6
  from nonebot.utils import escape_tag
7
7
  from nonebot.exception import WebSocketClosed
@@ -46,7 +46,7 @@ class Adapter(BaseAdapter):
46
46
 
47
47
  self.qq_config: Config = Config(**self.config.dict())
48
48
 
49
- self.tasks: List["asyncio.Task"] = []
49
+ self.tasks: list["asyncio.Task"] = []
50
50
  self.setup()
51
51
 
52
52
  @classmethod
@@ -137,7 +137,7 @@ class Adapter(BaseAdapter):
137
137
  # wait for session start concurrency limit
138
138
  await asyncio.sleep(gateway_info.session_start_limit.max_concurrency or 1)
139
139
 
140
- async def _forward_ws(self, bot: Bot, ws_url: URL, shard: Tuple[int, int]) -> None:
140
+ async def _forward_ws(self, bot: Bot, ws_url: URL, shard: tuple[int, int]) -> None:
141
141
  # ws setup request
142
142
  request = Request(
143
143
  "GET",
@@ -236,7 +236,7 @@ class Adapter(BaseAdapter):
236
236
  )
237
237
 
238
238
  async def _authenticate(
239
- self, bot: Bot, ws: WebSocket, shard: Tuple[int, int]
239
+ self, bot: Bot, ws: WebSocket, shard: tuple[int, int]
240
240
  ) -> Optional[Literal[True]]:
241
241
  """鉴权连接"""
242
242
  if not bot.ready:
@@ -318,7 +318,7 @@ class Adapter(BaseAdapter):
318
318
  )
319
319
 
320
320
  if ready_event:
321
- asyncio.create_task(bot.handle_event(ready_event))
321
+ asyncio.create_task(bot.handle_event(ready_event)) # noqa: RUF006
322
322
 
323
323
  return True
324
324
 
@@ -354,7 +354,7 @@ class Adapter(BaseAdapter):
354
354
  else:
355
355
  if isinstance(event, MessageAuditEvent):
356
356
  audit_result.add_result(event)
357
- asyncio.create_task(bot.handle_event(event))
357
+ asyncio.create_task(bot.handle_event(event)) # noqa: RUF006
358
358
  elif isinstance(payload, HeartbeatAck):
359
359
  log("TRACE", "Heartbeat ACK")
360
360
  continue
@@ -6,8 +6,6 @@ from typing import (
6
6
  IO,
7
7
  TYPE_CHECKING,
8
8
  Any,
9
- Dict,
10
- List,
11
9
  Union,
12
10
  Literal,
13
11
  NoReturn,
@@ -42,6 +40,7 @@ from .event import (
42
40
  QQMessageEvent,
43
41
  GuildMessageEvent,
44
42
  C2CMessageCreateEvent,
43
+ InteractionCreateEvent,
45
44
  DirectMessageCreateEvent,
46
45
  GroupAtMessageCreateEvent,
47
46
  )
@@ -110,7 +109,7 @@ async def _check_reply(
110
109
  if event.reply.author.id == bot.self_info.id:
111
110
  event.to_me = True
112
111
  except Exception as e:
113
- log("WARNING", f"Error when getting message reply info: {repr(e)}", e)
112
+ log("WARNING", f"Error when getting message reply info: {e!r}", e)
114
113
 
115
114
 
116
115
  def _check_at_me(
@@ -264,7 +263,7 @@ class Bot(BaseBot):
264
263
  return f"QQBot {await self.get_access_token()}"
265
264
  return f"Bot {self.bot_info.id}.{self.bot_info.token}"
266
265
 
267
- async def get_authorization_header(self) -> Dict[str, str]:
266
+ async def get_authorization_header(self) -> dict[str, str]:
268
267
  """获取当前 Bot 的鉴权信息"""
269
268
  headers = {"Authorization": await self._get_authorization_header()}
270
269
  if self.bot_info.is_group_bot:
@@ -284,7 +283,7 @@ class Bot(BaseBot):
284
283
  return _message
285
284
 
286
285
  @staticmethod
287
- def _extract_send_message(message: Message) -> Dict[str, Any]:
286
+ def _extract_send_message(message: Message) -> dict[str, Any]:
288
287
  kwargs = {}
289
288
  content = message.extract_content() or None
290
289
  kwargs["content"] = content
@@ -301,7 +300,7 @@ class Bot(BaseBot):
301
300
  return kwargs
302
301
 
303
302
  @staticmethod
304
- def _extract_guild_image(message: Message) -> Dict[str, Any]:
303
+ def _extract_guild_image(message: Message) -> dict[str, Any]:
305
304
  kwargs = {}
306
305
  if image := (message["image"] or None):
307
306
  kwargs["image"] = image[-1].data["url"]
@@ -310,7 +309,7 @@ class Bot(BaseBot):
310
309
  return kwargs
311
310
 
312
311
  @staticmethod
313
- def _extract_qq_media(message: Message) -> Dict[str, Any]:
312
+ def _extract_qq_media(message: Message) -> dict[str, Any]:
314
313
  kwargs = {}
315
314
  if image := message["image"]:
316
315
  kwargs["file_type"] = 1
@@ -433,7 +432,7 @@ class Bot(BaseBot):
433
432
  msg_type = 4
434
433
  elif kwargs.get("ark"):
435
434
  msg_type = 3
436
- elif kwargs.get("markdown"):
435
+ elif kwargs.get("markdown") or kwargs.get("keyboard"):
437
436
  msg_type = 2
438
437
  elif (
439
438
  message["image"]
@@ -507,6 +506,23 @@ class Bot(BaseBot):
507
506
  msg_id=event.id,
508
507
  msg_seq=event._reply_seq,
509
508
  )
509
+ elif isinstance(event, InteractionCreateEvent):
510
+ if gid := event.group_openid:
511
+ return await self.send_to_group(
512
+ group_openid=gid, event_id=event.event_id, message=message
513
+ )
514
+ elif cid := event.channel_id:
515
+ return await self.send_to_channel(
516
+ channel_id=cid, event_id=event.event_id, message=message
517
+ )
518
+ elif uid := event.user_openid:
519
+ return await self.send_to_c2c(
520
+ openid=uid, event_id=event.event_id, message=message
521
+ )
522
+ elif gid := event.guild_id:
523
+ return await self.send_to_dms(
524
+ guild_id=gid, event_id=event.event_id, message=message
525
+ )
510
526
 
511
527
  raise RuntimeError("Event cannot be replied to!")
512
528
 
@@ -589,13 +605,13 @@ class Bot(BaseBot):
589
605
  before: Optional[str] = None,
590
606
  after: Optional[str] = None,
591
607
  limit: Optional[float] = None,
592
- ) -> List[Guild]:
608
+ ) -> list[Guild]:
593
609
  request = Request(
594
610
  "GET",
595
611
  self.adapter.get_api_base().joinpath("users", "@me", "guilds"),
596
612
  params=exclude_none({"before": before, "after": after, "limit": limit}),
597
613
  )
598
- return type_validate_python(List[Guild], await self._request(request))
614
+ return type_validate_python(list[Guild], await self._request(request))
599
615
 
600
616
  # Guild API
601
617
  @API
@@ -608,12 +624,12 @@ class Bot(BaseBot):
608
624
 
609
625
  # Channel API
610
626
  @API
611
- async def get_channels(self, *, guild_id: str) -> List[Channel]:
627
+ async def get_channels(self, *, guild_id: str) -> list[Channel]:
612
628
  request = Request(
613
629
  "GET",
614
630
  self.adapter.get_api_base().joinpath("guilds", guild_id, "channels"),
615
631
  )
616
- return type_validate_python(List[Channel], await self._request(request))
632
+ return type_validate_python(list[Channel], await self._request(request))
617
633
 
618
634
  @API
619
635
  async def get_channel(self, *, channel_id: str) -> Channel:
@@ -634,10 +650,10 @@ class Bot(BaseBot):
634
650
  position: Optional[int] = None,
635
651
  parent_id: Optional[int] = None,
636
652
  private_type: Optional[Union[PrivateType, int]] = None,
637
- private_user_ids: Optional[List[str]] = None,
653
+ private_user_ids: Optional[list[str]] = None,
638
654
  speak_permission: Optional[Union[SpeakPermission, int]] = None,
639
655
  application_id: Optional[str] = None,
640
- ) -> List[Channel]:
656
+ ) -> list[Channel]:
641
657
  request = Request(
642
658
  "POST",
643
659
  self.adapter.get_api_base().joinpath("guilds", guild_id, "channels"),
@@ -655,7 +671,7 @@ class Bot(BaseBot):
655
671
  }
656
672
  ),
657
673
  )
658
- return type_validate_python(List[Channel], await self._request(request))
674
+ return type_validate_python(list[Channel], await self._request(request))
659
675
 
660
676
  @API
661
677
  async def patch_channel(
@@ -705,13 +721,13 @@ class Bot(BaseBot):
705
721
  guild_id: str,
706
722
  after: Optional[str] = None,
707
723
  limit: Optional[float] = None,
708
- ) -> List[Member]:
724
+ ) -> list[Member]:
709
725
  request = Request(
710
726
  "GET",
711
727
  self.adapter.get_api_base().joinpath("guilds", guild_id, "members"),
712
728
  params=exclude_none({"after": after, "limit": limit}),
713
729
  )
714
- return type_validate_python(List[Member], await self._request(request))
730
+ return type_validate_python(list[Member], await self._request(request))
715
731
 
716
732
  @API
717
733
  async def get_role_members(
@@ -940,7 +956,7 @@ class Bot(BaseBot):
940
956
  return type_validate_python(GuildMessage, result)
941
957
 
942
958
  @staticmethod
943
- def _parse_send_message(data: Dict[str, Any]) -> Dict[str, Any]:
959
+ def _parse_send_message(data: dict[str, Any]) -> dict[str, Any]:
944
960
  data = exclude_none(data)
945
961
  data = {
946
962
  k: v.dict(exclude_none=True) if isinstance(v, BaseModel) else v
@@ -948,8 +964,8 @@ class Bot(BaseBot):
948
964
  }
949
965
  if file_image := data.pop("file_image", None):
950
966
  # 使用 multipart/form-data
951
- multipart_files: Dict[str, Any] = {"file_image": ("file_image", file_image)}
952
- multipart_data: Dict[str, Any] = {}
967
+ multipart_files: dict[str, Any] = {"file_image": ("file_image", file_image)}
968
+ multipart_data: dict[str, Any] = {}
953
969
  for k, v in data.items():
954
970
  if isinstance(v, (dict, list)):
955
971
  # 当字段类型为对象或数组时需要将字段序列化为 JSON 字符串后进行调用
@@ -1154,10 +1170,10 @@ class Bot(BaseBot):
1154
1170
  self,
1155
1171
  *,
1156
1172
  guild_id: str,
1157
- user_ids: List[str],
1173
+ user_ids: list[str],
1158
1174
  mute_end_timestamp: Optional[Union[int, datetime]] = None,
1159
1175
  mute_seconds: Optional[Union[int, timedelta]] = None,
1160
- ) -> List[int]:
1176
+ ) -> list[int]:
1161
1177
  if isinstance(mute_end_timestamp, datetime):
1162
1178
  mute_end_timestamp = int(mute_end_timestamp.timestamp())
1163
1179
 
@@ -1175,7 +1191,7 @@ class Bot(BaseBot):
1175
1191
  }
1176
1192
  ),
1177
1193
  )
1178
- return type_validate_python(List[int], await self._request(request))
1194
+ return type_validate_python(list[int], await self._request(request))
1179
1195
 
1180
1196
  # Announce API
1181
1197
  @API
@@ -1186,7 +1202,7 @@ class Bot(BaseBot):
1186
1202
  message_id: Optional[str] = None,
1187
1203
  channel_id: Optional[str] = None,
1188
1204
  announces_type: Optional[int] = None,
1189
- recommend_channels: Optional[List[RecommendChannel]] = None,
1205
+ recommend_channels: Optional[list[RecommendChannel]] = None,
1190
1206
  ) -> None:
1191
1207
  request = Request(
1192
1208
  "POST",
@@ -1251,7 +1267,7 @@ class Bot(BaseBot):
1251
1267
  @API
1252
1268
  async def get_schedules(
1253
1269
  self, *, channel_id: str, since: Optional[Union[int, datetime]] = None
1254
- ) -> List[Schedule]:
1270
+ ) -> list[Schedule]:
1255
1271
  if isinstance(since, datetime):
1256
1272
  since = int(since.timestamp() * 1000)
1257
1273
 
@@ -1260,7 +1276,7 @@ class Bot(BaseBot):
1260
1276
  self.adapter.get_api_base() / f"channels/{channel_id}/schedules",
1261
1277
  json=exclude_none({"since": since}),
1262
1278
  )
1263
- return type_validate_python(List[Schedule], await self._request(request))
1279
+ return type_validate_python(list[Schedule], await self._request(request))
1264
1280
 
1265
1281
  @API
1266
1282
  async def get_schedule(self, *, channel_id: str, schedule_id: str) -> Schedule:
@@ -1442,7 +1458,7 @@ class Bot(BaseBot):
1442
1458
  audio_url: Optional[str] = None,
1443
1459
  text: Optional[str] = None,
1444
1460
  status: Union[AudioStatus, int],
1445
- ) -> Dict[Never, Never]:
1461
+ ) -> dict[Never, Never]:
1446
1462
  request = Request(
1447
1463
  "POST",
1448
1464
  self.adapter.get_api_base().joinpath("channels", channel_id, "audio"),
@@ -1453,7 +1469,7 @@ class Bot(BaseBot):
1453
1469
  return await self._request(request)
1454
1470
 
1455
1471
  @API
1456
- async def put_mic(self, *, channel_id: str) -> Dict[Never, Never]:
1472
+ async def put_mic(self, *, channel_id: str) -> dict[Never, Never]:
1457
1473
  request = Request(
1458
1474
  "PUT",
1459
1475
  self.adapter.get_api_base().joinpath("channels", channel_id, "mic"),
@@ -1461,7 +1477,7 @@ class Bot(BaseBot):
1461
1477
  return await self._request(request)
1462
1478
 
1463
1479
  @API
1464
- async def delete_mic(self, *, channel_id: str) -> Dict[Never, Never]:
1480
+ async def delete_mic(self, *, channel_id: str) -> dict[Never, Never]:
1465
1481
  request = Request(
1466
1482
  "DELETE",
1467
1483
  self.adapter.get_api_base().joinpath("channels", channel_id, "mic"),
@@ -1,4 +1,4 @@
1
- from typing import List, Tuple, Optional
1
+ from typing import Optional
2
2
 
3
3
  from pydantic import Field, HttpUrl, BaseModel
4
4
  from nonebot.compat import PYDANTIC_V2, ConfigDict
@@ -53,7 +53,7 @@ class BotInfo(BaseModel):
53
53
  id: str = Field(alias="id")
54
54
  token: str = Field(alias="token")
55
55
  secret: str = Field(alias="secret")
56
- shard: Optional[Tuple[int, int]] = None
56
+ shard: Optional[tuple[int, int]] = None
57
57
  intent: Intents = Field(default_factory=Intents)
58
58
 
59
59
  @property
@@ -67,4 +67,4 @@ class Config(BaseModel):
67
67
  qq_api_base: HttpUrl = Field("https://api.sgroup.qq.com/")
68
68
  qq_sandbox_api_base: HttpUrl = Field("https://sandbox.api.sgroup.qq.com")
69
69
  qq_auth_base: HttpUrl = Field("https://bots.qq.com/app/getAppAccessToken")
70
- qq_bots: List[BotInfo] = Field(default_factory=list)
70
+ qq_bots: list[BotInfo] = Field(default_factory=list)
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from datetime import datetime
3
3
  from typing_extensions import override
4
- from typing import Dict, Type, Tuple, TypeVar, Optional, cast
4
+ from typing import TypeVar, Optional, cast
5
5
 
6
6
  from nonebot.utils import escape_tag
7
7
 
@@ -145,10 +145,10 @@ class Event(BaseEvent):
145
145
  return False
146
146
 
147
147
 
148
- EVENT_CLASSES: Dict[str, Type[Event]] = {}
148
+ EVENT_CLASSES: dict[str, type[Event]] = {}
149
149
 
150
150
 
151
- def register_event_class(event_class: Type[E]) -> Type[E]:
151
+ def register_event_class(event_class: type[E]) -> type[E]:
152
152
  EVENT_CLASSES[event_class.__type__.value] = event_class
153
153
  return event_class
154
154
 
@@ -166,7 +166,7 @@ class ReadyEvent(MetaEvent):
166
166
  version: int
167
167
  session_id: str
168
168
  user: User
169
- shard: Tuple[int, int]
169
+ shard: tuple[int, int]
170
170
 
171
171
 
172
172
  @register_event_class
@@ -2,8 +2,11 @@ import re
2
2
  from io import BytesIO
3
3
  from pathlib import Path
4
4
  from dataclasses import dataclass
5
+ from collections.abc import Iterable
5
6
  from typing_extensions import Self, override
6
- from typing import TYPE_CHECKING, Type, Union, Iterable, Optional, TypedDict, overload
7
+ from typing import TYPE_CHECKING, Union, Optional, TypedDict, overload
8
+
9
+ from nonebot.compat import type_validate_python
7
10
 
8
11
  from nonebot.adapters import Message as BaseMessage
9
12
  from nonebot.adapters import MessageSegment as BaseMessageSegment
@@ -24,7 +27,7 @@ from .models import (
24
27
  class MessageSegment(BaseMessageSegment["Message"]):
25
28
  @classmethod
26
29
  @override
27
- def get_message_class(cls) -> Type["Message"]:
30
+ def get_message_class(cls) -> type["Message"]:
28
31
  return Message
29
32
 
30
33
  @staticmethod
@@ -166,6 +169,32 @@ class MessageSegment(BaseMessageSegment["Message"]):
166
169
  def is_text(self) -> bool:
167
170
  return self.type == "text"
168
171
 
172
+ @classmethod
173
+ @override
174
+ def _validate(cls, value) -> Self:
175
+ if isinstance(value, cls):
176
+ return value
177
+ if isinstance(value, MessageSegment):
178
+ raise ValueError(f"Type {type(value)} can not be converted to {cls}")
179
+ if not isinstance(value, dict):
180
+ raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
181
+ if "type" not in value:
182
+ raise ValueError(
183
+ f"Expected dict with 'type' for MessageSegment, got {value}"
184
+ )
185
+ _type = value["type"]
186
+ if _type not in SEGMENT_TYPE_MAP:
187
+ raise ValueError(f"Invalid MessageSegment type: {_type}")
188
+ segment_type = SEGMENT_TYPE_MAP[_type]
189
+
190
+ # casting value to subclass of MessageSegment
191
+ if cls is MessageSegment:
192
+ return type_validate_python(segment_type, value)
193
+ # init segment instance directly if type matched
194
+ if cls is segment_type:
195
+ return segment_type(type=_type, data=value.get("data", {}))
196
+ raise ValueError(f"Segment type {_type!r} can not be converted to {cls}")
197
+
169
198
 
170
199
  class _TextData(TypedDict):
171
200
  text: str
@@ -278,6 +307,18 @@ class Embed(MessageSegment):
278
307
  def __str__(self) -> str:
279
308
  return f"<embed:{self.data['embed']!r}>"
280
309
 
310
+ @classmethod
311
+ @override
312
+ def _validate(cls, value) -> Self:
313
+ instance = super()._validate(value)
314
+ if "embed" not in instance.data:
315
+ raise ValueError(
316
+ f"Expected dict with 'embed' in 'data' for Embed, got {value}"
317
+ )
318
+ if not isinstance(embed := instance.data["embed"], MessageEmbed):
319
+ instance.data["embed"] = type_validate_python(MessageEmbed, embed)
320
+ return instance
321
+
281
322
 
282
323
  class _ArkData(TypedDict):
283
324
  ark: MessageArk
@@ -292,6 +333,16 @@ class Ark(MessageSegment):
292
333
  def __str__(self) -> str:
293
334
  return f"<ark:{self.data['ark']!r}>"
294
335
 
336
+ @classmethod
337
+ @override
338
+ def _validate(cls, value) -> Self:
339
+ instance = super()._validate(value)
340
+ if "ark" not in instance.data:
341
+ raise ValueError(f"Expected dict with 'ark' in 'data' for Ark, got {value}")
342
+ if not isinstance(ark := instance.data["ark"], MessageArk):
343
+ instance.data["ark"] = type_validate_python(MessageArk, ark)
344
+ return instance
345
+
295
346
 
296
347
  class _ReferenceData(TypedDict):
297
348
  reference: MessageReference
@@ -306,6 +357,20 @@ class Reference(MessageSegment):
306
357
  def __str__(self) -> str:
307
358
  return f"<reference:{self.data['reference'].message_id}>"
308
359
 
360
+ @classmethod
361
+ @override
362
+ def _validate(cls, value) -> Self:
363
+ instance = super()._validate(value)
364
+ if "reference" not in instance.data:
365
+ raise ValueError(
366
+ f"Expected dict with 'reference' in 'data' for Reference, got {value}"
367
+ )
368
+ if not isinstance(reference := instance.data["reference"], MessageReference):
369
+ instance.data["reference"] = type_validate_python(
370
+ MessageReference, reference
371
+ )
372
+ return instance
373
+
309
374
 
310
375
  class _MarkdownData(TypedDict):
311
376
  markdown: MessageMarkdown
@@ -320,6 +385,18 @@ class Markdown(MessageSegment):
320
385
  def __str__(self) -> str:
321
386
  return f"<markdown:{self.data['markdown']!r}>"
322
387
 
388
+ @classmethod
389
+ @override
390
+ def _validate(cls, value) -> Self:
391
+ instance = super()._validate(value)
392
+ if "markdown" not in instance.data:
393
+ raise ValueError(
394
+ f"Expected dict with 'markdown' in 'data' for Markdown, got {value}"
395
+ )
396
+ if not isinstance(markdown := instance.data["markdown"], MessageMarkdown):
397
+ instance.data["markdown"] = type_validate_python(MessageMarkdown, markdown)
398
+ return instance
399
+
323
400
 
324
401
  class _KeyboardData(TypedDict):
325
402
  keyboard: MessageKeyboard
@@ -334,11 +411,45 @@ class Keyboard(MessageSegment):
334
411
  def __str__(self) -> str:
335
412
  return f"<keyboard:{self.data['keyboard']!r}>"
336
413
 
414
+ @classmethod
415
+ @override
416
+ def _validate(cls, value) -> Self:
417
+ instance = super()._validate(value)
418
+ if "keyboard" not in instance.data:
419
+ raise ValueError(
420
+ f"Expected dict with 'keyboard' in 'data' for Keyboard, got {value}"
421
+ )
422
+ if not isinstance(keyboard := instance.data["keyboard"], MessageKeyboard):
423
+ instance.data["keyboard"] = type_validate_python(MessageKeyboard, keyboard)
424
+ return instance
425
+
426
+
427
+ SEGMENT_TYPE_MAP: dict[str, type[MessageSegment]] = {
428
+ "text": Text,
429
+ "emoji": Emoji,
430
+ "mention_user": MentionUser,
431
+ "mention_channel": MentionChannel,
432
+ "mention_everyone": MentionEveryone,
433
+ "image": Attachment,
434
+ "file_image": LocalAttachment,
435
+ "audio": Attachment,
436
+ "file_audio": LocalAttachment,
437
+ "video": Attachment,
438
+ "file_video": LocalAttachment,
439
+ "file": Attachment,
440
+ "file_file": LocalAttachment,
441
+ "ark": Ark,
442
+ "embed": Embed,
443
+ "markdown": Markdown,
444
+ "keyboard": Keyboard,
445
+ "reference": Reference,
446
+ }
447
+
337
448
 
338
449
  class Message(BaseMessage[MessageSegment]):
339
450
  @classmethod
340
451
  @override
341
- def get_segment_class(cls) -> Type[MessageSegment]:
452
+ def get_segment_class(cls) -> type[MessageSegment]:
342
453
  return MessageSegment
343
454
 
344
455
  @override
@@ -1,6 +1,6 @@
1
- from .qq import *
2
- from .guild import *
3
- from .common import *
1
+ from .qq import * # noqa: F403
2
+ from .guild import * # noqa: F403
3
+ from .common import * # noqa: F403
4
4
  from .payload import Hello as Hello
5
5
  from .payload import Opcode as Opcode
6
6
  from .payload import Resume as Resume
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime
2
2
  from urllib.parse import urlparse
3
- from typing import List, Literal, Optional
3
+ from typing import Literal, Optional
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
@@ -33,7 +33,7 @@ class MessageEmbed(BaseModel):
33
33
  description: Optional[str] = None
34
34
  prompt: str
35
35
  thumbnail: Optional[MessageEmbedThumbnail] = None
36
- fields: Optional[List[MessageEmbedField]] = None
36
+ fields: Optional[list[MessageEmbedField]] = None
37
37
 
38
38
 
39
39
  # Message Ark
@@ -43,18 +43,18 @@ class MessageArkObjKv(BaseModel):
43
43
 
44
44
 
45
45
  class MessageArkObj(BaseModel):
46
- obj_kv: Optional[List[MessageArkObjKv]] = None
46
+ obj_kv: Optional[list[MessageArkObjKv]] = None
47
47
 
48
48
 
49
49
  class MessageArkKv(BaseModel):
50
50
  key: Optional[str] = None
51
51
  value: Optional[str] = None
52
- obj: Optional[List[MessageArkObj]] = None
52
+ obj: Optional[list[MessageArkObj]] = None
53
53
 
54
54
 
55
55
  class MessageArk(BaseModel):
56
56
  template_id: Optional[int] = None
57
- kv: Optional[List[MessageArkKv]] = None
57
+ kv: Optional[list[MessageArkKv]] = None
58
58
 
59
59
 
60
60
  # Message Reference
@@ -66,21 +66,21 @@ class MessageReference(BaseModel):
66
66
  # Message Markdown
67
67
  class MessageMarkdownParams(BaseModel):
68
68
  key: Optional[str] = None
69
- values: Optional[List[str]] = None
69
+ values: Optional[list[str]] = None
70
70
 
71
71
 
72
72
  class MessageMarkdown(BaseModel):
73
73
  template_id: Optional[int] = None
74
74
  custom_template_id: Optional[str] = None
75
- params: Optional[List[MessageMarkdownParams]] = None
75
+ params: Optional[list[MessageMarkdownParams]] = None
76
76
  content: Optional[str] = None
77
77
 
78
78
 
79
79
  # Message Keyboard
80
80
  class Permission(BaseModel):
81
81
  type: Optional[int] = None
82
- specify_role_ids: Optional[List[str]] = None
83
- specify_user_ids: Optional[List[str]] = None
82
+ specify_role_ids: Optional[list[str]] = None
83
+ specify_user_ids: Optional[list[str]] = None
84
84
 
85
85
 
86
86
  class Action(BaseModel):
@@ -108,11 +108,11 @@ class Button(BaseModel):
108
108
 
109
109
 
110
110
  class InlineKeyboardRow(BaseModel):
111
- buttons: Optional[List[Button]] = None
111
+ buttons: Optional[list[Button]] = None
112
112
 
113
113
 
114
114
  class InlineKeyboard(BaseModel):
115
- rows: Optional[List[InlineKeyboardRow]] = None
115
+ rows: Optional[list[InlineKeyboardRow]] = None
116
116
 
117
117
 
118
118
  class MessageKeyboard(BaseModel):
@@ -140,6 +140,8 @@ class ButtonInteractionContent(BaseModel):
140
140
  feature_id: Optional[str] = None
141
141
  button_id: Optional[str] = None
142
142
  button_data: Optional[str] = None
143
+ checked: Optional[int] = None
144
+ feedback_opt: Optional[str] = None
143
145
 
144
146
 
145
147
  class ButtonInteractionData(BaseModel):
@@ -148,7 +150,7 @@ class ButtonInteractionData(BaseModel):
148
150
 
149
151
  class ButtonInteraction(BaseModel):
150
152
  id: str
151
- type: Literal[11, 12]
153
+ type: Literal[11, 12, 13]
152
154
  version: int
153
155
  timestamp: str
154
156
  scene: str
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from enum import IntEnum
3
3
  from datetime import datetime
4
- from typing import List, Union, Generic, TypeVar, Optional
4
+ from typing import Union, Generic, TypeVar, Optional
5
5
 
6
6
  from pydantic import BaseModel
7
7
  from nonebot.compat import PYDANTIC_V2, model_fields, type_validate_python
@@ -91,12 +91,12 @@ class Channel(BaseModel):
91
91
  class Member(BaseModel):
92
92
  user: Optional[User] = None
93
93
  nick: Optional[str] = None
94
- roles: Optional[List[str]] = None
94
+ roles: Optional[list[str]] = None
95
95
  joined_at: datetime
96
96
 
97
97
 
98
98
  class GetRoleMembersReturn(BaseModel):
99
- data: List[Member]
99
+ data: list[Member]
100
100
  next: str
101
101
 
102
102
 
@@ -112,7 +112,7 @@ class Role(BaseModel):
112
112
 
113
113
  class GetGuildRolesReturn(BaseModel):
114
114
  guild_id: str
115
- roles: List[Role]
115
+ roles: list[Role]
116
116
  role_num_limit: int
117
117
 
118
118
 
@@ -145,9 +145,9 @@ class Message(BaseModel):
145
145
  edited_timestamp: Optional[datetime] = None
146
146
  mention_everyone: Optional[bool] = None
147
147
  author: User
148
- attachments: Optional[List[MessageAttachment]] = None
149
- embeds: Optional[List[MessageEmbed]] = None
150
- mentions: Optional[List[User]] = None
148
+ attachments: Optional[list[MessageAttachment]] = None
149
+ embeds: Optional[list[MessageEmbed]] = None
150
+ mentions: Optional[list[User]] = None
151
151
  member: Optional[Member] = None
152
152
  ark: Optional[MessageArk] = None
153
153
  seq: Optional[int] = None
@@ -166,7 +166,7 @@ class MessageDelete(BaseModel):
166
166
  class MessageSetting(BaseModel):
167
167
  disable_create_dm: bool
168
168
  disable_push_msg: bool
169
- channel_ids: List[str]
169
+ channel_ids: list[str]
170
170
  channel_push_max_num: int
171
171
 
172
172
 
@@ -188,14 +188,14 @@ class Announces(BaseModel):
188
188
  channel_id: Optional[str] = None
189
189
  message_id: Optional[str] = None
190
190
  announces_type: Optional[int] = None
191
- recommend_channels: Optional[List[RecommendChannel]] = None
191
+ recommend_channels: Optional[list[RecommendChannel]] = None
192
192
 
193
193
 
194
194
  # Pins
195
195
  class PinsMessage(BaseModel):
196
196
  guild_id: str
197
197
  channel_id: str
198
- message_ids: List[str]
198
+ message_ids: list[str]
199
199
 
200
200
 
201
201
  # Schedule
@@ -251,7 +251,7 @@ class MessageReaction(BaseModel):
251
251
 
252
252
 
253
253
  class GetReactionUsersReturn(BaseModel):
254
- users: List[User]
254
+ users: list[User]
255
255
  cookie: str
256
256
  is_end: bool
257
257
 
@@ -358,12 +358,12 @@ class ParagraphProps(BaseModel):
358
358
 
359
359
 
360
360
  class Paragraph(BaseModel):
361
- elems: List[Elem]
361
+ elems: list[Elem]
362
362
  props: Optional[ParagraphProps] = None
363
363
 
364
364
 
365
365
  class RichText(BaseModel):
366
- paragraphs: List[Paragraph]
366
+ paragraphs: list[Paragraph]
367
367
 
368
368
 
369
369
  class ThreadObjectInfo(BaseModel):
@@ -441,7 +441,7 @@ class ForumAuditResult(ForumSourceInfo):
441
441
 
442
442
 
443
443
  class GetThreadsListReturn(BaseModel):
444
- threads: List[Thread[str]]
444
+ threads: list[Thread[str]]
445
445
  is_finish: bool
446
446
 
447
447
 
@@ -476,7 +476,7 @@ class APIPermissionDemand(BaseModel):
476
476
 
477
477
 
478
478
  class GetGuildAPIPermissionReturn(BaseModel):
479
- apis: List[APIPermission]
479
+ apis: list[APIPermission]
480
480
 
481
481
 
482
482
  # WebSocket
@@ -1,6 +1,6 @@
1
1
  from enum import IntEnum
2
- from typing import Tuple, Union, Optional
3
- from typing_extensions import Literal, Annotated
2
+ from typing_extensions import Literal
3
+ from typing import Union, Optional, Annotated
4
4
 
5
5
  from pydantic import Field, BaseModel
6
6
  from nonebot.compat import PYDANTIC_V2, ConfigDict
@@ -54,7 +54,7 @@ class Heartbeat(Payload):
54
54
  class IdentifyData(BaseModel):
55
55
  token: str
56
56
  intents: int
57
- shard: Tuple[int, int]
57
+ shard: tuple[int, int]
58
58
  properties: dict
59
59
 
60
60
  if PYDANTIC_V2:
@@ -1,5 +1,5 @@
1
+ from typing import Optional
1
2
  from datetime import datetime
2
- from typing import List, Optional
3
3
  from urllib.parse import urlparse
4
4
 
5
5
  from pydantic import BaseModel
@@ -40,7 +40,7 @@ class QQMessage(BaseModel):
40
40
  id: str
41
41
  content: str
42
42
  timestamp: str
43
- attachments: Optional[List[Attachment]] = None
43
+ attachments: Optional[list[Attachment]] = None
44
44
 
45
45
 
46
46
  class PostC2CMessagesReturn(BaseModel):
@@ -71,7 +71,7 @@ class GroupMember(BaseModel):
71
71
 
72
72
 
73
73
  class PostGroupMembersReturn(BaseModel):
74
- members: List[GroupMember]
74
+ members: list[GroupMember]
75
75
  next_index: Optional[int] = None
76
76
 
77
77
 
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import TYPE_CHECKING, Dict, Optional
2
+ from typing import TYPE_CHECKING, Optional
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  from .event import MessageAuditEvent
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
7
7
 
8
8
  class AuditResultStore:
9
9
  def __init__(self) -> None:
10
- self._futures: Dict[str, asyncio.Future] = {}
10
+ self._futures: dict[str, asyncio.Future] = {}
11
11
 
12
12
  def add_result(self, result: "MessageAuditEvent"):
13
13
  audit_id = result.audit_id
@@ -1,17 +1,7 @@
1
1
  from functools import partial
2
+ from collections.abc import Awaitable
2
3
  from typing_extensions import ParamSpec, Concatenate
3
- from typing import (
4
- TYPE_CHECKING,
5
- Any,
6
- Dict,
7
- Type,
8
- Generic,
9
- TypeVar,
10
- Callable,
11
- Optional,
12
- Awaitable,
13
- overload,
14
- )
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, Callable, Optional, overload
15
5
 
16
6
  from nonebot.utils import logger_wrapper
17
7
 
@@ -33,7 +23,7 @@ def unescape(s: str) -> str:
33
23
  return s.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
34
24
 
35
25
 
36
- def exclude_none(data: Dict[str, Any]) -> Dict[str, Any]:
26
+ def exclude_none(data: dict[str, Any]) -> dict[str, Any]:
37
27
  return {k: v for k, v in data.items() if v is not None}
38
28
 
39
29
 
@@ -41,19 +31,19 @@ class API(Generic[B, P, R]):
41
31
  def __init__(self, func: Callable[Concatenate[B, P], Awaitable[R]]) -> None:
42
32
  self.func = func
43
33
 
44
- def __set_name__(self, owner: Type[B], name: str) -> None:
34
+ def __set_name__(self, owner: type[B], name: str) -> None:
45
35
  self.name = name
46
36
 
47
37
  @overload
48
- def __get__(self, obj: None, objtype: Type[B]) -> "API[B, P, R]": ...
38
+ def __get__(self, obj: None, objtype: type[B]) -> "API[B, P, R]": ...
49
39
 
50
40
  @overload
51
41
  def __get__(
52
- self, obj: B, objtype: Optional[Type[B]]
42
+ self, obj: B, objtype: Optional[type[B]]
53
43
  ) -> Callable[P, Awaitable[R]]: ...
54
44
 
55
45
  def __get__(
56
- self, obj: Optional[B], objtype: Optional[Type[B]] = None
46
+ self, obj: Optional[B], objtype: Optional[type[B]] = None
57
47
  ) -> "API[B, P, R] | Callable[P, Awaitable[R]]":
58
48
  if obj is None:
59
49
  return self
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "nonebot-adapter-qq"
3
- version = "1.5.1"
3
+ version = "1.5.2"
4
4
  description = "QQ adapter for nonebot2"
5
5
  authors = ["yanyongyu <yyy@nonebot.dev>"]
6
6
  license = "MIT"
@@ -48,17 +48,44 @@ extra_standard_library = ["typing_extensions"]
48
48
 
49
49
  [tool.ruff]
50
50
  line-length = 88
51
- target-version = "py38"
51
+ target-version = "py39"
52
52
 
53
53
  [tool.ruff.lint]
54
- select = ["E", "W", "F", "UP", "C", "T", "Q"]
55
- ignore = ["E402", "F403", "F405", "C901", "UP037"]
54
+ select = [
55
+ "F", # Pyflakes
56
+ "W", # pycodestyle warnings
57
+ "E", # pycodestyle errors
58
+ "UP", # pyupgrade
59
+ "ASYNC", # flake8-async
60
+ "C4", # flake8-comprehensions
61
+ "T10", # flake8-debugger
62
+ "T20", # flake8-print
63
+ "PYI", # flake8-pyi
64
+ "PT", # flake8-pytest-style
65
+ "Q", # flake8-quotes
66
+ "RUF", # Ruff-specific rules
67
+ ]
68
+ ignore = [
69
+ "E402", # module-import-not-at-top-of-file
70
+ "UP037", # quoted-annotation
71
+ "RUF001", # ambiguous-unicode-character-string
72
+ "RUF002", # ambiguous-unicode-character-docstring
73
+ "RUF003", # ambiguous-unicode-character-comment
74
+ ]
75
+
76
+ [tool.ruff.lint.flake8-pytest-style]
77
+ fixture-parentheses = false
78
+ mark-parentheses = false
56
79
 
57
80
  [tool.pyright]
81
+ pythonVersion = "3.9"
58
82
  pythonPlatform = "All"
59
- pythonVersion = "3.8"
60
83
  defineConstant = { PYDANTIC_V2 = true }
61
84
 
85
+ typeCheckingMode = "standard"
86
+ reportShadowedImports = false
87
+ disableBytesTypePromotions = true
88
+
62
89
  [build-system]
63
90
  requires = ["poetry-core>=1.0.0"]
64
91
  build-backend = "poetry.core.masonry.api"