easterobot 1.0.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/__init__.py +19 -0
- easterobot/__main__.py +6 -0
- easterobot/bot.py +584 -0
- easterobot/cli.py +127 -0
- easterobot/commands/__init__.py +28 -0
- easterobot/commands/base.py +171 -0
- easterobot/commands/basket.py +99 -0
- easterobot/commands/disable.py +29 -0
- easterobot/commands/edit.py +68 -0
- easterobot/commands/enable.py +35 -0
- easterobot/commands/help.py +33 -0
- easterobot/commands/reset.py +121 -0
- easterobot/commands/search.py +127 -0
- easterobot/commands/top.py +105 -0
- easterobot/config.py +401 -0
- easterobot/info.py +18 -0
- easterobot/logger.py +16 -0
- easterobot/models.py +58 -0
- easterobot/py.typed +1 -0
- easterobot/resources/config.example.yml +226 -0
- easterobot/resources/credits.txt +1 -0
- easterobot/resources/eggs/egg_01.png +0 -0
- easterobot/resources/eggs/egg_02.png +0 -0
- easterobot/resources/eggs/egg_03.png +0 -0
- easterobot/resources/eggs/egg_04.png +0 -0
- easterobot/resources/eggs/egg_05.png +0 -0
- easterobot/resources/eggs/egg_06.png +0 -0
- easterobot/resources/eggs/egg_07.png +0 -0
- easterobot/resources/eggs/egg_08.png +0 -0
- easterobot/resources/eggs/egg_09.png +0 -0
- easterobot/resources/eggs/egg_10.png +0 -0
- easterobot/resources/eggs/egg_11.png +0 -0
- easterobot/resources/eggs/egg_12.png +0 -0
- easterobot/resources/eggs/egg_13.png +0 -0
- easterobot/resources/eggs/egg_14.png +0 -0
- easterobot/resources/eggs/egg_15.png +0 -0
- easterobot/resources/eggs/egg_16.png +0 -0
- easterobot/resources/eggs/egg_17.png +0 -0
- easterobot/resources/eggs/egg_18.png +0 -0
- easterobot/resources/eggs/egg_19.png +0 -0
- easterobot/resources/eggs/egg_20.png +0 -0
- easterobot/resources/logging.conf +47 -0
- easterobot/resources/logo.png +0 -0
- easterobot-1.0.0.dist-info/METADATA +242 -0
- easterobot-1.0.0.dist-info/RECORD +48 -0
- easterobot-1.0.0.dist-info/WHEEL +4 -0
- easterobot-1.0.0.dist-info/entry_points.txt +2 -0
- easterobot-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
"""Module for reset command."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import cast
|
5
|
+
|
6
|
+
import discord
|
7
|
+
from sqlalchemy import and_, delete
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
|
+
|
10
|
+
from easterobot.bot import embed
|
11
|
+
from easterobot.models import Cooldown, Egg, Hunt
|
12
|
+
|
13
|
+
from .base import Context, Interaction, controlled_command, egg_command_group
|
14
|
+
|
15
|
+
|
16
|
+
@egg_command_group.command(
|
17
|
+
name="reset", description="Réinitialiser la chasse aux œufs"
|
18
|
+
)
|
19
|
+
@controlled_command(cooldown=True, administrator=True)
|
20
|
+
async def reset_command(ctx: Context) -> None:
|
21
|
+
"""Reset command."""
|
22
|
+
await ctx.response.defer(ephemeral=True)
|
23
|
+
view = discord.ui.View(timeout=None)
|
24
|
+
cancel: discord.ui.Button[discord.ui.View] = discord.ui.Button(
|
25
|
+
label="Annuler", style=discord.ButtonStyle.danger
|
26
|
+
)
|
27
|
+
view.add_item(cancel)
|
28
|
+
confirm: discord.ui.Button[discord.ui.View] = discord.ui.Button(
|
29
|
+
label="Confirmer", style=discord.ButtonStyle.success
|
30
|
+
)
|
31
|
+
view.add_item(confirm)
|
32
|
+
done = False
|
33
|
+
cancel_embed = embed(
|
34
|
+
title="Réinitialisation annulée",
|
35
|
+
description="Vous avez annulé la demande de réinitialisation.",
|
36
|
+
)
|
37
|
+
confirm_embed = embed(
|
38
|
+
title="Réinitialisation",
|
39
|
+
description=(
|
40
|
+
"L'ensemble des salons, œufs "
|
41
|
+
"et temps d'attentes ont été réinitialisatiés."
|
42
|
+
),
|
43
|
+
)
|
44
|
+
|
45
|
+
async def cancel_callback(
|
46
|
+
interaction: Interaction,
|
47
|
+
) -> None:
|
48
|
+
nonlocal done
|
49
|
+
done = True
|
50
|
+
cancel.disabled = True
|
51
|
+
confirm.disabled = True
|
52
|
+
view.stop()
|
53
|
+
await asyncio.gather(
|
54
|
+
message.edit(view=view),
|
55
|
+
interaction.response.send_message(
|
56
|
+
embed=cancel_embed,
|
57
|
+
ephemeral=True,
|
58
|
+
),
|
59
|
+
)
|
60
|
+
|
61
|
+
async def confirm_callback(
|
62
|
+
interaction: Interaction,
|
63
|
+
) -> None:
|
64
|
+
nonlocal done
|
65
|
+
done = True
|
66
|
+
cancel.disabled = True
|
67
|
+
confirm.disabled = True
|
68
|
+
view.stop()
|
69
|
+
await asyncio.gather(
|
70
|
+
message.edit(view=view),
|
71
|
+
interaction.response.defer(ephemeral=True),
|
72
|
+
)
|
73
|
+
|
74
|
+
async with AsyncSession(ctx.client.engine) as session:
|
75
|
+
await session.execute(
|
76
|
+
delete(Hunt).where(Hunt.guild_id == ctx.guild_id)
|
77
|
+
)
|
78
|
+
await session.execute(
|
79
|
+
delete(Egg).where(Egg.guild_id == ctx.guild_id)
|
80
|
+
)
|
81
|
+
await session.execute(
|
82
|
+
delete(Cooldown).where(
|
83
|
+
and_(
|
84
|
+
Cooldown.guild_id == ctx.guild_id,
|
85
|
+
Cooldown.command != "reset",
|
86
|
+
)
|
87
|
+
)
|
88
|
+
)
|
89
|
+
await session.commit()
|
90
|
+
await interaction.followup.send(
|
91
|
+
embed=confirm_embed,
|
92
|
+
ephemeral=True,
|
93
|
+
)
|
94
|
+
|
95
|
+
cancel.callback = cancel_callback # type: ignore[assignment]
|
96
|
+
confirm.callback = confirm_callback # type: ignore[assignment]
|
97
|
+
message = cast(
|
98
|
+
discord.WebhookMessage,
|
99
|
+
await ctx.followup.send(
|
100
|
+
embed=embed(
|
101
|
+
title="Demande de réinitialisation",
|
102
|
+
description=(
|
103
|
+
"L'ensemble des salons, œufs "
|
104
|
+
"et temps d'attentes vont être réinitialisatiés."
|
105
|
+
),
|
106
|
+
# TODO(dashstrom): add timer
|
107
|
+
footer="Vous avez 30 secondes pour confirmer",
|
108
|
+
),
|
109
|
+
ephemeral=True,
|
110
|
+
view=view,
|
111
|
+
),
|
112
|
+
)
|
113
|
+
await asyncio.sleep(30.0)
|
114
|
+
if not done:
|
115
|
+
cancel.disabled = True
|
116
|
+
confirm.disabled = True
|
117
|
+
view.stop()
|
118
|
+
await asyncio.gather(
|
119
|
+
message.edit(view=view),
|
120
|
+
ctx.followup.send(embed=cancel_embed, ephemeral=True),
|
121
|
+
)
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""Module for search command."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import discord
|
7
|
+
from sqlalchemy import and_, func, select
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
|
+
|
10
|
+
from easterobot.bot import embed
|
11
|
+
from easterobot.config import RAND, agree
|
12
|
+
from easterobot.models import Egg, Hunt
|
13
|
+
|
14
|
+
from .base import (
|
15
|
+
Context,
|
16
|
+
InterruptedCommandError,
|
17
|
+
controlled_command,
|
18
|
+
egg_command_group,
|
19
|
+
)
|
20
|
+
|
21
|
+
logger = logging.getLogger("easterobot")
|
22
|
+
|
23
|
+
|
24
|
+
@egg_command_group.command(name="search", description="Rechercher un œuf")
|
25
|
+
@controlled_command(cooldown=True)
|
26
|
+
async def search_command(ctx: Context) -> None:
|
27
|
+
"""Search command."""
|
28
|
+
async with AsyncSession(ctx.client.engine) as session:
|
29
|
+
hunt = await session.scalar(
|
30
|
+
select(Hunt).where(Hunt.channel_id == ctx.channel.id)
|
31
|
+
)
|
32
|
+
if hunt is None:
|
33
|
+
await ctx.response.send_message(
|
34
|
+
"La chasse aux œufs n'est pas activée dans ce salon",
|
35
|
+
ephemeral=True,
|
36
|
+
)
|
37
|
+
return
|
38
|
+
try:
|
39
|
+
await ctx.response.defer(ephemeral=False)
|
40
|
+
except discord.errors.NotFound as err:
|
41
|
+
raise InterruptedCommandError from err
|
42
|
+
name = ctx.user.display_name
|
43
|
+
|
44
|
+
async with AsyncSession(ctx.client.engine) as session:
|
45
|
+
egg_max = await session.scalar(
|
46
|
+
select(
|
47
|
+
func.count().label("max"),
|
48
|
+
)
|
49
|
+
.where(Egg.guild_id == ctx.guild_id)
|
50
|
+
.group_by(Egg.user_id)
|
51
|
+
.order_by(func.count().label("max").desc())
|
52
|
+
.limit(1)
|
53
|
+
)
|
54
|
+
egg_max = egg_max or 0
|
55
|
+
egg_count = await session.scalar(
|
56
|
+
select(func.count().label("count")).where(
|
57
|
+
and_(
|
58
|
+
Egg.guild_id == ctx.guild.id,
|
59
|
+
Egg.user_id == ctx.user.id,
|
60
|
+
)
|
61
|
+
)
|
62
|
+
)
|
63
|
+
if egg_count is None:
|
64
|
+
egg_count = 0
|
65
|
+
ratio = egg_count / egg_max if egg_max != 0 else 1.0
|
66
|
+
|
67
|
+
discovered = ctx.client.config.commands.search.discovered
|
68
|
+
prob_d = (discovered.max - discovered.min) * (1 - ratio) + discovered.min
|
69
|
+
|
70
|
+
sample_d = RAND.random()
|
71
|
+
if prob_d > sample_d or egg_count < discovered.shield:
|
72
|
+
sample_s = RAND.random()
|
73
|
+
spotted = ctx.client.config.commands.search.spotted
|
74
|
+
prob_s = (spotted.max - spotted.min) * ratio + spotted.min
|
75
|
+
logger.info("discovered: %.2f > %.2f", prob_d, sample_d)
|
76
|
+
if prob_s > sample_s and egg_count > spotted.shield:
|
77
|
+
logger.info("spotted: %.2f > %.2f", prob_s, sample_s)
|
78
|
+
|
79
|
+
async def send_method(
|
80
|
+
*args: Any, **kwargs: Any
|
81
|
+
) -> discord.Message:
|
82
|
+
return await ctx.followup.send(*args, **kwargs) # type: ignore[no-any-return]
|
83
|
+
|
84
|
+
await ctx.client.start_hunt(
|
85
|
+
ctx.channel_id,
|
86
|
+
ctx.client.config.spotted(ctx.user),
|
87
|
+
member_id=ctx.user.id,
|
88
|
+
send_method=send_method,
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
logger.info("found: %.2f > %.2f", prob_s, sample_s)
|
92
|
+
emoji = ctx.client.app_emojis.rand()
|
93
|
+
async with AsyncSession(ctx.client.engine) as session:
|
94
|
+
session.add(
|
95
|
+
Egg(
|
96
|
+
channel_id=ctx.channel_id,
|
97
|
+
guild_id=ctx.guild_id,
|
98
|
+
user_id=ctx.user.id,
|
99
|
+
emoji_id=emoji.id,
|
100
|
+
)
|
101
|
+
)
|
102
|
+
await session.commit()
|
103
|
+
|
104
|
+
logger.info(
|
105
|
+
"%s (%s) got an egg for a total %s in %s",
|
106
|
+
ctx.user,
|
107
|
+
ctx.user.id,
|
108
|
+
agree("{0} egg", "{0} eggs", egg_count),
|
109
|
+
ctx.channel.jump_url,
|
110
|
+
)
|
111
|
+
await ctx.followup.send(
|
112
|
+
embed=embed(
|
113
|
+
title=f"{name} récupère un œuf",
|
114
|
+
description=ctx.client.config.hidden(ctx.user),
|
115
|
+
thumbnail=emoji.url,
|
116
|
+
egg_count=egg_count + 1,
|
117
|
+
)
|
118
|
+
)
|
119
|
+
else:
|
120
|
+
logger.info("failed: %.2f > %.2f", prob_d, sample_d)
|
121
|
+
await ctx.followup.send(
|
122
|
+
embed=embed(
|
123
|
+
title=f"{name} repart bredouille",
|
124
|
+
description=ctx.client.config.failed(ctx.user),
|
125
|
+
egg_count=egg_count,
|
126
|
+
)
|
127
|
+
)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
"""Command top."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from math import floor
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import discord
|
8
|
+
from sqlalchemy import distinct, func, select
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
10
|
+
|
11
|
+
from easterobot.bot import embed
|
12
|
+
from easterobot.config import agree
|
13
|
+
from easterobot.models import Egg
|
14
|
+
|
15
|
+
from .base import Context, Interaction, controlled_command, egg_command_group
|
16
|
+
|
17
|
+
PAGE_SIZE = 10
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
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
|
+
async def embed_rank(
|
31
|
+
ctx: Context,
|
32
|
+
page: int,
|
33
|
+
colour: Optional[discord.Colour] = None,
|
34
|
+
) -> tuple[discord.Embed, bool]:
|
35
|
+
"""Embed for rank."""
|
36
|
+
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))
|
44
|
+
else:
|
45
|
+
morsels.append("\n:spider_web: Personne n'a d'œuf")
|
46
|
+
total = await session.scalar(
|
47
|
+
select(func.count(distinct(Egg.user_id)).label("count")).where(
|
48
|
+
Egg.guild_id == ctx.guild_id
|
49
|
+
)
|
50
|
+
)
|
51
|
+
if total is None:
|
52
|
+
total = 0
|
53
|
+
logger.warning("No total egg !")
|
54
|
+
total = floor(total / PAGE_SIZE)
|
55
|
+
text = "\n".join(morsels)
|
56
|
+
emb = embed(
|
57
|
+
title="Chasse aux œufs",
|
58
|
+
description=text,
|
59
|
+
footer=f"Page {page + 1}/{total + 1}",
|
60
|
+
)
|
61
|
+
if colour is not None:
|
62
|
+
emb.colour = colour
|
63
|
+
return emb, page >= total
|
64
|
+
|
65
|
+
|
66
|
+
@egg_command_group.command(
|
67
|
+
name="top", description="Classement des chasseurs d'œufs"
|
68
|
+
)
|
69
|
+
@controlled_command(cooldown=True)
|
70
|
+
async def top_command(ctx: Context) -> None:
|
71
|
+
"""Top command."""
|
72
|
+
await ctx.response.defer(ephemeral=True)
|
73
|
+
|
74
|
+
view = discord.ui.View(timeout=None)
|
75
|
+
previous_page: discord.ui.Button[discord.ui.View] = discord.ui.Button(
|
76
|
+
label="<", style=discord.ButtonStyle.gray, disabled=True
|
77
|
+
)
|
78
|
+
view.add_item(previous_page)
|
79
|
+
next_page: discord.ui.Button[discord.ui.View] = discord.ui.Button(
|
80
|
+
label=">", style=discord.ButtonStyle.gray
|
81
|
+
)
|
82
|
+
view.add_item(next_page)
|
83
|
+
page = 0
|
84
|
+
|
85
|
+
async def edit(interaction: Interaction) -> None:
|
86
|
+
previous_page.disabled = page <= 0
|
87
|
+
emb, next_page.disabled = await embed_rank(
|
88
|
+
ctx, page, base_embed.colour
|
89
|
+
)
|
90
|
+
await interaction.response.edit_message(view=view, embed=emb)
|
91
|
+
|
92
|
+
async def previous_callback(interaction: Interaction) -> None:
|
93
|
+
nonlocal page
|
94
|
+
page = max(page - 1, 0)
|
95
|
+
await edit(interaction)
|
96
|
+
|
97
|
+
async def next_callback(interaction: Interaction) -> None:
|
98
|
+
nonlocal page
|
99
|
+
page += 1
|
100
|
+
await edit(interaction)
|
101
|
+
|
102
|
+
previous_page.callback = previous_callback # type: ignore[assignment]
|
103
|
+
next_page.callback = next_callback # type: ignore[assignment]
|
104
|
+
base_embed, next_page.disabled = await embed_rank(ctx, page)
|
105
|
+
await ctx.followup.send(embed=base_embed, ephemeral=True, view=view)
|