easterobot 1.0.0__tar.gz → 1.1.1__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.
- {easterobot-1.0.0 → easterobot-1.1.1}/.vscode/settings.json +1 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/PKG-INFO +11 -5
- {easterobot-1.0.0 → easterobot-1.1.1}/README.rst +8 -3
- easterobot-1.1.1/easterobot/alembic/env.py +91 -0
- easterobot-1.1.1/easterobot/alembic/script.py.mako +28 -0
- easterobot-1.1.1/easterobot/alembic/versions/2f0d4305e320_init_database.py +67 -0
- easterobot-1.1.1/easterobot/alembic/versions/940c3b9c702d_add_lock_on_eggs.py +38 -0
- easterobot-1.1.1/easterobot/bot.py +215 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/cli.py +56 -17
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/__init__.py +8 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/base.py +34 -13
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/basket.py +10 -12
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/edit.py +4 -4
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/enable.py +5 -1
- easterobot-1.1.1/easterobot/commands/game.py +187 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/help.py +1 -1
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/reset.py +1 -1
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/search.py +4 -4
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/top.py +7 -18
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/config.py +67 -3
- easterobot-1.1.1/easterobot/games/__init__.py +14 -0
- easterobot-1.1.1/easterobot/games/connect.py +206 -0
- easterobot-1.1.1/easterobot/games/game.py +262 -0
- easterobot-1.1.1/easterobot/games/rock_paper_scissor.py +206 -0
- easterobot-1.1.1/easterobot/games/tic_tac_toe.py +168 -0
- easterobot-1.1.1/easterobot/hunts/__init__.py +14 -0
- easterobot-1.1.1/easterobot/hunts/hunt.py +428 -0
- easterobot-1.1.1/easterobot/hunts/rank.py +82 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/models.py +2 -1
- easterobot-1.1.1/easterobot/resources/alembic.ini +87 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/resources/config.example.yml +10 -2
- easterobot-1.1.1/easterobot/resources/credits.txt +5 -0
- easterobot-1.1.1/easterobot/resources/emotes/icons/arrow.png +0 -0
- easterobot-1.1.1/easterobot/resources/emotes/icons/end.png +0 -0
- easterobot-1.1.1/easterobot/resources/emotes/icons/versus.png +0 -0
- easterobot-1.1.1/easterobot/resources/emotes/icons/wait.png +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/pyproject.toml +8 -6
- {easterobot-1.0.0 → easterobot-1.1.1}/tests/test_config.py +4 -4
- {easterobot-1.0.0 → easterobot-1.1.1}/uv.lock +29 -1
- easterobot-1.0.0/easterobot/bot.py +0 -584
- easterobot-1.0.0/easterobot/resources/credits.txt +0 -1
- {easterobot-1.0.0 → easterobot-1.1.1}/.dockerignore +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.editorconfig +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.github/actions/setup-project/action.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.github/workflows/docs.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.github/workflows/lint.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.github/workflows/publish.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.github/workflows/tests.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.gitignore +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.pre-commit-config.yaml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.vscode/extensions.json +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.vscode/ltex.dictionary.en-US.txt +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/.vscode/ltex.hiddenFalsePositives.en-US.txt +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/Dockerfile +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/LICENSE +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/conftest.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/docker-compose.yml +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/docs/conf.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/docs/index.rst +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/docs/references.rst +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/docs/resources/favicon.png +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/__init__.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/__main__.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/commands/disable.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/info.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/logger.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/py.typed +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_01.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_02.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_03.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_04.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_05.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_06.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_07.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_08.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_09.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_10.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_11.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_12.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_13.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_14.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_15.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_16.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_17.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_18.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_19.png +0 -0
- {easterobot-1.0.0/easterobot/resources → easterobot-1.1.1/easterobot/resources/emotes}/eggs/egg_20.png +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/resources/logging.conf +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/easterobot/resources/logo.png +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/entrypoint.sh +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/tests/__init__.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/tests/test_cli.py +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/tools/chatgpt.txt +0 -0
- {easterobot-1.0.0 → easterobot-1.1.1}/tools/cropping.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: easterobot
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1.1
|
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 ::
|
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
|
146
|
-
├──
|
147
|
-
│
|
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
|
109
|
-
├──
|
110
|
-
│
|
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 .
|
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
|
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
|
78
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
134
|
+
namespace = parser.parse_args(args)
|
135
|
+
if namespace.action == "run":
|
136
|
+
setup_logging(namespace.verbose)
|
109
137
|
bot = Easterobot.from_config(
|
110
|
-
|
111
|
-
token=
|
112
|
-
env=
|
138
|
+
namespace.config,
|
139
|
+
token=namespace.token,
|
140
|
+
env=namespace.env,
|
113
141
|
)
|
114
142
|
bot.auto_run()
|
115
|
-
elif
|
143
|
+
elif namespace.action == "generate":
|
144
|
+
setup_logging(namespace.verbose)
|
116
145
|
Easterobot.generate(
|
117
|
-
destination=
|
118
|
-
token=
|
119
|
-
env=
|
120
|
-
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
|
|