easterobot 1.0.0__tar.gz → 1.1.0__tar.gz

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 (94) hide show
  1. {easterobot-1.0.0 → easterobot-1.1.0}/.vscode/settings.json +1 -0
  2. {easterobot-1.0.0 → easterobot-1.1.0}/PKG-INFO +11 -5
  3. {easterobot-1.0.0 → easterobot-1.1.0}/README.rst +8 -3
  4. easterobot-1.1.0/easterobot/alembic/env.py +91 -0
  5. easterobot-1.1.0/easterobot/alembic/script.py.mako +28 -0
  6. easterobot-1.1.0/easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
  7. easterobot-1.1.0/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
  8. easterobot-1.1.0/easterobot/bot.py +215 -0
  9. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/cli.py +56 -17
  10. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/__init__.py +8 -0
  11. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/base.py +3 -7
  12. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/basket.py +10 -12
  13. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/edit.py +4 -4
  14. easterobot-1.1.0/easterobot/commands/game.py +187 -0
  15. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/help.py +1 -1
  16. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/reset.py +1 -1
  17. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/search.py +3 -3
  18. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/top.py +7 -18
  19. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/config.py +67 -3
  20. easterobot-1.1.0/easterobot/games/__init__.py +14 -0
  21. easterobot-1.1.0/easterobot/games/connect.py +206 -0
  22. easterobot-1.1.0/easterobot/games/game.py +262 -0
  23. easterobot-1.1.0/easterobot/games/rock_paper_scissor.py +206 -0
  24. easterobot-1.1.0/easterobot/games/tic_tac_toe.py +168 -0
  25. easterobot-1.1.0/easterobot/hunts/__init__.py +14 -0
  26. easterobot-1.1.0/easterobot/hunts/hunt.py +428 -0
  27. easterobot-1.1.0/easterobot/hunts/rank.py +82 -0
  28. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/models.py +2 -1
  29. easterobot-1.1.0/easterobot/resources/alembic.ini +87 -0
  30. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/resources/config.example.yml +10 -2
  31. easterobot-1.1.0/easterobot/resources/credits.txt +5 -0
  32. easterobot-1.1.0/easterobot/resources/emotes/icons/arrow.png +0 -0
  33. easterobot-1.1.0/easterobot/resources/emotes/icons/end.png +0 -0
  34. easterobot-1.1.0/easterobot/resources/emotes/icons/versus.png +0 -0
  35. easterobot-1.1.0/easterobot/resources/emotes/icons/wait.png +0 -0
  36. {easterobot-1.0.0 → easterobot-1.1.0}/pyproject.toml +8 -6
  37. {easterobot-1.0.0 → easterobot-1.1.0}/tests/test_config.py +4 -4
  38. {easterobot-1.0.0 → easterobot-1.1.0}/uv.lock +29 -1
  39. easterobot-1.0.0/easterobot/bot.py +0 -584
  40. easterobot-1.0.0/easterobot/resources/credits.txt +0 -1
  41. {easterobot-1.0.0 → easterobot-1.1.0}/.dockerignore +0 -0
  42. {easterobot-1.0.0 → easterobot-1.1.0}/.editorconfig +0 -0
  43. {easterobot-1.0.0 → easterobot-1.1.0}/.github/actions/setup-project/action.yml +0 -0
  44. {easterobot-1.0.0 → easterobot-1.1.0}/.github/workflows/docs.yml +0 -0
  45. {easterobot-1.0.0 → easterobot-1.1.0}/.github/workflows/lint.yml +0 -0
  46. {easterobot-1.0.0 → easterobot-1.1.0}/.github/workflows/publish.yml +0 -0
  47. {easterobot-1.0.0 → easterobot-1.1.0}/.github/workflows/tests.yml +0 -0
  48. {easterobot-1.0.0 → easterobot-1.1.0}/.gitignore +0 -0
  49. {easterobot-1.0.0 → easterobot-1.1.0}/.pre-commit-config.yaml +0 -0
  50. {easterobot-1.0.0 → easterobot-1.1.0}/.vscode/extensions.json +0 -0
  51. {easterobot-1.0.0 → easterobot-1.1.0}/.vscode/ltex.dictionary.en-US.txt +0 -0
  52. {easterobot-1.0.0 → easterobot-1.1.0}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
  53. {easterobot-1.0.0 → easterobot-1.1.0}/Dockerfile +0 -0
  54. {easterobot-1.0.0 → easterobot-1.1.0}/LICENSE +0 -0
  55. {easterobot-1.0.0 → easterobot-1.1.0}/conftest.py +0 -0
  56. {easterobot-1.0.0 → easterobot-1.1.0}/docker-compose.yml +0 -0
  57. {easterobot-1.0.0 → easterobot-1.1.0}/docs/conf.py +0 -0
  58. {easterobot-1.0.0 → easterobot-1.1.0}/docs/index.rst +0 -0
  59. {easterobot-1.0.0 → easterobot-1.1.0}/docs/references.rst +0 -0
  60. {easterobot-1.0.0 → easterobot-1.1.0}/docs/resources/favicon.png +0 -0
  61. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/__init__.py +0 -0
  62. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/__main__.py +0 -0
  63. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/disable.py +0 -0
  64. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/commands/enable.py +0 -0
  65. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/info.py +0 -0
  66. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/logger.py +0 -0
  67. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/py.typed +0 -0
  68. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_01.png +0 -0
  69. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_02.png +0 -0
  70. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_03.png +0 -0
  71. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_04.png +0 -0
  72. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_05.png +0 -0
  73. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_06.png +0 -0
  74. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_07.png +0 -0
  75. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_08.png +0 -0
  76. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_09.png +0 -0
  77. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_10.png +0 -0
  78. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_11.png +0 -0
  79. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_12.png +0 -0
  80. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_13.png +0 -0
  81. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_14.png +0 -0
  82. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_15.png +0 -0
  83. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_16.png +0 -0
  84. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_17.png +0 -0
  85. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_18.png +0 -0
  86. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_19.png +0 -0
  87. {easterobot-1.0.0/easterobot/resources → easterobot-1.1.0/easterobot/resources/emotes}/eggs/egg_20.png +0 -0
  88. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/resources/logging.conf +0 -0
  89. {easterobot-1.0.0 → easterobot-1.1.0}/easterobot/resources/logo.png +0 -0
  90. {easterobot-1.0.0 → easterobot-1.1.0}/entrypoint.sh +0 -0
  91. {easterobot-1.0.0 → easterobot-1.1.0}/tests/__init__.py +0 -0
  92. {easterobot-1.0.0 → easterobot-1.1.0}/tests/test_cli.py +0 -0
  93. {easterobot-1.0.0 → easterobot-1.1.0}/tools/chatgpt.txt +0 -0
  94. {easterobot-1.0.0 → easterobot-1.1.0}/tools/cropping.py +0 -0
