easterobot 1.1.2__tar.gz → 1.3.1__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 (100) hide show
  1. {easterobot-1.1.2 → easterobot-1.3.1}/PKG-INFO +9 -5
  2. {easterobot-1.1.2 → easterobot-1.3.1}/README.rst +8 -4
  3. easterobot-1.3.1/conftest.py +44 -0
  4. {easterobot-1.1.2 → easterobot-1.3.1}/docker-compose.yml +1 -1
  5. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/bot.py +15 -6
  6. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/__init__.py +3 -0
  7. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/base.py +4 -1
  8. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/disable.py +1 -1
  9. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/enable.py +1 -1
  10. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/game.py +23 -21
  11. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/help.py +3 -3
  12. easterobot-1.3.1/easterobot/commands/info.py +61 -0
  13. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/reset.py +1 -1
  14. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/search.py +14 -40
  15. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/config.py +71 -1
  16. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/games/connect.py +8 -7
  17. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/games/game.py +105 -34
  18. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/games/rock_paper_scissor.py +22 -8
  19. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/games/tic_tac_toe.py +8 -7
  20. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/hunts/hunt.py +183 -84
  21. easterobot-1.3.1/easterobot/hunts/luck.py +53 -0
  22. easterobot-1.3.1/easterobot/query.py +11 -0
  23. easterobot-1.3.1/easterobot/resources/config.example.yml +443 -0
  24. easterobot-1.3.1/easterobot/utils.py +11 -0
  25. {easterobot-1.1.2 → easterobot-1.3.1}/pyproject.toml +7 -5
  26. easterobot-1.3.1/tests/constants.py +8 -0
  27. easterobot-1.3.1/tests/test_search.py +49 -0
  28. {easterobot-1.1.2 → easterobot-1.3.1}/tools/chatgpt.txt +2 -2
  29. {easterobot-1.1.2 → easterobot-1.3.1}/uv.lock +27 -1
  30. easterobot-1.1.2/conftest.py +0 -13
  31. easterobot-1.1.2/easterobot/resources/config.example.yml +0 -234
  32. {easterobot-1.1.2 → easterobot-1.3.1}/.dockerignore +0 -0
  33. {easterobot-1.1.2 → easterobot-1.3.1}/.editorconfig +0 -0
  34. {easterobot-1.1.2 → easterobot-1.3.1}/.github/actions/setup-project/action.yml +0 -0
  35. {easterobot-1.1.2 → easterobot-1.3.1}/.github/workflows/docs.yml +0 -0
  36. {easterobot-1.1.2 → easterobot-1.3.1}/.github/workflows/lint.yml +0 -0
  37. {easterobot-1.1.2 → easterobot-1.3.1}/.github/workflows/publish.yml +0 -0
  38. {easterobot-1.1.2 → easterobot-1.3.1}/.github/workflows/tests.yml +0 -0
  39. {easterobot-1.1.2 → easterobot-1.3.1}/.gitignore +0 -0
  40. {easterobot-1.1.2 → easterobot-1.3.1}/.pre-commit-config.yaml +0 -0
  41. {easterobot-1.1.2 → easterobot-1.3.1}/.vscode/extensions.json +0 -0
  42. {easterobot-1.1.2 → easterobot-1.3.1}/.vscode/ltex.dictionary.en-US.txt +0 -0
  43. {easterobot-1.1.2 → easterobot-1.3.1}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
  44. {easterobot-1.1.2 → easterobot-1.3.1}/.vscode/settings.json +0 -0
  45. {easterobot-1.1.2 → easterobot-1.3.1}/Dockerfile +0 -0
  46. {easterobot-1.1.2 → easterobot-1.3.1}/LICENSE +0 -0
  47. {easterobot-1.1.2 → easterobot-1.3.1}/docs/conf.py +0 -0
  48. {easterobot-1.1.2 → easterobot-1.3.1}/docs/index.rst +0 -0
  49. {easterobot-1.1.2 → easterobot-1.3.1}/docs/references.rst +0 -0
  50. {easterobot-1.1.2 → easterobot-1.3.1}/docs/resources/favicon.png +0 -0
  51. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/__init__.py +0 -0
  52. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/__main__.py +0 -0
  53. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/alembic/env.py +0 -0
  54. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/alembic/script.py.mako +0 -0
  55. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/alembic/versions/2f0d4305e320_init_database.py +0 -0
  56. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +0 -0
  57. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/cli.py +0 -0
  58. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/basket.py +0 -0
  59. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/edit.py +0 -0
  60. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/commands/top.py +0 -0
  61. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/games/__init__.py +0 -0
  62. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/hunts/__init__.py +0 -0
  63. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/hunts/rank.py +0 -0
  64. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/info.py +0 -0
  65. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/logger.py +0 -0
  66. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/models.py +0 -0
  67. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/py.typed +0 -0
  68. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/alembic.ini +0 -0
  69. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/credits.txt +0 -0
  70. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_01.png +0 -0
  71. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_02.png +0 -0
  72. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_03.png +0 -0
  73. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_04.png +0 -0
  74. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_05.png +0 -0
  75. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_06.png +0 -0
  76. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_07.png +0 -0
  77. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_08.png +0 -0
  78. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_09.png +0 -0
  79. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_10.png +0 -0
  80. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_11.png +0 -0
  81. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_12.png +0 -0
  82. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_13.png +0 -0
  83. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_14.png +0 -0
  84. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_15.png +0 -0
  85. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_16.png +0 -0
  86. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_17.png +0 -0
  87. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_18.png +0 -0
  88. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_19.png +0 -0
  89. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/eggs/egg_20.png +0 -0
  90. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/icons/arrow.png +0 -0
  91. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/icons/end.png +0 -0
  92. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/icons/versus.png +0 -0
  93. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/emotes/icons/wait.png +0 -0
  94. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/logging.conf +0 -0
  95. {easterobot-1.1.2 → easterobot-1.3.1}/easterobot/resources/logo.png +0 -0
  96. {easterobot-1.1.2 → easterobot-1.3.1}/entrypoint.sh +0 -0
  97. {easterobot-1.1.2 → easterobot-1.3.1}/tests/__init__.py +0 -0
  98. {easterobot-1.1.2 → easterobot-1.3.1}/tests/test_cli.py +0 -0
  99. {easterobot-1.1.2 → easterobot-1.3.1}/tests/test_config.py +0 -0
  100. {easterobot-1.1.2 → easterobot-1.3.1}/tools/cropping.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easterobot
