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.
- easterobot/alembic/env.py +91 -0
- easterobot/alembic/script.py.mako +28 -0
- easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
- easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
- easterobot/bot.py +93 -462
- easterobot/cli.py +56 -17
- easterobot/commands/__init__.py +8 -0
- easterobot/commands/base.py +3 -7
- easterobot/commands/basket.py +10 -12
- easterobot/commands/edit.py +4 -4
- easterobot/commands/game.py +187 -0
- easterobot/commands/help.py +1 -1
- easterobot/commands/reset.py +1 -1
- easterobot/commands/search.py +3 -3
- easterobot/commands/top.py +7 -18
- easterobot/config.py +67 -3
- easterobot/games/__init__.py +14 -0
- easterobot/games/connect.py +206 -0
- easterobot/games/game.py +262 -0
- easterobot/games/rock_paper_scissor.py +206 -0
- easterobot/games/tic_tac_toe.py +168 -0
- easterobot/hunts/__init__.py +14 -0
- easterobot/hunts/hunt.py +428 -0
- easterobot/hunts/rank.py +82 -0
- easterobot/models.py +2 -1
- easterobot/resources/alembic.ini +87 -0
- easterobot/resources/config.example.yml +10 -2
- easterobot/resources/credits.txt +5 -1
- easterobot/resources/emotes/icons/arrow.png +0 -0
- easterobot/resources/emotes/icons/end.png +0 -0
- easterobot/resources/emotes/icons/versus.png +0 -0
- easterobot/resources/emotes/icons/wait.png +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/METADATA +11 -5
- easterobot-1.1.0.dist-info/RECORD +66 -0
- easterobot-1.0.0.dist-info/RECORD +0 -48
- /easterobot/resources/{eggs → emotes/eggs}/egg_01.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_02.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_03.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_04.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_05.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_06.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_07.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_08.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_09.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_10.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_11.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_12.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_13.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_14.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_15.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_16.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_17.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_18.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_19.png +0 -0
- /easterobot/resources/{eggs → emotes/eggs}/egg_20.png +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/WHEEL +0 -0
- {easterobot-1.0.0.dist-info → easterobot-1.1.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
25
|
-
from sqlalchemy.ext.asyncio import
|
26
|
-
|
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
|
-
|
27
|
+
DEFAULT_CONFIG_PATH,
|
28
|
+
EXAMPLE_CONFIG_PATH,
|
30
29
|
RESOURCES,
|
31
30
|
MConfig,
|
32
31
|
RandomItem,
|
33
|
-
agree,
|
34
32
|
dump_yaml,
|
35
|
-
|
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
|
-
|
66
|
-
|
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:
|
78
|
-
|
79
|
-
|
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
|
-
|
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 =
|
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 =
|
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
|
-
#
|
154
|
-
|
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
|
-
#
|
177
|
-
|
178
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
501
|
-
|
502
|
-
|
207
|
+
image_data = emote.read_bytes()
|
208
|
+
emoji = await self.create_application_emoji(
|
209
|
+
name=name,
|
210
|
+
image=image_data,
|
503
211
|
)
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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]
|