@@ -8,6 +8,7 @@
8
8
  "files.associations": {
9
9
  "settings.json": "jsonc",
10
10
  "extensions.json": "jsonc",
11
+ "*.py.mako": "txt",
11
12
  "LICENSE": "txt"
12
13
  },
13
14
  // Files to exclude
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easterobot
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Discord bot for Easter.
5
5
  Project-URL: Homepage, https://github.com/Dashstrom/easterobot
6
6
  Project-URL: Repository, https://github.com/Dashstrom/easterobot
@@ -8,7 +8,7 @@ Project-URL: Documentation, https://dashstrom.github.io/easterobot
8
8
  Author-email: Dashstrom <dashstrom.pro@gmail.com>
9
9
  License-File: LICENSE
10
10
  Keywords: bot,discord,easter,eggs,hunt
11
- Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Framework :: Pytest
14
14
  Classifier: Framework :: Sphinx
@@ -27,6 +27,7 @@ Classifier: Programming Language :: Python :: 3.13
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: <4.0,>=3.9
29
29
  Requires-Dist: aiosqlite>=0.21.0
30
+ Requires-Dist: alembic>=1.15.2
30
31
  Requires-Dist: asyncpg>=0.30.0
31
32
  Requires-Dist: discord-py>=2.5.2
32
33
  Requires-Dist: msgspec>=0.19.0
@@ -135,6 +136,7 @@ Configuration directory
135
136
  .. code-block:: text
136
137
 
137
138
  data Root directory
139
+ ├── .gitignore Avoid pushing sensitive data
138
140
  ├── config.yml Configuration file
139
141
  ├── easterobot.db Database
140
142
  ├── logs Logging directory
@@ -142,10 +144,14 @@ Configuration directory
142
144
  │ └── easterobot.log.1 Rotating log file
143
145
  └── resources Resource directory
144
146
  ├── config.example.yml An example of config