3
- Version: 1.1.2
3
+ Version: 1.3.1
4
4
  Summary: Discord bot for Easter.
5
5
  Project-URL: Homepage, https://github.com/Dashstrom/easterobot
6
6
  Project-URL: Repository, https://github.com/Dashstrom/easterobot
@@ -121,10 +121,8 @@ from `PyPI <https://pypi.org/project>`_
121
121
  cd easterobot
122
122
  echo "DISCORD_TOKEN=YOU_MUST_PUT_YOUR_TOKEN_HERE" > .env
123
123
 
124
- # Can be unsafe
125
- chmod -R 700 .
126
- mkdir data
127
- chmod 777 data
124
+ # Can be unsafe (and for each update)
125
+ chmod -R 700 . && mkdir data -p && chmod 777 data
128
126
 
129
127
  # Run the docker container
130
128
  docker compose up -d
@@ -135,6 +133,12 @@ from `PyPI <https://pypi.org/project>`_
135
133
  # Remove the container (not the data)
136
134
  docker compose down --rmi all
137
135
 
136
+ # Update
137
+ git reset --hard HEAD && git pull
138
+
139
+ # One-line update
140
+ docker compose down --rmi all && git reset --hard HEAD && git pull && chmod -R 700 . && mkdir data -p && chmod 777 data && docker compose up -d
141
+
138
142
  Configuration directory
139
143
  #######################
