easterobot 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. easterobot/alembic/env.py +91 -0
  2. easterobot/alembic/script.py.mako +28 -0
  3. easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
  4. easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
  5. easterobot/bot.py +93 -462
  6. easterobot/cli.py +56 -17
  7. easterobot/commands/__init__.py +8 -0
  8. easterobot/commands/base.py +3 -7
  9. easterobot/commands/basket.py +10 -12
  10. easterobot/commands/edit.py +4 -4
  11. easterobot/commands/game.py +187 -0
  12. easterobot/commands/help.py +1 -1
  13. easterobot/commands/reset.py +1 -1
  14. easterobot/commands/search.py +3 -3
  15. easterobot/commands/top.py +7 -18
  16. easterobot/config.py +67 -3
  17. easterobot/games/__init__.py +14 -0
  18. easterobot/games/connect.py +206 -0
  19. easterobot/games/game.py +262 -0
  20. easterobot/games/rock_paper_scissor.py +206 -0
  21. easterobot/games/tic_tac_toe.py +168 -0
  22. easterobot/hunts/__init__.py +14 -0
  23. easterobot/hunts/hunt.py +428 -0
  24. easterobot/hunts/rank.py +82 -0
  25. easterobot/models.py +2 -1
  26. easterobot/resources/alembic.ini +87 -0
  27. easterobot/resources/config.example.yml +10 -2
  28. easterobot/resources/credits.txt +5 -1
  29. easterobot/resources/emotes/icons/arrow.png +0 -0
  30. easterobot/resources/emotes/icons/end.png +0 -0
  31. easterobot/resources/emotes/icons/versus.png +0 -0
  32. easterobot/resources/emotes/icons/wait.png +0 -0
  33. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/METADATA +11 -5
  34. easterobot-1.1.0.dist-info/RECORD +66 -0
  35. easterobot-1.0.0.dist-info/RECORD +0 -48
  36. /easterobot/resources/{eggs → emotes/eggs}/egg_01.png +0 -0
  37. /easterobot/resources/{eggs → emotes/eggs}/egg_02.png +0 -0
  38. /easterobot/resources/{eggs → emotes/eggs}/egg_03.png +0 -0
  39. /easterobot/resources/{eggs → emotes/eggs}/egg_04.png +0 -0
  40. /easterobot/resources/{eggs → emotes/eggs}/egg_05.png +0 -0
  41. /easterobot/resources/{eggs → emotes/eggs}/egg_06.png +0 -0
  42. /easterobot/resources/{eggs → emotes/eggs}/egg_07.png +0 -0
  43. /easterobot/resources/{eggs → emotes/eggs}/egg_08.png +0 -0
  44. /easterobot/resources/{eggs → emotes/eggs}/egg_09.png +0 -0
  45. /easterobot/resources/{eggs → emotes/eggs}/egg_10.png +0 -0
  46. /easterobot/resources/{eggs → emotes/eggs}/egg_11.png +0 -0
  47. /easterobot/resources/{eggs → emotes/eggs}/egg_12.png +0 -0
  48. /easterobot/resources/{eggs → emotes/eggs}/egg_13.png +0 -0
  49. /easterobot/resources/{eggs → emotes/eggs}/egg_14.png +0 -0
  50. /easterobot/resources/{eggs → emotes/eggs}/egg_15.png +0 -0
  51. /easterobot/resources/{eggs → emotes/eggs}/egg_16.png +0 -0
  52. /easterobot/resources/{eggs → emotes/eggs}/egg_17.png +0 -0
  53. /easterobot/resources/{eggs → emotes/eggs}/egg_18.png +0 -0
  54. /easterobot/resources/{eggs → emotes/eggs}/egg_19.png +0 -0
  55. /easterobot/resources/{eggs → emotes/eggs}/egg_20.png +0 -0
  56. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/WHEEL +0 -0
  57. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/entry_points.txt +0 -0
  58. {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/licenses/LICENSE +0 -0
easterobot/cli.py CHANGED
@@ -6,11 +6,17 @@ import sys
6
6
  from collections.abc import Sequence
7
7
  from typing import NoReturn, Optional
8
8
 
9
- from .bot import DEFAULT_CONFIG_PATH, Easterobot
9
+ from alembic.config import CommandLine
10
+
11
+ from easterobot.config import load_config_from_path
12
+
13
+ from .bot import Easterobot
14
+ from .config import DEFAULT_CONFIG_PATH
10
15
  from .info import __issues__, __summary__, __version__
11
16
 
12
17
  LOG_LEVELS = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
13
18
  logger = logging.getLogger(__name__)
19
+ cmd_alembic = CommandLine(prog="easterobot alembic")
14
20
 
15
21
 
16
22
  class HelpArgumentParser(argparse.ArgumentParser):
@@ -61,7 +67,7 @@ def get_parser() -> argparse.ArgumentParser:
61
67
  action="store_true",
62
68
  )