145
- ├── credits.txt Credits of eggs emoji
146
- ├── eggs Directory for eggs
147
- └── egg_01.png Emoji to use
147
+ ├── credits.txt Credits of emotes
148
+ ├── emotes Directory loaded as application emotes
149
+ ├── eggs Directory for eggs
150
+ │ | └── egg_01.png Emoji to use for egg
151
+ │ └── icons Misc emotes to load
152
+ │ └── arrow.png Emoji used in messages
148
153
  ├── logging.conf Logging configuration
154
+ ├── alembic.ini Configure for alembic
149
155
  └── logo.png Logo used by the bot
150
156
 
151
157
  Development
@@ -98,6 +98,7 @@ Configuration directory
98
98
  .. code-block:: text
99
99
 
100
100
  data Root directory
101
+ ├── .gitignore Avoid pushing sensitive data
101
102
  ├── config.yml Configuration file
102
103
  ├── easterobot.db Database
103
104
  ├── logs Logging directory
@@ -105,10 +106,14 @@ Configuration directory
105
106
  │ └── easterobot.log.1 Rotating log file
106
107
  └── resources Resource directory
107
108
  ├── config.example.yml An example of config
108
- ├── credits.txt Credits of eggs emoji
109
- ├── eggs Directory for eggs
110
- └── egg_01.png Emoji to use
109
+ ├── credits.txt Credits of emotes
110
+ ├── emotes Directory loaded as application emotes
111
+ ├── eggs Directory for eggs
112
+ │ | └── egg_01.png Emoji to use for egg
113
+ │ └── icons Misc emotes to load
114
+ │ └── arrow.png Emoji used in messages
111
115
  ├── logging.conf Logging configuration
116
+ ├── alembic.ini Configure for alembic
112
117
  └── logo.png Logo used by the bot
113
118
 
114
119
  Development