140
144
 
@@ -83,10 +83,8 @@ from `PyPI <https://pypi.org/project>`_
83
83
  cd easterobot
84
84
  echo "DISCORD_TOKEN=YOU_MUST_PUT_YOUR_TOKEN_HERE" > .env
85
85
 
86
- # Can be unsafe
87
- chmod -R 700 .
88
- mkdir data
89
- chmod 777 data
86
+ # Can be unsafe (and for each update)
87
+ chmod -R 700 . && mkdir data -p && chmod 777 data
90
88
 
91
89
  # Run the docker container
92
90
  docker compose up -d
@@ -97,6 +95,12 @@ from `PyPI <https://pypi.org/project>`_
97
95
  # Remove the container (not the data)
98
96
  docker compose down --rmi all
99
97
 
98
+ # Update
99
+ git reset --hard HEAD && git pull
100
+
101
+ # One-line update
102
+ docker compose down --rmi all && git reset --hard HEAD && git pull && chmod -R 700 . && mkdir data -p && chmod 777 data && docker compose up -d
103
+
100
104
  Configuration directory
101
105
  #######################
102
106
 
@@ -0,0 +1,44 @@
1
+ """Configuration for all tests."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from typing import Any
5
+
6
+ import py
7
+ import pytest
8
+ import pytest_asyncio
9
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
10
+
11
+ from easterobot import __author__
12
+ from easterobot.bot import Easterobot
13
+ from easterobot.config import MConfig
14
+
15
+
16
+ @pytest.fixture(autouse=True)
17
+ def _add_author(doctest_namespace: dict[str, Any]) -> None:
18
+ """Update doctest namespace."""
19
+ doctest_namespace["author"] = __author__
20
+
21
+
22
+ @pytest.fixture
23
+ def bot(tmpdir: py.path.LocalPath) -> Easterobot:
24
+ """Get a bot ready-to-use."""
25
+ return Easterobot.generate(str(tmpdir), env=True)
26
+
27
+
28
+ @pytest.fixture
29
+ def engine(bot: Easterobot) -> AsyncEngine:
30
+ """Get a bot ready-to-use."""
31
+ return bot.engine
32
+
33
+
34
+ @pytest_asyncio.fixture
35
+ async def session(engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
36
+ """Get a bot ready-to-use."""
37
+ async with AsyncSession(engine, expire_on_commit=False) as session:
38
+ yield session
39
+
40
+
41
+ @pytest.fixture
42
+ def config(bot: Easterobot) -> MConfig:
43
+ """Get a bot ready-to-use."""
44
+ return bot.config
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  bot:
3
- container_name: easterobot_bot
3
+ container_name: bot
4
4
  stdin_open: true
5
5
  tty: true
6
6
  build:
@@ -1,7 +1,6 @@
1
1
  """Main program."""
2
2
 
3
3
  import logging
4
- import logging.config
5
4
  import pathlib
6
5
  import shutil
7
6
  from getpass import getpass
@@ -37,7 +36,8 @@ from .config import (
37
36
  T = TypeVar("T")
38
37
 
39
38
  logger = logging.getLogger(__name__)
40
- INTENTS = discord.Intents.all()
39
+ INTENTS = discord.Intents.default()
40
+ INTENTS.message_content = True
41
41
 
42
42
 
43
43
  class Easterobot(discord.ext.commands.Bot):
@@ -47,15 +47,24 @@ class Easterobot(discord.ext.commands.Bot):
47
47
 
48
48
  def __init__(self, config: MConfig) -> None:
49
49
  """Initialise Easterbot."""
50
+ # Initialize intents
51
+ intents = discord.Intents.default()
52
+ if config.message_content:
53
+ intents.message_content = True
54
+
55
+ # Remove warning about VoiceClient
56
+ discord.VoiceClient.warn_nacl = False
57
+
58
+ # Initialize Bot
50
59
  super().__init__(
51
60
  command_prefix=".",
52
61
  description="Bot discord pour faire la chasse aux œufs",
53
62
  activity=discord.Game(name="rechercher des œufs"),
54
63
  intents=INTENTS,
55
64
  )
56
- self.config = config
57
65
 
58
66
  # Attributes
67
+ self.config = config
59
68
  self.app_commands: list[discord.app_commands.AppCommand] = []
60
69
  self.app_emojis: dict[str, discord.Emoji] = {}
61
70
 
@@ -89,9 +98,9 @@ class Easterobot(discord.ext.commands.Bot):
89
98
  cls,
90
99
  destination: Union[Path, str],
91
100
  *,
92
- token: Optional[str],
93
- env: bool,
94
- interactive: bool,
101
+ token: Optional[str] = None,
102
+ env: bool = False,
103
+ interactive: bool = False,
95
104
  ) -> "Easterobot":
96
105
  """Generate all data."""
97
106
  destination = Path(destination).resolve()
@@ -12,6 +12,7 @@ from easterobot.commands.game import (
12
12
  tictactoe_command,
13
13
  )
14
14
  from easterobot.commands.help import help_command
15
+ from easterobot.commands.info import info_command
15
16
  from easterobot.commands.reset import reset_command
16
17
  from easterobot.commands.search import search_command
17
18
  from easterobot.commands.top import top_command
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "egg_command_group",
25
26
  "enable_command",
26
27
  "help_command",
28
+ "info_command",
27
29
  "reset_command",
28
30
  "rockpaperscissor_command",
29
31
  "search_command",
@@ -33,4 +35,5 @@ __all__ = [
33
35
 
34
36
 
35
37
  async def setup(bot: Easterobot) -> None:
38
+ egg_command_group.name = bot.config.group
36
39
  bot.tree.add_command(egg_command_group)
@@ -170,7 +170,10 @@ def controlled_command( # noqa: C901, PLR0915
170
170
  try:
171
171
  await f(cast(Context, interaction), *args, **kwargs)
172
172
  except InterruptedCommandError:
173
- logger.exception("InterruptedCommandError occur")
173
+ logger.warning(
174
+ "InterruptedCommandError occur for %s",
175
+ event_repr,
176
+ )
174
177
  async with (
175
178
  AsyncSession(interaction.client.engine) as session,
176
179
  cooldown_lock,
@@ -10,7 +10,7 @@ from .base import Context, controlled_command, egg_command_group
10
10
 
11
11
  @egg_command_group.command(
12
12
  name="disable",
13
- description="Désactiver la chasse aux œufs dans le salon"
13
+ description="Désactiver la chasse aux œufs dans le salon",
14
14
  )
15
15
  @controlled_command(cooldown=True, manage_channels=True)
16
16
  async def disable_command(ctx: Context) -> None:
@@ -10,7 +10,7 @@ from .base import Context, controlled_command, egg_command_group
10
10
 
11
11
  @egg_command_group.command(
12
12
  name="enable",
13
- description="Activer la chasse dans le salon"
13
+ description="Activer la chasse dans le salon",
14
14
  )
15
15
  @controlled_command(
16
16
  cooldown=True,
@@ -130,26 +130,28 @@ async def game_dual( # noqa: C901, D103, PLR0912
130
130
  await session.commit()
131
131
 
132
132
  # Play the game
133
- game = cls(ctx.user, member, msg)
134
- await ctx.client.game.run(game)
135
- winner = await game.wait_winner()
136
-
137
- # Give eggs to the winner or remove previous one
138
- async with lock:
139
- for e in e1:
140
- e.lock = False
141
- if winner:
142
- e.user_id = winner.id
143
- for e in e2:
144
- e.lock = False
145
- if winner:
146
- e.user_id = winner.id
147
- await session.commit()
133
+ winner = None
134
+ try:
135
+ game = cls(ctx.user, member, msg)
136
+ await ctx.client.game.run(game)
137
+ winner = await game.wait_winner()
138
+ finally:
139
+ # Give eggs to the winner or remove previous one
140
+ async with lock:
141
+ for e in e1:
142
+ e.lock = False
143
+ if winner:
144
+ e.user_id = winner.id
145
+ for e in e2:
146
+ e.lock = False
147
+ if winner:
148
+ e.user_id = winner.id
149
+ await session.commit()
148
150
 
149
151
 
150
152
  @egg_command_group.command(
151
153
  name="connect4",
152
- description="Lancer une partie de puissance 4"
154
+ description="Lancer une partie de puissance 4",
153
155
  )
154
156
  @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
155
157
  async def connect4_command(
@@ -163,12 +165,12 @@ async def connect4_command(
163
165
 
164
166
  @egg_command_group.command(
165
167
  name="tictactoe",
166
- description="Lancer une partie de morpion"
168
+ description="Lancer une partie de morpion",
167
169
  )
168
- @controlled_command(cooldown=True)
170
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
169
171
  async def tictactoe_command(
170
172
  ctx: Context,
171
- member: discord.Member,
173
+ member: Optional[discord.Member] = None,
172
174
  bet: app_commands.Range[int, 0] = 0,
173
175
  ) -> None:
174
176
  """Run a tictactoe."""
@@ -179,10 +181,10 @@ async def tictactoe_command(
179
181
  name="rockpaperscissor",
180
182
  description="Lancer une partie de pierre papier ciseaux",
181
183
  )
182
- @controlled_command(cooldown=True)
184
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
183
185
  async def rockpaperscissor_command(
184
186
  ctx: Context,
185
- member: discord.Member,
187
+ member: Optional[discord.Member] = None,
186
188
  bet: app_commands.Range[int, 0] = 0,
187
189
  ) -> None:
188
190
  """Run a rockpaperscissor."""
@@ -9,9 +9,9 @@ from .base import Context, controlled_command, egg_command_group
9
9
 
10
10
  @egg_command_group.command(
11
11
  name="help",
12
- description="Obtenir l'aide des commandes"
12
+ description="Obtenir l'aide des commandes",
13
13
  )
14
- @controlled_command(cooldown=True)
14
+ @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
15
15
  async def help_command(ctx: Context) -> None:
16
16
  """Help command."""
17
17
  emb = embed(
@@ -33,4 +33,4 @@ async def help_command(ctx: Context) -> None:
33
33
  value=f"{option.description}",
34
34
  inline=False,
35
35
  )
36
- await ctx.response.send_message(embed=emb, ephemeral=True)
36
+ await ctx.response.send_message(embed=emb)
@@ -0,0 +1,61 @@
1
+ """Command basket."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+
6
+ import discord
7
+ from discord import app_commands
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from easterobot.commands.base import (
11
+ Context,
12
+ controlled_command,
13
+ egg_command_group,
14
+ )
15
+ from easterobot.hunts.hunt import embed
16
+ from easterobot.hunts.rank import Ranking
17
+
18
+
19
+ @egg_command_group.command(
20
+ name="info",
21
+ description="Avoir des informations sur la chance d'un joueur",
22
+ )
23
+ @app_commands.describe(
24
+ user="Joueur a inspecter",
25
+ )
26
+ @controlled_command(cooldown=True)
27
+ async def info_command(
28
+ ctx: Context, user: Optional[discord.Member] = None
29
+ ) -> None:
30
+ """Show current user basket."""
31
+ # Delay the response
32
+ await ctx.response.defer(ephemeral=True)
33
+
34
+ # Set the user of the basket
35
+ hunter = user or ctx.user
36
+
37
+ async with AsyncSession(ctx.client.engine) as session:
38
+ ranking, member_luck = await asyncio.gather(
39
+ Ranking.from_guild(session, ctx.guild_id),
40
+ ctx.client.hunt.get_luck(
41
+ session=session,
42
+ guild_id=hunter.guild.id,
43
+ user_id=hunter.id,
44
+ sleep_hours=False,
45
+ ),
46
+ )
47
+ hunter_rank = ranking.get(hunter.id)
48
+
49
+ await ctx.followup.send(
50
+ embed=embed(
51
+ title=f"Informations sur {hunter.display_name}",
52
+ description=(
53
+ f"Classement : {hunter_rank.badge}\n"
54
+ f"Nombre d'oeufs : `{hunter_rank.eggs}`\n"
55
+ f"Chance brute : `{member_luck.luck:.0%}`\n"
56
+ f"Chance de trouver un oeuf : `{member_luck.discovered:.0%}`\n"
57
+ f"Chance de se faire voler : `{member_luck.spotted:.0%}`"
58
+ ),
59
+ ),
60
+ ephemeral=True,
61
+ )
@@ -15,7 +15,7 @@ from .base import Context, Interaction, controlled_command, egg_command_group
15
15
 
