diff --git a/.editorconfig b/.editorconfig index 9de9928..9c16a69 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,4 +3,4 @@ root = true [{*.yml, *.yaml}] indent_size = 2 indent_style = space -max_line_length=80 \ No newline at end of file +max_line_length=120 diff --git a/.gitignore b/.gitignore index 4784e9c..a14d6d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,156 @@ -files/lastTasks.json -files/c4.json -files/hangman.json -files/stats.json -files/lost.json -files/locked.json -files/ufora_notifications.json -files/compbio_benchmarks_2.json -files/compbio_benchmarks_4.json -.idea/ -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -/venv/ -.pytest_cache \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ diff --git a/database/engine.py b/database/engine.py new file mode 100644 index 0000000..fbdbfd2 --- /dev/null +++ b/database/engine.py @@ -0,0 +1,22 @@ +from urllib.parse import quote_plus + +from sqlalchemy.engine import URL +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker + +import settings + +encoded_password = quote_plus(settings.DB_PASSWORD) +engine = create_async_engine( + URL.create( + drivername="postgresql+asyncpg", + username=settings.DB_USERNAME, + password=encoded_password, + host=settings.DB_HOST, + port=settings.DB_PORT, + database=settings.DB_NAME, + ), + pool_pre_ping=True, +) + +DBSession = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession, expire_on_commit=False) diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000..8dc24d0 --- /dev/null +++ b/database/models.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from sqlalchemy.orm import declarative_base + +Base = declarative_base() diff --git a/didier/__init__.py b/didier/__init__.py index e69de29..62f3e85 100644 --- a/didier/__init__.py +++ b/didier/__init__.py @@ -0,0 +1 @@ +from .didier import Didier diff --git a/didier/data/__init__.py b/didier/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/didier/data/constants.py b/didier/data/constants.py new file mode 100644 index 0000000..d5c1021 --- /dev/null +++ b/didier/data/constants.py @@ -0,0 +1 @@ +PREFIXES = ["didier", "big d"] diff --git a/didier/didier.py b/didier/didier.py new file mode 100644 index 0000000..ff47281 --- /dev/null +++ b/didier/didier.py @@ -0,0 +1,36 @@ +import discord +from discord import Message +from discord.ext import commands +from sqlalchemy.ext.asyncio import AsyncSession + +import settings +from database.engine import DBSession +from didier.utils.prefix import get_prefix + + +class Didier(commands.Bot): + """DIDIER <3""" + + def __init__(self): + activity = discord.Activity(type=discord.ActivityType.playing, name=settings.DISCORD_STATUS_MESSAGE) + status = discord.Status.online + + intents = discord.Intents.default() + intents.members = True + intents.message_content = True + + super().__init__( + command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status + ) + + @property + def db_session(self) -> AsyncSession: + """Obtain a database session""" + return DBSession() + + async def on_ready(self): + """Event triggered when the bot is ready""" + print(settings.DISCORD_READY_MESSAGE) + + async def on_message(self, message: Message, /) -> None: + print(message.content) diff --git a/didier/utils/__init__.py b/didier/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/didier/utils/prefix.py b/didier/utils/prefix.py new file mode 100644 index 0000000..53e5a3e --- /dev/null +++ b/didier/utils/prefix.py @@ -0,0 +1,27 @@ +import re + +from discord import Message +from discord.ext import commands + +from didier.data import constants + + +def get_prefix(client: commands.Bot, message: Message) -> str: + """Match a prefix against a message + This is done dynamically to allow variable amounts of whitespace, + and through regexes to allow case-insensitivity among other things. + """ + mention = f"<@!{client.user.id}>" + regex = r"^({})\s*" + + # Check which prefix was used + for prefix in [*constants.PREFIXES, mention]: + match = re.match(regex.format(prefix), message.content, flags=re.I) + + if match is not None: + # Get the part of the message that was matched + # .group() is inconsistent with whitespace, so that can't be used + return message.content[: match.end()] + + # Matched nothing + return "didier" diff --git a/main.py b/main.py index e69de29..58f9e1b 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,34 @@ +import logging +from logging.handlers import RotatingFileHandler + +import asyncio + +import settings +from didier import Didier + + +async def run_bot(): + """Run Didier""" + didier = Didier() + await didier.start(settings.DISCORD_TOKEN) + + +def setup_logging(): + """Configure custom loggers""" + max_log_size = 32 * 1024 * 1024 + + didier_log = logging.getLogger("didier") + + handler = RotatingFileHandler(settings.LOGFILE, mode="a", maxBytes=max_log_size, backupCount=5) + handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s]: %(message)s")) + handler.setLevel(logging.INFO) + + didier_log.addHandler(handler) + + logging.getLogger("discord").setLevel(logging.ERROR) + + +if __name__ == "__main__": + setup_logging() + + asyncio.run(run_bot()) diff --git a/requirements.txt b/requirements.txt index 4915fea..499739d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiohttp==3.8.1 alembic==1.8.0 +asyncpg==0.25.0 # Dev version of dpy git+https://github.com/Rapptz/discord.py environs==9.5.0 diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..5ef9350 --- /dev/null +++ b/settings.py @@ -0,0 +1,22 @@ +from environs import Env + +# Read the .env file (if present) +env = Env() +env.read_env() + +"""General config""" +SANDBOX: bool = env.bool("SANDBOX", True) +LOGFILE: str = env.str("LOGFILE", "didier.log") + +"""Database""" +DB_NAME: str = env.str("DB_NAME", "didier") +DB_USERNAME: str = env.str("DB_USERNAME", "postgres") +DB_PASSWORD: str = env.str("DB_PASSWORD", "") +DB_HOST: str = env.str("DB_HOST", "localhost") +DB_PORT: int = env.int("DB_PORT", "5432") + +"""Discord""" +DISCORD_TOKEN: str = env.str("DISC_TOKEN") +DISCORD_READY_MESSAGE: str = env.str("DISC_READY_MESSAGE", "I'M READY I'M READY I'M READY") +DISCORD_STATUS_MESSAGE: str = env.str("DISC_STATUS_MESSAGE", "with your Didier Dinks.") +DISCORD_TEST_GUILDS: list[int] = env.list("DISC_TEST_GUILDS", [], subcast=int)