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/bot.py CHANGED
@@ -1,18 +1,13 @@
1
1
  """Main program."""
2
2
 
3
- import asyncio
4
3
  import logging
5
4
  import logging.config
6
5
  import pathlib
7
6
  import shutil
8
- import time
9
- from collections.abc import Awaitable
10
- from datetime import datetime, timezone
11
7
  from getpass import getpass
12
8
  from pathlib import Path
13
9
  from typing import (
14
- Any,
15
- Callable,
10
+ TYPE_CHECKING,
16
11
  Optional,
17
12
  TypeVar,
18
13
  Union,
@@ -21,37 +16,34 @@ from typing import (
21
16
  import discord
22
17
  import discord.app_commands
23
18
  import discord.ext.commands
24
- from sqlalchemy import and_, func, select
25
- from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
26
- from sqlalchemy.sql.expression import Select
19
+ from alembic.command import upgrade
20
+ from sqlalchemy.ext.asyncio import create_async_engine
21
+
22
+ if TYPE_CHECKING:
23
+ from easterobot.games.game import GameCog
24
+ from easterobot.hunts.hunt import HuntCog
27
25
 
28
26
  from .config import (
29
- RAND,
27
+ DEFAULT_CONFIG_PATH,
28
+ EXAMPLE_CONFIG_PATH,
30
29
  RESOURCES,
31
30
  MConfig,
32
31
  RandomItem,
33
- agree,
34
32
  dump_yaml,
35
- load_config,
33
+ load_config_from_buffer,
34
+ load_config_from_path,
36
35
  )
37
- from .models import Base, Egg, Hunt
38
36
 
39
37
  T = TypeVar("T")
40
38
 
41
39
  logger = logging.getLogger(__name__)
42
-
43
- HERE = Path(__file__).parent
44
-
45
- DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
46
-
47
- DEFAULT_CONFIG_PATH = pathlib.Path("config.yml")
48
- EXAMPLE_CONFIG_PATH = RESOURCES / "config.example.yml"
49
- RANK_MEDAL = {1: "🥇", 2: "🥈", 3: "🥉"}
50
40
  INTENTS = discord.Intents.all()
51
41
 
52
42
 
53
43
  class Easterobot(discord.ext.commands.Bot):
54
44
  owner: discord.User
45
+ game: "GameCog"
46
+ hunt: "HuntCog"
55
47
 
56
48
  def __init__(self, config: MConfig) -> None:
57
49
  """Initialise Easterbot."""
@@ -62,38 +54,34 @@ class Easterobot(discord.ext.commands.Bot):
62
54
  intents=INTENTS,
63
55
  )
64
56
  self.config = config
65
- defaults = {"data": self.config.working_directory.as_posix()}
66
- if self.config.use_logging_file:
67
- logging_file = self.config.resources / "logging.conf"
68
- if not logging_file.is_file():
69
- error_message = f"Cannot find message: {str(logging_file)!r}"
70
- raise FileNotFoundError(error_message)
71
- logging.config.fileConfig(
72
- logging_file,
73
- disable_existing_loggers=False,
74
- defaults=defaults,
75
- )
57
+
58
+ # Attributes
76
59
  self.app_commands: list[discord.app_commands.AppCommand] = []
77
- self.app_emojis: RandomItem[discord.Emoji] = RandomItem([])
78
- database_uri = self.config.database.replace(
79
- "%(data)s", "/" + self.config.working_directory.as_posix()
60
+ self.app_emojis: dict[str, discord.Emoji] = {}
61
+
62
+ # Configure logging
63
+ self.config.configure_logging()
64
+
65
+ # Update database
66
+ upgrade(self.config.alembic_config(), "head")
67
+
68
+ # Open database
69
+ logger.info("Open database %s", self.config.database_uri)
70
+ self.engine = create_async_engine(
71
+ self.config.database_uri,
72
+ echo=False,
80
73
  )
81
- logger.info("Open database %s", database_uri)
82
- self.engine = create_async_engine(database_uri, echo=False)
83
74
 
84
75
  @classmethod
85
76
  def from_config(
86
77
  cls,
87
- config_path: Union[str, Path] = DEFAULT_CONFIG_PATH,
78
+ path: Union[str, Path] = DEFAULT_CONFIG_PATH,
88
79
  *,
89
80
  token: Optional[str] = None,
90
81
  env: bool = False,
91
82
  ) -> "Easterobot":
92
83
  """Instantiate Easterobot from config."""
93
- path = pathlib.Path(config_path)
94
- data = pathlib.Path(path).read_bytes()
95
- config = load_config(data, token=token, env=env)
96
- config.attach_default_working_directory(path.parent)
84
+ config = load_config_from_path(path, token=token, env=env)
97
85
  return Easterobot(config)
98
86
 
99
87
  @classmethod
@@ -109,7 +97,7 @@ class Easterobot(discord.ext.commands.Bot):
109
97
  destination = Path(destination).resolve()
110
98
  destination.mkdir(parents=True, exist_ok=True)
111
99
  config_data = EXAMPLE_CONFIG_PATH.read_bytes()
112
- config = load_config(config_data, token=token, env=env)
100
+ config = load_config_from_buffer(config_data, token=token, env=env)
113
101
  config.attach_default_working_directory(destination)
114
102
  if interactive:
115
103
  while True:
@@ -127,12 +115,44 @@ class Easterobot(discord.ext.commands.Bot):
127
115
  (destination / ".gitignore").write_bytes(b"*\n")
128
116
  return Easterobot(config)
129
117
 
118
+ def is_super_admin(
119
+ self,
120
+ user: Union[discord.User, discord.Member],
121
+ ) -> bool:
122
+ """Get if user is admin."""
123
+ return (
124
+ user.id in self.config.admins
125
+ or user.id in (self.owner.id, self.owner_id)
126
+ or (self.owner_ids is not None and user.id in self.owner_ids)
127
+ )
128
+
129
+ async def resolve_channel(
130
+ self,
131
+ channel_id: int,
132
+ ) -> Optional[discord.TextChannel]:
133
+ """Resolve channel."""
134
+ channel = self.get_channel(channel_id)
135
+ if channel is None:
136
+ try:
137
+ channel = await self.fetch_channel(channel_id)
138
+ except (discord.NotFound, discord.Forbidden):
139
+ return None
140
+ if not isinstance(channel, discord.TextChannel):
141
+ return None
142
+ return channel
143
+
130
144
  # Method that loads cogs
131
145
  async def setup_hook(self) -> None:
132
146
  """Setup hooks."""
133
147
  await self.load_extension(
134
148
  "easterobot.commands", package="easterobot.commands.__init__"
135
149
  )
150
+ await self.load_extension(
151
+ "easterobot.games", package="easterobot.games.__init__"
152
+ )
153
+ await self.load_extension(
154
+ "easterobot.hunts", package="easterobot.hunts.__init__"
155
+ )
136
156
 
137
157
  def auto_run(self) -> None:
138
158
  """Run the bot with the given token."""
@@ -150,32 +170,14 @@ class Easterobot(discord.ext.commands.Bot):
150
170
  self.owner = app_info.owner
151
171
  logger.info("Owner is %s (%s)", self.owner.display_name, self.owner.id)
152
172
 
153
- # Sync emojis
154
- emojis = {
155
- emoji.name: emoji
156
- for emoji in await self.fetch_application_emojis()
157
- }
158
- eggs_path = (self.config.resources / "eggs").resolve()
159
- self.app_emojis = RandomItem([])
160
- for egg in eggs_path.iterdir():
161
- if egg.stem not in emojis:
162
- logger.info(
163
- "Missing emoji %s, create emoji on application",
164
- egg.stem,
165
- )
166
- image_data = egg.read_bytes()
167
- emoji = await self.create_application_emoji(
168
- name=egg.stem,
169
- image=image_data,
170
- )
171
- self.app_emojis.choices.append(emoji)
172
- else:
173
- logger.info("Load emoji %s", egg.stem)
174
- self.app_emojis.choices.append(emojis[egg.stem])
173
+ # Load emojis
174
+ await self._load_emojis()
175
175
 
176
- # Create the tables
177
- async with self.engine.begin() as session:
178
- await session.run_sync(Base.metadata.create_all, checkfirst=True)
176
+ # Load eggs
177
+ eggs_path = (self.config.resources / "emotes" / "eggs").resolve()
178
+ self.egg_emotes = RandomItem(
179
+ [self.app_emojis[path.stem] for path in eggs_path.glob("**/*")]
180
+ )
179
181
 
180
182
  # Log all available guilds
181
183
  async for guild in self.fetch_guilds():
@@ -185,400 +187,29 @@ class Easterobot(discord.ext.commands.Bot):
185
187
  self.user,
186
188
  getattr(self.user, "id", "unknown"),
187
189
  )
188
- pending_hunts: set[asyncio.Task[Any]] = set()
189
- while True:
190
- if pending_hunts:
191
- try:
192
- _, pending_hunts = await asyncio.wait(
193
- pending_hunts, timeout=1
194
- )
195
- except Exception as err: # noqa: BLE001
196
- logger.critical("Unattended exception", exc_info=err)
197
- await asyncio.sleep(5)
198
- pending_hunts.add(asyncio.create_task(self.loop_hunt()))
199
-
200
- async def start_hunt( # noqa: C901, PLR0912, PLR0915
201
- self,
202
- hunt_id: int,
203
- description: str,
204
- *,
205
- member_id: Optional[int] = None,
206
- send_method: Optional[
207
- Callable[..., Awaitable[discord.Message]]
208
- ] = None,
209
- ) -> None:
210
- """Start an hunt in a channel."""
211
- # Get the hunt channel of resolve it
212
- channel = self.get_channel(hunt_id)
213
- if channel is None:
214
- channel = await self.fetch_channel(hunt_id)
215
- if not isinstance(channel, discord.TextChannel):
216
- logger.warning("Invalid channel type %s", channel)
217
- return
218
-
219
- # Get from config
220
- action = self.config.action.rand()
221
- guild = channel.guild
222
- emoji = self.app_emojis.rand()
223
-
224
- # Label and hunters
225
- hunters: list[discord.Member] = []
226
- label = action.text
227
- if member_id is not None:
228
- member = channel.guild.get_member(member_id)
229
- if member:
230
- hunters.append(member)
231
- label += " (1)"
232
-
233
- # Start hunt
234
- logger.info("Start hunt in %s", channel.jump_url)
235
- timeout = self.config.hunt.timeout + 1
236
- view = discord.ui.View(timeout=timeout)
237
- button: discord.ui.Button[Any] = discord.ui.Button(
238
- label=label,
239
- style=discord.ButtonStyle.primary,
240
- emoji=emoji,
241
- )
242
- view.add_item(button)
243
- waiting = False
244
- active = False
245
-
246
- async def button_callback(
247
- interaction: discord.Interaction[Any],
248
- ) -> None:
249
- nonlocal waiting, active
250
- # Respond later
251
- await interaction.response.defer()
252
- message = interaction.message
253
- user = interaction.user
254
- if (
255
- message is None # Message must be loaded
256
- or not isinstance(user, discord.Member) # Must be a member
257
- ):
258
- logger.warning("Invalid callback for %s", guild)
259
- return
260
- # Check if user doesn't already claim the egg
261
- for hunter in hunters:
262
- if hunter.id == user.id:
263
- logger.info(
264
- "Already hunt by %s (%s) on %s",
265
- user,
266
- user.id,
267
- message.jump_url,
268
- )
269
- return
270
-
271
- # Add the user to the current users
272
- hunters.append(user)
273
-
274
- # Show information about the hunter
275
- logger.info(
276
- "Hunt (%d) by %s (%s) on %s",
277
- len(hunters),
278
- user,
279
- user.id,
280
- message.jump_url,
281
- )
282
-
283
- # TODO(dashstrom): must refactor this lock ?
284
- if active and not waiting:
285
- waiting = True
286
- while active: # noqa: ASYNC110
287
- await asyncio.sleep(0.01)
288
- waiting = False
289
-
290
- active = True
291
- button.label = action.text + f" ({len(hunters)})"
292
- await message.edit(view=view)
293
- active = False
294
-
295
- # Set the button callback
296
- button.callback = button_callback # type: ignore[method-assign]
297
190
 
298
- # Set next hunt
299
- next_hunt = time.time() + timeout
300
-
301
- # Create and embed
302
- emb = embed(
303
- title="Un œuf a été découvert !",
304
- description=description
305
- + f"\n\nTirage du vinqueur : <t:{next_hunt:.0f}:R>",
306
- thumbnail=emoji.url,
307
- )
308
-
309
- # Send the embed in the hunt channel
310
- if send_method is None:
311
- message = await channel.send(embed=emb, view=view)
312
- else:
313
- message = await send_method(embed=emb, view=view)
314
-
315
- # TODO(dashstrom): channel is wrong due to the send message !
316
- # Wait the end of the hunt
317
- message_url = f"{channel.jump_url}/{message.id}"
318
- async with channel.typing():
319
- try:
320
- await asyncio.wait_for(
321
- view.wait(), timeout=self.config.hunt.timeout
322
- )
323
- except asyncio.TimeoutError:
324
- logger.info("End hunt for %s", message_url)
325
-
326
- # Disable button and view after hunt
327
- button.disabled = True
328
- view.stop()
329
- await message.edit(view=view) # Send the stop info
330
-
331
- # Get if hunt is valid
332
- async with AsyncSession(self.engine) as session:
333
- has_hunt = await session.scalar(
334
- select(Hunt).where(
335
- and_(
336
- Hunt.guild_id == guild.id,
337
- Hunt.channel_id == channel.id,
338
- )
339
- )
340
- )
341
-
342
- # The egg was not collected
343
- if not hunters or not has_hunt:
344
- button.label = "L'œuf n'a pas été ramassé"
345
- button.style = discord.ButtonStyle.danger
346
- logger.info("No Hunter for %s", message_url)
347
-
348
- # Process the winner
349
- else:
350
- async with AsyncSession(self.engine) as session:
351
- # Get the count of egg by user
352
- res = await session.execute(
353
- select(Egg.user_id, func.count().label("count"))
354
- .where(
355
- and_(
356
- Egg.guild_id == guild.id,
357
- Egg.user_id.in_(hunter.id for hunter in hunters),
358
- )
359
- )
360
- .group_by(Egg.user_id)
361
- )
362
- eggs: dict[int, int] = dict(res.all()) # type: ignore[arg-type]
363
- logger.info("Winner draw for %s", message_url)
364
-
365
- # If only one hunter, give the egg to him
366
- if len(hunters) == 1:
367
- winner = hunters[0]
368
- loser = None
369
- logger.info("100%% - %s (%s)", winner, winner.id)
370
- else:
371
- lh = len(hunters)
372
- min_eggs = min(eggs.values(), default=0)
373
- max_eggs = max(eggs.values(), default=0)
374
- diff_eggs = max_eggs - min_eggs
375
- w_egg = self.config.hunt.weights.egg
376
- w_speed = self.config.hunt.weights.speed
377
- weights = []
378
-
379
- # Compute chances of each hunters
380
- for i, h in enumerate(hunters, start=1):
381
- if diff_eggs != 0:
382
- egg = eggs.get(h.id, 0) - min_eggs
383
- p_egg = (1 - egg / diff_eggs) * w_egg + 1 - w_egg
384
- else:
385
- p_egg = 1.0
386
- if lh != 0:
387
- p_speed = (1 - i / lh) * w_speed + 1 - w_speed
388
- else:
389
- p_speed = 1.0
390
- weights.append(p_egg * p_speed)
391
- r = sum(weights)
392
- chances = [(h, p / r) for h, p in zip(hunters, weights)]
393
- for h, c in chances:
394
- logger.info("%.2f%% - %s (%s)", c * 100, h, h.id)
395
-
396
- # Get the winner
397
- n = RAND.random()
398
- for h, p in chances:
399
- if n < p:
400
- winner = h
401
- break
402
- n -= p
403
- else:
404
- winner = hunters[-1]
405
-
406
- # Get a random loser
407
- hunters.remove(winner)
408
- loser = RAND.choice(hunters)
409
-
410
- # Add the egg to the member
411
- session.add(
412
- Egg(
413
- channel_id=channel.id,
414
- guild_id=channel.guild.id,
415
- user_id=winner.id,
416
- emoji_id=emoji.id,
417
- )
418
- )
419
- await session.commit()
420
-
421
- # Show the embed to loser
422
- if loser:
423
- loser_name = loser.display_name
424
- if len(hunters) == 1:
425
- text = f"{loser_name} rate un œuf"
426
- else:
427
- text = agree(
428
- "{1} et {0} autre chasseur ratent un œuf",
429
- "{1} et {0} autres chasseurs ratent un œuf",
430
- len(hunters) - 1,
431
- loser_name,
432
- )
433
- emb = embed(
434
- title=text,
435
- description=action.fail.text(loser),
436
- image=action.fail.gif,
437
- )
438
- await channel.send(embed=emb, reference=message)
439
-
440
- # Send embed for the winner
441
- winner_eggs = eggs.get(winner.id, 0) + 1
442
- emb = embed(
443
- title=f"{winner.display_name} récupère un œuf",
444
- description=action.success.text(winner),
445
- image=action.success.gif,
446
- thumbnail=emoji.url,
447
- egg_count=winner_eggs,
448
- )
449
- await channel.send(embed=emb, reference=message)
450
-
451
- # Update button
452
- button.label = f"L'œuf a été ramassé par {winner.display_name}"
453
- button.style = discord.ButtonStyle.success
454
- logger.info(
455
- "Winner is %s (%s) with %s",
456
- winner,
457
- winner.id,
458
- agree("{0} egg", "{0} eggs", winner_eggs),
459
- )
460
-
461
- # Remove emoji and edit view
462
- button.emoji = None
463
- await message.edit(view=view)
464
-
465
- async def loop_hunt(self) -> None:
466
- """Manage the schedule of run."""
467
- # Create a async session
468
- async with AsyncSession(
469
- self.engine, expire_on_commit=False
470
- ) as session:
471
- # Find hunt with next egg available
472
- now = time.time()
473
- hunts = (
474
- await session.scalars(select(Hunt).where(Hunt.next_egg <= now))
475
- ).all()
476
-
477
- # For each hunt, set the next run and store the channel ids
478
- if hunts:
479
- for hunt in hunts:
480
- next_egg = now + self.config.hunt.cooldown.rand()
481
- dt_next = datetime.fromtimestamp(next_egg, tz=timezone.utc)
482
- logger.info(
483
- "Next hunt at %s on %s",
484
- hunt.jump_url,
485
- dt_next.strftime(DATE_FORMAT),
486
- )
487
- hunt.next_egg = next_egg
488
- await session.commit()
489
- hunt_ids = [hunt.channel_id for hunt in hunts]
490
-
491
- # Call start_hunt for each hunt
492
- if hunt_ids:
493
- try:
494
- await asyncio.gather(
495
- *[
496
- self.start_hunt(hunt_id, self.config.appear.rand())
497
- for hunt_id in hunt_ids
498
- ]
191
+ async def _load_emojis(self) -> None:
192
+ emojis = {
193
+ emoji.name: emoji
194
+ for emoji in await self.fetch_application_emojis()
195
+ }
196
+ emotes_path = (self.config.resources / "emotes").resolve()
197
+ self.app_emojis = {}
198
+ for emote in emotes_path.glob("**/*"):
199
+ if not emote.is_file():
200
+ continue
201
+ name = emote.stem
202
+ if emote.stem not in emojis:
203
+ logger.info(
204
+ "Missing emoji %s, create emoji on application",
205
+ name,
499
206
  )
500
- except Exception as err:
501
- logger.exception(
502
- "An error occurred during start hunt", exc_info=err
207
+ image_data = emote.read_bytes()
208
+ emoji = await self.create_application_emoji(
209
+ name=name,
210
+ image=image_data,
503
211
  )
504
-
505
- async def get_rank(
506
- self,
507
- session: AsyncSession,
508
- guild_id: int,
509
- user_id: int,
510
- ) -> Optional[tuple[int, str, int]]:
511
- """Get the rank of single user."""
512
- query = _prepare_rank(guild_id)
513
- subq = query.subquery()
514
- select(subq).where(subq.c.user_id == user_id)
515
- ranks = await _compute_rank(session, query)
516
- return ranks[0] if ranks else None
517
-
518
- async def get_ranks(
519
- self,
520
- session: AsyncSession,
521
- guild_id: int,
522
- limit: Optional[int] = None,
523
- page: Optional[int] = None,
524
- ) -> list[tuple[int, str, int]]:
525
- """Get ranks by page."""
526
- query = _prepare_rank(guild_id)
527
- if limit is not None:
528
- query = query.limit(limit)
529
- if page is not None:
530
- query = query.offset(page * limit)
531
- return await _compute_rank(session, query)
532
-
533
-
534
- def _prepare_rank(guild_id: int) -> Select[Any]:
535
- """Create a select query with order user by egg count."""
536
- return (
537
- select(
538
- Egg.user_id,
539
- func.rank().over(order_by=func.count().desc()).label("row"),
540
- func.count().label("count"),
541
- )
542
- .where(Egg.guild_id == guild_id)
543
- .group_by(Egg.user_id)
544
- .order_by(func.count().desc())
545
- )
546
-
547
-
548
- async def _compute_rank(
549
- session: AsyncSession, query: Select[Any]
550
- ) -> list[tuple[int, str, int]]:
551
- res = await session.execute(query)
552
- return [
553
- (member_id, RANK_MEDAL.get(rank, f"`#{rank}`"), egg_count)
554
- for member_id, rank, egg_count in res.all()
555
- ]
556
-
557
-
558
- def embed( # noqa: PLR0913
559
- *,
560
- title: str,
561
- description: Optional[str] = None,
562
- image: Optional[str] = None,
563
- thumbnail: Optional[str] = None,
564
- egg_count: Optional[int] = None,
565
- footer: Optional[str] = None,
566
- ) -> discord.Embed:
567
- """Create an embed with default format."""
568
- new_embed = discord.Embed(
569
- title=title,
570
- description=description,
571
- colour=RAND.randint(0, 1 << 24),
572
- type="gifv" if image else "rich",
573
- )
574
- if image is not None:
575
- new_embed.set_image(url=image)
576
- if thumbnail is not None:
577
- new_embed.set_thumbnail(url=thumbnail)
578
- if egg_count is not None:
579
- footer = (footer + " - ") if footer else ""
580
- footer += "Cela lui fait un total de "
581
- footer += agree("{0} œuf", "{0} œufs", egg_count)
582
- if footer:
583
- new_embed.set_footer(text=footer)
584
- return new_embed
212
+ self.app_emojis[name] = emoji
213
+ else:
214
+ logger.info("Load emoji %s", name)
215
+ self.app_emojis[name] = emojis[name]