16
16
  @egg_command_group.command(
17
17
  name="reset",
18
- description="Réinitialiser la chasse aux œufs"
18
+ description="Réinitialiser la chasse aux œufs",
19
19
  )
20
20
  @controlled_command(cooldown=True, administrator=True)
21
21
  async def reset_command(ctx: Context) -> None:
@@ -4,10 +4,10 @@ import logging
4
4
  from typing import Any
5
5
 
6
6
  import discord
7
- from sqlalchemy import and_, func, select
7
+ from sqlalchemy import select
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
- from easterobot.config import RAND, agree
10
+ from easterobot.config import agree
11
11
  from easterobot.hunts.hunt import embed
12
12
  from easterobot.models import Egg, Hunt
13
13
 
@@ -23,7 +23,7 @@ logger = logging.getLogger("easterobot")
23
23
 
24
24
  @egg_command_group.command(
25
25
  name="search",
26
- description="Rechercher un œuf"
26
+ description="Rechercher un œuf",
27
27
  )
28
28
  @controlled_command(cooldown=True, channel_permissions={"send_messages": True})
29
29
  async def search_command(ctx: Context) -> None:
@@ -37,7 +37,7 @@ async def search_command(ctx: Context) -> None:
37
37
  "La chasse aux œufs n'est pas activée dans ce salon",