63
69
 
64
- # Parser of hello command
70
+ # Parser for run command
65
71
  run_parser = subparsers.add_parser(
66
72
  "run",
67
73
  parents=[parent_parser],
@@ -74,19 +80,40 @@ def get_parser() -> argparse.ArgumentParser:
74
80
  default=DEFAULT_CONFIG_PATH,
75
81
  )
76
82
 
77
- # Parser of hello command
78
- run_parser = subparsers.add_parser(
83
+ # Parser for generate command
84
+ generate_parser = subparsers.add_parser(
79
85
  "generate",
80
86
  parents=[parent_parser],
81
87
  help="generate a configuration.",
82
88
  )
83
- run_parser.add_argument(
89
+ generate_parser.add_argument(
84
90
  "-i",
85
91
  "--interactive",
86
92
  help="ask questions for create a ready to use config.",
87
93
  action="store_true",
88
94
  )
89
- run_parser.add_argument("destination", default=".")
95
+ generate_parser.add_argument("destination", default=".")
96
+
97
+ # Parser for alembic
98
+ alembic_parser = subparsers.add_parser(
99
+ "alembic",
100
+ parents=[cmd_alembic.parser], # type: ignore[list-item]
101
+ help="use alembic with bot context.",
102
+ add_help=False,
103
+ )
104
+ for action in alembic_parser._actions: # noqa: SLF001
105
+ if "--config" in action.option_strings:
106
+ alembic_parser._handle_conflict_resolve( # noqa: SLF001
107
+ None, # type: ignore[arg-type]
108
+ [("--config", action), ("-c", action)],
109
+ )
110
+ break
111
+ alembic_parser.add_argument(
112
+ "-c",
113
+ "--config",
114
+ help=f"path to configuration, default to {DEFAULT_CONFIG_PATH}.",
115
+ default=DEFAULT_CONFIG_PATH,
116
+ )
90
117
  return parser
91
118
 
92
119
 
@@ -101,27 +128,39 @@ def setup_logging(verbose: Optional[bool] = None) -> None:
101
128
 
102
129
  def entrypoint(argv: Optional[Sequence[str]] = None) -> None:
103
130
  """Entrypoint for command line interface."""
131
+ args = list(sys.argv[1:] if argv is None else argv)
104
132
  try:
105
133
  parser = get_parser()
106
- args = parser.parse_args(argv)
107
- setup_logging(args.verbose)
108
- if args.action == "run":
134
+ namespace = parser.parse_args(args)
135
+ if namespace.action == "run":
136
+ setup_logging(namespace.verbose)
109
137
  bot = Easterobot.from_config(
110
- args.config,
111
- token=args.token,
112
- env=args.env,
138
+ namespace.config,
139
+ token=namespace.token,
140
+ env=namespace.env,
113
141
  )
114
142
  bot.auto_run()
115
- elif args.action == "generate":
143
+ elif namespace.action == "generate":
144
+ setup_logging(namespace.verbose)
116
145
  Easterobot.generate(
117
- destination=args.destination,
118
- token=args.token,
119
- env=args.env,
120
- interactive=args.interactive,
146
+ destination=namespace.destination,
147
+ token=namespace.token,
148
+ env=namespace.env,
149
+ interactive=namespace.interactive,
121
150
  )
151
+ elif namespace.action == "alembic":
152
+ if not hasattr(namespace, "cmd"):
153
+ # see http://bugs.python.org/issue9253, argparse
154
+ # behavior changed incompatibly in py3.3
155
+ parser.error("too few arguments")
156
+ else:
157
+ config = load_config_from_path(namespace.config)
158
+ cfg = config.alembic_config(namespace)
159
+ cmd_alembic.run_cmd(cfg, namespace)
122
160
  else:
123
161
  parser.error("No command specified") # pragma: no cover
124
162
  except Exception as err: # NoQA: BLE001 # pragma: no cover
163
+ setup_logging(verbose=True)
125
164
  logger.critical("Unexpected error", exc_info=err)
126
165
  logger.critical("Please, report this error to %s.", __issues__)
127
166
  sys.exit(1)
@@ -6,6 +6,11 @@ from easterobot.commands.basket import basket_command
6
6
  from easterobot.commands.disable import disable_command
7
7
  from easterobot.commands.edit import edit_command
8
8
  from easterobot.commands.enable import enable_command
9
+ from easterobot.commands.game import (
10
+ connect4_command,
11
+ rockpaperscissor_command,
12
+ tictactoe_command,
13
+ )
9
14
  from easterobot.commands.help import help_command
10
15
  from easterobot.commands.reset import reset_command
11
16
  from easterobot.commands.search import search_command
@@ -13,13 +18,16 @@ from easterobot.commands.top import top_command
13
18
 
14
19
  __all__ = [
15
20
  "basket_command",
21
+ "connect4_command",
16
22
  "disable_command",
17
23
  "edit_command",
18
24
  "egg_command_group",
19
25
  "enable_command",
20
26
  "help_command",
21
27
  "reset_command",
28
+ "rockpaperscissor_command",
22
29
  "search_command",
30
+ "tictactoe_command",
23
31
  "top_command",
24
32
  ]
25
33
 
@@ -33,8 +33,6 @@ InteractionChannel = Union[
33
33
  discord.VoiceChannel,
34
34
  discord.StageChannel,
35
35
  discord.TextChannel,
36
- discord.ForumChannel,
37
- discord.CategoryChannel,
38
36
  discord.Thread,
39
37
  discord.DMChannel,
40
38
  discord.GroupChannel,
@@ -98,10 +96,8 @@ def controlled_command(
98
96
  have_perms = interaction.user.guild_permissions.is_superset(
99
97
  needed_perms
100
98
  )
101
- admin_ids = interaction.client.config.admins
102
- is_super_admin = (
103
- interaction.user.id in admin_ids
104
- or interaction.user.id == interaction.client.owner_id
99
+ is_super_admin = interaction.client.is_super_admin(
100
+ interaction.user
105
101
  )
106
102
  if not have_perms and not is_super_admin:
107
103
  logger.warning("%s failed for wrong permissions", event_repr)
@@ -113,7 +109,7 @@ def controlled_command(
113
109
 
114
110
  # Check command cooldown
115
111
  cmd = interaction.command.name
116
- if cooldown:
112
+ if cooldown and not is_super_admin:
117
113
  available_at = None
118
114
  async with (
119
115
  AsyncSession(interaction.client.engine) as session,
@@ -7,13 +7,14 @@ from discord import app_commands
7
7
  from sqlalchemy import and_, func, select
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
- from easterobot.bot import embed
11
10
  from easterobot.commands.base import (
12
11
  Context,
13
12
  controlled_command,
14
13
  egg_command_group,
15
14
  )
16
15
  from easterobot.config import agree
16
+ from easterobot.hunts.hunt import embed
17
+ from easterobot.hunts.rank import Ranking
17
18
  from easterobot.models import Egg
18
19
 
19
20
 
@@ -41,10 +42,8 @@ async def basket_command(
41
42
  async with AsyncSession(ctx.client.engine) as session:
42
43
  morsels = []
43
44
  missing = []
44
- user_egg_count = await ctx.client.get_rank(
45
- session, ctx.guild_id, hunter.id
46
- )
47
- rank = user_egg_count[1] if user_egg_count else None
45
+ ranking = await Ranking.from_guild(session, ctx.guild_id)
46
+ hunter_rank = ranking.get(hunter.id)
48
47
  res = await session.execute(
49
48
  select(
50
49
  Egg.emoji_id,
@@ -60,7 +59,7 @@ async def basket_command(
60
59
  )
61
60
  egg_counts: dict[int, int] = dict(res.all()) # type: ignore[arg-type]
62
61
  egg_count = 0
63
- for emoji in ctx.client.app_emojis.choices:
62
+ for emoji in ctx.client.egg_emotes.choices:
64
63
  try:
65
64
  type_count = egg_counts.pop(emoji.id)
66
65
  egg_count += type_count
@@ -73,12 +72,11 @@ async def basket_command(
73
72
  egg_count += absent_count
74
73
  morsels.insert(0, f"🥚 \xd7 {absent_count}")
75
74
 
76
- if rank is not None:
77
- il = ctx.client.config.conjugate("{Iel} est", hunter)
78
- morsels.insert(
79
- 0,
80
- f"**{'Tu es' if you else il} au rang** {rank}\n",
81
- )
75
+ il = ctx.client.config.conjugate("{Iel} est", hunter)
76
+ morsels.insert(
77
+ 0,
78
+ f"**{'Tu es' if you else il} au rang** {hunter_rank.badge}\n",
79
+ )
82
80
 
83
81
  their = "te" if you else "lui"
84
82
  if missing:
@@ -1,11 +1,12 @@
1
1
  """Module for edit command."""
2
2
 
3
3
  import discord
4
+ from discord import app_commands
4
5
  from sqlalchemy import and_, delete, select
5
6
  from sqlalchemy.ext.asyncio import AsyncSession
6
7
 
7
- from easterobot.bot import embed
8
8
  from easterobot.config import RAND, agree
9
+ from easterobot.hunts.hunt import embed
9
10
  from easterobot.models import Egg
10
11
 
11
12
  from .base import Context, controlled_command, egg_command_group
@@ -18,10 +19,9 @@ from .base import Context, controlled_command, egg_command_group
18
19
  async def edit_command(
19
20
  ctx: Context,
20
21
  user: discord.Member,
21
- oeufs: int,
22
+ oeufs: app_commands.Range[int, 0, 10_000],
22
23
  ) -> None:
23
24
  """Edit command."""
24
- oeufs = min(max(oeufs, 0), 100_000)
25
25
  await ctx.response.defer(ephemeral=True)
26
26
  async with AsyncSession(ctx.client.engine) as session:
27
27
  eggs: list[Egg] = list(
@@ -52,7 +52,7 @@ async def edit_command(
52
52
  guild_id=ctx.guild_id,
53
53
  channel_id=ctx.channel_id,
54
54
  user_id=user.id,
55
- emoji_id=ctx.client.app_emojis.rand().id,
55
+ emoji_id=ctx.client.egg_emotes.rand().id,
56
56
  )
57
57
  )
58
58
  await session.commit()
@@ -0,0 +1,187 @@
1
+ """Module for disable hunt."""
2
+
3
+ import asyncio
4
+ from typing import Callable, Optional
5
+
6
+ import discord
7
+ from discord import app_commands
8
+ from sqlalchemy import and_, func, not_, select
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from easterobot.config import RAND
12
+ from easterobot.games.connect import Connect4
13
+ from easterobot.games.game import Game
14
+ from easterobot.games.rock_paper_scissor import RockPaperScissor
15
+ from easterobot.games.tic_tac_toe import TicTacToe
16
+ from easterobot.hunts.rank import Ranking
17
+ from easterobot.models import Egg
18
+
19
+ from .base import Context, controlled_command, egg_command_group
20
+
21
+ lock = asyncio.Lock()
22
+
23
+
24
+ async def get_unlocked_eggs(
25
+ session: AsyncSession, member: discord.Member, counter: int
26
+ ) -> list[Egg]:
27
+ """Get the count of unlocked eggs."""
28
+ return list(
29
+ (
30
+ await session.scalars(
31
+ select(Egg)
32
+ .where(
33
+ and_(
34
+ Egg.guild_id == member.guild.id,
35
+ Egg.user_id == member.id,
36
+ not_(Egg.lock),
37
+ )
38
+ )
39
+ .order_by(func.random()) # Randomize
40
+ .limit(counter)
41
+ )
42
+ ).all()
43
+ )
44
+
45
+
46
+ async def game_dual( # noqa: C901, D103, PLR0912
47
+ ctx: Context,
48
+ member: Optional[discord.Member],
49
+ bet: int,
50
+ cls: Callable[[discord.Member, discord.Member, discord.Message], Game],
51
+ ) -> None:
52
+ # If no member choose a random play in the guild with enough egg
53
+ if member is None:
54
+ if bet == 0:
55
+ members = [m for m in ctx.guild.members if m.id != ctx.user.id]
56
+ else:
57
+ # TODO(dashstrom): can chose member with locked eggs
58
+ async with AsyncSession(ctx.client.engine) as session:
59
+ ranking = await Ranking.from_guild(session, ctx.guild_id)
60
+ hunters = ranking.over(bet)
61
+ mapper_member = {m.id: m for m in ctx.guild.members}
62
+ members = [
63
+ mapper_member[h.member_id]
64
+ for h in hunters
65
+ if h.member_id != ctx.user.id and h.member_id in mapper_member
66
+ ]
67
+ if members:
68
+ member = RAND.choice(members)
69
+ else:
70
+ await ctx.response.send_message(
71
+ "Aucun utilisateur trouvé !",
72
+ ephemeral=True,
73
+ )
74
+ return
75
+
76
+ # Validate user
77
+ if (member.bot or member == ctx.user) and not ctx.client.is_super_admin(
78
+ member
79
+ ):
80
+ await ctx.response.send_message(
81
+ "L'utilisateur n'est pas valide !",
82
+ ephemeral=True,
83
+ )
84
+ return
85
+
86
+ # Check if user has enough eggs for ask
87
+ async with AsyncSession(ctx.client.engine) as session:
88
+ e1, e2 = await asyncio.gather(
89
+ get_unlocked_eggs(session, ctx.user, bet),
90
+ get_unlocked_eggs(session, member, bet),
91
+ )
92
+ if len(e1) < bet:
93
+ await ctx.response.send_message(
94
+ "Vous n'avez pas assez d'oeufs",
95
+ ephemeral=True,
96
+ )
97
+ return
98
+ if len(e2) < bet:
99
+ await ctx.response.send_message(
100
+ f"{member.mention} n'a pas assez d'oeufs",
101
+ ephemeral=True,
102
+ )
103
+ return
104
+
105
+ msg = await ctx.client.game.ask_dual(ctx, member, bet=bet)
106
+ if msg:
107
+ # Check if user still have enough eggs and lock them
108
+ async with AsyncSession(ctx.client.engine) as session:
109
+ async with lock:
110
+ e1, e2 = await asyncio.gather(
111
+ get_unlocked_eggs(session, ctx.user, bet),
112
+ get_unlocked_eggs(session, member, bet),
113
+ )
114
+ if len(e1) < bet:
115
+ await ctx.response.send_message(
116
+ "Vous n'avez plus assez d'oeufs",
117
+ ephemeral=True,
118
+ )
119
+ return
120
+ for e in e1:
121
+ e.lock = True
122
+ if len(e2) < bet:
123
+ await ctx.response.send_message(
124
+ f"{member.mention} n'a plus assez d'oeufs",
125
+ ephemeral=True,
126
+ )
127
+ return
128
+ for e in e2:
129
+ e.lock = True
130
+ await session.commit()
131
+
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()
148
+
149
+
150
+ @egg_command_group.command(
151
+ name="connect4", description="Lancer une partie de puissance 4."
152
+ )
153
+ @controlled_command(cooldown=True)
154
+ async def connect4_command(
155
+ ctx: Context,
156
+ member: Optional[discord.Member] = None,
157
+ bet: app_commands.Range[int, 0] = 0,
158
+ ) -> None:
159
+ """Run a Connect4."""
160
+ await game_dual(ctx, member, bet, Connect4)
161
+
162
+
163
+ @egg_command_group.command(
164
+ name="tictactoe", description="Lancer une partie de morpion."
165
+ )
166
+ @controlled_command(cooldown=True)
167
+ async def tictactoe_command(
168
+ ctx: Context,
169
+ member: discord.Member,
170
+ bet: app_commands.Range[int, 0] = 0,
171
+ ) -> None:
172
+ """Run a tictactoe."""
173
+ await game_dual(ctx, member, bet, TicTacToe)
174
+
175
+
176
+ @egg_command_group.command(
177
+ name="rockpaperscissor",
178
+ description="Lancer une partie de pierre papier ciseaux.",
179
+ )
180
+ @controlled_command(cooldown=True)
181
+ async def rockpaperscissor_command(
182
+ ctx: Context,
183
+ member: discord.Member,
184
+ bet: app_commands.Range[int, 0] = 0,
185
+ ) -> None:
186
+ """Run a rockpaperscissor."""
187
+ await game_dual(ctx, member, bet, RockPaperScissor)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from discord.app_commands import AppCommandGroup
4
4
 
5
- from easterobot.bot import embed
5
+ from easterobot.hunts.hunt import embed
6
6
 
7
7
  from .base import Context, controlled_command, egg_command_group
8
8
 
@@ -7,7 +7,7 @@ import discord
7
7
  from sqlalchemy import and_, delete
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
- from easterobot.bot import embed
10
+ from easterobot.hunts.hunt import embed
11
11
  from easterobot.models import Cooldown, Egg, Hunt
12
12
 
13
13
  from .base import Context, Interaction, controlled_command, egg_command_group
@@ -7,8 +7,8 @@ import discord
7
7
  from sqlalchemy import and_, func, select
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
- from easterobot.bot import embed
11
10
  from easterobot.config import RAND, agree
11
+ from easterobot.hunts.hunt import embed
12
12
  from easterobot.models import Egg, Hunt
13
13
 
14
14
  from .base import (
@@ -81,7 +81,7 @@ async def search_command(ctx: Context) -> None:
81
81
  ) -> discord.Message:
82
82
  return await ctx.followup.send(*args, **kwargs) # type: ignore[no-any-return]
83
83
 
84
- await ctx.client.start_hunt(
84
+ await ctx.client.hunt.start_hunt(
85
85
  ctx.channel_id,
86
86
  ctx.client.config.spotted(ctx.user),
87
87
  member_id=ctx.user.id,
@@ -89,7 +89,7 @@ async def search_command(ctx: Context) -> None:
89
89
  )
90
90
  else:
91
91
  logger.info("found: %.2f > %.2f", prob_s, sample_s)
92
- emoji = ctx.client.app_emojis.rand()
92
+ emoji = ctx.client.egg_emotes.rand()
93
93
  async with AsyncSession(ctx.client.engine) as session:
94
94
  session.add(
95
95
  Egg(
@@ -8,8 +8,8 @@ import discord
8
8
  from sqlalchemy import distinct, func, select
9
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
10
 
11
- from easterobot.bot import embed
12
- from easterobot.config import agree
11
+ from easterobot.hunts.hunt import embed
12
+ from easterobot.hunts.rank import Ranking
13
13
  from easterobot.models import Egg
14
14
 
15
15
  from .base import Context, Interaction, controlled_command, egg_command_group
@@ -18,15 +18,6 @@ PAGE_SIZE = 10
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
- def record_top(rank: str, user_id: int, count: int) -> str:
22
- """Format the current user."""
23
- return (
24
- f"{rank} <@{user_id}>\n"
25
- f"\u2004\u2004\u2004\u2004\u2004"
26
- f"➥ {agree('{0} œuf', '{0} œufs', count)}"
27
- )
28
-
29
-
30
21
  async def embed_rank(
31
22
  ctx: Context,
32
23
  page: int,
@@ -34,13 +25,11 @@ async def embed_rank(
34
25
  ) -> tuple[discord.Embed, bool]:
35
26
  """Embed for rank."""
36
27
  async with AsyncSession(ctx.client.engine) as session:
37
- egg_counts = await ctx.client.get_ranks(
38
- session, ctx.guild_id, PAGE_SIZE, page
39
- )
40
- morsels = []
41
- if egg_counts:
42
- for user_id, rank, egg_count in egg_counts[:10]:
43
- morsels.append(record_top(rank, user_id, egg_count))
28
+ ranking = await Ranking.from_guild(session, ctx.guild_id)
29
+ hunters = ranking.page(page, limit=PAGE_SIZE)
30
+ morsels: list[str] = []
31
+ if hunters:
32
+ morsels.extend(hunter.record for hunter in hunters)
44
33
  else:
45
34
  morsels.append("\n:spider_web: Personne n'a d'œuf")
46
35
  total = await session.scalar(