nonebot-plugin-werewolf 1.1.7__py3-none-any.whl → 1.1.9__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.
Files changed (38) hide show
  1. nonebot_plugin_werewolf/__init__.py +1 -1
  2. nonebot_plugin_werewolf/config.py +74 -15
  3. nonebot_plugin_werewolf/constant.py +59 -46
  4. nonebot_plugin_werewolf/exception.py +2 -4
  5. nonebot_plugin_werewolf/game.py +200 -171
  6. nonebot_plugin_werewolf/matchers/__init__.py +1 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +4 -4
  8. nonebot_plugin_werewolf/matchers/edit_behavior.py +217 -0
  9. nonebot_plugin_werewolf/matchers/edit_preset.py +11 -11
  10. nonebot_plugin_werewolf/matchers/message_in_game.py +3 -1
  11. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +8 -5
  12. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +3 -3
  13. nonebot_plugin_werewolf/matchers/start_game.py +214 -175
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +3 -3
  15. nonebot_plugin_werewolf/models.py +46 -22
  16. nonebot_plugin_werewolf/player.py +366 -0
  17. nonebot_plugin_werewolf/player_set.py +40 -22
  18. nonebot_plugin_werewolf/players/__init__.py +1 -2
  19. nonebot_plugin_werewolf/players/civilian.py +3 -3
  20. nonebot_plugin_werewolf/players/guard.py +27 -20
  21. nonebot_plugin_werewolf/players/hunter.py +6 -5
  22. nonebot_plugin_werewolf/players/idiot.py +27 -19
  23. nonebot_plugin_werewolf/players/jester.py +29 -0
  24. nonebot_plugin_werewolf/players/prophet.py +20 -14
  25. nonebot_plugin_werewolf/players/shooter.py +54 -0
  26. nonebot_plugin_werewolf/players/werewolf.py +88 -29
  27. nonebot_plugin_werewolf/players/witch.py +48 -24
  28. nonebot_plugin_werewolf/players/wolfking.py +14 -8
  29. nonebot_plugin_werewolf/utils.py +107 -8
  30. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/METADATA +30 -20
  31. nonebot_plugin_werewolf-1.1.9.dist-info/RECORD +35 -0
  32. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/WHEEL +1 -1
  33. nonebot_plugin_werewolf/players/can_shoot.py +0 -54
  34. nonebot_plugin_werewolf/players/joker.py +0 -25
  35. nonebot_plugin_werewolf/players/player.py +0 -226
  36. nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +0 -34
  37. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info/licenses}/LICENSE +0 -0
  38. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,29 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
3
+ from ..constant import stop_command_prompt
5
4
  from ..models import Role, RoleGroup
6
- from .player import Player
5
+ from ..player import InteractProvider, Player
7
6
 
8
7
 
9
- @Player.register_role(Role.Prophet, RoleGroup.GoodGuy)
10
- class Prophet(Player):
8
+ class ProphetInteractProvider(InteractProvider["Prophet"]):
11
9
  @override
12
10
  async def interact(self) -> None:
13
- players = self.game.players.alive().exclude(self)
14
- await self.send(
15
- UniMessage.text("💫请选择需要查验身份的玩家:\n")
16
- .text(players.show())
17
- .text("\n\n🔮发送编号选择玩家")
18
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不查验身份)")
11
+ players = self.game.players.alive().exclude(self.p)
12
+ await self.p.send(
13
+ "💫请选择需要查验身份的玩家:\n"
14
+ f"{players.show()}\n\n"
15
+ "🔮发送编号选择玩家\n"
16
+ f"❌发送 “{stop_command_prompt()}” 结束回合(不查验身份)",
17
+ stop_btn_label="结束回合",
18
+ select_players=players,
19
19
  )
20
20
 