38
38
  ephemeral=True,
39
39
  )
40
- return
40
+ raise InterruptedCommandError
41
41
  try:
42
42
  await ctx.response.defer(ephemeral=False)
43
43
  except discord.errors.NotFound as err:
@@ -45,39 +45,15 @@ async def search_command(ctx: Context) -> None:
45
45
  name = ctx.user.display_name
46
46
 
47
47
  async with AsyncSession(ctx.client.engine) as session:
48
- egg_max = await session.scalar(
49
- select(
50
- func.count().label("max"),
51
- )
52
- .where(Egg.guild_id == ctx.guild_id)
53
- .group_by(Egg.user_id)
54
- .order_by(func.count().label("max").desc())
55
- .limit(1)
48
+ luck = await ctx.client.hunt.get_luck(
49
+ guild_id=ctx.guild_id,
50
+ user_id=ctx.user.id,
51
+ session=session,
52
+ sleep_hours=ctx.client.config.in_sleep_hours(),
56
53
  )
57
- egg_max = egg_max or 0
58
- egg_count = await session.scalar(
59
- select(func.count().label("count")).where(
60
- and_(
61
- Egg.guild_id == ctx.guild.id,
62
- Egg.user_id == ctx.user.id,
63
- )
64
- )
65
- )
66
- if egg_count is None:
67
- egg_count = 0
68
- ratio = egg_count / egg_max if egg_max != 0 else 1.0
69
-
70
- discovered = ctx.client.config.commands.search.discovered
71
- prob_d = (discovered.max - discovered.min) * (1 - ratio) + discovered.min
72
54
 
73
- sample_d = RAND.random()
74
- if prob_d > sample_d or egg_count < discovered.shield:
75
- sample_s = RAND.random()
76
- spotted = ctx.client.config.commands.search.spotted
77
- prob_s = (spotted.max - spotted.min) * ratio + spotted.min
78
- logger.info("discovered: %.2f > %.2f", prob_d, sample_d)
79
- if prob_s > sample_s and egg_count > spotted.shield:
80
- logger.info("spotted: %.2f > %.2f", prob_s, sample_s)
55
+ if luck.sample_discovered():
56
+ if luck.sample_spotted():
81
57
 
82
58
  async def send_method(
83
59
  *args: Any, **kwargs: Any
@@ -91,7 +67,6 @@ async def search_command(ctx: Context) -> None:
91
67
  send_method=send_method,
92
68
  )
93
69
  else:
94
- logger.info("found: %.2f > %.2f", prob_s, sample_s)
95
70
  emoji = ctx.client.egg_emotes.rand()
96
71
  async with AsyncSession(ctx.client.engine) as session:
97
72
  session.add(
@@ -108,7 +83,7 @@ async def search_command(ctx: Context) -> None:
108
83
  "%s (%s) got an egg for a total %s in %s",
109
84
  ctx.user,
110
85
  ctx.user.id,
111
- agree("{0} egg", "{0} eggs", egg_count),
86
+ agree("{0} egg", "{0} eggs", luck.egg_count),
112
87
  ctx.channel.jump_url,
113
88
  )
114
89
  await ctx.followup.send(
@@ -116,15 +91,14 @@ async def search_command(ctx: Context) -> None:
116
91
  title=f"{name} récupère un œuf",
117
92
  description=ctx.client.config.hidden(ctx.user),
118
93
  thumbnail=emoji.url,
119
- egg_count=egg_count + 1,
94
+ egg_count=luck.egg_count + 1,
120
95
  )
121
96
  )