@@ -0,0 +1,91 @@
1
+ """Environnement."""
2
+
3
+ import asyncio
4
+
5
+ from alembic import context
6
+ from sqlalchemy import pool
7
+ from sqlalchemy.engine import Connection
8
+ from sqlalchemy.ext.asyncio import async_engine_from_config
9
+
10
+ from easterobot.config import (
11
+ EXAMPLE_CONFIG_PATH,
12
+ MConfig,
13
+ load_config_from_path,
14
+ )
15
+ from easterobot.models import Base
16
+
17
+ # this is the Alembic Config object, which provides
18
+ # access to the values within the .ini file in use.
19
+ config = context.config
20
+
21
+ # Interpret the config file for Python logging.
22
+ # This line sets up loggers basically.
23
+ if config.config_file_name is not None:
24
+ if "easterobot_config" in config.attributes:
25
+ easterobot_config: MConfig = config.attributes["easterobot_config"]
26
+ else:
27
+ easterobot_config = load_config_from_path(EXAMPLE_CONFIG_PATH)
28
+ easterobot_config.configure_logging()
29
+ # add your model's MetaData object here
30
+ # for 'autogenerate' support
31
+ target_metadata = [Base.metadata]
32
+
33
+
34
+ def run_migrations_offline() -> None:
35
+ """Run migrations in 'offline' mode.
36
+
37
+ This configures the context with just a URL
38
+ and not an Engine, though an Engine is acceptable
39
+ here as well. By skipping the Engine creation
40
+ we don't even need a DBAPI to be available.
41
+
42
+ Calls to context.execute() here emit the given string to the
43
+ script output.
44
+
45
+ """
46
+ url = config.get_main_option("sqlalchemy.url")
47
+ context.configure(
48
+ url=url,
49
+ target_metadata=target_metadata,
50
+ literal_binds=True,
51
+ dialect_opts={"paramstyle": "named"},
52
+ )
53
+
54
+ with context.begin_transaction():
55
+ context.run_migrations()
56
+
57
+
58
+ def do_run_migrations(connection: Connection) -> None:
59
+ """Run all migrations."""
60
+ context.configure(connection=connection, target_metadata=target_metadata)
61
+
62
+ with context.begin_transaction():
63
+ context.run_migrations()
64
+
65
+
66
+ async def run_async_migrations() -> None:
67
+ """In this scenario we need to create an Engine.
68
+
69
+ Then we associate a connection with the context.
70
+ """
71
+ connectable = async_engine_from_config(
72
+ config.get_section(config.config_ini_section, {}),
73
+ prefix="sqlalchemy.",
74
+ poolclass=pool.NullPool,
75
+ )
76
+
77
+ async with connectable.connect() as connection:
78
+ await connection.run_sync(do_run_migrations)
79
+
80
+ await connectable.dispose()
81
+
82
+
83
+ def run_migrations_online() -> None:
84
+ """Run migrations in 'online' mode."""
85
+ asyncio.run(run_async_migrations())
86
+
87
+
88
+ if context.is_offline_mode():
89
+ run_migrations_offline()
90
+ else:
91
+ run_migrations_online()
@@ -0,0 +1,28 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,67 @@
1
+ """init database.
2
+
3
+ Revision ID: 2f0d4305e320
4
+ Revises:
5
+ Create Date: 2025-04-19 12:04:53.107440
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+ from typing import Union
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "2f0d4305e320"
17
+ down_revision: Union[str, None] = None
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "cooldown",
27
+ sa.Column("user_id", sa.BigInteger(), nullable=False),
28
+ sa.Column("guild_id", sa.BigInteger(), nullable=False),
29
+ sa.Column("command", sa.String(), nullable=False),
30
+ sa.Column("timestamp", sa.Float(), nullable=False),
31
+ sa.PrimaryKeyConstraint("user_id", "guild_id", "command"),
32
+ )
33
+ op.create_table(
34
+ "egg",
35
+ sa.Column(
36
+ "id",
37
+ sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
38
+ autoincrement=True,
39
+ nullable=False,
40
+ ),
41
+ sa.Column("guild_id", sa.BigInteger(), nullable=False),
42
+ sa.Column("channel_id", sa.BigInteger(), nullable=False),
43
+ sa.Column("user_id", sa.BigInteger(), nullable=False),
44
+ sa.Column("emoji_id", sa.BigInteger(), nullable=True),
45
+ sa.PrimaryKeyConstraint("id"),
46
+ )
47
+ op.create_index(op.f("ix_egg_guild_id"), "egg", ["guild_id"], unique=False)
48
+ op.create_index(op.f("ix_egg_user_id"), "egg", ["user_id"], unique=False)
49
+ op.create_table(
50
+ "hunt",
51
+ sa.Column("channel_id", sa.BigInteger(), nullable=False),
52
+ sa.Column("guild_id", sa.BigInteger(), nullable=False),
53
+ sa.Column("next_egg", sa.Float(), nullable=False),
54
+ sa.PrimaryKeyConstraint("channel_id"),
55
+ )
56
+ # ### end Alembic commands ###
57
+
58
+
59
+ def downgrade() -> None:
60
+ """Downgrade schema."""
61
+ # ### commands auto generated by Alembic - please adjust! ###
62
+ op.drop_table("hunt")
63
+ op.drop_index(op.f("ix_egg_user_id"), table_name="egg")
64
+ op.drop_index(op.f("ix_egg_guild_id"), table_name="egg")
65
+ op.drop_table("egg")
66
+ op.drop_table("cooldown")
67
+ # ### end Alembic commands ###
@@ -0,0 +1,38 @@
1
+ """add lock on eggs.
2
+
3
+ Revision ID: 940c3b9c702d
4
+ Revises: 2f0d4305e320
5
+ Create Date: 2025-04-19 12:52:02.245048
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+ from typing import Union
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "940c3b9c702d"
17
+ down_revision: Union[str, None] = "2f0d4305e320"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.add_column(
26
+ "egg",
27
+ sa.Column(
28
+ "lock", sa.Boolean(), nullable=False, server_default=sa.text("0")
29
+ ),
30
+ )
31
+ # ### end Alembic commands ###
32
+
33
+
34
+ def downgrade() -> None:
35
+ """Downgrade schema."""
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ op.drop_column("egg", "lock")
38
+ # ### end Alembic commands ###
@@ -0,0 +1,215 @@
1
+ """Main program."""
2
+
3
+ import logging
4
+ import logging.config
5
+ import pathlib
6
+ import shutil
7
+ from getpass import getpass
8
+ from pathlib import Path
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Optional,
12
+ TypeVar,
13
+ Union,
14
+ )
15
+
16
+ import discord
17
+ import discord.app_commands
18
+ import discord.ext.commands
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
25
+
26
+ from .config import (
27
+ DEFAULT_CONFIG_PATH,
28
+ EXAMPLE_CONFIG_PATH,
29
+ RESOURCES,
30
+ MConfig,
31
+ RandomItem,
32
+ dump_yaml,
33
+ load_config_from_buffer,
34
+ load_config_from_path,
35
+ )
36
+
37
+ T = TypeVar("T")
38
+
39
+ logger = logging.getLogger(__name__)
40
+ INTENTS = discord.Intents.all()
41
+
42
+
43
+ class Easterobot(discord.ext.commands.Bot):
44
+ owner: discord.User
45
+ game: "GameCog"
46
+ hunt: "HuntCog"
47
+
48
+ def __init__(self, config: MConfig) -> None:
49
+ """Initialise Easterbot."""
50
+ super().__init__(
51
+ command_prefix=".",
52
+ description="Bot discord pour faire la chasse aux œufs",
53
+ activity=discord.Game(name="rechercher des œufs"),
54
+ intents=INTENTS,
55
+ )
56
+ self.config = config
57
+
58
+ # Attributes
59
+ self.app_commands: list[discord.app_commands.AppCommand] = []
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,
73
+ )
74
+
75
+ @classmethod
76
+ def from_config(
77
+ cls,
78
+ path: Union[str, Path] = DEFAULT_CONFIG_PATH,
79
+ *,
80
+ token: Optional[str] = None,
81
+ env: bool = False,
82
+ ) -> "Easterobot":
83
+ """Instantiate Easterobot from config."""
84
+ config = load_config_from_path(path, token=token, env=env)
85
+ return Easterobot(config)
86
+
87
+ @classmethod
88
+ def generate(
89
+ cls,
90
+ destination: Union[Path, str],
91
+ *,
92
+ token: Optional[str],
93
+ env: bool,
94
+ interactive: bool,
95
+ ) -> "Easterobot":
96
+ """Generate all data."""
97
+ destination = Path(destination).resolve()
98
+ destination.mkdir(parents=True, exist_ok=True)
99
+ config_data = EXAMPLE_CONFIG_PATH.read_bytes()
100
+ config = load_config_from_buffer(config_data, token=token, env=env)
101
+ config.attach_default_working_directory(destination)
102
+ if interactive:
103
+ while True:
104
+ try:
105
+ config.verified_token()
106
+ break
107
+ except (ValueError, TypeError):
108
+ config.token = getpass("Token: ")
109
+ config._resources = pathlib.Path("resources") # noqa: SLF001
110
+ shutil.copytree(
111
+ RESOURCES, destination / "resources", dirs_exist_ok=True
112
+ )
113
+ config_path = destination / "config.yml"
114
+ config_path.write_bytes(dump_yaml(config))
115
+ (destination / ".gitignore").write_bytes(b"*\n")
116
+ return Easterobot(config)
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
+
144
+ # Method that loads cogs
145
+ async def setup_hook(self) -> None:
146
+ """Setup hooks."""
147
+ await self.load_extension(
148
+ "easterobot.commands", package="easterobot.commands.__init__"
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
+ )
156
+
157
+ def auto_run(self) -> None:
158
+ """Run the bot with the given token."""
159
+ self.run(token=self.config.verified_token())
160
+
161
+ async def on_ready(self) -> None:
162
+ """Handle ready event, can be trigger many time if disconnected."""
163
+ # Sync bot commands
164
+ logger.info("Syncing command")
165
+ await self.tree.sync()
166
+ self.app_commands = await self.tree.fetch_commands()
167
+
168
+ # Sync bot owner
169
+ app_info = await self.application_info()
170
+ self.owner = app_info.owner
171
+ logger.info("Owner is %s (%s)", self.owner.display_name, self.owner.id)
172
+
173
+ # Load emojis
174
+ await self._load_emojis()
175
+
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
+ )
181
+
182
+ # Log all available guilds
183
+ async for guild in self.fetch_guilds():
184
+ logger.info("Guild %s (%s)", guild, guild.id)
185
+ logger.info(
186
+ "Logged on as %s (%s) !",
187
+ self.user,
188
+ getattr(self.user, "id", "unknown"),
189
+ )
190
+
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,
206
+ )
207
+ image_data = emote.read_bytes()
208
+ emoji = await self.create_application_emoji(
209
+ name=name,
210
+ image=image_data,
211
+ )
212
+ self.app_emojis[name] = emoji
213
+ else:
214
+ logger.info("Load emoji %s", name)
215
+ self.app_emojis[name] = emojis[name]
@@ -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