21
- if selected := await self._select_player(players):
22
- result = "狼人" if selected.role_group == RoleGroup.Werewolf else "好人"
23
- await self.send(f"✏️玩家 {selected.name} 的阵营是『{result}』")
21
+ if selected := await self.p.select_player(players, stop_btn_label="结束回合"):
22
+ result = "狼人" if selected.role_group == RoleGroup.WEREWOLF else "好人"
23
+ await self.p.send(f"✏️玩家 {selected.name} 的阵营是『{result}』")
24
+
25
+
26
+ class Prophet(Player):
27
+ role = Role.PROPHET
28
+ role_group = RoleGroup.GOODGUY
29
+ interact_provider = ProphetInteractProvider
@@ -0,0 +1,54 @@
1
+ from typing_extensions import override
2
+
3
+ from nonebot_plugin_alconna.uniseg import UniMessage
4
+
5
+ from ..constant import stop_command_prompt
6
+ from ..models import KillReason
7
+ from ..player import KillProvider, Player
8
+
9
+
10
+ class ShooterKillProvider(KillProvider["Player"]):
11
+ @override
12
+ async def post_kill(self) -> None:
13
+ if self.kill_info and self.kill_info.reason == KillReason.POISON:
14
+ await self.p.send("⚠️你昨晚被女巫毒杀,无法使用技能")
15
+ return await super().post_kill()
16
+
17
+ await self.game.send(
18
+ UniMessage.text("🕵️玩家 ")
19
+ .at(self.user_id)
20
+ .text(" 死了\n请在私聊决定射杀目标...")
21
+ )
22
+
23
+ self.game.state.shooter = None
24
+ shoot = await self.shoot()
25
+ msg = UniMessage.text("玩家 ").at(self.user_id).text(" ")
26
+ if shoot is not None:
27
+ self.game.state.shooter = self.p
28
+ await self.game.send("🔫" + msg.text("射杀了玩家 ").at(shoot.user_id))
29
+ await shoot.kill(KillReason.SHOOT, self.p)
30
+ self.selected = shoot
31
+ else:
32
+ await self.game.send("ℹ️" + msg.text("选择了取消技能"))
33
+
34
+ return await super().post_kill()
35
+
36
+ async def shoot(self) -> Player | None:
37
+ players = self.game.players.alive().exclude(self.p)
38
+ await self.p.send(
39
+ "💫请选择需要射杀的玩家:\n"
40
+ f"{players.show()}\n\n"
41
+ "🔫发送编号选择玩家\n"
42
+ f"❌发送 “{stop_command_prompt()}” 取消技能",
43
+ stop_btn_label="取消技能",
44
+ select_players=players,
45
+ )
46
+
47
+ if selected := await self.p.select_player(
48
+ players,
49
+ on_stop="ℹ️已取消技能,回合结束",
50
+ stop_btn_label="取消技能",
51
+ ):
52
+ await self.p.send(f"🎯选择射杀的玩家: {selected.name}")
53
+
54
+ return selected
@@ -1,55 +1,58 @@
1
+ import secrets
1
2
  from typing import TYPE_CHECKING
3
+ from typing_extensions import override
2
4
 
3
5
  import anyio
4
6
  from nonebot_plugin_alconna.uniseg import UniMessage
5
- from typing_extensions import override
6
7
 
7
- from ..constant import STOP_COMMAND, STOP_COMMAND_PROMPT
8
+ from ..constant import STOP_COMMAND, stop_command_prompt
8
9
  from ..models import Role, RoleGroup
9
- from ..utils import ObjectStream, check_index
10
- from .player import Player
10
+ from ..player import InteractProvider, NotifyProvider, Player
11
+ from ..utils import ObjectStream, as_player_set, check_index
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from ..player_set import PlayerSet
14
15
 
15
16
 
16
- @Player.register_role(Role.Werewolf, RoleGroup.Werewolf)
17
- class Werewolf(Player):
17
+ class WerewolfInteractProvider(InteractProvider["Werewolf"]):
18
18
  stream: ObjectStream[str | UniMessage]
19
19
 
20
20
  @override
21
- async def notify_role(self) -> None:
22
- await super().notify_role()
23
- partners = self.game.players.alive().select(RoleGroup.Werewolf).exclude(self)
24
- if partners:
25
- await self.send(
26
- "🐺你的队友:\n"
27
- + "\n".join(f" {p.role_name}: {p.name}" for p in partners)
28
- )
21
+ async def before(self) -> None:
22
+ self.game.state.werewolf_start()
29
23
 
30
- async def _handle_interact(self, players: "PlayerSet") -> None:
24
+ async def handle_interact(self, players: "PlayerSet") -> None:
31
25
  self.selected = None
32
26
 
33
27
  while True:
34
- input_msg = await self.receive()
28
+ input_msg = await self.p.receive()
35
29
  text = input_msg.extract_plain_text()
36
30
  index = check_index(text, len(players))
37
31
  if index is not None:
38
32
  self.selected = players[index - 1]
39
33
  msg = f"当前选择玩家: {self.selected.name}"
