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.
Files changed (48) hide show
  1. easterobot/__init__.py +19 -0
  2. easterobot/__main__.py +6 -0
  3. easterobot/bot.py +584 -0
  4. easterobot/cli.py +127 -0
  5. easterobot/commands/__init__.py +28 -0
  6. easterobot/commands/base.py +171 -0
  7. easterobot/commands/basket.py +99 -0
  8. easterobot/commands/disable.py +29 -0
  9. easterobot/commands/edit.py +68 -0
  10. easterobot/commands/enable.py +35 -0
  11. easterobot/commands/help.py +33 -0
  12. easterobot/commands/reset.py +121 -0
  13. easterobot/commands/search.py +127 -0
  14. easterobot/commands/top.py +105 -0
  15. easterobot/config.py +401 -0
  16. easterobot/info.py +18 -0
  17. easterobot/logger.py +16 -0
  18. easterobot/models.py +58 -0
  19. easterobot/py.typed +1 -0
  20. easterobot/resources/config.example.yml +226 -0
  21. easterobot/resources/credits.txt +1 -0
  22. easterobot/resources/eggs/egg_01.png +0 -0
  23. easterobot/resources/eggs/egg_02.png +0 -0
  24. easterobot/resources/eggs/egg_03.png +0 -0
  25. easterobot/resources/eggs/egg_04.png +0 -0
  26. easterobot/resources/eggs/egg_05.png +0 -0
  27. easterobot/resources/eggs/egg_06.png +0 -0
  28. easterobot/resources/eggs/egg_07.png +0 -0
  29. easterobot/resources/eggs/egg_08.png +0 -0
  30. easterobot/resources/eggs/egg_09.png +0 -0
  31. easterobot/resources/eggs/egg_10.png +0 -0
  32. easterobot/resources/eggs/egg_11.png +0 -0
  33. easterobot/resources/eggs/egg_12.png +0 -0
  34. easterobot/resources/eggs/egg_13.png +0 -0
  35. easterobot/resources/eggs/egg_14.png +0 -0
  36. easterobot/resources/eggs/egg_15.png +0 -0
  37. easterobot/resources/eggs/egg_16.png +0 -0
  38. easterobot/resources/eggs/egg_17.png +0 -0
  39. easterobot/resources/eggs/egg_18.png +0 -0
  40. easterobot/resources/eggs/egg_19.png +0 -0
  41. easterobot/resources/eggs/egg_20.png +0 -0
  42. easterobot/resources/logging.conf +47 -0
  43. easterobot/resources/logo.png +0 -0
  44. easterobot-1.0.0.dist-info/METADATA +242 -0
  45. easterobot-1.0.0.dist-info/RECORD +48 -0
  46. easterobot-1.0.0.dist-info/WHEEL +4 -0
  47. easterobot-1.0.0.dist-info/entry_points.txt +2 -0
  48. 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)