122
97
  else:
123
- logger.info("failed: %.2f > %.2f", prob_d, sample_d)
124
98
  await ctx.followup.send(
125
99
  embed=embed(
126
100
  title=f"{name} repart bredouille",
127
101
  description=ctx.client.config.failed(ctx.user),
128
- egg_count=egg_count,
102
+ egg_count=luck.egg_count,
129
103
  )
130
104
  )
@@ -5,9 +5,11 @@ import logging.config
5
5
  import os
6
6
  import pathlib
7
7
  import random
8
+ import re
8
9
  from abc import ABC, abstractmethod
9
10
  from argparse import Namespace
10
11
  from collections.abc import Iterable
12
+ from datetime import datetime, time, timezone
11
13
  from typing import (
12
14
  Any,
13
15
  Generic,
@@ -80,7 +82,7 @@ class ConjugableText(Serializable[str]):
80
82
  def gender(member: discord.Member) -> Literal["man", "woman"]:
81
83
  """Get the gender of a people."""
82
84
  if any(
83
- marker in role.name.casefold()
85
+ marker in tokenize(role.name)
84
86
  for role in member.roles
85
87
  for marker in (
86
88
  "woman",
@@ -88,6 +90,7 @@ class ConjugableText(Serializable[str]):
88
90
  "femme",
89
91
  "fille",
90
92
  "elle",
93
+ "elles",
91
94
  "her",
92
95
  "she",
93
96
  )
@@ -163,6 +166,14 @@ class RandomConjugableText(RandomItem[ConjugableText]):
163
166
  return cls(convert(obj, typ=list[ConjugableText]))
164
167
 
165
168
 
169
+ class MSleep(msgspec.Struct):
170
+ start: time
171
+ end: time
172
+ divide_hunt: float
173
+ divide_discovered: float
174
+ divide_spotted: float
175
+
176
+
166
177
  class MCooldown(msgspec.Struct):
167
178
  min: float
168
179
  max: float
@@ -194,12 +205,20 @@ class MDiscovered(msgspec.Struct):
194
205
  min: float
195
206
  max: float
196
207
 
208
+ def probability(self, luck: float) -> float:
209
+ """Get discover probability."""
210
+ return (self.max - self.min) * luck + self.min
211
+
197
212
 
198
213
  class MSpotted(msgspec.Struct):
199
214
  shield: int
200
215
  min: float
201
216
  max: float
202
217
 
218
+ def probability(self, luck: float) -> float:
219
+ """Get discover probability."""
220
+ return (self.max - self.min) * (1 - luck) + self.min
221
+
203
222
 
204
223
  class SearchCommand(MCommand):
205
224
  discovered: MDiscovered
@@ -239,6 +258,7 @@ class MCommands(msgspec.Struct, forbid_unknown_fields=True):
239
258
  help: MCommand
240
259
  edit: MCommand
241
260
  connect4: MCommand
261
+ info: MCommand
242
262
  tictactoe: MCommand
243
263
  rockpaperscissor: MCommand
244
264
 
@@ -269,6 +289,16 @@ class MConfig(msgspec.Struct, dict=True):
269
289
  appear: RandomItem[str]
270
290
  action: RandomItem[MText]
271
291
  commands: MCommands
292
+ sleep: MSleep = msgspec.field(
293
+ default_factory=lambda: MSleep(
294
+ start=time(hour=23),
295
+ end=time(hour=9),
296
+ divide_hunt=2.0,
297
+ divide_discovered=2.0,
298
+ divide_spotted=1.5,
299
+ )
300
+ )
301
+ message_content: bool = True
272
302
  token: Optional[Union[str, msgspec.UnsetType]] = msgspec.UNSET
273
303
  _resources: Optional[Union[pathlib.Path, msgspec.UnsetType]] = (
274
304
  msgspec.field(name="resources", default=msgspec.UNSET)
@@ -284,6 +314,17 @@ class MConfig(msgspec.Struct, dict=True):
284
314
  "%(data)s", "/" + self.working_directory.as_posix()
285
315
  )
286
316
 
317
+ def in_sleep_hours(self) -> bool:
318
+ """Get if bot is currently in sleep mode."""
319
+ hour = datetime.now(tz=timezone.utc).time()
320
+ if self.sleep.start < self.sleep.end:
321
+ if self.sleep.start < hour < self.sleep.end:
322
+ return True
323
+ elif self.sleep.start > self.sleep.end: # noqa: SIM102
324
+ if not self.sleep.start < hour < self.sleep.end:
325
+ return True
326
+ return False
327
+
287
328
  def verified_token(self) -> str:
288
329
  """Get the safe token."""
289
330
  if self.token is None or self.token is msgspec.UNSET:
@@ -370,6 +411,14 @@ class MConfig(msgspec.Struct, dict=True):
370
411
  )
371
412
  self.__logging_flag = True
372
413
 
414
+ def __str__(self) -> str:
415
+ """Represent the Configuration."""
416
+ return f"<Config {str(self.working_directory)!r}>"
417
+
418
+ def __repr__(self) -> str:
419
+ """Represent the Configuration."""
420
+ return f"<Config {str(self.working_directory)!r}>"
421
+
373
422
 
374
423
  def _dec_hook(typ: type[T], obj: Any) -> T:
375
424
  # Get the base type
@@ -463,3 +512,24 @@ def agree(
463
512
  if amount is None or amount in (-1, 0, 1):
464
513
  return singular.format(amount, *args)
465
514
  return plural.format(amount, *args)
515
+
516
+
517
+ RE_VALID = re.compile(r"[^a-zA-Z0-9éàèê]")
518
+
519
+
520
+ def tokenize(text: str) -> list[str]:
521
+ """Get token from text.
522
+
523
+ Examples:
524
+ >>> tokenize("Activités manuelles")
525
+ ['activités', 'manuelles']
526
+ >>> tokenize("Elle")
527
+ ['elle']
528
+ >>> tokenize("Iel/Iels")
529
+ ['iel', 'iels']
530
+ >>> tokenize("🦉 Elle")
531
+ ['elle']
532
+ """
533
+ text = text.casefold()
534
+ text = RE_VALID.sub(" ", text)
535
+ return text.strip().split()