40
- await self.send(f"🎯{msg}\n发送 “{STOP_COMMAND_PROMPT}” 结束回合")
41
- await self.stream.send(f"📝队友 {self.name} {msg}")
34
+ await self.p.send(
35
+ f"🎯{msg}\n发送 {stop_command_prompt()}” 结束回合",
36
+ stop_btn_label="结束回合",
37
+ select_players=players,
38
+ )
39
+ await self.stream.send(f"📝队友 {self.p.name} {msg}")
42
40
  if text == STOP_COMMAND:
43
41
  if self.selected is not None:
44
- await self.send("✅你已结束当前回合")
45
- await self.stream.send(f"📝队友 {self.name} 结束当前回合")
42
+ await self.p.send("✅你已结束当前回合")
43
+ await self.stream.send(f"📝队友 {self.p.name} 结束当前回合")
46
44
  self.stream.close()
47
45
  return
48
- await self.send("⚠️当前未选择玩家,无法结束回合")
46
+ await self.p.send(
47
+ "⚠️当前未选择玩家,无法结束回合",
48
+ select_players=players,
49
+ )
49
50
  else:
50
- await self.stream.send(UniMessage(f"💬队友 {self.name}:\n") + input_msg)
51
+ await self.stream.send(
52
+ UniMessage.text(f"💬队友 {self.p.name}:\n") + input_msg
53
+ )
51
54
 
52
- async def _handle_broadcast(self, partners: "PlayerSet") -> None:
55
+ async def handle_broadcast(self, partners: "PlayerSet") -> None:
53
56
  while not self.stream.closed:
54
57
  try:
55
58
  message = await self.stream.recv()
@@ -61,7 +64,7 @@ class Werewolf(Player):
61
64
  @override
62
65
  async def interact(self) -> None:
63
66
  players = self.game.players.alive()
64
- partners = players.select(RoleGroup.Werewolf).exclude(self)
67
+ partners = players.select(RoleGroup.WEREWOLF).exclude(self.p)
65
68
 
66
69
  msg = UniMessage()
67
70
  if partners:
@@ -70,19 +73,75 @@ class Werewolf(Player):
70
73
  .text("\n".join(f" {p.role_name}: {p.name}" for p in partners))
71
74
  .text("\n所有私聊消息将被转发至队友\n\n")
72
75
  )
