diff --git a/.dockerignore b/.dockerignore index bd3c727cda..63ed0735cb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,6 @@ !/cogs !/core !/plugins -!/src !*.py !LICENSE !pdm.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a40d678bd3..8244b44eeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,13 +8,13 @@ repos: hooks: - id: ruff - repo: https://github.com/pdm-project/pdm - rev: 2.10.0 # a PDM release exposing the hook + rev: 2.11.2 # a PDM release exposing the hook hooks: - id: pdm-export # command arguments, e.g.: args: [ '-o', 'requirements.txt', '--without-hashes' ] files: ^pdm.lock$ - repo: https://github.com/pdm-project/pdm - rev: 2.10.0 # a PDM release exposing the hook + rev: 2.11.2 # a PDM release exposing the hook hooks: - id: pdm-lock-check \ No newline at end of file diff --git a/bot.py b/bot.py index 403fce99f3..007a78c100 100644 --- a/bot.py +++ b/bot.py @@ -24,6 +24,10 @@ from emoji import UNICODE_EMOJI from pkg_resources import parse_version +from core.attachments.attachment_handler import IAttachmentHandler +from core.attachments.errors import AttachmentSizeException +from core.attachments.mongo_attachment_client import MongoAttachmentHandler +from core.attachments.s3_attachment_client import S3AttachmentHandler from core.blocklist import Blocklist, BlockReason try: @@ -92,6 +96,27 @@ def __init__(self): self.blocklist = Blocklist(bot=self) + if self.config["attachment_datastore"] == "internal": + logger.info("Using internal attachment handler.") + self.attachment_handler: IAttachmentHandler = MongoAttachmentHandler(self.api.db) + elif self.config["attachment_datastore"] == "s3": + logger.info("Using S3 attachment handler.") + endpoint = self.config["s3_endpoint"] + if endpoint is None: + logger.critical("S3 endpoint must be set when using the S3 attachment datastore.") + raise InvalidConfigError("s3_endpoint must be set.") + self.attachment_handler: IAttachmentHandler = S3AttachmentHandler( + endpoint=endpoint, + access_key=self.config["s3_access_key"] or None, + secret_key=self.config["s3_secret_key"] or None, + region=self.config["s3_region"] or None, + bucket=self.config["s3_bucket"] or None, + ) + else: + raise InvalidConfigError("Invalid image_store option set.") + if self.config["max_attachment_size"] is not None: + self.attachment_handler.max_size = self.config["max_attachment_size"] + self.startup() def get_guild_icon( @@ -961,11 +986,22 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) - return await message.channel.send(embed=embed) + await message.channel.send(embed=embed) + return if not thread.cancelled: try: await thread.send(message) + except AttachmentSizeException as e: + await self.add_reaction(message, blocked_emoji) + await message.channel.send( + embed=discord.Embed( + title="Attachment too large", + description=str(e), + color=self.error_color, + ) + ) + return except Exception: logger.error("Failed to send message:", exc_info=True) await self.add_reaction(message, blocked_emoji) diff --git a/core/attachments/attachment_handler.py b/core/attachments/attachment_handler.py new file mode 100644 index 0000000000..1b05778618 --- /dev/null +++ b/core/attachments/attachment_handler.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod + +from discord.message import Message + + +class IAttachmentHandler(ABC): + _attachment_max_size: int = 1024 * 1024 * 500 + + @abstractmethod + async def upload_attachments(self, message: Message) -> list[dict]: + """ + Uploads all attachments from a message to the database + Parameters + ---------- + message + Returns + ------- + A dict containing what should be appended to the thread documents attachments field + """ + pass + + @property + def max_size(self) -> int: + """ + The maximum size of an attachment in bytes + Returns + ------- + int + """ + return self._attachment_max_size + + @max_size.setter + def max_size(self, value: int): + """ + Set the maximum size of an attachment in bytes + """ + self._attachment_max_size = value diff --git a/core/attachments/errors.py b/core/attachments/errors.py new file mode 100644 index 0000000000..bbe910d314 --- /dev/null +++ b/core/attachments/errors.py @@ -0,0 +1,13 @@ +class AttachmentSizeException(Exception): + """Raised when an attachment is too large. + Attributes: + size -- size of the attachment + max -- maximum allowed size of the attachment + message -- explanation of why the specific transition is invalid + """ + + def __init__(self, size: int, max_size: int, message: str): + self.size = size + self.max = max_size + self.message = message + super().__init__(self.message) diff --git a/core/attachments/mongo_attachment_client.py b/core/attachments/mongo_attachment_client.py new file mode 100644 index 0000000000..2dc4b3354c --- /dev/null +++ b/core/attachments/mongo_attachment_client.py @@ -0,0 +1,113 @@ +import asyncio +import datetime +from typing import Any, Final + +from discord.message import Attachment, Message +from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorDatabase +from pymongo.results import InsertOneResult + +from core.attachments.attachment_handler import IAttachmentHandler +from core.attachments.errors import AttachmentSizeException +from core.models import getLogger + + +def _mongo_attachment_dict(attachment: Attachment, data: bytes) -> dict: + """ + Convert a discord attachment to a dict that can be stored in the database + Parameters + ---------- + attachment + data + + Returns + ------- + dict + """ + return { + # same as discord id + "_id": attachment.id, + "filename": attachment.filename, + "content_type": attachment.content_type, + "width": attachment.width, + "height": attachment.height, + "description": attachment.description, + "size": attachment.size, + "data": data, + "uploaded_at": datetime.datetime.now(datetime.timezone.utc), + } + + +class MongoAttachmentHandler(IAttachmentHandler): + logger = getLogger(__name__) + + # 8 MB to bytes + MONGODB_MAX_SIZE: Final[int] = 1024 * 1024 * 15 + + def __init__(self, database: AsyncIOMotorDatabase) -> None: + self.client = database + self.attachment_collection: AsyncIOMotorCollection = database["attachments"] + self._attachment_max_size = self.MONGODB_MAX_SIZE + # self.log_collection: AsyncIOMotorCollection = database["logs"] + + @IAttachmentHandler.max_size.setter + def max_size(self, size: int) -> None: + if size > self.MONGODB_MAX_SIZE: + self.logger.warning( + f"MongoDB attachment storage has a maximum attachment size of {self.MONGODB_MAX_SIZE} bytes. " + f"The max attachment size will be set to this value.." + ) + return + self._attachment_max_size = size + + async def _store_attachments_bulk(self, attachments: list[Attachment]) -> Any: + attachment_data = await asyncio.gather(*[attachment.read() for attachment in attachments]) + + results = await self.attachment_collection.insert_many( + [ + _mongo_attachment_dict(attachment, attachment_data[index]) + for index, attachment in enumerate(attachments) + ] + ) + + return results.inserted_ids + + async def _store_attachment(self, attachment: Attachment) -> Any: + result: InsertOneResult = await self.attachment_collection.insert_one( + _mongo_attachment_dict(attachment, await attachment.read()) + ) + return result.inserted_id + + async def upload_attachments( + self, + message: Message, + ) -> list[dict]: + attachments = [] + + for attachment in message.attachments: + if attachment.size > self.max_size: + self.logger.warning( + "Attachment %s is too large to be stored in the database. It will not be uploaded...", + attachment.filename, + ) + raise AttachmentSizeException("Attachment too large") + + if len(message.attachments) > 1: + await self._store_attachments_bulk(message.attachments) + else: + await self._store_attachment(message.attachments[0]) + + for attachment in message.attachments: + attachments.append( + { + "id": attachment.id, + "filename": attachment.filename, + "type": "internal", + # URL points to the original discord URL + "url": attachment.url, + "content_type": attachment.content_type, + "width": attachment.width, + "height": attachment.height, + } + ) + + return attachments diff --git a/core/attachments/s3_attachment_client.py b/core/attachments/s3_attachment_client.py new file mode 100644 index 0000000000..fa86339696 --- /dev/null +++ b/core/attachments/s3_attachment_client.py @@ -0,0 +1,108 @@ +import datetime +import io + +from discord import Message +from minio import Minio +from minio.commonconfig import Tags + +from core.attachments.attachment_handler import IAttachmentHandler +from core.models import getLogger + + +class S3AttachmentHandler(IAttachmentHandler): + logger = getLogger(__name__) + + def __init__( + self, + endpoint: str, + access_key: str | None = None, + secret_key: str | None = None, + region: str | None = None, + bucket: str | None = "modmail-attachments", + ) -> None: + """ + Initialize the S3AttachmentHandler with the given access key, secret key, and endpoint. + + Parameters + ---------- + access_key : str | None + The access key for the S3 bucket. + secret_key : str | None + The secret key for the S3 bucket. + endpoint : str + The endpoint for the S3 bucket. + region : str | None + The region for the S3 bucket. + bucket : str | None + The name of the S3 bucket. + """ + self.bucket = bucket + self.region = region + + self.client = Minio( + endpoint, access_key=access_key, secret_key=secret_key, secure=False, region=region + ) + + try: + self.client.bucket_exists(self.bucket) + except Exception as e: + self.logger.error(f"An error occurred while checking if the s3 bucket exists: {e}", exc_info=True) + raise + + if not self.client.bucket_exists(self.bucket): + self.client.make_bucket(self.bucket) + + async def upload_attachments(self, message: Message) -> list[dict]: + """ + Upload attachments from a given message to the S3 bucket. + + Parameters + ---------- + message : Message + The message containing the attachments to upload. + + Returns + ------- + list[dict] + A list of dictionaries containing information about the uploaded attachments. + """ + attachments = [] + + # Setup metadata for S3 object + tags = Tags.new_object_tags() + tags["message_id"] = str(message.id) + tags["channel_id"] = str(message.channel.id) + # tags["bot_id"] = str(bot.bot_id) + for attachment in message.attachments: + if attachment.size > self.max_size: + raise ValueError( + f"Attachment {attachment.filename} is too large. Max size is {self.max_size} bytes." + ) + + for attachment in message.attachments: + self.logger.debug("Uploading attachment %s to S3", attachment.filename) + result = self.client.put_object( + self.bucket, + str(attachment.id), + io.BytesIO(await attachment.read()), + length=attachment.size, + content_type=attachment.content_type, + tags=tags, + ) + attachments.append( + { + "id": attachment.id, + "filename": attachment.filename, + "type": "s3", + "s3_object": result.object_name, + "s3_bucket": result.bucket_name, + "content_type": attachment.content_type, + "width": attachment.width, + "height": attachment.height, + "description": attachment.description, + "size": attachment.size, + "uploaded_at": datetime.datetime.now(datetime.timezone.utc), + } + ) + + return attachments diff --git a/core/clients.py b/core/clients.py index e80225d9e9..d645cda562 100644 --- a/core/clients.py +++ b/core/clients.py @@ -7,7 +7,7 @@ from aiohttp import ClientResponse, ClientResponseError from discord import DMChannel, Member, Message, TextChannel from discord.ext import commands -from motor.motor_asyncio import AsyncIOMotorClient +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase from pymongo.errors import ConfigurationError from pymongo.uri_parser import parse_uri @@ -299,7 +299,7 @@ class ApiClient: def __init__(self, bot, db): self.bot = bot - self.db = db + self.db: AsyncIOMotorDatabase = db self.session = bot.session async def request( @@ -452,7 +452,7 @@ def __init__(self, bot): try: database = parse_uri(mongo_uri).get("database") or "modmail_bot" - db = AsyncIOMotorClient(mongo_uri)[database] + db: AsyncIOMotorDatabase = AsyncIOMotorClient(mongo_uri)[database] except ConfigurationError as e: logger.critical( "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " @@ -668,18 +668,7 @@ async def append_log( }, "content": message.content, "type": type_, - "attachments": [ - { - "id": a.id, - "filename": a.filename, - # In previous versions this was true for both videos and images - "is_image": a.content_type.startswith("image/"), - "size": a.size, - "url": a.url, - "content_type": a.content_type, - } - for a in message.attachments - ], + "attachments": await self.bot.attachment_handler.upload_attachments(message), } return await self.logs.find_one_and_update( diff --git a/core/config.py b/core/config.py index b8bb35d40c..8fb5f24c1c 100644 --- a/core/config.py +++ b/core/config.py @@ -39,6 +39,7 @@ class ConfigManager: "reply_without_command": False, "anon_reply_without_command": False, "plain_reply_without_command": False, + "max_attachment_size": None, # logging "log_channel_id": None, "mention_channel_id": None, @@ -179,6 +180,12 @@ class ConfigManager: "log_level": "INFO", # data collection "data_collection": True, + "attachment_datastore": "internal", + "s3_endpoint": None, + "s3_access_key": None, + "s3_secret_key": None, + "s3_region": None, + "s3_bucket": "modmail-attachments", } colors = {"mod_color", "recipient_color", "main_color", "error_color"} diff --git a/core/config_help.json b/core/config_help.json index 80ebf1a48a..f46b2577d5 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1170,5 +1170,71 @@ "If this configuration is enabled, only roles that are hoisted (displayed seperately in member list) will be used. If a user has no hoisted roles, it will return 'None'.", "If you would like to display the top role of a user regardless of if it's hoisted or not, disable `use_hoisted_top_role`." ] + }, + "attachment_datastore": { + "default": "internal", + "description": "Controls how images are stored.", + "examples": [], + "notes": [ + "Can be set to `internal` or `s3`.", + "If set to `s3`, you must also supply `s3_endpoint` along with authentication credentials.", + "If set to `internal`, images will be stored within Mongo.", + "This configuration can only be set through `.env` file or environment (config) variables." + ] + }, + "s3_endpoint": { + "default": "None", + "description": "The endpoint for your S3 bucket.", + "examples": [], + "notes": [ + "This configuration can only be set through `.env` file or environment (config) variables.", + "This is only required if `attachment_datastore` is set to `s3`." + ] + }, + "s3_access_key": { + "default": "None", + "description": "The access key for your S3 bucket.", + "examples": [], + "notes": [ + "This configuration can only be set through `.env` file or environment (config) variables.", + "This is only required if `attachment_datastore` is set to `s3`." + ] + }, + "s3_secret_key": { + "default": "None", + "description": "The secret key for your S3 bucket.", + "examples": [], + "notes": [ + "This configuration can only be set through `.env` file or environment (config) variables.", + "This is only required if `attachment_datastore` is set to `s3`." + ] + }, + "s3_bucket": { + "default": "modmail-attachments", + "description": "The bucket name for your S3 bucket.", + "examples": [], + "notes": [ + "This configuration can only be set through `.env` file or environment (config) variables.", + "This is only functional if `attachment_datastore` is set to `s3`." + ] + }, + "s3_region": { + "default": "None", + "description": "The region for your S3 bucket.", + "examples": [], + "notes": [ + "This configuration can only be set through `.env` file or environment (config) variables.", + "This is only functional if `attachment_datastore` is set to `s3`." + ] + }, + "max_attachment_size": { + "default": "None", + "description": "The maximum size of an attachment in bytes.", + "examples": [], + "notes": [ + "Messages will fail to send if they have an attachment over this size.", + "Depending on your attachment handler, the actual maximum size may be lower than this.", + "Requires a restart to take effect." + ] } } \ No newline at end of file diff --git a/core/thread.py b/core/thread.py index 2518acd6d5..eac1ff06ac 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,6 +12,7 @@ from discord.ext.commands import CommandError, MissingRequiredArgument from discord.types.user import PartialUser as PartialUserPayload, User as UserPayload +from core.attachments.errors import AttachmentSizeException from core.models import DMDisabled, DummyMessage, getLogger from core.utils import ( AcceptButton, @@ -869,6 +870,8 @@ async def reply( "messages from friends, or the bot was " "blocked by the recipient." ) + elif isinstance(e, AttachmentSizeException): + description = e else: description = ( "Your message could not be delivered due " @@ -940,6 +943,14 @@ async def send( ) ) + for attachment in message.attachments: + if attachment.size > self.bot.attachment_handler.max_size: + raise AttachmentSizeException( + attachment.size, + self.bot.attachment_handler.max_size, + f"An attachment was above the maximum allowed size of {self.bot.attachment_handler.max_size} bytes.", + ) + if not self.ready: await self.wait_until_ready() diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 8480f24826..3bf0880f7c 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -15,6 +15,16 @@ services: - mongodb:/data/db ports: - 127.0.0.1:27017:27017 + minio: + # minioadmin:minioadmin + image: minio/minio + command: server /data --console-address ":9001" + ports: + - 127.0.0.1:9000:9000 + - 127.0.0.1:9001:9001 + volumes: + - minio:/data volumes: mongodb: + minio: diff --git a/pdm.lock b/pdm.lock index 80900b1498..f870f38c49 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,8 +4,8 @@ [metadata] groups = ["default", "dev"] strategy = ["cross_platform"] -lock_version = "4.4" -content_hash = "sha256:4c67644ed00e2a2cf4a09ff3714ef9f8460fbafcfa70dd6e56beaefba85a63c5" +lock_version = "4.4.1" +content_hash = "sha256:12836da682e472ace6c00ce49cac0243354d188ca09475c2bbcdfbd09d7af390" [[package]] name = "aiohttp" @@ -68,6 +68,51 @@ files = [ {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] +[[package]] +name = "argon2-cffi" +version = "23.1.0" +requires_python = ">=3.7" +summary = "Argon2 for Python" +dependencies = [ + "argon2-cffi-bindings", +] +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +requires_python = ">=3.6" +summary = "Low-level CFFI bindings for Argon2" +dependencies = [ + "cffi>=1.0.1", +] +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + [[package]] name = "async-timeout" version = "4.0.3" @@ -378,6 +423,22 @@ files = [ {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, ] +[[package]] +name = "minio" +version = "7.2.3" +summary = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" +dependencies = [ + "argon2-cffi", + "certifi", + "pycryptodome", + "typing-extensions", + "urllib3", +] +files = [ + {file = "minio-7.2.3-py3-none-any.whl", hash = "sha256:e6b5ce0a9b4368da50118c3f0c4df5dbf33885d44d77fce6c0aa1c485e6af7a1"}, + {file = "minio-7.2.3.tar.gz", hash = "sha256:4971dfb1a71eeefd38e1ce2dc7edc4e6eb0f07f1c1d6d70c15457e3280cfc4b9"}, +] + [[package]] name = "motor" version = "3.1.2" @@ -530,6 +591,35 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[[package]] +name = "pycryptodome" +version = "3.19.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Cryptographic library for Python" +files = [ + {file = "pycryptodome-3.19.1-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:67939a3adbe637281c611596e44500ff309d547e932c449337649921b17b6297"}, + {file = "pycryptodome-3.19.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:11ddf6c9b52116b62223b6a9f4741bc4f62bb265392a4463282f7f34bb287180"}, + {file = "pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e6f89480616781d2a7f981472d0cdb09b9da9e8196f43c1234eff45c915766"}, + {file = "pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e1efcb68993b7ce5d1d047a46a601d41281bba9f1971e6be4aa27c69ab8065"}, + {file = "pycryptodome-3.19.1-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c6273ca5a03b672e504995529b8bae56da0ebb691d8ef141c4aa68f60765700"}, + {file = "pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b0bfe61506795877ff974f994397f0c862d037f6f1c0bfc3572195fc00833b96"}, + {file = "pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:f34976c5c8eb79e14c7d970fb097482835be8d410a4220f86260695ede4c3e17"}, + {file = "pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7c9e222d0976f68d0cf6409cfea896676ddc1d98485d601e9508f90f60e2b0a2"}, + {file = "pycryptodome-3.19.1-cp35-abi3-win32.whl", hash = "sha256:4805e053571140cb37cf153b5c72cd324bb1e3e837cbe590a19f69b6cf85fd03"}, + {file = "pycryptodome-3.19.1-cp35-abi3-win_amd64.whl", hash = "sha256:a470237ee71a1efd63f9becebc0ad84b88ec28e6784a2047684b693f458f41b7"}, + {file = "pycryptodome-3.19.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:ed932eb6c2b1c4391e166e1a562c9d2f020bfff44a0e1b108f67af38b390ea89"}, + {file = "pycryptodome-3.19.1-pp27-pypy_73-win32.whl", hash = "sha256:81e9d23c0316fc1b45d984a44881b220062336bbdc340aa9218e8d0656587934"}, + {file = "pycryptodome-3.19.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37e531bf896b70fe302f003d3be5a0a8697737a8d177967da7e23eff60d6483c"}, + {file = "pycryptodome-3.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd4e95b0eb4b28251c825fe7aa941fe077f993e5ca9b855665935b86fbb1cc08"}, + {file = "pycryptodome-3.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22c80246c3c880c6950d2a8addf156cee74ec0dc5757d01e8e7067a3c7da015"}, + {file = "pycryptodome-3.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e70f5c839c7798743a948efa2a65d1fe96bb397fe6d7f2bde93d869fe4f0ad69"}, + {file = "pycryptodome-3.19.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6c3df3613592ea6afaec900fd7189d23c8c28b75b550254f4bd33fe94acb84b9"}, + {file = "pycryptodome-3.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08b445799d571041765e7d5c9ca09c5d3866c2f22eeb0dd4394a4169285184f4"}, + {file = "pycryptodome-3.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:954d156cd50130afd53f8d77f830fe6d5801bd23e97a69d358fed068f433fbfe"}, + {file = "pycryptodome-3.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b7efd46b0b4ac869046e814d83244aeab14ef787f4850644119b1c8b0ec2d637"}, + {file = "pycryptodome-3.19.1.tar.gz", hash = "sha256:8ae0dd1bcfada451c35f9e29a3e5db385caabc190f98e4a80ad02a61098fb776"}, +] + [[package]] name = "pymongo" version = "4.6.0" diff --git a/pyproject.toml b/pyproject.toml index 6fffa0af3d..b208db5044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ name = "Discord-OpenModmail" version = "4.2.0" description = "ForkĀ² of a feature rich Discord Modmail bot written in Python." authors = [ - {name = "", email = ""}, + { name = "", email = "" }, ] dependencies = [ "aiohttp~=3.8.1", @@ -76,10 +76,12 @@ dependencies = [ "strenum", "discord-py~=2.3.0", "setuptools>=69.0.3", + "typing-extensions>=4.6.3", + "minio~=7.2.3", ] requires-python = ">=3.10" readme = "README.md" -license = {text = "AGPL-3.0-or-later"} +license = { text = "AGPL-3.0-or-later" } [build-system] requires = ["pdm-backend"] @@ -92,6 +94,7 @@ requirements = "pdm export -o requirements.txt --without-hashes" # OR # $ pdm bot bot = "bot.py" +check = "pdm run ruff check ." # Manually sync with pdm sync-pre-commit [tool.sync-pre-commit-lock] diff --git a/requirements.txt b/requirements.txt index 0a63625a6e..e3b9ea97b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ aiohttp==3.8.6 aiosignal==1.3.1 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 async-timeout==4.0.3 attrs==23.1.0 black==23.10.1 @@ -22,6 +24,7 @@ frozenlist==1.4.0 identify==2.5.31 idna==3.4 isodate==0.6.1 +minio==7.2.3 motor==3.1.2 multidict==6.0.4 mypy-extensions==1.0.0 @@ -33,6 +36,7 @@ pathspec==0.11.2 platformdirs==3.11.0 pre-commit==3.5.0 pycparser==2.21 +pycryptodome==3.19.1 pymongo==4.6.0 python-dateutil==2.8.2 python-dotenv==1.0.0