73
- await self.send(
76
+ await self.p.send(
74
77
  msg.text("💫请选择今晚的目标:\n")
75
78
  .text(players.show())
76
79
  .text("\n\n🔪发送编号选择玩家")
77
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
78
- .text("\n\n⚠️意见未统一将空刀")
80
+ .text(f"\n❌发送 “{stop_command_prompt()}” 结束回合")
81
+ .text("\n\n⚠️意见未统一将空刀"),
82
+ select_players=players,
79
83
  )
80
84
 
81
85
  self.stream = ObjectStream[str | UniMessage](8)
82
86
 
83
87
  try:
84
88
  async with anyio.create_task_group() as tg:
85
- tg.start_soon(self._handle_interact, players)
86
- tg.start_soon(self._handle_broadcast, partners)
89
+ tg.start_soon(self.handle_interact, players)
90
+ tg.start_soon(self.handle_broadcast, partners)
87
91
  finally:
88
92
  del self.stream
93
+
94
+ async def finalize(self) -> None:
95
+ w = self.game.players.alive().select(RoleGroup.WEREWOLF)
96
+ match w.player_selected().shuffled:
97
+ case []:
98
+ await w.broadcast("⚠️狼人未选择目标,此晚空刀")
99
+ case [killed]:
100
+ self.game.state.killed = killed
101
+ await w.broadcast(f"🔪今晚选择的目标为: {killed.name}")
102
+ case [killed, *_] if self.game.behavior.werewolf_multi_select:
103
+ self.game.state.killed = killed
104
+ await w.broadcast(
105
+ "⚠️狼人阵营意见未统一,随机选择目标\n\n"
106
+ f"🔪今晚选择的目标为: {killed.name}"
107
+ )
108
+ case players:
109
+ await w.broadcast(
110
+ f"⚠️狼人阵营意见未统一,此晚空刀\n\n"
111
+ f"📝选择的玩家:\n{as_player_set(*players).show()}"
112
+ )
113
+
114
+ @override
115
+ async def after(self) -> None:
116
+ if self.game.state.werewolf_end():
117
+ await self.finalize()
118
+
119
+ if not self.game.players.alive().select(Role.WITCH):
120
+ await anyio.sleep(5 + secrets.randbelow(15))
121
+
122
+
123
+ class WerewolfNotifyProvider(NotifyProvider["Werewolf"]):
124
+ @override
125
+ def message(self, message: UniMessage) -> UniMessage:
126
+ if (
127
+ partners := self.game.players.alive()
128
+ .select(RoleGroup.WEREWOLF)
129
+ .exclude(self.p)
130
+ ):
131
+ message = message.text(
132
+ "\n🐺你的队友:\n\n"
133
+ + "".join(f" {p.role_name}: {p.name}\n" for p in partners)
134
+ )
135
+ return message
136
+
137
+
138
+ class Werewolf(Player):
139
+ role = Role.WEREWOLF
140
+ role_group = RoleGroup.WEREWOLF
141
+ interact_provider = WerewolfInteractProvider
142
+ notify_provider = WerewolfNotifyProvider
143
+
144
+ @property
145
+ @override
146
+ def interact_timeout(self) -> float:
147
+ return self.game.behavior.timeout.werewolf
@@ -1,42 +1,52 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
3
+ from nonebot_plugin_alconna.uniseg import UniMessage
4
+
5
+ from ..constant import stop_command_prompt
5
6
  from ..models import Role, RoleGroup
7
+ from ..player import InteractProvider, Player
6
8
  from ..utils import as_player_set
7
- from .player import Player
8
9
 
9
10
 
10
- @Player.register_role(Role.Witch, RoleGroup.GoodGuy)
11
- class Witch(Player):
12
- antidote: bool = True
13
- poison: bool = True
11
+ class WitchInteractProvider(InteractProvider["Witch"]):
12
+ antidote = InteractProvider.proxy(bool)
13
+ poison = InteractProvider.proxy(bool)
14
+
15
+ @override
16
+ async def before(self) -> None:
17
+ await self.p.send("ℹ️请等待狼人决定目标...")
18
+ await self.game.state.werewolf_finished.wait()
14
19
 
15
20
  async def handle_killed(self) -> bool:
16
21
  if (killed := self.game.state.killed) is None:
17
- await self.send("ℹ️今晚没有人被刀")
22
+ await self.p.send("ℹ️今晚没有人被刀")
18
23
  return False
19
24
 
20
25
  msg = UniMessage.text(f"🔪今晚 {killed.name} 被刀了\n\n")
21
26
 
22
27
  if not self.antidote:
23
- await self.send(msg.text("⚙️你已经用过解药了"))
28
+ await self.p.send(msg.text("⚙️你已经用过解药了"))
24
29
  return False
25
30
 
26
- msg.text(f"✏️使用解药请发送 “1”\n❌不使用解药请发送 “{STOP_COMMAND_PROMPT}”")
27
- await self.send(msg)
31
+ msg.text(f"✏️使用解药请发送 “1”\n❌不使用解药请发送 “{stop_command_prompt()}”")
32
+ await self.p.send(
33
+ msg,
34
+ stop_btn_label="不使用解药",
35
+ select_players=as_player_set(killed),
36
+ )
28
37
 
29
- if not await self._select_player(
38
+ if not await self.p.select_player(
30
39
  as_player_set(killed),
31
40
  on_stop=f"ℹ️你选择不对 {killed.name} 使用解药",
32
- on_index_error=f"⚠️输入错误: 请输入 “1” 或 “{STOP_COMMAND_PROMPT}”",
41
+ on_index_error=f"⚠️输入错误: 请输入 “1” 或 “{stop_command_prompt()}”",
42
+ stop_btn_label="不使用解药",
33
43
  ):
34
44
  return False
35
45
 
36
46
  self.antidote = False
37
47
  self.selected = killed
38
48
  self.game.state.antidote.add(killed)
39
- await self.send(f"✅你对 {killed.name} 使用了解药,回合结束")
49
+ await self.p.send(f"✅你对 {killed.name} 使用了解药,回合结束")
40
50
  return True
41
51
 
42
52
  @override
@@ -45,23 +55,37 @@ class Witch(Player):
45
55
  return
46
56
 
47
57
  if not self.poison:
48
- await self.send("⚙️你没有可以使用的药水,回合结束")
58
+ await self.p.send("⚙️你没有可以使用的药水,回合结束")
49
59
  return
50
60
 
51
61
  players = self.game.players.alive()
52
- await self.send(
53
- UniMessage.text("💫你有一瓶毒药\n")
54
- .text("玩家列表:\n")
55
- .text(players.show())
56
- .text("\n\n🧪发送玩家编号使用毒药")
57
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不使用药水)")
62
+ await self.p.send(
63
+ "💫你有一瓶毒药\n"
64
+ "玩家列表:\n"
65
+ f"{players.show()}\n\n"
66
+ "🧪发送玩家编号使用毒药\n"
67
+ f"❌发送 “{stop_command_prompt()}” 结束回合(不使用药水)",
68
+ stop_btn_label="结束回合",
69
+ select_players=players,
58
70
  )
59
71
 
60
- if selected := await self._select_player(
72
+ if selected := await self.p.select_player(
61
73
  players,
62
74
  on_stop="ℹ️你选择不使用毒药,回合结束",
75
+ stop_btn_label="结束回合",
63
76
  ):
64
77
  self.poison = False
65
78
  self.selected = selected
66
- self.game.state.poison.add(self)
67
- await self.send(f"✅当前回合选择对玩家 {selected.name} 使用毒药\n回合结束")
79
+ self.game.state.poison.add(self.p)
80
+ await self.p.send(
81
+ f"✅当前回合选择对玩家 {selected.name} 使用毒药\n回合结束"
82
+ )
83
+
84
+
85
+ class Witch(Player):
86
+ role = Role.WITCH
87
+ role_group = RoleGroup.GOODGUY
88
+ interact_provider = WitchInteractProvider
89
+
90
+ antidote: bool = True
91
+ poison: bool = True
@@ -1,14 +1,20 @@
1
1
  from typing_extensions import override
2
2
 
3
+ from nonebot_plugin_alconna import UniMessage
4
+
3
5
  from ..models import Role, RoleGroup
4
- from .can_shoot import CanShoot
5
- from .player import Player
6
- from .werewolf import Werewolf
6
+ from .shooter import ShooterKillProvider
7
+ from .werewolf import Werewolf, WerewolfNotifyProvider
7
8
 
8
9
 
9
- @Player.register_role(Role.WolfKing, RoleGroup.Werewolf)
10
- class WolfKing(CanShoot, Werewolf):
10
+ class WolfKingNotifyProvider(WerewolfNotifyProvider):
11
11
  @override
12
- async def notify_role(self) -> None:
13
- await super().notify_role()
14
- await self.send("⚙️作为狼王,你可以在死后射杀一名玩家")
12
+ def message(self, message: UniMessage) -> UniMessage:
13
+ return super().message(message).text("⚙️作为狼王,你可以在死后射杀一名玩家")
14
+
15
+
16
+ class WolfKing(Werewolf):
17
+ role = Role.WOLFKING
18
+ role_group = RoleGroup.WEREWOLF
19
+ kill_provider = ShooterKillProvider
20
+ notify_provider = WolfKingNotifyProvider
@@ -1,20 +1,33 @@
1
+ import abc
1
2
  import functools
2
3
  import itertools
3
4
  from collections import defaultdict
4
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
5
+ from collections.abc import Iterable
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, ParamSpec, TypeVar
5
7
 
6
8
  import anyio
7
9
  import anyio.streams.memory
8
- from nonebot_plugin_alconna import UniMessage
10
+ from nonebot.adapters import Bot, Event
11
+ from nonebot.internal.matcher import current_bot
12
+ from nonebot_plugin_alconna.uniseg import (
13
+ Button,
14
+ FallbackStrategy,
15
+ Keyboard,
16
+ Receipt,
17
+ Target,
18
+ UniMessage,
19
+ )
9
20
  from nonebot_plugin_uninfo import Session
10
21
 
11
- from .constant import STOP_COMMAND
22
+ from .config import config
23
+ from .constant import STOP_COMMAND, stop_command_prompt
12
24
 
13
25
  if TYPE_CHECKING:
26
+ from .player import Player
14
27
  from .player_set import PlayerSet
15
- from .players import Player
16
28
 
17
29
  T = TypeVar("T")
30
+ P = ParamSpec("P")
18
31
 
19
32
 
20
33
  def check_index(text: str, arrlen: int) -> int | None:
@@ -85,7 +98,7 @@ class InputStore:
85
98
  task.set(msg)
86
99
 
87
100
  @classmethod
88
- def cleanup(cls, players: list[str], group_id: str) -> None:
101
+ def cleanup(cls, players: Iterable[str], group_id: str) -> None:
89
102
  for p, g in itertools.product(players, (group_id, None)):
90
103
  key = cls._key(p, g)
91
104
  if key in cls.locks:
@@ -106,7 +119,10 @@ def as_player_set(*player: "Player") -> "PlayerSet":
106
119
 
107
120
 
108
121
  class ObjectStream(Generic[T]):
109
- __unset: Any = object()
122
+ class Unset: ...
123
+
124
+ __UNSET: ClassVar[Unset] = Unset()
125
+
110
126
  _send: anyio.streams.memory.MemoryObjectSendStream[T]
111
127
  _recv: anyio.streams.memory.MemoryObjectReceiveStream[T]
112
128
  _closed: anyio.Event
@@ -119,7 +135,7 @@ class ObjectStream(Generic[T]):
119
135
  await self._send.send(obj)
120
136
 
121
137
  async def recv(self) -> T:
122
- result = self.__unset
138
+ result: Any = self.__UNSET
123
139
 
124
140
  async def _recv() -> None:
125
141
  nonlocal result
@@ -134,7 +150,7 @@ class ObjectStream(Generic[T]):
134
150
  tg.start_soon(_recv)
135
151
  tg.start_soon(_cancel)
136
152
 
137
- if result is self.__unset:
153
+ if result is self.__UNSET:
138
154
  raise anyio.EndOfStream
139
155
 
140
156
  return result
@@ -148,3 +164,86 @@ class ObjectStream(Generic[T]):
148
164
 
149
165
  async def wait_closed(self) -> None:
150
166
  await self._closed.wait()
167
+
168
+
169
+ def _btn(label: str, text: str, /) -> Button:
170
+ return Button(flag="input", label=label, text=text)
171
+
172
+
173
+ def add_stop_button(msg: str | UniMessage, label: str | None = None) -> UniMessage:
174
+ if isinstance(msg, str):
175
+ msg = UniMessage.text(msg)
176
+
177
+ stop = stop_command_prompt()
178
+ return msg.keyboard(_btn(label or stop, stop))
179
+
180
+
181
+ def add_players_button(msg: str | UniMessage, players: "PlayerSet") -> UniMessage:
182
+ if isinstance(msg, str):
183
+ msg = UniMessage.text(msg)
184
+
185
+ it = enumerate(players, 1)
186
+ while line := tuple(itertools.islice(it, 3)):
187
+ msg.keyboard(*(_btn(p.name, str(i)) for i, p in line))
188
+ return msg
189
+
190
+
191
+ class SendHandler(abc.ABC, Generic[P]):
192
+ bot: Bot
193
+ target: Event | Target
194
+ reply_to: bool | None = None
195
+ last_msg: UniMessage | None = None
196
+ last_receipt: Receipt | None = None
197
+
198
+ def update(self, target: Event | Target, bot: Bot | None = None) -> None:
199
+ self.bot = bot or current_bot.get()
200
+ self.target = target
201
+
202
+ async def _edit(self) -> None:
203
+ last = self.last_receipt
204
+ if (
205
+ config.enable_button
206
+ and self.last_msg is not None
207
+ and last is not None
208
+ and last.editable
209
+ ):
210
+ await last.edit(self.last_msg.exclude(Keyboard))
211
+
212
+ async def _send(self, message: UniMessage) -> None:
213
+ if not config.enable_button:
214
+ message = message.exclude(Keyboard)
215
+ receipt = await message.send(
216
+ target=self.target,
217
+ bot=self.bot,
218
+ reply_to=self.reply_to,
219
+ fallback=FallbackStrategy.ignore,
220
+ )
221
+ self.last_msg = message
222
+ self.last_receipt = receipt
223
+
224
+ @abc.abstractmethod
225
+ def solve_msg(
226
+ self,
227
+ msg: UniMessage,
228
+ *args: P.args,
229
+ **kwargs: P.kwargs,
230
+ ) -> UniMessage:
231
+ raise NotImplementedError
232
+
233
+ async def send(
234
+ self,
235
+ msg: str | UniMessage,
236
+ *args: P.args,
237
+ **kwargs: P.kwargs,
238
+ ) -> Receipt:
239
+ msg = UniMessage.text(msg) if isinstance(msg, str) else msg
240
+ msg = self.solve_msg(msg, *args, **kwargs)
241
+
242
+ async with anyio.create_task_group() as tg:
243
+ tg.start_soon(self._edit)
244
+ tg.start_soon(self._send, msg)
245
+
246
+ if TYPE_CHECKING:
247
+ assert self.last_receipt is not None
248
+
249
+ return self.last_receipt